From 70a96d07543c97295c20471a89b2fe40e4c31177 Mon Sep 17 00:00:00 2001 From: fujie Date: Wed, 14 Jan 2026 23:46:56 +0800 Subject: [PATCH] fix: resolve mkdocs build warnings and broken links --- docs/development/plugin-guide.md | 7 +- docs/development/plugin-guide.zh.md | 39 +++-- docs/index.md | 6 +- docs/index.zh.md | 4 +- docs/plugins/actions/index.zh.md | 16 +- docs/plugins/filters/index.zh.md | 8 - docs/plugins/index.md | 18 +-- docs/plugins/index.zh.md | 18 +-- docs/plugins/pipes/index.zh.md | 12 -- mkdocs.yml | 12 +- .../export_to_excel/export_to_excel.py | 152 +++++++++++++----- .../export_to_excel/export_to_excel_cn.py | 152 +++++++++++++----- 12 files changed, 287 insertions(+), 157 deletions(-) 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 从数据库获取对话标题"""