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")