diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 1d566ef..d3851b7 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -39,6 +39,14 @@ README 文件应包含以下内容:
- 故障排除指南 / Troubleshooting guide
- 版本和作者信息 / Version and author information
+### 官方文档 (Official Documentation)
+
+如果插件被合并到主仓库,还需更新 `docs/` 目录下的相关文档:
+- `docs/plugins/{type}/plugin-name.md`
+- `docs/plugins/{type}/plugin-name.zh.md`
+
+其中 `{type}` 对应插件类型(如 `actions`, `filters`, `pipes` 等)。
+
---
## 📝 文档字符串规范 (Docstring Standard)
@@ -370,6 +378,94 @@ from open_webui.models.users import Users
---
+## 📄 文件导出与命名规范 (File Export and Naming)
+
+对于涉及文件导出的插件(通常是 Action),必须提供灵活的标题生成策略。
+
+### Valves 配置 (Valves Configuration)
+
+应包含 `TITLE_SOURCE` 选项:
+
+```python
+class Valves(BaseModel):
+ TITLE_SOURCE: str = Field(
+ default="chat_title",
+ description="Title Source: 'chat_title', 'ai_generated', 'markdown_title'",
+ )
+```
+
+### 标题获取逻辑 (Title Retrieval Logic)
+
+1. **chat_title**: 尝试从 `body` 获取,若失败且有 `chat_id`,则从数据库获取 (`Chats.get_chat_by_id`)。
+2. **markdown_title**: 从 Markdown 内容提取第一个 H1 或 H2。
+3. **ai_generated**: 使用轻量级 Prompt 让 AI 生成简短标题。
+
+### 优先级与回退 (Priority and Fallback)
+
+代码应根据 `TITLE_SOURCE` 优先尝试指定方法,若失败则按以下顺序回退:
+`chat_title` -> `markdown_title` -> `user_name + date`
+
+```python
+# 核心逻辑示例
+if self.valves.TITLE_SOURCE == "chat_title":
+ title = chat_title
+elif self.valves.TITLE_SOURCE == "markdown_title":
+ title = self.extract_title(content)
+elif self.valves.TITLE_SOURCE == "ai_generated":
+ title = await self.generate_title_using_ai(...)
+```
+
+### AI 标题生成实现 (AI Title Generation Implementation)
+
+如果支持 `ai_generated` 选项,应实现类似以下的方法:
+
+```python
+async def generate_title_using_ai(
+ self,
+ body: dict,
+ content: str,
+ user_id: str,
+ request: Any
+) -> str:
+ """Generates a short title using the current LLM model."""
+ 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] # 限制上下文长度以节省 Token
+ }
+ ],
+ "stream": False,
+ }
+
+ # 调用 OpenWebUI 内部生成接口
+ 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:
+ logger.error(f"Error generating title: {e}")
+
+ return ""
+```
+
+---
+
## ✅ 开发检查清单 (Development Checklist)
开发新插件时,请确保完成以下检查:
diff --git a/docs/plugins/actions/export-to-word.md b/docs/plugins/actions/export-to-word.md
index e24c0c9..1adebbb 100644
--- a/docs/plugins/actions/export-to-word.md
+++ b/docs/plugins/actions/export-to-word.md
@@ -17,7 +17,17 @@ The Export to Word plugin converts chat messages from Markdown to a polished Wor
- :material-format-bold: **Rich Markdown Support**: Headings, bold/italic, lists, tables
- :material-code-tags: **Syntax Highlighting**: Pygments-powered code blocks
- :material-format-quote-close: **Styled Blockquotes**: Left-border gray quote styling
-- :material-file-document-outline: **Smart Filenames**: Prefers chat title → Markdown title → user/date
+- :material-file-document-outline: **Smart Filenames**: Configurable title source (Chat Title, AI Generated, or Markdown Title)
+
+---
+
+## Configuration
+
+You can configure the following settings via the **Valves** button in the plugin settings:
+
+| Valve | Description | Default |
+| :------------- | :------------------------------------------------------------------------------------------ | :----------- |
+| `TITLE_SOURCE` | Source for document title/filename. Options: `chat_title`, `ai_generated`, `markdown_title` | `chat_title` |
---
@@ -39,21 +49,21 @@ The Export to Word plugin converts chat messages from Markdown to a polished Wor
## Supported Markdown
-| Syntax | Word Result |
-| :---------------------------------- | :-------------------------------- |
-| `# Heading 1` to `###### Heading 6` | Heading levels 1-6 |
-| `**bold**` / `__bold__` | Bold text |
-| `*italic*` / `_italic_` | Italic text |
-| `***bold italic***` | Bold + Italic |
-| `` `inline code` `` | Monospace with gray background |
-| ``` code block ``` | Syntax-highlighted code block |
-| `> blockquote` | Left-bordered gray italic text |
-| `[link](url)` | Blue underlined link |
-| `~~strikethrough~~` | Strikethrough |
-| `- item` / `* item` | Bullet list |
-| `1. item` | Numbered list |
-| Markdown tables | Grid table |
-| `---` / `***` | Horizontal rule |
+| Syntax | Word Result |
+| :---------------------------------- | :----------------------------- |
+| `# Heading 1` to `###### Heading 6` | Heading levels 1-6 |
+| `**bold**` / `__bold__` | Bold text |
+| `*italic*` / `_italic_` | Italic text |
+| `***bold italic***` | Bold + Italic |
+| `` `inline code` `` | Monospace with gray background |
+| ``` code block ``` | Syntax-highlighted code block |
+| `> blockquote` | Left-bordered gray italic text |
+| `[link](url)` | Blue underlined link |
+| `~~strikethrough~~` | Strikethrough |
+| `- item` / `* item` | Bullet list |
+| `1. item` | Numbered list |
+| Markdown tables | Grid table |
+| `---` / `***` | Horizontal rule |
---
diff --git a/docs/plugins/actions/export-to-word.zh.md b/docs/plugins/actions/export-to-word.zh.md
index 231fdea..3dd9471 100644
--- a/docs/plugins/actions/export-to-word.zh.md
+++ b/docs/plugins/actions/export-to-word.zh.md
@@ -17,7 +17,17 @@ Export to Word 插件会把聊天消息从 Markdown 转成精致的 Word 文档
- :material-format-bold: **丰富 Markdown 支持**:标题、粗斜体、列表、表格
- :material-code-tags: **语法高亮**:Pygments 驱动的代码块上色
- :material-format-quote-close: **引用样式**:左侧边框的灰色斜体引用
-- :material-file-document-outline: **智能文件名**:优先对话标题 → Markdown 标题 → 用户/日期
+- :material-file-document-outline: **智能文件名**:可配置标题来源(对话标题、AI 生成或 Markdown 标题)
+
+---
+
+## 配置
+
+您可以通过插件设置中的 **Valves** 按钮配置以下选项:
+
+| Valve | 说明 | 默认值 |
+| :------------- | :--------------------------------------------------------------------------------------------------------------- | :----------- |
+| `TITLE_SOURCE` | 文档标题/文件名的来源。选项:`chat_title` (对话标题), `ai_generated` (AI 生成), `markdown_title` (Markdown 标题) | `chat_title` |
---
@@ -39,21 +49,21 @@ Export to Word 插件会把聊天消息从 Markdown 转成精致的 Word 文档
## 支持的 Markdown
-| 语法 | Word 效果 |
-| :---------------------------------- | :-------------------------------- |
-| `# 标题1` 到 `###### 标题6` | 标题级别 1-6 |
-| `**粗体**` / `__粗体__` | 粗体文本 |
-| `*斜体*` / `_斜体_` | 斜体文本 |
-| `***粗斜体***` | 粗体 + 斜体 |
-| `` `行内代码` `` | 等宽字体 + 灰色背景 |
-| ``` 代码块 ``` | 语法高亮代码块 |
-| `> 引用文本` | 左侧边框的灰色斜体 |
-| `[链接](url)` | 蓝色下划线链接 |
-| `~~删除线~~` | 删除线 |
-| `- 项目` / `* 项目` | 无序列表 |
-| `1. 项目` | 有序列表 |
-| Markdown 表格 | 带边框表格 |
-| `---` / `***` | 水平分割线 |
+| 语法 | Word 效果 |
+| :-------------------------- | :------------------ |
+| `# 标题1` 到 `###### 标题6` | 标题级别 1-6 |
+| `**粗体**` / `__粗体__` | 粗体文本 |
+| `*斜体*` / `_斜体_` | 斜体文本 |
+| `***粗斜体***` | 粗体 + 斜体 |
+| `` `行内代码` `` | 等宽字体 + 灰色背景 |
+| ``` 代码块 ``` | 语法高亮代码块 |
+| `> 引用文本` | 左侧边框的灰色斜体 |
+| `[链接](url)` | 蓝色下划线链接 |
+| `~~删除线~~` | 删除线 |
+| `- 项目` / `* 项目` | 无序列表 |
+| `1. 项目` | 有序列表 |
+| Markdown 表格 | 带边框表格 |
+| `---` / `***` | 水平分割线 |
---
diff --git a/plugins/actions/export_to_docx/README.md b/plugins/actions/export_to_docx/README.md
index 8fd72ce..fd78ab4 100644
--- a/plugins/actions/export_to_docx/README.md
+++ b/plugins/actions/export_to_docx/README.md
@@ -9,7 +9,16 @@ Export current conversation from Markdown to Word (.docx) with **syntax highligh
- **Syntax Highlighting**: Code blocks are highlighted with Pygments (supports 500+ languages).
- **Blockquote Support**: Markdown blockquotes are rendered with left border and gray styling.
- **Multi-language Support**: Properly handles both Chinese and English text without garbled characters.
-- **Smarter Filenames**: Prefers chat title (from body or chat_id lookup) → first Markdown h1/h2 → user + date.
+- **Smarter Filenames**: Configurable title source (Chat Title, AI Generated, or Markdown Title).
+
+## Configuration
+
+You can configure the following settings via the **Valves** button in the plugin settings:
+
+- **TITLE_SOURCE**: Choose how the document title/filename is generated.
+ - `chat_title`: Use the conversation title (default).
+ - `ai_generated`: Use AI to generate a short title based on the content.
+ - `markdown_title`: Extract the first h1/h2 heading from the Markdown content.
## Supported Markdown Syntax
diff --git a/plugins/actions/export_to_docx/README_CN.md b/plugins/actions/export_to_docx/README_CN.md
index 9965341..4482257 100644
--- a/plugins/actions/export_to_docx/README_CN.md
+++ b/plugins/actions/export_to_docx/README_CN.md
@@ -9,7 +9,16 @@
- **代码语法高亮**:使用 Pygments 库为代码块添加语法高亮(支持 500+ 种语言)。
- **引用块支持**:Markdown 引用块会渲染为带左侧边框的灰色斜体样式。
- **多语言支持**:正确处理中文和英文文本,无乱码问题。
-- **更智能的文件名**:优先使用对话标题(来自请求体或基于 chat_id 查询),其次 Markdown 一级/二级标题,最后用户+日期。
+- **更智能的文件名**:可配置标题来源(对话标题、AI 生成或 Markdown 标题)。
+
+## 配置 (Configuration)
+
+您可以通过插件设置中的 **Valves** 按钮配置以下选项:
+
+- **TITLE_SOURCE**:选择文档标题/文件名的生成方式。
+ - `chat_title`:使用对话标题(默认)。
+ - `ai_generated`:使用 AI 根据内容生成简短标题。
+ - `markdown_title`:从 Markdown 内容中提取第一个一级或二级标题。
## 支持的 Markdown 语法
diff --git a/plugins/actions/export_to_docx/export_to_word.py b/plugins/actions/export_to_docx/export_to_word.py
index 4f0b2ca..b372eb9 100644
--- a/plugins/actions/export_to_docx/export_to_word.py
+++ b/plugins/actions/export_to_docx/export_to_word.py
@@ -25,6 +25,9 @@ from docx.enum.style import WD_STYLE_TYPE
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
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
# Pygments for syntax highlighting
try:
@@ -45,8 +48,14 @@ logger = logging.getLogger(__name__)
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(
@@ -60,6 +69,7 @@ class Action:
__event_emitter__=None,
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
__metadata__: Optional[dict] = None,
+ __request__: Optional[Any] = None,
):
logger.info(f"action:{__name__}")
@@ -101,26 +111,54 @@ class Action:
)
return
- # Generate filename (prefer chat title; fetch via chat_id if missing; then markdown title; then fallback)
+ # Generate filename
+ title = ""
chat_id = self.extract_chat_id(body, __metadata__)
- chat_title = self.extract_chat_title(body)
- if not chat_title and chat_id:
+
+ # 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)
- title = self.extract_title(message_content)
+
+ 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":
+ title = await self.generate_title_using_ai(
+ body, message_content, user_id, __request__
+ )
+
+ # Fallback logic
+ if not title:
+ if self.valves.TITLE_SOURCE != "chat_title" and chat_title:
+ title = chat_title
+ elif self.valves.TITLE_SOURCE != "markdown_title":
+ extracted = self.extract_title(message_content)
+ if extracted:
+ title = extracted
+
current_datetime = datetime.datetime.now()
formatted_date = current_datetime.strftime("%Y%m%d")
- if chat_title:
- filename = f"{self.clean_filename(chat_title)}.docx"
- elif title:
+ if title:
filename = f"{self.clean_filename(title)}.docx"
else:
filename = f"{user_name}_{formatted_date}.docx"
+ top_heading = ""
+ if chat_title:
+ top_heading = chat_title
+ elif title:
+ top_heading = title
+
# Create Word document; if no h1 exists, inject chat title as h1
has_h1 = bool(re.search(r"^#\s+.+$", message_content, re.MULTILINE))
doc = self.markdown_to_docx(
- message_content, top_heading=chat_title, has_h1=has_h1
+ message_content, top_heading=top_heading, has_h1=has_h1
)
# Save to memory
@@ -194,6 +232,36 @@ class Action:
f"Error exporting Word document: {str(e)}",
)
+ 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:
+ logger.error(f"Error generating title: {e}")
+
+ return ""
+
def extract_title(self, content: str) -> str:
"""Extract title from Markdown h1/h2 only"""
lines = content.split("\n")
diff --git a/plugins/actions/export_to_docx/导出为Word.py b/plugins/actions/export_to_docx/导出为Word.py
index eb3ad48..ff0bf0f 100644
--- a/plugins/actions/export_to_docx/导出为Word.py
+++ b/plugins/actions/export_to_docx/导出为Word.py
@@ -25,6 +25,9 @@ from docx.enum.style import WD_STYLE_TYPE
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
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
# Pygments for syntax highlighting
try:
@@ -45,8 +48,14 @@ logger = logging.getLogger(__name__)
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(
@@ -60,6 +69,7 @@ class Action:
__event_emitter__=None,
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
__metadata__: Optional[dict] = None,
+ __request__: Optional[Any] = None,
):
logger.info(f"action:{__name__}")
@@ -98,26 +108,54 @@ class Action:
)
return
- # 生成文件名(优先对话标题;若缺失则通过 chat_id 查询;再到 Markdown 标题;最后用户+日期)
+ # 生成文件名
+ title = ""
chat_id = self.extract_chat_id(body, __metadata__)
- chat_title = self.extract_chat_title(body)
- if not chat_title and chat_id:
+
+ # 直接通过 chat_id 获取标题,因为 body 中通常不包含标题
+ chat_title = ""
+ if chat_id:
chat_title = await self.fetch_chat_title(chat_id, user_id)
- title = self.extract_title(message_content)
+
+ # 根据配置决定文件名使用的标题
+ 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":
+ title = await self.generate_title_using_ai(
+ body, message_content, user_id, __request__
+ )
+
current_datetime = datetime.datetime.now()
formatted_date = current_datetime.strftime("%Y%m%d")
- if chat_title:
- filename = f"{self.clean_filename(chat_title)}.docx"
- elif title:
+ if title:
filename = f"{self.clean_filename(title)}.docx"
else:
filename = f"{user_name}_{formatted_date}.docx"
# 创建 Word 文档;若正文无一级标题,使用对话标题作为一级标题
+ # 如果选择了 chat_title 且获取到了,则作为 top_heading
+ # 如果选择了其他方式,title 就是文件名,也可以作为 top_heading
+
+ # 保持原有逻辑:top_heading 主要是为了在文档开头补充标题
+ # 这里我们尽量使用 chat_title 作为 top_heading,如果它存在的话,因为它通常是对话的主题
+ # 即使文件名是 AI 生成的,文档内的标题用 chat_title 也是合理的
+ # 但如果用户选择了 markdown_title,可能不希望插入 chat_title
+
+ top_heading = ""
+ if chat_title:
+ top_heading = chat_title
+ elif title:
+ top_heading = title
+
has_h1 = bool(re.search(r"^#\s+.+$", message_content, re.MULTILINE))
doc = self.markdown_to_docx(
- message_content, top_heading=chat_title, has_h1=has_h1
+ message_content, top_heading=top_heading, has_h1=has_h1
)
# 保存到内存
@@ -189,6 +227,36 @@ class Action:
__event_emitter__, "error", f"导出 Word 文档时出错: {str(e)}"
)
+ 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:
+ logger.error(f"Error generating title: {e}")
+
+ return ""
+
def extract_title(self, content: str) -> str:
"""从 Markdown 内容提取一级/二级标题"""
lines = content.split("\n")