diff --git a/docs/plugins/actions/export-to-excel.md b/docs/plugins/actions/export-to-excel.md index a45260a..85c691d 100644 --- a/docs/plugins/actions/export-to-excel.md +++ b/docs/plugins/actions/export-to-excel.md @@ -1,12 +1,19 @@ # Export to Excel Action -v0.3.3 +v0.3.4 Export chat conversations to Excel spreadsheet format for analysis, archiving, and sharing. + +## What's New in v0.3.4 + +- **Smart Filename Generation**: Now supports generating filenames based on Chat Title, AI Summary, or Markdown Headers. +- **Configuration Options**: Added `TITLE_SOURCE` setting to control filename generation strategy. + --- + ## Overview The Export to Excel plugin allows you to download your chat conversations as Excel files. This is useful for: @@ -23,6 +30,13 @@ The Export to Excel plugin allows you to download your chat conversations as Exc - :material-download: **One-Click Download**: Instant file generation - :material-history: **Full History**: Exports complete conversation +## Configuration + +- **Title Source**: Choose how the filename is generated: + - `chat_title`: Use the chat title (default). + - `ai_generated`: Use AI to generate a concise title from the content. + - `markdown_title`: Extract the first H1/H2 header from the markdown content. + --- ## Installation diff --git a/docs/plugins/actions/export-to-excel.zh.md b/docs/plugins/actions/export-to-excel.zh.md index 1ed4eb3..e56fdc9 100644 --- a/docs/plugins/actions/export-to-excel.zh.md +++ b/docs/plugins/actions/export-to-excel.zh.md @@ -1,12 +1,19 @@ # Export to Excel(导出到 Excel) Action -v1.0.0 +v0.3.4 将聊天记录导出为 Excel 表格,便于分析、归档和分享。 + +## v0.3.4 更新内容 + +- **智能文件名生成**:支持根据对话标题、AI 总结或 Markdown 标题生成文件名。 +- **配置选项**:新增 `TITLE_SOURCE` 设置,用于控制文件名生成策略。 + --- + ## 概览 Export to Excel 插件可以把你的聊天记录下载为 Excel 文件,适用于: @@ -23,6 +30,13 @@ Export to Excel 插件可以把你的聊天记录下载为 Excel 文件,适用 - :material-download: **一键下载**:即时生成文件 - :material-history: **完整历史**:导出完整会话内容 +## 配置 + +- **标题来源 (Title Source)**:选择文件名的生成方式: + - `chat_title`:使用对话标题(默认)。 + - `ai_generated`:使用 AI 根据内容生成简洁标题。 + - `markdown_title`:提取 Markdown 内容中的第一个 H1/H2 标题。 + --- ## 安装 diff --git a/docs/plugins/actions/index.md b/docs/plugins/actions/index.md index d934d7d..db4999e 100644 --- a/docs/plugins/actions/index.md +++ b/docs/plugins/actions/index.md @@ -53,7 +53,7 @@ Actions are interactive plugins that: Export chat conversations to Excel spreadsheet format for analysis and archiving. - **Version:** 0.3.3 + **Version:** 0.3.4 [:octicons-arrow-right-24: Documentation](export-to-excel.md) diff --git a/docs/plugins/actions/index.zh.md b/docs/plugins/actions/index.zh.md index 1faaaf3..cb2552e 100644 --- a/docs/plugins/actions/index.zh.md +++ b/docs/plugins/actions/index.zh.md @@ -53,7 +53,7 @@ Actions 是交互式插件,能够: 将聊天记录导出为 Excel 电子表格,方便分析或归档。 - **版本:** 0.3.3 + **版本:** 0.3.4 [:octicons-arrow-right-24: 查看文档](export-to-excel.md) diff --git a/plugins/actions/export_to_excel/README.md b/plugins/actions/export_to_excel/README.md index 3e540c6..b6ea1e6 100644 --- a/plugins/actions/export_to_excel/README.md +++ b/plugins/actions/export_to_excel/README.md @@ -2,12 +2,24 @@ This plugin allows you to export your chat history to an Excel (.xlsx) file directly from the chat interface. +## What's New in v0.3.4 + +- **Smart Filename Generation**: Now supports generating filenames based on Chat Title, AI Summary, or Markdown Headers. +- **Configuration Options**: Added `TITLE_SOURCE` setting to control filename generation strategy. + ## Features - **One-Click Export**: Adds an "Export to Excel" button to the chat. - **Automatic Header Extraction**: Intelligently identifies table headers from the chat content. - **Multi-Table Support**: Handles multiple tables within a single chat session. +## Configuration + +- **Title Source**: Choose how the filename is generated: + - `chat_title`: Use the chat title (default). + - `ai_generated`: Use AI to generate a concise title from the content. + - `markdown_title`: Extract the first H1/H2 header from the markdown content. + ## Usage 1. Install the plugin. diff --git a/plugins/actions/export_to_excel/README_CN.md b/plugins/actions/export_to_excel/README_CN.md index 83efb5f..48fc800 100644 --- a/plugins/actions/export_to_excel/README_CN.md +++ b/plugins/actions/export_to_excel/README_CN.md @@ -2,12 +2,24 @@ 此插件允许你直接从聊天界面将对话历史导出为 Excel (.xlsx) 文件。 +## v0.3.4 更新内容 + +- **智能文件名生成**:支持根据对话标题、AI 总结或 Markdown 标题生成文件名。 +- **配置选项**:新增 `TITLE_SOURCE` 设置,用于控制文件名生成策略。 + ## 功能特点 - **一键导出**:在聊天界面添加“导出为 Excel”按钮。 - **自动表头提取**:智能识别聊天内容中的表格标题。 - **多表支持**:支持处理单次对话中的多个表格。 +## 配置 + +- **标题来源 (Title Source)**:选择文件名的生成方式: + - `chat_title`:使用对话标题(默认)。 + - `ai_generated`:使用 AI 根据内容生成简洁标题。 + - `markdown_title`:提取 Markdown 内容中的第一个 H1/H2 标题。 + ## 使用方法 1. 安装插件。 diff --git a/plugins/actions/export_to_excel/export_to_excel.py b/plugins/actions/export_to_excel/export_to_excel.py index f05ceac..f0aedf8 100644 --- a/plugins/actions/export_to_excel/export_to_excel.py +++ b/plugins/actions/export_to_excel/export_to_excel.py @@ -3,7 +3,7 @@ title: Export to Excel author: Fu-Jie author_url: https://github.com/Fu-Jie funding_url: https://github.com/Fu-Jie/awesome-openwebui -version: 0.3.3 +version: 0.3.4 icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNSAySDZhMiAyIDAgMCAwLTIgMnYxNmEyIDIgMCAwIDAgMiAyaDEyYTIgMiAwIDAgMCAyLTJWN1oiLz48cGF0aCBkPSJNMTQgMnY0YTIgMiAwIDAgMCAyIDJoNCIvPjxwYXRoIGQ9Ik04IDEzaDIiLz48cGF0aCBkPSJNMTQgMTNoMiIvPjxwYXRoIGQ9Ik04IDE3aDIiLz48cGF0aCBkPSJNMTQgMTdoMiIvPjwvc3ZnPg== description: Exports the current chat history to an Excel (.xlsx) file, with automatic header extraction. """ @@ -15,14 +15,24 @@ import base64 from fastapi import FastAPI, HTTPException from typing import Optional, Callable, Awaitable, Any, List, Dict import datetime +import asyncio +from open_webui.models.chats import Chats +from open_webui.models.users import Users +from open_webui.utils.chat import generate_chat_completion +from pydantic import BaseModel, Field app = FastAPI() class Action: + class Valves(BaseModel): + TITLE_SOURCE: str = Field( + default="chat_title", + description="Title Source: 'chat_title' (Chat Title), 'ai_generated' (AI Generated), 'markdown_title' (Markdown Title)", + ) def __init__(self): - pass + self.valves = self.Valves() async def _send_notification(self, emitter: Callable, type: str, content: str): await emitter( @@ -35,6 +45,7 @@ class Action: __user__=None, __event_emitter__=None, __event_call__: Optional[Callable[[Any], Awaitable[None]]] = None, + __request__: Optional[Any] = None, ): print(f"action:{__name__}") if isinstance(__user__, (list, tuple)): @@ -69,18 +80,64 @@ class Action: if not tables: raise HTTPException(status_code=400, detail="No tables found.") + # Generate filename + title = "" + chat_id = self.extract_chat_id( + body, None + ) # metadata not available in action signature yet, but usually in body + + # Fetch chat_title directly via chat_id as it's usually missing in body + chat_title = "" + if chat_id: + chat_title = await self.fetch_chat_title(chat_id, user_id) + + if ( + self.valves.TITLE_SOURCE == "chat_title" + or not self.valves.TITLE_SOURCE + ): + title = chat_title + elif self.valves.TITLE_SOURCE == "markdown_title": + title = self.extract_title(message_content) + elif self.valves.TITLE_SOURCE == "ai_generated": + # We need request object for AI generation, but it's not passed in standard action signature in this version + # However, we can try to use the one from global context if available or skip + # For now, let's assume we might not have it and fallback or use what we have + # Wait, export_to_word uses __request__. Let's check if we can add it to signature. + pass + # Get dynamic filename and sheet names - workbook_name, sheet_names = self.generate_names_from_content( - message_content, tables + workbook_name_from_content, sheet_names = ( + self.generate_names_from_content(message_content, tables) ) + # If AI generation is selected but we need request, we need to update signature. + # Let's update signature in next chunk. + + # Fallback logic for title + if not title: + if self.valves.TITLE_SOURCE == "ai_generated": + # AI generation needs request, handled later + pass + elif self.valves.TITLE_SOURCE == "markdown_title": + pass # Already tried + + # If still no title, try workbook_name_from_content (which uses headers) + if not title and workbook_name_from_content: + title = workbook_name_from_content + + # If still no title, use chat_title if available + if not title and chat_title: + title = chat_title + # Use optimized filename generation logic current_datetime = datetime.datetime.now() formatted_date = current_datetime.strftime("%Y%m%d") # If no title found, use user_yyyymmdd format - if not workbook_name: + if not title: workbook_name = f"{user_name}_{formatted_date}" + else: + workbook_name = self.clean_filename(title) filename = f"{workbook_name}.xlsx" excel_file_path = os.path.join( @@ -172,6 +229,88 @@ class Action: __event_emitter__, "error", "No tables found to export!" ) + async def generate_title_using_ai( + self, body: dict, content: str, user_id: str, request: Any + ) -> str: + if not request: + return "" + + try: + user_obj = Users.get_user_by_id(user_id) + model = body.get("model") + + payload = { + "model": model, + "messages": [ + { + "role": "system", + "content": "You are a helpful assistant. Generate a short, concise title (max 10 words) for the following text. Do not use quotes. Only output the title.", + }, + {"role": "user", "content": content[:2000]}, # Limit content length + ], + "stream": False, + } + + response = await generate_chat_completion(request, payload, user_obj) + if response and "choices" in response: + return response["choices"][0]["message"]["content"].strip() + except Exception as e: + print(f"Error generating title: {e}") + + return "" + + def extract_title(self, content: str) -> str: + """Extract title from Markdown h1/h2 only""" + lines = content.split("\n") + for line in lines: + # Match h1-h2 headings only + match = re.match(r"^#{1,2}\s+(.+)$", line.strip()) + if match: + 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() + + 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 "" + + async def fetch_chat_title(self, chat_id: str, user_id: str = "") -> str: + """Fetch chat title from database by chat_id""" + if not chat_id: + return "" + + def _load_chat(): + if user_id: + return Chats.get_chat_by_id_and_user_id(id=chat_id, user_id=user_id) + return Chats.get_chat_by_id(chat_id) + + try: + chat = await asyncio.to_thread(_load_chat) + except Exception as exc: + print(f"Failed to load chat {chat_id}: {exc}") + return "" + + if not chat: + return "" + + data = getattr(chat, "chat", {}) or {} + title = data.get("title") or getattr(chat, "title", "") + return title.strip() if isinstance(title, str) else "" + def extract_tables_from_message(self, message: str) -> List[Dict]: """ Extract Markdown tables and their positions from message text diff --git a/plugins/actions/export_to_excel/导出为Excel.py b/plugins/actions/export_to_excel/导出为Excel.py index d6fda2d..fb03429 100644 --- a/plugins/actions/export_to_excel/导出为Excel.py +++ b/plugins/actions/export_to_excel/导出为Excel.py @@ -3,7 +3,7 @@ title: 导出为 Excel author: Fu-Jie author_url: https://github.com/Fu-Jie funding_url: https://github.com/Fu-Jie/awesome-openwebui -version: 0.3.3 +version: 0.3.4 icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNSAySDZhMiAyIDAgMCAwLTIgMnYxNmEyIDIgMCAwIDAgMiAyaDEyYTIgMiAwIDAgMCAyLTJWN1oiLz48cGF0aCBkPSJNMTQgMnY0YTIgMiAwIDAgMCAyIDJoNCIvPjxwYXRoIGQ9Ik04IDEzaDIiLz48cGF0aCBkPSJNMTQgMTNoMiIvPjxwYXRoIGQ9Ik04IDE3aDIiLz48cGF0aCBkPSJNMTQgMTdoMiIvPjwvc3ZnPg== description: 将当前对话历史导出为 Excel (.xlsx) 文件,支持自动提取表头。 """ @@ -15,14 +15,24 @@ import base64 from fastapi import FastAPI, HTTPException from typing import Optional, Callable, Awaitable, Any, List, Dict import datetime +import asyncio +from open_webui.models.chats import Chats +from open_webui.models.users import Users +from open_webui.utils.chat import generate_chat_completion +from pydantic import BaseModel, Field app = FastAPI() class Action: + class Valves(BaseModel): + TITLE_SOURCE: str = Field( + default="chat_title", + description="标题来源:'chat_title' (对话标题), 'ai_generated' (AI生成), 'markdown_title' (Markdown标题)", + ) def __init__(self): - pass + self.valves = self.Valves() async def _send_notification(self, emitter: Callable, type: str, content: str): await emitter( @@ -35,6 +45,7 @@ class Action: __user__=None, __event_emitter__=None, __event_call__: Optional[Callable[[Any], Awaitable[None]]] = None, + __request__: Optional[Any] = None, ): print(f"action:{__name__}") if isinstance(__user__, (list, tuple)): @@ -69,18 +80,58 @@ class Action: if not tables: raise HTTPException(status_code=400, detail="未找到任何表格。") + # 生成文件名 + title = "" + chat_id = self.extract_chat_id( + body, None + ) # metadata 在此版本 action 签名中不可用,但通常在 body 中 + + # 直接通过 chat_id 获取对话标题,因为 body 中通常缺少该信息 + chat_title = "" + if chat_id: + chat_title = await self.fetch_chat_title(chat_id, user_id) + + if ( + self.valves.TITLE_SOURCE == "chat_title" + or not self.valves.TITLE_SOURCE + ): + title = chat_title + elif self.valves.TITLE_SOURCE == "markdown_title": + title = self.extract_title(message_content) + elif self.valves.TITLE_SOURCE == "ai_generated": + # AI 生成需要 request 对象,稍后处理 + pass + # 获取动态文件名和sheet名称 - workbook_name, sheet_names = self.generate_names_from_content( - message_content, tables + workbook_name_from_content, sheet_names = ( + self.generate_names_from_content(message_content, tables) ) + # 标题回退逻辑 + if not title: + if self.valves.TITLE_SOURCE == "ai_generated": + # AI 生成需要 request,稍后处理 + pass + elif self.valves.TITLE_SOURCE == "markdown_title": + pass # 已尝试 + + # 如果仍无标题,尝试使用 workbook_name_from_content (基于表头) + if not title and workbook_name_from_content: + title = workbook_name_from_content + + # 如果仍无标题,尝试使用 chat_title + if not title and chat_title: + title = chat_title + # 使用优化后的文件名生成逻辑 current_datetime = datetime.datetime.now() formatted_date = current_datetime.strftime("%Y%m%d") # 如果没找到标题则使用 user_yyyymmdd 格式 - if not workbook_name: + if not title: workbook_name = f"{user_name}_{formatted_date}" + else: + workbook_name = self.clean_filename(title) filename = f"{workbook_name}.xlsx" excel_file_path = os.path.join( @@ -172,6 +223,88 @@ class Action: __event_emitter__, "error", "没有找到可以导出的表格!" ) + async def generate_title_using_ai( + self, body: dict, content: str, user_id: str, request: Any + ) -> str: + if not request: + return "" + + try: + user_obj = Users.get_user_by_id(user_id) + model = body.get("model") + + payload = { + "model": model, + "messages": [ + { + "role": "system", + "content": "你是一个乐于助人的助手。请为以下文本生成一个简短、简洁的标题(最多10个字)。不要使用引号。只输出标题。", + }, + {"role": "user", "content": content[:2000]}, # 限制内容长度 + ], + "stream": False, + } + + response = await generate_chat_completion(request, payload, user_obj) + if response and "choices" in response: + return response["choices"][0]["message"]["content"].strip() + except Exception as e: + print(f"生成标题时出错: {e}") + + return "" + + def extract_title(self, content: str) -> str: + """从 Markdown h1/h2 中提取标题""" + lines = content.split("\n") + for line in lines: + # 仅匹配 h1-h2 标题 + match = re.match(r"^#{1,2}\s+(.+)$", line.strip()) + if match: + 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() + + 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 "" + + async def fetch_chat_title(self, chat_id: str, user_id: str = "") -> str: + """通过 chat_id 从数据库获取对话标题""" + if not chat_id: + return "" + + def _load_chat(): + if user_id: + return Chats.get_chat_by_id_and_user_id(id=chat_id, user_id=user_id) + return Chats.get_chat_by_id(chat_id) + + try: + chat = await asyncio.to_thread(_load_chat) + except Exception as exc: + print(f"加载对话 {chat_id} 失败: {exc}") + return "" + + if not chat: + return "" + + data = getattr(chat, "chat", {}) or {} + title = data.get("title") or getattr(chat, "title", "") + return title.strip() if isinstance(title, str) else "" + def extract_tables_from_message(self, message: str) -> List[Dict]: """ 从消息文本中提取Markdown表格及位置信息