更新导出为 Word 功能,增加代码语法高亮和引用块支持,优化文档说明和依赖项

This commit is contained in:
Jeff fu
2025-12-30 14:56:57 +08:00
parent 1bf1d7ac23
commit 7f43e45049
4 changed files with 328 additions and 53 deletions

View File

@@ -1,30 +1,33 @@
# Export to Word
Export current conversation from Markdown to Word (.docx) with proper Chinese and English encoding and smarter filenames.
Export current conversation from Markdown to Word (.docx) with **syntax highlighting**, **blockquote support**, and smarter filenames.
## Features
- **One-Click Export**: Adds an "Export to Word" action button to the chat.
- **Markdown Conversion**: Converts Markdown syntax to Word formatting (headings, bold, italic, code, tables, lists).
- **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.
## Supported Markdown Syntax
| Syntax | Word Result |
| :---------------------------------- | :----------------------------- |
| `# Heading 1` to `###### Heading 6` | Heading levels 1-6 |
| `**bold**` or `__bold__` | Bold text |
| `*italic*` or `_italic_` | Italic text |
| `***bold italic***` | Bold + Italic |
| `` `inline code` `` | Monospace with gray background |
| ` ``` code block ``` ` | Code block with indentation |
| `[link](url)` | Blue underlined link text |
| `~~strikethrough~~` | Strikethrough text |
| `- item` or `* item` | Bullet list |
| `1. item` | Numbered list |
| Markdown tables | Table with grid |
| `---` or `***` | Horizontal rule |
| Syntax | Word Result |
| :---------------------------------- | :-------------------------------- |
| `# Heading 1` to `###### Heading 6` | Heading levels 1-6 |
| `**bold**` or `__bold__` | Bold text |
| `*italic*` or `_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 text |
| `~~strikethrough~~` | Strikethrough text |
| `- item` or `* item` | Bullet list |
| `1. item` | Numbered list |
| Markdown tables | Table with grid |
| `---` or `***` | Horizontal rule |
## Usage
@@ -41,7 +44,10 @@ Export current conversation from Markdown to Word (.docx) with proper Chinese an
### Requirements
- python-docx==1.1.2 (already declared in the plugin docstring; ensure installed in your environment).
- `python-docx==1.1.2` - Word document generation
- `Pygments>=2.15.0` - Syntax highlighting (optional but recommended)
Both are declared in the plugin docstring; ensure they are installed in your environment.
## Font Configuration

View File

@@ -1,30 +1,33 @@
# 导出为 Word
将当前对话内容从 Markdown 转换并导出为 Word (.docx) 文件,支持中英文无乱码,且具备更智能的文件命名。
将当前对话内容从 Markdown 转换并导出为 Word (.docx) 文件,支持**代码语法高亮**、**引用块样式**和更智能的文件命名。
## 功能特点
- **一键导出**:在聊天界面添加"导出为 Word"动作按钮。
- **一键导出**:在聊天界面添加导出为 Word动作按钮。
- **Markdown 转换**:将 Markdown 语法转换为 Word 格式(标题、粗体、斜体、代码、表格、列表)。
- **代码语法高亮**:使用 Pygments 库为代码块添加语法高亮(支持 500+ 种语言)。
- **引用块支持**Markdown 引用块会渲染为带左侧边框的灰色斜体样式。
- **多语言支持**:正确处理中文和英文文本,无乱码问题。
- **更智能的文件名**:优先使用对话标题(来自请求体或基于 chat_id 查询),其次 Markdown 一级/二级标题,最后用户+日期。
## 支持的 Markdown 语法
| 语法 | Word 效果 |
| :-------------------------- | :------------------ |
| `# 标题1``###### 标题6` | 标题级别 1-6 |
| `**粗体**``__粗体__` | 粗体文本 |
| `*斜体*``_斜体_` | 斜体文本 |
| `***粗斜体***` | 粗体 + 斜体 |
| `` `行内代码` `` | 等宽字体 + 灰色背景 |
| ` ``` 代码块 ``` ` | 带缩进的代码块 |
| `[链接](url)` | 蓝色下划线链接文本 |
| `~~删除线~~` | 删除线文本 |
| `- 项目``* 项目` | 无序列表 |
| `1. 项目` | 有序列表 |
| Markdown 表格 | 带边框表格 |
| `---``***` | 水平分割线 |
| 语法 | Word 效果 |
| :-------------------------- | :----------------------- |
| `# 标题1``###### 标题6` | 标题级别 1-6 |
| `**粗体**``__粗体__` | 粗体文本 |
| `*斜体*``_斜体_` | 斜体文本 |
| `***粗斜体***` | 粗体 + 斜体 |
| `` `行内代码` `` | 等宽字体 + 灰色背景 |
| ` ``` 代码块 ``` ` | **语法高亮**的代码块 |
| `> 引用文本` | 带左侧边框的灰色斜体文本 |
| `[链接](url)` | 蓝色下划线链接文本 |
| `~~删除线~~` | 删除线文本 |
| `- 项目` `* 项目` | 无序列表 |
| `1. 项目` | 有序列表 |
| Markdown 表格 | 带边框表格 |
| `---``***` | 水平分割线 |
## 使用方法
@@ -40,7 +43,10 @@
### 依赖
- python-docx==1.1.2(已在插件文档字符串中声明,请确保环境已安装)。
- `python-docx==1.1.2` - Word 文档生成
- `Pygments>=2.15.0` - 语法高亮(可选但建议安装)
两者已在插件文档字符串中声明,请确保环境已安装。
## 字体配置

View File

@@ -5,8 +5,8 @@ author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui
version: 0.1.0
icon_url: data:image/svg+xml;base64,PHN2ZwogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICB3aWR0aD0iMjQiCiAgaGVpZ2h0PSIyNCIKICB2aWV3Qm94PSIwIDAgMjQgMjQiCiAgZmlsbD0ibm9uZSIKICBzdHJva2U9ImN1cnJlbnRDb2xvciIKICBzdHJva2Utd2lkdGg9IjIiCiAgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIgogIHN0cm9rZS1saW5lam9pbj0icm91bmQiCj4KICA8cGF0aCBkPSJNNiAyMmEyIDIgMCAwIDEtMi0yVjRhMiAyIDAgMCAxIDItMmg4YTIuNCAyLjQgMCAwIDEgMS43MDQuNzA2bDMuNTg4IDMuNTg4QTIuNCAyLjQgMCAwIDEgMjAgOHYxMmEyIDIgMCAwIDEtMiAyeiIgLz4KICA8cGF0aCBkPSJNMTQgMnY1YTEgMSAwIDAgMCAxIDFoNSIgLz4KICA8cGF0aCBkPSJNMTAgOUg4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxM0g4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxN0g4IiAvPgo8L3N2Zz4K
requirements: python-docx==1.1.2
description: Export current conversation from Markdown to Word (.docx) file with proper Chinese and English encoding.
requirements: python-docx==1.1.2, Pygments>=2.15.0
description: Export current conversation from Markdown to Word (.docx) file with syntax highlighting and blockquote support.
"""
import os
@@ -26,6 +26,16 @@ from docx.oxml.ns import qn
from docx.oxml import OxmlElement
from open_webui.models.chats import Chats
# Pygments for syntax highlighting
try:
from pygments import lex
from pygments.lexers import get_lexer_by_name, TextLexer
from pygments.token import Token
PYGMENTS_AVAILABLE = True
except ImportError:
PYGMENTS_AVAILABLE = False
logging.basicConfig(
level=logging.INFO,
@@ -375,6 +385,24 @@ class Action:
i += 1
continue
# Handle blockquotes
if line.strip().startswith(">"):
# Process pending list first
if in_list and list_items:
self.add_list_to_doc(doc, list_items, list_type)
list_items = []
in_list = False
# Collect consecutive quote lines
blockquote_lines = []
while i < len(lines) and lines[i].strip().startswith(">"):
# Remove leading > and optional space
quote_line = re.sub(r"^>\s?", "", lines[i])
blockquote_lines.append(quote_line)
i += 1
self.add_blockquote(doc, "\n".join(blockquote_lines))
continue
# Handle horizontal rules
if re.match(r"^[-*_]{3,}$", line.strip()):
# Process pending list first
@@ -552,23 +580,93 @@ class Action:
paragraph.add_run(text[pos:])
def add_code_block(self, doc: Document, code: str, language: str = ""):
"""Add code block"""
"""Add code block with syntax highlighting"""
# Token color mapping (based on common IDE themes)
TOKEN_COLORS = {
Token.Keyword: RGBColor(0, 0, 255), # Blue - keywords
Token.Keyword.Constant: RGBColor(0, 0, 255),
Token.Keyword.Declaration: RGBColor(0, 0, 255),
Token.Keyword.Namespace: RGBColor(0, 0, 255),
Token.Keyword.Type: RGBColor(0, 0, 255),
Token.Name.Function: RGBColor(136, 18, 128), # Purple - function names
Token.Name.Class: RGBColor(38, 127, 153), # Cyan - class names
Token.Name.Decorator: RGBColor(255, 128, 0), # Orange - decorators
Token.Name.Builtin: RGBColor(0, 112, 32), # Green - builtins
Token.String: RGBColor(163, 21, 21), # Red - strings
Token.String.Doc: RGBColor(128, 128, 128), # Gray - docstrings
Token.Comment: RGBColor(128, 128, 128), # Gray - comments
Token.Comment.Single: RGBColor(128, 128, 128),
Token.Comment.Multiline: RGBColor(128, 128, 128),
Token.Number: RGBColor(9, 134, 88), # Green - numbers
Token.Number.Integer: RGBColor(9, 134, 88),
Token.Number.Float: RGBColor(9, 134, 88),
Token.Operator: RGBColor(104, 118, 135), # Gray-blue - operators
Token.Punctuation: RGBColor(64, 64, 64), # Dark gray - punctuation
}
def get_token_color(token_type):
"""Recursively find token color"""
while token_type:
if token_type in TOKEN_COLORS:
return TOKEN_COLORS[token_type]
token_type = token_type.parent
return None
# Add language label if available
if language:
lang_para = doc.add_paragraph()
lang_para.paragraph_format.space_before = Pt(6)
lang_para.paragraph_format.space_after = Pt(0)
lang_para.paragraph_format.left_indent = Cm(0.5)
lang_run = lang_para.add_run(language.upper())
lang_run.font.name = "Consolas"
lang_run.font.size = Pt(8)
lang_run.font.color.rgb = RGBColor(100, 100, 100)
lang_run.font.bold = True
# Add code block paragraph
paragraph = doc.add_paragraph()
paragraph.paragraph_format.left_indent = Cm(0.5)
paragraph.paragraph_format.space_before = Pt(6)
paragraph.paragraph_format.space_before = Pt(3) if language else Pt(6)
paragraph.paragraph_format.space_after = Pt(6)
# Set code block font
run = paragraph.add_run(code)
run.font.name = "Consolas"
run._element.rPr.rFonts.set(qn("w:eastAsia"), "SimHei")
run.font.size = Pt(10)
# Add light gray background
shading = OxmlElement("w:shd")
shading.set(qn("w:fill"), "F5F5F5")
paragraph._element.pPr.append(shading)
# Try to use Pygments for syntax highlighting
if PYGMENTS_AVAILABLE and language:
try:
lexer = get_lexer_by_name(language, stripall=False)
except Exception:
lexer = TextLexer()
tokens = list(lex(code, lexer))
for token_type, token_value in tokens:
if not token_value:
continue
run = paragraph.add_run(token_value)
run.font.name = "Consolas"
run._element.rPr.rFonts.set(qn("w:eastAsia"), "SimHei")
run.font.size = Pt(10)
# Apply color
color = get_token_color(token_type)
if color:
run.font.color.rgb = color
# Bold keywords
if token_type in Token.Keyword:
run.font.bold = True
else:
# No syntax highlighting, plain text display
run = paragraph.add_run(code)
run.font.name = "Consolas"
run._element.rPr.rFonts.set(qn("w:eastAsia"), "SimHei")
run.font.size = Pt(10)
def add_table(self, doc: Document, table_lines: List[str]):
"""Add table"""
if len(table_lines) < 2:
@@ -661,3 +759,37 @@ class Action:
bottom.set(qn("w:color"), "auto")
pBdr.append(bottom)
pPr.append(pBdr)
def add_blockquote(self, doc: Document, text: str):
"""Add blockquote with left border and gray background"""
for line in text.split("\n"):
paragraph = doc.add_paragraph()
paragraph.paragraph_format.left_indent = Cm(1.0)
paragraph.paragraph_format.space_before = Pt(3)
paragraph.paragraph_format.space_after = Pt(3)
# Add left border
pPr = paragraph._element.get_or_add_pPr()
pBdr = OxmlElement("w:pBdr")
left = OxmlElement("w:left")
left.set(qn("w:val"), "single")
left.set(qn("w:sz"), "24") # Border thickness
left.set(qn("w:space"), "4") # Space between border and text
left.set(qn("w:color"), "CCCCCC") # Gray border
pBdr.append(left)
pPr.append(pBdr)
# Add light gray background
shading = OxmlElement("w:shd")
shading.set(qn("w:fill"), "F9F9F9")
pPr.append(shading)
# Add formatted text
self.add_formatted_text(paragraph, line)
# Set font to italic gray
for run in paragraph.runs:
run.font.name = "Times New Roman"
run._element.rPr.rFonts.set(qn("w:eastAsia"), "KaiTi")
run.font.color.rgb = RGBColor(85, 85, 85) # Dark gray text
run.italic = True

View File

@@ -5,8 +5,8 @@ author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui
version: 0.1.0
icon_url: data:image/svg+xml;base64,PHN2ZwogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICB3aWR0aD0iMjQiCiAgaGVpZ2h0PSIyNCIKICB2aWV3Qm94PSIwIDAgMjQgMjQiCiAgZmlsbD0ibm9uZSIKICBzdHJva2U9ImN1cnJlbnRDb2xvciIKICBzdHJva2Utd2lkdGg9IjIiCiAgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIgogIHN0cm9rZS1saW5lam9pbj0icm91bmQiCj4KICA8cGF0aCBkPSJNNiAyMmEyIDIgMCAwIDEtMi0yVjRhMiAyIDAgMCAxIDItMmg4YTIuNCAyLjQgMCAwIDEgMS43MDQuNzA2bDMuNTg4IDMuNTg4QTIuNCAyLjQgMCAwIDEgMjAgOHYxMmEyIDIgMCAwIDEtMiAyeiIgLz4KICA8cGF0aCBkPSJNMTQgMnY1YTEgMSAwIDAgMCAxIDFoNSIgLz4KICA8cGF0aCBkPSJNMTAgOUg4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxM0g4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxN0g4IiAvPgo8L3N2Zz4K
requirements: python-docx==1.1.2
description: 将当前对话内容从 Markdown 转换并导出为 Word (.docx) 文件,支持中英文无乱码
requirements: python-docx==1.1.2, Pygments>=2.15.0
description: 将当前对话内容从 Markdown 转换并导出为 Word (.docx) 文件,支持代码语法高亮和引用块
"""
import os
@@ -26,6 +26,16 @@ from docx.oxml.ns import qn
from docx.oxml import OxmlElement
from open_webui.models.chats import Chats
# Pygments for syntax highlighting
try:
from pygments import lex
from pygments.lexers import get_lexer_by_name, TextLexer
from pygments.token import Token
PYGMENTS_AVAILABLE = True
except ImportError:
PYGMENTS_AVAILABLE = False
logging.basicConfig(
level=logging.INFO,
@@ -370,6 +380,24 @@ class Action:
i += 1
continue
# 处理引用块
if line.strip().startswith(">"):
# 先处理之前积累的列表
if in_list and list_items:
self.add_list_to_doc(doc, list_items, list_type)
list_items = []
in_list = False
# 收集连续的引用行
blockquote_lines = []
while i < len(lines) and lines[i].strip().startswith(">"):
# 移除开头的 > 和可能的空格
quote_line = re.sub(r"^>\s?", "", lines[i])
blockquote_lines.append(quote_line)
i += 1
self.add_blockquote(doc, "\n".join(blockquote_lines))
continue
# 处理水平分割线
if re.match(r"^[-*_]{3,}$", line.strip()):
# 先处理之前积累的列表
@@ -551,24 +579,93 @@ class Action:
paragraph.add_run(text[pos:])
def add_code_block(self, doc: Document, code: str, language: str = ""):
"""添加代码块"""
"""添加代码块,支持语法高亮"""
# 语法高亮颜色映射 (基于常见的 IDE 配色)
TOKEN_COLORS = {
Token.Keyword: RGBColor(0, 0, 255), # 蓝色 - 关键字
Token.Keyword.Constant: RGBColor(0, 0, 255),
Token.Keyword.Declaration: RGBColor(0, 0, 255),
Token.Keyword.Namespace: RGBColor(0, 0, 255),
Token.Keyword.Type: RGBColor(0, 0, 255),
Token.Name.Function: RGBColor(136, 18, 128), # 紫色 - 函数名
Token.Name.Class: RGBColor(38, 127, 153), # 青色 - 类名
Token.Name.Decorator: RGBColor(255, 128, 0), # 橙色 - 装饰器
Token.Name.Builtin: RGBColor(0, 112, 32), # 绿色 - 内置函数
Token.String: RGBColor(163, 21, 21), # 红色 - 字符串
Token.String.Doc: RGBColor(128, 128, 128), # 灰色 - 文档字符串
Token.Comment: RGBColor(128, 128, 128), # 灰色 - 注释
Token.Comment.Single: RGBColor(128, 128, 128),
Token.Comment.Multiline: RGBColor(128, 128, 128),
Token.Number: RGBColor(9, 134, 88), # 绿色 - 数字
Token.Number.Integer: RGBColor(9, 134, 88),
Token.Number.Float: RGBColor(9, 134, 88),
Token.Operator: RGBColor(104, 118, 135), # 灰蓝色 - 运算符
Token.Punctuation: RGBColor(64, 64, 64), # 深灰 - 标点
}
def get_token_color(token_type):
"""递归查找 token 颜色"""
while token_type:
if token_type in TOKEN_COLORS:
return TOKEN_COLORS[token_type]
token_type = token_type.parent
return None
# 添加语言标签(如果有)
if language:
lang_para = doc.add_paragraph()
lang_para.paragraph_format.space_before = Pt(6)
lang_para.paragraph_format.space_after = Pt(0)
lang_para.paragraph_format.left_indent = Cm(0.5)
lang_run = lang_para.add_run(language.upper())
lang_run.font.name = "Consolas"
lang_run.font.size = Pt(8)
lang_run.font.color.rgb = RGBColor(100, 100, 100)
lang_run.font.bold = True
# 添加代码块段落
paragraph = doc.add_paragraph()
paragraph.paragraph_format.left_indent = Cm(0.5)
paragraph.paragraph_format.space_before = Pt(6)
paragraph.paragraph_format.space_before = Pt(3) if language else Pt(6)
paragraph.paragraph_format.space_after = Pt(6)
# 设置代码块背景
run = paragraph.add_run(code)
run.font.name = "Consolas"
run._element.rPr.rFonts.set(qn("w:eastAsia"), "SimHei")
run.font.size = Pt(10)
# 添加浅灰色背景
shading = OxmlElement("w:shd")
shading.set(qn("w:fill"), "F5F5F5")
paragraph._element.pPr.append(shading)
# 尝试使用 Pygments 进行语法高亮
if PYGMENTS_AVAILABLE and language:
try:
lexer = get_lexer_by_name(language, stripall=False)
except Exception:
lexer = TextLexer()
tokens = list(lex(code, lexer))
for token_type, token_value in tokens:
if not token_value:
continue
run = paragraph.add_run(token_value)
run.font.name = "Consolas"
run._element.rPr.rFonts.set(qn("w:eastAsia"), "SimHei")
run.font.size = Pt(10)
# 应用颜色
color = get_token_color(token_type)
if color:
run.font.color.rgb = color
# 关键字加粗
if token_type in Token.Keyword:
run.font.bold = True
else:
# 无语法高亮,纯文本显示
run = paragraph.add_run(code)
run.font.name = "Consolas"
run._element.rPr.rFonts.set(qn("w:eastAsia"), "SimHei")
run.font.size = Pt(10)
def add_table(self, doc: Document, table_lines: List[str]):
"""添加表格"""
if len(table_lines) < 2:
@@ -661,3 +758,37 @@ class Action:
bottom.set(qn("w:color"), "auto")
pBdr.append(bottom)
pPr.append(pBdr)
def add_blockquote(self, doc: Document, text: str):
"""添加引用块,带有左侧边框和灰色背景"""
for line in text.split("\n"):
paragraph = doc.add_paragraph()
paragraph.paragraph_format.left_indent = Cm(1.0)
paragraph.paragraph_format.space_before = Pt(3)
paragraph.paragraph_format.space_after = Pt(3)
# 添加左侧边框
pPr = paragraph._element.get_or_add_pPr()
pBdr = OxmlElement("w:pBdr")
left = OxmlElement("w:left")
left.set(qn("w:val"), "single")
left.set(qn("w:sz"), "24") # 边框粗细
left.set(qn("w:space"), "4") # 边框与文字间距
left.set(qn("w:color"), "CCCCCC") # 灰色边框
pBdr.append(left)
pPr.append(pBdr)
# 添加浅灰色背景
shading = OxmlElement("w:shd")
shading.set(qn("w:fill"), "F9F9F9")
pPr.append(shading)
# 添加格式化文本
self.add_formatted_text(paragraph, line)
# 设置字体为斜体灰色
for run in paragraph.runs:
run.font.name = "Times New Roman"
run._element.rPr.rFonts.set(qn("w:eastAsia"), "楷体")
run.font.color.rgb = RGBColor(85, 85, 85) # 深灰色文字
run.italic = True