feat: optimize export to excel filename generation and update docs
This commit is contained in:
@@ -1,12 +1,19 @@
|
|||||||
# Export to Excel
|
# Export to Excel
|
||||||
|
|
||||||
<span class="category-badge action">Action</span>
|
<span class="category-badge action">Action</span>
|
||||||
<span class="version-badge">v0.3.3</span>
|
<span class="version-badge">v0.3.4</span>
|
||||||
|
|
||||||
Export chat conversations to Excel spreadsheet format for analysis, archiving, and sharing.
|
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
|
## Overview
|
||||||
|
|
||||||
The Export to Excel plugin allows you to download your chat conversations as Excel files. This is useful for:
|
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-download: **One-Click Download**: Instant file generation
|
||||||
- :material-history: **Full History**: Exports complete conversation
|
- :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
|
## Installation
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
# Export to Excel(导出到 Excel)
|
# Export to Excel(导出到 Excel)
|
||||||
|
|
||||||
<span class="category-badge action">Action</span>
|
<span class="category-badge action">Action</span>
|
||||||
<span class="version-badge">v1.0.0</span>
|
<span class="version-badge">v0.3.4</span>
|
||||||
|
|
||||||
将聊天记录导出为 Excel 表格,便于分析、归档和分享。
|
将聊天记录导出为 Excel 表格,便于分析、归档和分享。
|
||||||
|
|
||||||
|
|
||||||
|
## v0.3.4 更新内容
|
||||||
|
|
||||||
|
- **智能文件名生成**:支持根据对话标题、AI 总结或 Markdown 标题生成文件名。
|
||||||
|
- **配置选项**:新增 `TITLE_SOURCE` 设置,用于控制文件名生成策略。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
## 概览
|
## 概览
|
||||||
|
|
||||||
Export to Excel 插件可以把你的聊天记录下载为 Excel 文件,适用于:
|
Export to Excel 插件可以把你的聊天记录下载为 Excel 文件,适用于:
|
||||||
@@ -23,6 +30,13 @@ Export to Excel 插件可以把你的聊天记录下载为 Excel 文件,适用
|
|||||||
- :material-download: **一键下载**:即时生成文件
|
- :material-download: **一键下载**:即时生成文件
|
||||||
- :material-history: **完整历史**:导出完整会话内容
|
- :material-history: **完整历史**:导出完整会话内容
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
- **标题来源 (Title Source)**:选择文件名的生成方式:
|
||||||
|
- `chat_title`:使用对话标题(默认)。
|
||||||
|
- `ai_generated`:使用 AI 根据内容生成简洁标题。
|
||||||
|
- `markdown_title`:提取 Markdown 内容中的第一个 H1/H2 标题。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ Actions are interactive plugins that:
|
|||||||
|
|
||||||
Export chat conversations to Excel spreadsheet format for analysis and archiving.
|
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)
|
[:octicons-arrow-right-24: Documentation](export-to-excel.md)
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ Actions 是交互式插件,能够:
|
|||||||
|
|
||||||
将聊天记录导出为 Excel 电子表格,方便分析或归档。
|
将聊天记录导出为 Excel 电子表格,方便分析或归档。
|
||||||
|
|
||||||
**版本:** 0.3.3
|
**版本:** 0.3.4
|
||||||
|
|
||||||
[:octicons-arrow-right-24: 查看文档](export-to-excel.md)
|
[:octicons-arrow-right-24: 查看文档](export-to-excel.md)
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,24 @@
|
|||||||
|
|
||||||
This plugin allows you to export your chat history to an Excel (.xlsx) file directly from the chat interface.
|
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
|
## Features
|
||||||
|
|
||||||
- **One-Click Export**: Adds an "Export to Excel" button to the chat.
|
- **One-Click Export**: Adds an "Export to Excel" button to the chat.
|
||||||
- **Automatic Header Extraction**: Intelligently identifies table headers from the chat content.
|
- **Automatic Header Extraction**: Intelligently identifies table headers from the chat content.
|
||||||
- **Multi-Table Support**: Handles multiple tables within a single chat session.
|
- **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
|
## Usage
|
||||||
|
|
||||||
1. Install the plugin.
|
1. Install the plugin.
|
||||||
|
|||||||
@@ -2,12 +2,24 @@
|
|||||||
|
|
||||||
此插件允许你直接从聊天界面将对话历史导出为 Excel (.xlsx) 文件。
|
此插件允许你直接从聊天界面将对话历史导出为 Excel (.xlsx) 文件。
|
||||||
|
|
||||||
|
## v0.3.4 更新内容
|
||||||
|
|
||||||
|
- **智能文件名生成**:支持根据对话标题、AI 总结或 Markdown 标题生成文件名。
|
||||||
|
- **配置选项**:新增 `TITLE_SOURCE` 设置,用于控制文件名生成策略。
|
||||||
|
|
||||||
## 功能特点
|
## 功能特点
|
||||||
|
|
||||||
- **一键导出**:在聊天界面添加“导出为 Excel”按钮。
|
- **一键导出**:在聊天界面添加“导出为 Excel”按钮。
|
||||||
- **自动表头提取**:智能识别聊天内容中的表格标题。
|
- **自动表头提取**:智能识别聊天内容中的表格标题。
|
||||||
- **多表支持**:支持处理单次对话中的多个表格。
|
- **多表支持**:支持处理单次对话中的多个表格。
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
- **标题来源 (Title Source)**:选择文件名的生成方式:
|
||||||
|
- `chat_title`:使用对话标题(默认)。
|
||||||
|
- `ai_generated`:使用 AI 根据内容生成简洁标题。
|
||||||
|
- `markdown_title`:提取 Markdown 内容中的第一个 H1/H2 标题。
|
||||||
|
|
||||||
## 使用方法
|
## 使用方法
|
||||||
|
|
||||||
1. 安装插件。
|
1. 安装插件。
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Export to Excel
|
|||||||
author: Fu-Jie
|
author: Fu-Jie
|
||||||
author_url: https://github.com/Fu-Jie
|
author_url: https://github.com/Fu-Jie
|
||||||
funding_url: https://github.com/Fu-Jie/awesome-openwebui
|
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==
|
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNSAySDZhMiAyIDAgMCAwLTIgMnYxNmEyIDIgMCAwIDAgMiAyaDEyYTIgMiAwIDAgMCAyLTJWN1oiLz48cGF0aCBkPSJNMTQgMnY0YTIgMiAwIDAgMCAyIDJoNCIvPjxwYXRoIGQ9Ik04IDEzaDIiLz48cGF0aCBkPSJNMTQgMTNoMiIvPjxwYXRoIGQ9Ik04IDE3aDIiLz48cGF0aCBkPSJNMTQgMTdoMiIvPjwvc3ZnPg==
|
||||||
description: Exports the current chat history to an Excel (.xlsx) file, with automatic header extraction.
|
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 fastapi import FastAPI, HTTPException
|
||||||
from typing import Optional, Callable, Awaitable, Any, List, Dict
|
from typing import Optional, Callable, Awaitable, Any, List, Dict
|
||||||
import datetime
|
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()
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
class Action:
|
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):
|
def __init__(self):
|
||||||
pass
|
self.valves = self.Valves()
|
||||||
|
|
||||||
async def _send_notification(self, emitter: Callable, type: str, content: str):
|
async def _send_notification(self, emitter: Callable, type: str, content: str):
|
||||||
await emitter(
|
await emitter(
|
||||||
@@ -35,6 +45,7 @@ class Action:
|
|||||||
__user__=None,
|
__user__=None,
|
||||||
__event_emitter__=None,
|
__event_emitter__=None,
|
||||||
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
|
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
|
||||||
|
__request__: Optional[Any] = None,
|
||||||
):
|
):
|
||||||
print(f"action:{__name__}")
|
print(f"action:{__name__}")
|
||||||
if isinstance(__user__, (list, tuple)):
|
if isinstance(__user__, (list, tuple)):
|
||||||
@@ -69,18 +80,64 @@ class Action:
|
|||||||
if not tables:
|
if not tables:
|
||||||
raise HTTPException(status_code=400, detail="No tables found.")
|
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
|
# Get dynamic filename and sheet names
|
||||||
workbook_name, sheet_names = self.generate_names_from_content(
|
workbook_name_from_content, sheet_names = (
|
||||||
message_content, tables
|
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
|
# Use optimized filename generation logic
|
||||||
current_datetime = datetime.datetime.now()
|
current_datetime = datetime.datetime.now()
|
||||||
formatted_date = current_datetime.strftime("%Y%m%d")
|
formatted_date = current_datetime.strftime("%Y%m%d")
|
||||||
|
|
||||||
# If no title found, use user_yyyymmdd format
|
# If no title found, use user_yyyymmdd format
|
||||||
if not workbook_name:
|
if not title:
|
||||||
workbook_name = f"{user_name}_{formatted_date}"
|
workbook_name = f"{user_name}_{formatted_date}"
|
||||||
|
else:
|
||||||
|
workbook_name = self.clean_filename(title)
|
||||||
|
|
||||||
filename = f"{workbook_name}.xlsx"
|
filename = f"{workbook_name}.xlsx"
|
||||||
excel_file_path = os.path.join(
|
excel_file_path = os.path.join(
|
||||||
@@ -172,6 +229,88 @@ class Action:
|
|||||||
__event_emitter__, "error", "No tables found to export!"
|
__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]:
|
def extract_tables_from_message(self, message: str) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Extract Markdown tables and their positions from message text
|
Extract Markdown tables and their positions from message text
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: 导出为 Excel
|
|||||||
author: Fu-Jie
|
author: Fu-Jie
|
||||||
author_url: https://github.com/Fu-Jie
|
author_url: https://github.com/Fu-Jie
|
||||||
funding_url: https://github.com/Fu-Jie/awesome-openwebui
|
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==
|
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNSAySDZhMiAyIDAgMCAwLTIgMnYxNmEyIDIgMCAwIDAgMiAyaDEyYTIgMiAwIDAgMCAyLTJWN1oiLz48cGF0aCBkPSJNMTQgMnY0YTIgMiAwIDAgMCAyIDJoNCIvPjxwYXRoIGQ9Ik04IDEzaDIiLz48cGF0aCBkPSJNMTQgMTNoMiIvPjxwYXRoIGQ9Ik04IDE3aDIiLz48cGF0aCBkPSJNMTQgMTdoMiIvPjwvc3ZnPg==
|
||||||
description: 将当前对话历史导出为 Excel (.xlsx) 文件,支持自动提取表头。
|
description: 将当前对话历史导出为 Excel (.xlsx) 文件,支持自动提取表头。
|
||||||
"""
|
"""
|
||||||
@@ -15,14 +15,24 @@ import base64
|
|||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException
|
||||||
from typing import Optional, Callable, Awaitable, Any, List, Dict
|
from typing import Optional, Callable, Awaitable, Any, List, Dict
|
||||||
import datetime
|
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()
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
class Action:
|
class Action:
|
||||||
|
class Valves(BaseModel):
|
||||||
|
TITLE_SOURCE: str = Field(
|
||||||
|
default="chat_title",
|
||||||
|
description="标题来源:'chat_title' (对话标题), 'ai_generated' (AI生成), 'markdown_title' (Markdown标题)",
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
self.valves = self.Valves()
|
||||||
|
|
||||||
async def _send_notification(self, emitter: Callable, type: str, content: str):
|
async def _send_notification(self, emitter: Callable, type: str, content: str):
|
||||||
await emitter(
|
await emitter(
|
||||||
@@ -35,6 +45,7 @@ class Action:
|
|||||||
__user__=None,
|
__user__=None,
|
||||||
__event_emitter__=None,
|
__event_emitter__=None,
|
||||||
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
|
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
|
||||||
|
__request__: Optional[Any] = None,
|
||||||
):
|
):
|
||||||
print(f"action:{__name__}")
|
print(f"action:{__name__}")
|
||||||
if isinstance(__user__, (list, tuple)):
|
if isinstance(__user__, (list, tuple)):
|
||||||
@@ -69,18 +80,58 @@ class Action:
|
|||||||
if not tables:
|
if not tables:
|
||||||
raise HTTPException(status_code=400, detail="未找到任何表格。")
|
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名称
|
# 获取动态文件名和sheet名称
|
||||||
workbook_name, sheet_names = self.generate_names_from_content(
|
workbook_name_from_content, sheet_names = (
|
||||||
message_content, tables
|
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()
|
current_datetime = datetime.datetime.now()
|
||||||
formatted_date = current_datetime.strftime("%Y%m%d")
|
formatted_date = current_datetime.strftime("%Y%m%d")
|
||||||
|
|
||||||
# 如果没找到标题则使用 user_yyyymmdd 格式
|
# 如果没找到标题则使用 user_yyyymmdd 格式
|
||||||
if not workbook_name:
|
if not title:
|
||||||
workbook_name = f"{user_name}_{formatted_date}"
|
workbook_name = f"{user_name}_{formatted_date}"
|
||||||
|
else:
|
||||||
|
workbook_name = self.clean_filename(title)
|
||||||
|
|
||||||
filename = f"{workbook_name}.xlsx"
|
filename = f"{workbook_name}.xlsx"
|
||||||
excel_file_path = os.path.join(
|
excel_file_path = os.path.join(
|
||||||
@@ -172,6 +223,88 @@ class Action:
|
|||||||
__event_emitter__, "error", "没有找到可以导出的表格!"
|
__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]:
|
def extract_tables_from_message(self, message: str) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
从消息文本中提取Markdown表格及位置信息
|
从消息文本中提取Markdown表格及位置信息
|
||||||
|
|||||||
Reference in New Issue
Block a user