diff --git a/docs/development/plugin-guide.md b/docs/development/plugin-guide.md
index 070a327..baada89 100644
--- a/docs/development/plugin-guide.md
+++ b/docs/development/plugin-guide.md
@@ -7,10 +7,10 @@
## 📚 Table of Contents
1. [Quick Start](#1-quick-start)
-2. [Core Concepts & SDK Details](#2-core-concepts--sdk-details)
+2. [Core Concepts & SDK Details](#2-core-concepts-sdk-details)
3. [Deep Dive into Plugin Types](#3-deep-dive-into-plugin-types)
4. [Advanced Development Patterns](#4-advanced-development-patterns)
-5. [Best Practices & Design Principles](#5-best-practices--design-principles)
+5. [Best Practices & Design Principles](#5-best-practices-design-principles)
6. [Troubleshooting](#6-troubleshooting)
---
@@ -351,8 +351,7 @@ async def action(self, body, __event_call__, __metadata__, ...):
#### Reference Implementations
-- `plugins/actions/js-render-poc/infographic_markdown.py` - AntV Infographic + Data URL
-- `plugins/actions/js-render-poc/js_render_poc.py` - Basic proof of concept
+- `plugins/actions/infographic/infographic.py` - Production-ready implementation using AntV + Data URL
---
diff --git a/docs/development/plugin-guide.zh.md b/docs/development/plugin-guide.zh.md
index a127872..fa9c700 100644
--- a/docs/development/plugin-guide.zh.md
+++ b/docs/development/plugin-guide.zh.md
@@ -4,19 +4,19 @@
## 📚 目录
-1. [插件开发快速入门](#1-插件开发快速入门)
-2. [核心概念与 SDK 详解](#2-核心概念与-sdk-详解)
-3. [插件类型深度解析](#3-插件类型深度解析)
- * [Action (动作)](#31-action-动作)
- * [Filter (过滤器)](#32-filter-过滤器)
- * [Pipe (管道)](#33-pipe-管道)
-4. [高级开发模式](#4-高级开发模式)
-5. [最佳实践与设计原则](#5-最佳实践与设计原则)
-6. [故障排查](#6-故障排查)
+1. [插件开发快速入门](#1-quick-start)
+2. [核心概念与 SDK 详解](#2-core-concepts-sdk-details)
+3. [插件类型深度解析](#3-plugin-types)
+ * [Action (动作)](#31-action)
+ * [Filter (过滤器)](#32-filter)
+ * [Pipe (管道)](#33-pipe)
+4. [高级开发模式](#4-advanced-patterns)
+5. [最佳实践与设计原则](#5-best-practices)
+6. [故障排查](#6-troubleshooting)
---
-## 1. 插件开发快速入门
+## 1. 插件开发快速入门 {: #1-quick-start }
### 1.1 什么是 OpenWebUI 插件?
@@ -64,7 +64,7 @@ class Action:
---
-## 2. 核心概念与 SDK 详解
+## 2. 核心概念与 SDK 详解 {: #2-core-concepts-sdk-details }
### 2.1 ⚠️ 重要:同步与异步
@@ -107,9 +107,9 @@ class Filter:
---
-## 3. 插件类型深度解析
+## 3. 插件类型深度解析 {: #3-plugin-types }
-### 3.1 Action (动作)
+### 3.1 Action (动作) {: #31-action }
**定位**:在消息下方添加按钮,用户点击触发。
@@ -134,7 +134,7 @@ async def action(self, body, __event_call__):
await __event_call__({"type": "execute", "data": {"code": js}})
```
-### 3.2 Filter (过滤器)
+### 3.2 Filter (过滤器) {: #32-filter }
**定位**:中间件,拦截并修改请求/响应。
@@ -155,7 +155,7 @@ async def inlet(self, body, __metadata__):
return body
```
-### 3.3 Pipe (管道)
+### 3.3 Pipe (管道) {: #33-pipe }
**定位**:自定义模型/代理。
@@ -177,7 +177,7 @@ class Pipe:
---
-## 4. 高级开发模式
+## 4. 高级开发模式 {: #4-advanced-patterns }
### 4.1 Pipe 与 Filter 协同
利用 `__request__.app.state` 在不同插件间共享数据。
@@ -315,10 +315,9 @@ async def action(self, body, __event_call__, __metadata__, ...):
#### 参考实现
-- `plugins/actions/js-render-poc/infographic_markdown.py` - AntV 信息图 + Data URL
-- `plugins/actions/js-render-poc/js_render_poc.py` - 基础概念验证
+- `plugins/actions/infographic/infographic.py` - 基于 AntV + Data URL 的生产级实现
-## 5. 最佳实践与设计原则
+## 5. 最佳实践与设计原则 {: #5-best-practices }
### 5.1 命名与定位
* **简短有力**:如 "闪记卡", "精读"。避免 "文本分析助手" 这种泛词。
@@ -344,7 +343,7 @@ except Exception as e:
---
-## 6. 故障排查
+## 6. 故障排查 {: #6-troubleshooting }
* **HTML 不显示?** 确保包裹在 ` ```html ... ``` ` 代码块中。
* **数据库报错?** 检查是否在 `async` 函数中直接调用了同步的 DB 方法,请使用 `asyncio.to_thread`。
diff --git a/docs/index.md b/docs/index.md
index 510b725..8bb0fb3 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -73,13 +73,13 @@ hide:
[:octicons-arrow-right-24: Learn More](plugins/actions/smart-mind-map.md)
-- :material-card-text:{ .lg .middle } **Knowledge Card**
+- :material-card-text:{ .lg .middle } **Flash Card**
---
- Quickly generates beautiful learning memory cards, perfect for studying and quick memorization.
+ Quickly generates beautiful flashcards from text, extracting key points and categories.
- [:octicons-arrow-right-24: Learn More](plugins/actions/knowledge-card.md)
+ [:octicons-arrow-right-24: Learn More](plugins/actions/flash-card.md)
- :material-arrow-collapse-vertical:{ .lg .middle } **Async Context Compression**
diff --git a/docs/index.zh.md b/docs/index.zh.md
index 807d946..2d5f0c1 100644
--- a/docs/index.zh.md
+++ b/docs/index.zh.md
@@ -73,13 +73,13 @@ hide:
[:octicons-arrow-right-24: 了解更多](plugins/actions/smart-mind-map.md)
-- :material-card-text:{ .lg .middle } **知识卡片**
+- :material-card-text:{ .lg .middle } **Flash Card(闪记卡)**
---
快速生成精美的学习记忆卡片,非常适合学习和快速记忆。
- [:octicons-arrow-right-24: 了解更多](plugins/actions/knowledge-card.md)
+ [:octicons-arrow-right-24: 了解更多](plugins/actions/flash-card.md)
- :material-arrow-collapse-vertical:{ .lg .middle } **异步上下文压缩**
diff --git a/docs/plugins/actions/index.zh.md b/docs/plugins/actions/index.zh.md
index ac139bd..7ee074b 100644
--- a/docs/plugins/actions/index.zh.md
+++ b/docs/plugins/actions/index.zh.md
@@ -37,15 +37,15 @@ Actions 是交互式插件,能够:
[:octicons-arrow-right-24: 查看文档](smart-infographic.md)
-- :material-card-text:{ .lg .middle } **Knowledge Card**
+- :material-card-text:{ .lg .middle } **Flash Card(闪记卡)**
---
- 快速生成精美的学习记忆卡片,适合学习与记忆。
+ 快速生成精美的学习记忆卡片,非常适合学习和快速记忆。
- **版本:** 0.2.2
+ **版本:** 0.2.4
- [:octicons-arrow-right-24: 查看文档](knowledge-card.md)
+ [:octicons-arrow-right-24: 查看文档](flash-card.md)
- :material-file-excel:{ .lg .middle } **Export to Excel**
@@ -77,15 +77,7 @@ Actions 是交互式插件,能够:
[:octicons-arrow-right-24: 查看文档](deep-dive.zh.md)
-- :material-image-text:{ .lg .middle } **信息图转 Markdown**
- ---
-
- AI 驱动的信息图生成器,渲染 SVG 并以 Markdown Data URL 图片嵌入。
-
- **版本:** 1.0.0
-
- [:octicons-arrow-right-24: 查看文档](infographic-markdown.zh.md)
diff --git a/docs/plugins/filters/index.zh.md b/docs/plugins/filters/index.zh.md
index 70d3ff7..9bb7f2c 100644
--- a/docs/plugins/filters/index.zh.md
+++ b/docs/plugins/filters/index.zh.md
@@ -36,15 +36,7 @@ Filter 充当消息管线中的中间件:
[:octicons-arrow-right-24: 查看文档](context-enhancement.md)
-- :material-google:{ .lg .middle } **Gemini Manifold Companion**
- ---
-
- Gemini Manifold Pipe 插件的伴随过滤器。
-
- **版本:** 1.7.0
-
- [:octicons-arrow-right-24: 查看文档](gemini-manifold-companion.md)
- :material-format-paint:{ .lg .middle } **Markdown Normalizer**
diff --git a/docs/plugins/index.md b/docs/plugins/index.md
index 72befbb..b39b8d4 100644
--- a/docs/plugins/index.md
+++ b/docs/plugins/index.md
@@ -48,15 +48,15 @@ OpenWebUI supports four types of plugins, each serving a different purpose:
| Plugin | Type | Description | Version |
|--------|------|-------------|---------|
-| [Smart Mind Map](actions/smart-mind-map.md) | Action | Generate interactive mind maps from text | 0.8.0 |
-| [Smart Infographic](actions/smart-infographic.md) | Action | Transform text into professional infographics | 1.0.0 |
-| [Knowledge Card](actions/knowledge-card.md) | Action | Create beautiful learning flashcards | 0.2.0 |
-| [Export to Excel](actions/export-to-excel.md) | Action | Export chat history to Excel files | 1.0.0 |
-| [Export to Word](actions/export-to-word.md) | Action | Export chat content to Word (.docx) with formatting | 0.1.0 |
-| [Async Context Compression](filters/async-context-compression.md) | Filter | Intelligent context compression | 1.0.0 |
-| [Context Enhancement](filters/context-enhancement.md) | Filter | Enhance chat context | 1.0.0 |
-| [Gemini Manifold Companion](filters/gemini-manifold-companion.md) | Filter | Companion for Gemini Manifold | 1.0.0 |
-| [Gemini Manifold](pipes/gemini-manifold.md) | Pipe | Gemini model integration | 1.0.0 |
+| [Smart Mind Map](actions/smart-mind-map.md) | Action | Generate interactive mind maps from text | 0.9.1 |
+| [Smart Infographic](actions/smart-infographic.md) | Action | Transform text into professional infographics | 1.4.9 |
+| [Flash Card](actions/flash-card.md) | Action | Create beautiful learning flashcards | 0.2.4 |
+| [Export to Excel](actions/export-to-excel.md) | Action | Export chat history to Excel files | 0.3.7 |
+| [Export to Word](actions/export-to-word.md) | Action | Export chat content to Word (.docx) with formatting | 0.4.3 |
+| [Async Context Compression](filters/async-context-compression.md) | Filter | Intelligent context compression | 1.1.3 |
+| [Context Enhancement](filters/context-enhancement.md) | Filter | Enhance chat context | 0.3.0 |
+| [Multi-Model Context Merger](filters/multi-model-context-merger.md) | Filter | Merge context from multiple models | 0.1.0 |
+| [Web Gemini Multimodal Filter](filters/web-gemini-multimodel.md) | Filter | Multimodal capabilities for any model | 0.3.2 |
| [MoE Prompt Refiner](pipelines/moe-prompt-refiner.md) | Pipeline | Multi-model prompt refinement | 1.0.0 |
---
diff --git a/docs/plugins/index.zh.md b/docs/plugins/index.zh.md
index 56f2c1c..994ae81 100644
--- a/docs/plugins/index.zh.md
+++ b/docs/plugins/index.zh.md
@@ -48,15 +48,15 @@ OpenWebUI 支持四种类型的插件,每种都有不同的用途:
| 插件 | 类型 | 描述 | 版本 |
|--------|------|-------------|---------|
-| [Smart Mind Map(智能思维导图)](actions/smart-mind-map.md) | Action | 从文本生成交互式思维导图 | 0.8.0 |
-| [Smart Infographic(智能信息图)](actions/smart-infographic.md) | Action | 将文本转成专业信息图 | 1.0.0 |
-| [Knowledge Card(知识卡片)](actions/knowledge-card.md) | Action | 生成精美学习卡片 | 0.2.0 |
-| [Export to Excel(导出到 Excel)](actions/export-to-excel.md) | Action | 导出聊天记录为 Excel | 1.0.0 |
-| [Export to Word(导出为 Word)](actions/export-to-word.md) | Action | 将聊天内容导出为 Word (.docx) 并保留格式 | 0.1.0 |
-| [Async Context Compression(异步上下文压缩)](filters/async-context-compression.md) | Filter | 智能上下文压缩 | 1.0.0 |
-| [Context Enhancement(上下文增强)](filters/context-enhancement.md) | Filter | 提升对话上下文 | 1.0.0 |
-| [Gemini Manifold Companion](filters/gemini-manifold-companion.md) | Filter | Gemini Manifold 伴侣 | 1.0.0 |
-| [Gemini Manifold](pipes/gemini-manifold.md) | Pipe | Gemini 模型集成 | 1.0.0 |
+| [Smart Mind Map(智能思维导图)](actions/smart-mind-map.md) | Action | 从文本生成交互式思维导图 | 0.9.1 |
+| [Smart Infographic(智能信息图)](actions/smart-infographic.md) | Action | 将文本转成专业信息图 | 1.4.9 |
+| [Flash Card(闪记卡)](actions/flash-card.md) | Action | 生成精美学习卡片 | 0.2.4 |
+| [Export to Excel(导出到 Excel)](actions/export-to-excel.md) | Action | 导出聊天记录为 Excel | 0.3.7 |
+| [Export to Word(导出为 Word)](actions/export-to-word.md) | Action | 将聊天内容导出为 Word (.docx) 并保留格式 | 0.4.3 |
+| [Async Context Compression(异步上下文压缩)](filters/async-context-compression.md) | Filter | 智能上下文压缩 | 1.1.3 |
+| [Context Enhancement(上下文增强)](filters/context-enhancement.md) | Filter | 提升对话上下文 | 0.3.0 |
+| [Multi-Model Context Merger(多模型上下文合并)](filters/multi-model-context-merger.md) | Filter | 合并多个模型的上下文 | 0.1.0 |
+| [Web Gemini Multimodal Filter(Web Gemini 多模态过滤器)](filters/web-gemini-multimodel.md) | Filter | 为任何模型提供多模态能力 | 0.3.2 |
| [MoE Prompt Refiner](pipelines/moe-prompt-refiner.md) | Pipeline | 多模型提示词优化 | 1.0.0 |
---
diff --git a/docs/plugins/pipes/index.zh.md b/docs/plugins/pipes/index.zh.md
index ced6401..5d825df 100644
--- a/docs/plugins/pipes/index.zh.md
+++ b/docs/plugins/pipes/index.zh.md
@@ -15,19 +15,7 @@ Pipes 可以用于:
## 可用的 Pipe 插件
-
-- :material-google:{ .lg .middle } **Gemini Manifold**
-
- ---
-
- 面向 Google Gemini 的集成流水线,支持完整流式返回。
-
- **版本:** 1.0.0
-
- [:octicons-arrow-right-24: 查看文档](gemini-manifold.md)
-
-
---
diff --git a/mkdocs.yml b/mkdocs.yml
index 1732995..5b8c9be 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -97,14 +97,14 @@ plugins:
Documentation Guide: 文档编写指南
Smart Mind Map: 智能思维导图
Smart Infographic: 智能信息图
- Knowledge Card: 知识卡片
+ Flash Card: 闪记卡
Export to Excel: 导出到 Excel
Export to Word: 导出为 Word
Summary: 摘要
Async Context Compression: 异步上下文压缩
Context Enhancement: 上下文增强
- Gemini Manifold Companion: Gemini Manifold 伴侣
- Gemini Manifold: Gemini Manifold
+ Multi-Model Context Merger: 多模型上下文合并
+ Web Gemini Multimodal Filter: Web Gemini 多模态过滤器
MoE Prompt Refiner: MoE 提示词优化器
- minify:
minify_html: true
@@ -184,17 +184,17 @@ nav:
- plugins/actions/index.md
- Smart Mind Map: plugins/actions/smart-mind-map.md
- Smart Infographic: plugins/actions/smart-infographic.md
- - Knowledge Card: plugins/actions/knowledge-card.md
+ - Flash Card: plugins/actions/flash-card.md
- Export to Excel: plugins/actions/export-to-excel.md
- Export to Word: plugins/actions/export-to-word.md
- Filters:
- plugins/filters/index.md
- Async Context Compression: plugins/filters/async-context-compression.md
- Context Enhancement: plugins/filters/context-enhancement.md
- - Gemini Manifold Companion: plugins/filters/gemini-manifold-companion.md
+ - Multi-Model Context Merger: plugins/filters/multi-model-context-merger.md
+ - Web Gemini Multimodal Filter: plugins/filters/web-gemini-multimodel.md
- Pipes:
- plugins/pipes/index.md
- - Gemini Manifold: plugins/pipes/gemini-manifold.md
- Pipelines:
- plugins/pipelines/index.md
- MoE Prompt Refiner: plugins/pipelines/moe-prompt-refiner.md
diff --git a/plugins/actions/export_to_excel/export_to_excel.py b/plugins/actions/export_to_excel/export_to_excel.py
index d6e5e0f..3b3ff8b 100644
--- a/plugins/actions/export_to_excel/export_to_excel.py
+++ b/plugins/actions/export_to_excel/export_to_excel.py
@@ -1,8 +1,8 @@
"""
title: Export to Excel
author: Fu-Jie
-author_url: https://github.com/Fu-Jie
-funding_url: https://github.com/Fu-Jie/awesome-openwebui
+author_url: https://github.com/Fu-Jie/awesome-openwebui
+funding_url: https://github.com/open-webui
version: 0.3.7
openwebui_id: 244b8f9d-7459-47d6-84d3-c7ae8e3ec710
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNSAySDZhMiAyIDAgMCAwLTIgMnYxNmEyIDIgMCAwIDAgMiAyaDEyYTIgMiAwIDAgMCAyLTJWN1oiLz48cGF0aCBkPSJNMTQgMnY0YTIgMiAwIDAgMCAyIDJoNCIvPjxwYXRoIGQ9Ik04IDEzaDIiLz48cGF0aCBkPSJNMTQgMTNoMiIvPjxwYXRoIGQ9Ik04IDE3aDIiLz48cGF0aCBkPSJNMTQgMTdoMiIvPjwvc3ZnPg==
@@ -32,6 +32,10 @@ class Action:
default="chat_title",
description="Title Source: 'chat_title' (Chat Title), 'ai_generated' (AI Generated), 'markdown_title' (Markdown Title)",
)
+ SHOW_STATUS: bool = Field(
+ default=True,
+ description="Whether to show operation status updates.",
+ )
EXPORT_SCOPE: Literal["last_message", "all_messages"] = Field(
default="last_message",
description="Export Scope: 'last_message' (Last Message Only), 'all_messages' (All Messages)",
@@ -40,14 +44,57 @@ class Action:
default="",
description="Model ID for AI title generation. Leave empty to use the current chat model.",
)
+ SHOW_DEBUG_LOG: bool = Field(
+ default=False,
+ description="Whether to print debug logs in the browser console.",
+ )
def __init__(self):
self.valves = self.Valves()
- async def _send_notification(self, emitter: Callable, type: str, content: str):
- await emitter(
- {"type": "notification", "data": {"type": type, "content": content}}
- )
+ async def _emit_status(
+ self,
+ emitter: Optional[Callable[[Any], Awaitable[None]]],
+ description: str,
+ done: bool = False,
+ ):
+ """Emits a status update event."""
+ if self.valves.SHOW_STATUS and emitter:
+ await emitter(
+ {"type": "status", "data": {"description": description, "done": done}}
+ )
+
+ async def _emit_notification(
+ self,
+ emitter: Optional[Callable[[Any], Awaitable[None]]],
+ content: str,
+ ntype: str = "info",
+ ):
+ """Emits a notification event (info, success, warning, error)."""
+ if emitter:
+ await emitter(
+ {"type": "notification", "data": {"type": ntype, "content": content}}
+ )
+
+ async def _emit_debug_log(self, emitter, title: str, data: dict):
+ """Print structured debug logs in the browser console"""
+ if not self.valves.SHOW_DEBUG_LOG or not emitter:
+ return
+
+ try:
+ import json
+
+ js_code = f"""
+ (async function() {{
+ console.group("🛠️ {title}");
+ console.log({json.dumps(data, ensure_ascii=False)});
+ console.groupEnd();
+ }})();
+ """
+
+ await emitter({"type": "execute", "data": {"code": js_code}})
+ except Exception as e:
+ print(f"Error emitting debug log: {e}")
async def action(
self,
@@ -190,17 +237,18 @@ class Action:
# Notify user about the number of tables found
table_count = len(all_tables)
if self.valves.EXPORT_SCOPE == "all_messages":
- await self._send_notification(
+ await self._emit_notification(
__event_emitter__,
- "info",
f"Found {table_count} table(s) in all messages.",
+ "info",
)
# Wait a moment for user to see the notification before download dialog
await asyncio.sleep(1.5)
# Generate Workbook Title (Filename)
# Use the title of the chat, or the first header of the first message with tables
title = ""
- chat_id = self.extract_chat_id(body, None)
+ chat_ctx = self._get_chat_context(body, None)
+ chat_id = chat_ctx["chat_id"]
chat_title = ""
if chat_id:
chat_title = await self.fetch_chat_title(chat_id, user_id)
@@ -330,8 +378,8 @@ class Action:
},
}
)
- await self._send_notification(
- __event_emitter__, "error", "No tables found to export!"
+ await self._emit_notification(
+ __event_emitter__, "No tables found to export!", "error"
)
raise e
except Exception as e:
@@ -345,8 +393,8 @@ class Action:
},
}
)
- await self._send_notification(
- __event_emitter__, "error", "No tables found to export!"
+ await self._emit_notification(
+ __event_emitter__, "No tables found to export!", "error"
)
async def generate_title_using_ai(
@@ -389,20 +437,20 @@ class Action:
async def notification_task():
# Send initial notification immediately
if event_emitter:
- await self._send_notification(
+ await self._emit_notification(
event_emitter,
- "info",
"AI is generating a filename for your Excel file...",
+ "info",
)
# Subsequent notifications every 5 seconds
while True:
await asyncio.sleep(5)
if event_emitter:
- await self._send_notification(
+ await self._emit_notification(
event_emitter,
- "info",
"Still generating filename, please be patient...",
+ "info",
)
# Run tasks concurrently
@@ -432,10 +480,10 @@ class Action:
except Exception as e:
print(f"Error generating title: {e}")
if event_emitter:
- await self._send_notification(
+ await self._emit_notification(
event_emitter,
- "warning",
f"AI title generation failed, using default title. Error: {str(e)}",
+ "warning",
)
return ""
@@ -450,24 +498,56 @@ class Action:
return match.group(1).strip()
return ""
- 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") or body.get("id")
- if isinstance(chat_id, str) and chat_id.strip():
- return chat_id.strip()
+ def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
+ """Safely extracts user context information."""
+ if isinstance(__user__, (list, tuple)):
+ user_data = __user__[0] if __user__ else {}
+ elif isinstance(__user__, dict):
+ user_data = __user__
+ else:
+ user_data = {}
- for key in ("chat", "conversation"):
- nested = body.get(key)
- if isinstance(nested, dict):
- nested_id = nested.get("id") or nested.get("chat_id")
- if isinstance(nested_id, str) and nested_id.strip():
- return nested_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 ""
+ return {
+ "user_id": user_data.get("id", "unknown_user"),
+ "user_name": user_data.get("name", "User"),
+ "user_language": user_data.get("language", "en-US"),
+ }
+
+ def _get_chat_context(
+ self, body: dict, __metadata__: Optional[dict] = None
+ ) -> Dict[str, str]:
+ """
+ Unified extraction of chat context information (chat_id, message_id).
+ Prioritizes extraction from body, then metadata.
+ """
+ chat_id = ""
+ message_id = ""
+
+ # 1. Try to get from body
+ if isinstance(body, dict):
+ chat_id = body.get("chat_id", "")
+ message_id = body.get("id", "") # message_id is usually 'id' in body
+
+ # Check body.metadata as fallback
+ if not chat_id or not message_id:
+ body_metadata = body.get("metadata", {})
+ if isinstance(body_metadata, dict):
+ if not chat_id:
+ chat_id = body_metadata.get("chat_id", "")
+ if not message_id:
+ message_id = body_metadata.get("message_id", "")
+
+ # 2. Try to get from __metadata__ (as supplement)
+ if __metadata__ and isinstance(__metadata__, dict):
+ if not chat_id:
+ chat_id = __metadata__.get("chat_id", "")
+ if not message_id:
+ message_id = __metadata__.get("message_id", "")
+
+ return {
+ "chat_id": str(chat_id).strip(),
+ "message_id": str(message_id).strip(),
+ }
async def fetch_chat_title(self, chat_id: str, user_id: str = "") -> str:
"""Fetch chat title from database by chat_id"""
diff --git a/plugins/actions/export_to_excel/export_to_excel_cn.py b/plugins/actions/export_to_excel/export_to_excel_cn.py
index 8c73e0c..3404504 100644
--- a/plugins/actions/export_to_excel/export_to_excel_cn.py
+++ b/plugins/actions/export_to_excel/export_to_excel_cn.py
@@ -1,8 +1,8 @@
"""
title: 导出为 Excel
author: Fu-Jie
-author_url: https://github.com/Fu-Jie
-funding_url: https://github.com/Fu-Jie/awesome-openwebui
+author_url: https://github.com/Fu-Jie/awesome-openwebui
+funding_url: https://github.com/open-webui
version: 0.3.7
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNSAySDZhMiAyIDAgMCAwLTIgMnYxNmEyIDIgMCAwIDAgMiAyaDEyYTIgMiAwIDAgMCAyLTJWN1oiLz48cGF0aCBkPSJNMTQgMnY0YTIgMiAwIDAgMCAyIDJoNCIvPjxwYXRoIGQ9Ik04IDEzaDIiLz48cGF0aCBkPSJNMTQgMTNoMiIvPjxwYXRoIGQ9Ik04IDE3aDIiLz48cGF0aCBkPSJNMTQgMTdoMiIvPjwvc3ZnPg==
description: 从聊天消息中提取表格并导出为 Excel (.xlsx) 文件,支持智能格式化。
@@ -31,6 +31,10 @@ class Action:
default="chat_title",
description="标题来源: 'chat_title' (对话标题), 'ai_generated' (AI生成), 'markdown_title' (Markdown标题)",
)
+ SHOW_STATUS: bool = Field(
+ default=True,
+ description="是否显示操作状态更新。",
+ )
EXPORT_SCOPE: Literal["last_message", "all_messages"] = Field(
default="last_message",
description="导出范围: 'last_message' (仅最后一条消息), 'all_messages' (所有消息)",
@@ -39,14 +43,57 @@ class Action:
default="",
description="AI 标题生成模型 ID。留空则使用当前对话模型。",
)
+ SHOW_DEBUG_LOG: bool = Field(
+ default=False,
+ description="是否在浏览器控制台打印调试日志。",
+ )
def __init__(self):
self.valves = self.Valves()
- async def _send_notification(self, emitter: Callable, type: str, content: str):
- await emitter(
- {"type": "notification", "data": {"type": type, "content": content}}
- )
+ async def _emit_status(
+ self,
+ emitter: Optional[Callable[[Any], Awaitable[None]]],
+ description: str,
+ done: bool = False,
+ ):
+ """Emits a status update event."""
+ if self.valves.SHOW_STATUS and emitter:
+ await emitter(
+ {"type": "status", "data": {"description": description, "done": done}}
+ )
+
+ async def _emit_notification(
+ self,
+ emitter: Optional[Callable[[Any], Awaitable[None]]],
+ content: str,
+ ntype: str = "info",
+ ):
+ """Emits a notification event (info, success, warning, error)."""
+ if emitter:
+ await emitter(
+ {"type": "notification", "data": {"type": ntype, "content": content}}
+ )
+
+ async def _emit_debug_log(self, emitter, title: str, data: dict):
+ """在浏览器控制台打印结构化调试日志"""
+ if not self.valves.SHOW_DEBUG_LOG or not emitter:
+ return
+
+ try:
+ import json
+
+ js_code = f"""
+ (async function() {{
+ console.group("🛠️ {title}");
+ console.log({json.dumps(data, ensure_ascii=False)});
+ console.groupEnd();
+ }})();
+ """
+
+ await emitter({"type": "execute", "data": {"code": js_code}})
+ except Exception as e:
+ print(f"Error emitting debug log: {e}")
async def action(
self,
@@ -180,17 +227,18 @@ class Action:
# 通知用户提取到的表格数量
table_count = len(all_tables)
if self.valves.EXPORT_SCOPE == "all_messages":
- await self._send_notification(
+ await self._emit_notification(
__event_emitter__,
- "info",
f"从所有消息中提取到 {table_count} 个表格。",
+ "info",
)
# 等待片刻让用户看到通知,再触发下载
await asyncio.sleep(1.5)
# Generate Workbook Title (Filename)
title = ""
- chat_id = self.extract_chat_id(body, None)
+ chat_ctx = self._get_chat_context(body, None)
+ chat_id = chat_ctx["chat_id"]
chat_title = ""
if chat_id:
chat_title = await self.fetch_chat_title(chat_id, user_id)
@@ -318,8 +366,8 @@ class Action:
},
}
)
- await self._send_notification(
- __event_emitter__, "error", "未找到可导出的表格!"
+ await self._emit_notification(
+ __event_emitter__, "未找到可导出的表格!", "error"
)
raise e
except Exception as e:
@@ -333,8 +381,8 @@ class Action:
},
}
)
- await self._send_notification(
- __event_emitter__, "error", "未找到可导出的表格!"
+ await self._emit_notification(
+ __event_emitter__, "未找到可导出的表格!", "error"
)
async def generate_title_using_ai(
@@ -377,20 +425,20 @@ class Action:
async def notification_task():
# 立即发送首次通知
if event_emitter:
- await self._send_notification(
+ await self._emit_notification(
event_emitter,
- "info",
"AI 正在为您生成文件名,请稍候...",
+ "info",
)
# 之后每5秒通知一次
while True:
await asyncio.sleep(5)
if event_emitter:
- await self._send_notification(
+ await self._emit_notification(
event_emitter,
- "info",
"文件名生成中,请耐心等待...",
+ "info",
)
# 并发运行任务
@@ -420,10 +468,10 @@ class Action:
except Exception as e:
print(f"生成标题时出错: {e}")
if event_emitter:
- await self._send_notification(
+ await self._emit_notification(
event_emitter,
- "warning",
f"AI 文件名生成失败,将使用默认名称。错误: {str(e)}",
+ "warning",
)
return ""
@@ -438,24 +486,56 @@ class Action:
return match.group(1).strip()
return ""
- 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") or body.get("id")
- if isinstance(chat_id, str) and chat_id.strip():
- return chat_id.strip()
+ def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
+ """安全提取用户上下文信息。"""
+ if isinstance(__user__, (list, tuple)):
+ user_data = __user__[0] if __user__ else {}
+ elif isinstance(__user__, dict):
+ user_data = __user__
+ else:
+ user_data = {}
- for key in ("chat", "conversation"):
- nested = body.get(key)
- if isinstance(nested, dict):
- nested_id = nested.get("id") or nested.get("chat_id")
- if isinstance(nested_id, str) and nested_id.strip():
- return nested_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 ""
+ return {
+ "user_id": user_data.get("id", "unknown_user"),
+ "user_name": user_data.get("name", "用户"),
+ "user_language": user_data.get("language", "zh-CN"),
+ }
+
+ def _get_chat_context(
+ self, body: dict, __metadata__: Optional[dict] = None
+ ) -> Dict[str, str]:
+ """
+ 统一提取聊天上下文信息 (chat_id, message_id)。
+ 优先从 body 中提取,其次从 metadata 中提取。
+ """
+ chat_id = ""
+ message_id = ""
+
+ # 1. 尝试从 body 获取
+ if isinstance(body, dict):
+ chat_id = body.get("chat_id", "")
+ message_id = body.get("id", "") # message_id 在 body 中通常是 id
+
+ # 再次检查 body.metadata
+ if not chat_id or not message_id:
+ body_metadata = body.get("metadata", {})
+ if isinstance(body_metadata, dict):
+ if not chat_id:
+ chat_id = body_metadata.get("chat_id", "")
+ if not message_id:
+ message_id = body_metadata.get("message_id", "")
+
+ # 2. 尝试从 __metadata__ 获取 (作为补充)
+ if __metadata__ and isinstance(__metadata__, dict):
+ if not chat_id:
+ chat_id = __metadata__.get("chat_id", "")
+ if not message_id:
+ message_id = __metadata__.get("message_id", "")
+
+ return {
+ "chat_id": str(chat_id).strip(),
+ "message_id": str(message_id).strip(),
+ }
async def fetch_chat_title(self, chat_id: str, user_id: str = "") -> str:
"""通过 chat_id 从数据库获取对话标题"""