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表格及位置信息