Compare commits
22 Commits
v2026.01.0
...
v2026.01.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88038b35cc | ||
|
|
1fd7d90284 | ||
|
|
aee9c93bfb | ||
|
|
3951f7f91d | ||
|
|
3680fcf39f | ||
|
|
593a9ce22b | ||
|
|
fe497cccb7 | ||
|
|
88aa7e156a | ||
|
|
dbfce27986 | ||
|
|
9be6fe08fa | ||
|
|
782378eed8 | ||
|
|
4e59bb6518 | ||
|
|
3e73fcb3f0 | ||
|
|
c460337c43 | ||
|
|
e775b23503 | ||
|
|
b3cdb8e26e | ||
|
|
0e6f902d16 | ||
|
|
c15c73897f | ||
|
|
035439ce02 | ||
|
|
b84ff4a3a2 | ||
|
|
e22744abd0 | ||
|
|
54c90238f7 |
@@ -55,9 +55,13 @@ When adding or updating a plugin, you **MUST** update the following documentatio
|
||||
Reference: `.github/workflows/release.yml`
|
||||
|
||||
### Version Bumping
|
||||
- **Rule**: Any change to plugin logic **MUST** be accompanied by a version bump in the docstring.
|
||||
- **Rule**: Version bump is required **ONLY when the user explicitly requests a release**. Regular code changes do NOT require version bumps.
|
||||
- **Format**: Semantic Versioning (e.g., `1.0.0` -> `1.0.1`).
|
||||
- **Consistency**: Update version in **ALL** locations:
|
||||
- **When to Bump**: Only update the version when:
|
||||
- User says "发布" / "release" / "bump version"
|
||||
- User explicitly asks to prepare for release
|
||||
- **Agent Initiative**: After completing significant changes (new features, bug fixes, or multiple code modifications), the agent **SHOULD proactively ask** the user if they want to release a new version. If confirmed, update all version-related files.
|
||||
- **Consistency**: When bumping, update version in **ALL** locations:
|
||||
1. English Code (`.py`)
|
||||
2. Chinese Code (`.py`)
|
||||
3. English README (`README.md`)
|
||||
@@ -91,3 +95,9 @@ Before committing:
|
||||
- [ ] `docs/` index and detail pages are updated?
|
||||
- [ ] Root `README.md` is updated?
|
||||
- [ ] All version numbers match exactly?
|
||||
|
||||
## 5. Git Operations (Agent Rules)
|
||||
|
||||
Strictly follow the rules defined in `.github/copilot-instructions.md` → **Git Operations (Agent Rules)** section.
|
||||
|
||||
|
||||
|
||||
151
.github/copilot-instructions.md
vendored
151
.github/copilot-instructions.md
vendored
@@ -13,13 +13,13 @@ This document defines the standard conventions and best practices for OpenWebUI
|
||||
每个插件必须提供两个版本:
|
||||
|
||||
1. **英文版本**: `plugin_name.py` - 英文界面、提示词和注释
|
||||
2. **中文版本**: `plugin_name_cn.py` 或 `插件中文名.py` - 中文界面、提示词和注释
|
||||
2. **中文版本**: `plugin_name_cn.py` - 中文界面、提示词和注释
|
||||
|
||||
示例:
|
||||
```
|
||||
plugins/actions/export_to_docx/
|
||||
├── export_to_word.py # English version
|
||||
├── 导出为Word.py # Chinese version
|
||||
├── export_to_word_cn.py # Chinese version
|
||||
├── README.md # English documentation
|
||||
└── README_CN.md # Chinese documentation
|
||||
```
|
||||
@@ -798,6 +798,49 @@ For iframe plugins to access parent document theme information, users need to co
|
||||
- [ ] 使用 logging 而非 print
|
||||
- [ ] 测试双语界面
|
||||
- [ ] **一致性检查 (Consistency Check)**:
|
||||
|
||||
---
|
||||
|
||||
## 🚀 高级开发模式 (Advanced Development Patterns)
|
||||
|
||||
### 混合服务端-客户端生成 (Hybrid Server-Client Generation)
|
||||
|
||||
对于需要复杂前端渲染(如 Mermaid 图表、ECharts)但最终生成文件(如 DOCX、PDF)的场景,建议采用混合模式:
|
||||
|
||||
1. **服务端 (Python)**:
|
||||
* 处理文本解析、Markdown 转换、文档结构构建。
|
||||
* 为复杂组件生成**占位符**(如带有特定 ID 或元数据的图片/文本块)。
|
||||
* 将半成品文件(如 Base64 编码的 ZIP/DOCX)发送给前端。
|
||||
|
||||
2. **客户端 (JavaScript)**:
|
||||
* 在浏览器中加载半成品文件(使用 JSZip 等库)。
|
||||
* 利用浏览器能力渲染复杂组件(如 `mermaid.render`)。
|
||||
* 将渲染结果(SVG/PNG)回填到占位符位置。
|
||||
* 触发最终文件的下载。
|
||||
|
||||
**优势**:
|
||||
* 无需在服务端安装 Headless Browser(如 Puppeteer),降低部署复杂度。
|
||||
* 利用用户浏览器的计算能力。
|
||||
* 支持动态、交互式内容的静态化导出。
|
||||
|
||||
### 原生 Word 公式支持 (Native Word Math Support)
|
||||
|
||||
对于需要生成高质量数学公式的 Word 文档,推荐使用 `latex2mathml` + `mathml2omml` 组合:
|
||||
|
||||
1. **LaTeX -> MathML**: 使用 `latex2mathml` 将 LaTeX 字符串转换为标准 MathML。
|
||||
2. **MathML -> OMML**: 使用 `mathml2omml` 将 MathML 转换为 Office Math Markup Language (OMML)。
|
||||
3. **插入 Word**: 将 OMML XML 插入到 `python-docx` 的段落中。
|
||||
|
||||
```python
|
||||
# 示例代码
|
||||
from latex2mathml.converter import convert as latex2mathml
|
||||
from mathml2omml import convert as mathml2omml
|
||||
|
||||
def add_math(paragraph, latex_str):
|
||||
mathml = latex2mathml(latex_str)
|
||||
omml = mathml2omml(mathml)
|
||||
# ... 插入 OMML 到 paragraph._element ...
|
||||
```
|
||||
- [ ] 更新 `README.md` 插件列表
|
||||
- [ ] 更新 `README_CN.md` 插件列表
|
||||
- [ ] 更新/创建 `docs/` 下的对应文档
|
||||
@@ -833,13 +876,35 @@ For iframe plugins to access parent document theme information, users need to co
|
||||
|
||||
### 发布前必须完成 (Pre-release Requirements)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 版本号**仅在用户明确要求发布时**才需要更新。日常代码更改**无需**更新版本号。
|
||||
|
||||
**触发版本更新的关键词**:
|
||||
- 用户说 "发布"、"release"、"bump version"
|
||||
- 用户明确要求准备发布
|
||||
|
||||
**Agent 主动询问发布 (Agent-Initiated Release Prompt)**:
|
||||
|
||||
当 Agent 完成以下类型的更改后,**应主动询问**用户是否需要发布新版本:
|
||||
|
||||
| 更改类型 | 示例 | 是否询问发布 |
|
||||
|---------|------|-------------|
|
||||
| 新功能 | 新增导出格式、新的配置选项 | ✅ 询问 |
|
||||
| 重要 Bug 修复 | 修复导致崩溃或数据丢失的问题 | ✅ 询问 |
|
||||
| 累积多次更改 | 同一插件在会话中被修改 >= 3 次 | ✅ 询问 |
|
||||
| 小优化 | 代码清理、格式符号处理 | ❌ 不询问 |
|
||||
| 文档更新 | 只改 README、注释 | ❌ 不询问 |
|
||||
|
||||
如果用户确认发布,Agent 需要更新所有版本相关的文件(代码、README、docs 等)。
|
||||
|
||||
**发布时需要完成**:
|
||||
1. ✅ **更新版本号** - 修改插件文档字符串中的 `version` 字段
|
||||
2. ✅ **中英文版本同步** - 确保两个版本的版本号一致
|
||||
|
||||
```python
|
||||
"""
|
||||
title: My Plugin
|
||||
version: 0.2.0 # <- 必须更新这里!
|
||||
version: 0.2.0 # <- 发布时更新这里!
|
||||
...
|
||||
"""
|
||||
```
|
||||
@@ -984,3 +1049,83 @@ Follow the [Conventional Commits](https://www.conventionalcommits.org/) specific
|
||||
❌ **Bad:**
|
||||
- `新增导出PDF插件` (Chinese is not allowed)
|
||||
- `update code` (Too vague)
|
||||
|
||||
---
|
||||
|
||||
## 🤖 Git Operations (Agent Rules)
|
||||
|
||||
**重要规则 (CRITICAL RULES FOR AI AGENTS)**:
|
||||
|
||||
AI Agent(如 Copilot、Gemini、Claude 等)在执行 Git 操作时必须遵守以下规则:
|
||||
|
||||
| 操作 (Operation) | 允许 (Allowed) | 说明 (Description) |
|
||||
|-----------------|---------------|---------------------|
|
||||
| 创建功能分支 | ✅ 允许 | `git checkout -b feature/xxx` |
|
||||
| 推送到功能分支 | ✅ 允许 | `git push origin feature/xxx` |
|
||||
| 直接推送到 main | ❌ 禁止 | `git push origin main` 需要用户手动执行 |
|
||||
| 合并到 main | ❌ 禁止 | 任何合并操作需要用户明确批准 |
|
||||
| Rebase 到 main | ❌ 禁止 | 任何 rebase 操作需要用户明确批准 |
|
||||
|
||||
**规则详解 (Rule Details)**:
|
||||
|
||||
1. **Feature Branches Allowed**: Agent **可以**创建新的功能分支并推送到远程仓库
|
||||
2. **No Direct Push to Main**: Agent **禁止**直接推送任何更改到 `main` 分支
|
||||
3. **No Auto-Merge**: Agent **禁止**在未经用户明确批准的情况下合并任何分支到 `main`
|
||||
4. **User Approval Required**: 任何影响 `main` 分支的操作(push、merge、rebase)都需要用户明确批准
|
||||
|
||||
> [!CAUTION]
|
||||
> 违反上述规则可能导致代码库不稳定或触发意外的 CI/CD 流程。Agent 应始终在功能分支上工作,并让用户决定何时合并到主分支。
|
||||
|
||||
---
|
||||
|
||||
## ⏳ 长时间运行任务通知 (Long-running Task Notifications)
|
||||
|
||||
如果一个前台任务(Foreground Task)的运行时间预计超过 **3秒**,必须实现用户通知机制,以避免用户感到困惑。
|
||||
|
||||
**要求 (Requirements):**
|
||||
|
||||
1. **初始通知 (Initial Notification)**: 任务开始时**立即**发送第一条通知,告知用户正在处理中(例如:“正在使用 AI 生成中...”)。
|
||||
2. **周期性通知 (Periodic Notification)**: 之后每隔 **5秒** 发送一次通知,告知用户任务仍在运行中。
|
||||
3. **完成清理 (Cleanup)**: 任务完成后,应自动取消通知任务。
|
||||
|
||||
**代码示例 (Code Example):**
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
async def long_running_task_with_notification(self, event_emitter, ...):
|
||||
# 定义实际任务
|
||||
async def actual_task():
|
||||
# ... 执行耗时操作 ...
|
||||
return result
|
||||
|
||||
# 定义通知任务
|
||||
async def notification_task():
|
||||
# 立即发送首次通知
|
||||
if event_emitter:
|
||||
await self._send_notification(event_emitter, "info", "正在使用 AI 生成中...")
|
||||
|
||||
# 之后每5秒通知一次
|
||||
while True:
|
||||
await asyncio.sleep(5)
|
||||
if event_emitter:
|
||||
await self._send_notification(event_emitter, "info", "仍在处理中,请耐心等待...")
|
||||
|
||||
# 并发运行任务
|
||||
task_future = asyncio.ensure_future(actual_task())
|
||||
notify_future = asyncio.ensure_future(notification_task())
|
||||
|
||||
# 等待任务完成
|
||||
done, pending = await asyncio.wait(
|
||||
[task_future, notify_future],
|
||||
return_when=asyncio.FIRST_COMPLETED
|
||||
)
|
||||
|
||||
# 取消通知任务
|
||||
if not notify_future.done():
|
||||
notify_future.cancel()
|
||||
|
||||
# 获取结果
|
||||
if task_future in done:
|
||||
return task_future.result()
|
||||
```
|
||||
|
||||
48
.github/workflows/release.yml
vendored
48
.github/workflows/release.yml
vendored
@@ -54,6 +54,9 @@ permissions:
|
||||
jobs:
|
||||
check-changes:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
LANG: en_US.UTF-8
|
||||
LC_ALL: en_US.UTF-8
|
||||
outputs:
|
||||
has_changes: ${{ steps.detect.outputs.has_changes }}
|
||||
changed_plugins: ${{ steps.detect.outputs.changed_plugins }}
|
||||
@@ -65,6 +68,12 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config --global core.quotepath false
|
||||
git config --global i18n.commitencoding utf-8
|
||||
git config --global i18n.logoutputencoding utf-8
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -131,6 +140,7 @@ jobs:
|
||||
|
||||
echo "changed_plugins<<EOF" >> $GITHUB_OUTPUT
|
||||
cat changed_files.txt >> $GITHUB_OUTPUT
|
||||
echo "" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
@@ -138,6 +148,7 @@ jobs:
|
||||
{
|
||||
echo 'release_notes<<EOF'
|
||||
cat changes.md
|
||||
echo ""
|
||||
echo 'EOF'
|
||||
} >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -145,6 +156,10 @@ jobs:
|
||||
needs: check-changes
|
||||
if: needs.check-changes.outputs.has_changes == 'true' || github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
LANG: en_US.UTF-8
|
||||
LC_ALL: en_US.UTF-8
|
||||
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -152,6 +167,12 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config --global core.quotepath false
|
||||
git config --global i18n.commitencoding utf-8
|
||||
git config --global i18n.logoutputencoding utf-8
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -205,6 +226,17 @@ jobs:
|
||||
echo "=== Collected Files ==="
|
||||
find release_plugins -name "*.py" -type f | head -20
|
||||
|
||||
- name: Debug Filenames
|
||||
run: |
|
||||
python3 -c "import sys; print(f'Filesystem encoding: {sys.getfilesystemencoding()}')"
|
||||
ls -R release_plugins
|
||||
|
||||
- name: Upload Debug Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: debug-plugins
|
||||
path: release_plugins/
|
||||
|
||||
- name: Get commit messages
|
||||
id: commits
|
||||
if: github.event_name == 'push'
|
||||
@@ -220,8 +252,9 @@ jobs:
|
||||
{
|
||||
echo 'commits<<EOF'
|
||||
echo "$COMMITS"
|
||||
echo ""
|
||||
echo 'EOF'
|
||||
} >> $GITHUB_OUTPUT
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Generate release notes
|
||||
id: notes
|
||||
@@ -301,10 +334,21 @@ jobs:
|
||||
prerelease: ${{ github.event.inputs.prerelease || false }}
|
||||
files: |
|
||||
plugin_versions.json
|
||||
release_plugins/**/*.py
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload Release Assets
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Check if there are any .py files to upload
|
||||
if [ -d release_plugins ] && [ -n "$(find release_plugins -type f -name '*.py' 2>/dev/null)" ]; then
|
||||
echo "Uploading plugin files..."
|
||||
find release_plugins -type f -name "*.py" -print0 | xargs -0 gh release upload ${{ steps.version.outputs.version }} --clobber
|
||||
else
|
||||
echo "No plugin files to upload. Skipping asset upload."
|
||||
fi
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "## 🚀 Release Created Successfully!" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
Open WebUI 通过文件顶部的特定格式注释来识别和展示插件信息。
|
||||
|
||||
**代码示例 (`思维导图.py`):**
|
||||
**代码示例 (`smart_mind_map_cn.py`):**
|
||||
|
||||
```python
|
||||
"""
|
||||
@@ -45,7 +45,7 @@ description: 智能分析文本内容,生成交互式思维导图,帮助用户
|
||||
|
||||
通过在 `Action` 类内部定义一个 `Valves` Pydantic 模型,可以为插件创建可在 Web UI 中配置的参数。
|
||||
|
||||
**代码示例 (`思维导图.py`):**
|
||||
**代码示例 (`smart_mind_map_cn.py`):**
|
||||
|
||||
```python
|
||||
class Action:
|
||||
@@ -83,7 +83,7 @@ class Action:
|
||||
|
||||
`action` 方法是插件的执行入口,它是一个异步函数,接收 Open WebUI 传入的上下文信息。
|
||||
|
||||
**代码示例 (`思维导图.py`):**
|
||||
**代码示例 (`smart_mind_map_cn.py`):**
|
||||
|
||||
```python
|
||||
async def action(
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
# Export to Excel
|
||||
|
||||
<span class="category-badge action">Action</span>
|
||||
<span class="version-badge">v0.3.4</span>
|
||||
<span class="version-badge">v0.3.7</span>
|
||||
|
||||
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.
|
||||
### What's New in v0.3.6
|
||||
- **OpenWebUI-Style Theme**: Modern dark header with light gray zebra striping for better readability.
|
||||
- **Zebra Striping**: Alternating row colors for improved visual scanning.
|
||||
- **Smart Data Type Conversion**: Automatically converts columns to numeric or datetime types.
|
||||
- **Full Cell Bold/Italic**: Supports Markdown bold/italic formatting in Excel.
|
||||
- **Partial Markdown Cleanup**: Removes partial Markdown symbols for cleaner output.
|
||||
- **Export Scope**: Choose between "Last Message" or "All Messages".
|
||||
- **Smart Sheet Naming**: Names sheets based on Markdown headers or message index.
|
||||
- **Smart Filename Generation**: Generates filenames based on Chat Title, AI Summary, or Markdown Headers.
|
||||
- **AI Title Generation**: Supports using a specific model (`MODEL_ID`) for title generation with progress notifications.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
# Export to Excel(导出到 Excel)
|
||||
|
||||
<span class="category-badge action">Action</span>
|
||||
<span class="version-badge">v0.3.4</span>
|
||||
<span class="version-badge">v0.3.7</span>
|
||||
|
||||
将聊天记录导出为 Excel 表格,便于分析、归档和分享。
|
||||
|
||||
|
||||
## v0.3.4 更新内容
|
||||
|
||||
- **智能文件名生成**:支持根据对话标题、AI 总结或 Markdown 标题生成文件名。
|
||||
- **配置选项**:新增 `TITLE_SOURCE` 设置,用于控制文件名生成策略。
|
||||
### v0.3.6 更新内容
|
||||
- **OpenWebUI 风格主题**:现代深灰表头,搭配浅灰斑马纹,提升可读性。
|
||||
- **斑马纹效果**:隔行变色,方便视觉扫描。
|
||||
- **智能数据类型转换**:自动将列转换为数字或日期类型。
|
||||
- **全单元格粗体/斜体**:支持 Markdown 粗体/斜体格式。
|
||||
- **部分 Markdown 清理**:移除部分 Markdown 符号,输出更整洁。
|
||||
- **导出范围**:可选择导出"最后一条消息"或"所有消息"。
|
||||
- **智能 Sheet 命名**:根据 Markdown 标题或消息索引命名 Sheet。
|
||||
- **智能文件名生成**:支持对话标题、AI 总结或 Markdown 标题生成文件名。
|
||||
- **AI 标题生成**:支持指定模型 (`MODEL_ID`) 生成标题,并提供生成进度通知。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Export to Word
|
||||
|
||||
<span class="category-badge action">Action</span>
|
||||
<span class="version-badge">v0.1.0</span>
|
||||
<span class="version-badge">v0.2.0</span>
|
||||
|
||||
Export chat conversations to Word (.docx) with Markdown formatting, syntax highlighting, and smarter filenames.
|
||||
Export conversation to Word (.docx) with **syntax highlighting**, **native math equations**, **Mermaid diagrams**, **citations**, and **enhanced table formatting**.
|
||||
|
||||
---
|
||||
|
||||
@@ -13,11 +13,17 @@ The Export to Word plugin converts chat messages from Markdown to a polished Wor
|
||||
|
||||
## Features
|
||||
|
||||
- :material-file-word-box: **DOCX Export**: Generate Word files with one click
|
||||
- :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**: Configurable title source (Chat Title, AI Generated, or Markdown Title)
|
||||
- :material-file-word-box: **One-Click Export**: Adds an "Export to Word" action button to the chat.
|
||||
- :material-format-bold: **Markdown Conversion**: Converts Markdown syntax to Word formatting (headings, bold, italic, code, tables, lists).
|
||||
- :material-code-tags: **Syntax Highlighting**: Code blocks are highlighted with Pygments (supports 500+ languages).
|
||||
- :material-sigma: **Native Math Equations**: LaTeX math (`$$...$$`, `\[...\]`, `$...$`, `\(...\)`) converted to editable Word equations.
|
||||
- :material-graph: **Mermaid Diagrams**: Mermaid flowcharts and sequence diagrams rendered as images in the document.
|
||||
- :material-book-open-page-variant: **Citations & References**: Auto-generates a References section from OpenWebUI sources with clickable citation links.
|
||||
- :material-brain-off: **Reasoning Stripping**: Automatically removes AI thinking blocks (`<think>`, `<analysis>`) from exports.
|
||||
- :material-table: **Enhanced Tables**: Smart column widths, column alignment (`:---`, `---:`, `:---:`), header row repeat across pages.
|
||||
- :material-format-quote-close: **Blockquote Support**: Markdown blockquotes are rendered with left border and gray styling.
|
||||
- :material-translate: **Multi-language Support**: Properly handles both Chinese and English text.
|
||||
- :material-file-document-outline: **Smarter Filenames**: Configurable title source (Chat Title, AI Generated, or Markdown Title).
|
||||
|
||||
---
|
||||
|
||||
@@ -25,9 +31,14 @@ The Export to Word plugin converts chat messages from Markdown to a polished Wor
|
||||
|
||||
You can configure the following settings via the **Valves** button in the plugin settings:
|
||||
|
||||
| Valve | Description | Default |
|
||||
| :------------- | :------------------------------------------------------------------------------------------ | :----------- |
|
||||
| Valve | Description | Default |
|
||||
| :--- | :--- | :--- |
|
||||
| `TITLE_SOURCE` | Source for document title/filename. Options: `chat_title`, `ai_generated`, `markdown_title` | `chat_title` |
|
||||
| `MERMAID_JS_URL` | URL for the Mermaid.js library (for diagram rendering). | `https://cdn.jsdelivr.net/npm/mermaid@10.9.1/dist/mermaid.min.js` |
|
||||
| `MERMAID_PNG_SCALE` | Scale factor for Mermaid PNG generation (Resolution). Higher = clearer but larger file size. | `3.0` |
|
||||
| `MERMAID_DISPLAY_SCALE` | Scale factor for Mermaid visual size in Word. >1.0 to enlarge, <1.0 to shrink. | `1.5` |
|
||||
| `MERMAID_OPTIMIZE_LAYOUT` | Automatically convert LR (Left-Right) flowcharts to TD (Top-Down) for better fit. | `True` |
|
||||
| `MERMAID_CAPTIONS_ENABLE` | Enable/disable figure captions for Mermaid diagrams. | `True` |
|
||||
|
||||
---
|
||||
|
||||
@@ -47,31 +58,37 @@ You can configure the following settings via the **Valves** button in the plugin
|
||||
|
||||
---
|
||||
|
||||
## Supported Markdown
|
||||
## Supported Markdown Syntax
|
||||
|
||||
| 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>``` code block ```</code> | 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**` 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 | **Enhanced table** with smart widths |
|
||||
| `---` or `***` | Horizontal rule |
|
||||
| `$$LaTeX$$` or `\[LaTeX\]` | **Native Word equation** (display) |
|
||||
| `$LaTeX$` or `\(LaTeX\)` | **Native Word equation** (inline) |
|
||||
| ` ```mermaid ... ``` ` | **Mermaid diagram** as image |
|
||||
| `[1]` citation markers | **Clickable links** to References |
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
!!! note "Prerequisites"
|
||||
- `python-docx==1.1.2` (document generation)
|
||||
- `Pygments>=2.15.0` (syntax highlighting, optional but recommended)
|
||||
- `python-docx==1.1.2` - Word document generation
|
||||
- `Pygments>=2.15.0` - Syntax highlighting
|
||||
- `latex2mathml` - LaTeX to MathML conversion
|
||||
- `mathml2omml` - MathML to Office Math (OMML) conversion
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Export to Word(导出为 Word)
|
||||
|
||||
<span class="category-badge action">Action</span>
|
||||
<span class="version-badge">v0.1.0</span>
|
||||
<span class="version-badge">v0.2.0</span>
|
||||
|
||||
将聊天记录按 Markdown 格式导出为 Word (.docx),支持语法高亮、引用样式和更智能的文件命名。
|
||||
将当前对话导出为完美格式的 Word 文档,支持**代码语法高亮**、**原生数学公式**、**Mermaid 图表**、**引用资料**以及**增强表格**渲染。
|
||||
|
||||
---
|
||||
|
||||
@@ -13,11 +13,17 @@ Export to Word 插件会把聊天消息从 Markdown 转成精致的 Word 文档
|
||||
|
||||
## 功能特性
|
||||
|
||||
- :material-file-word-box: **DOCX 导出**:一键生成 Word 文件
|
||||
- :material-format-bold: **丰富 Markdown 支持**:标题、粗斜体、列表、表格
|
||||
- :material-code-tags: **语法高亮**:Pygments 驱动的代码块上色
|
||||
- :material-format-quote-close: **引用样式**:左侧边框的灰色斜体引用
|
||||
- :material-file-document-outline: **智能文件名**:可配置标题来源(对话标题、AI 生成或 Markdown 标题)
|
||||
- :material-file-word-box: **一键导出**:在聊天界面添加"导出为 Word"动作按钮。
|
||||
- :material-format-bold: **Markdown 转换**:将 Markdown 语法转换为 Word 格式(标题、粗体、斜体、代码、表格、列表)。
|
||||
- :material-code-tags: **代码语法高亮**:使用 Pygments 库为代码块添加语法高亮(支持 500+ 种语言)。
|
||||
- :material-sigma: **原生数学公式**:LaTeX 公式(`$$...$$`、`\[...\]`、`$...$`、`\(...\)`)转换为可编辑的 Word 公式。
|
||||
- :material-graph: **Mermaid 图表**:Mermaid 流程图和时序图渲染为文档中的图片。
|
||||
- :material-book-open-page-variant: **引用与参考**:自动从 OpenWebUI 来源生成参考资料章节,支持可点击的引用链接。
|
||||
- :material-brain-off: **移除思考过程**:自动移除 AI 思考块(`<think>`、`<analysis>`)。
|
||||
- :material-table: **增强表格**:智能列宽、列对齐(`:---`、`---:`、`:---:`)、表头跨页重复。
|
||||
- :material-format-quote-close: **引用块支持**:Markdown 引用块渲染为带左侧边框的灰色斜体样式。
|
||||
- :material-translate: **多语言支持**:正确处理中文和英文文本,无乱码问题。
|
||||
- :material-file-document-outline: **智能文件名**:可配置标题来源(对话标题、AI 生成或 Markdown 标题)。
|
||||
|
||||
---
|
||||
|
||||
@@ -25,9 +31,14 @@ Export to Word 插件会把聊天消息从 Markdown 转成精致的 Word 文档
|
||||
|
||||
您可以通过插件设置中的 **Valves** 按钮配置以下选项:
|
||||
|
||||
| Valve | 说明 | 默认值 |
|
||||
| :------------- | :--------------------------------------------------------------------------------------------------------------- | :----------- |
|
||||
| Valve | 说明 | 默认值 |
|
||||
| :--- | :--- | :--- |
|
||||
| `TITLE_SOURCE` | 文档标题/文件名的来源。选项:`chat_title` (对话标题), `ai_generated` (AI 生成), `markdown_title` (Markdown 标题) | `chat_title` |
|
||||
| `MERMAID_JS_URL` | Mermaid.js 库的 URL(用于图表渲染)。 | `https://cdn.jsdelivr.net/npm/mermaid@10.9.1/dist/mermaid.min.js` |
|
||||
| `MERMAID_PNG_SCALE` | Mermaid PNG 生成缩放比例(分辨率)。越高越清晰但文件越大。 | `3.0` |
|
||||
| `MERMAID_DISPLAY_SCALE` | Mermaid 在 Word 中的显示比例(视觉大小)。>1.0 放大, <1.0 缩小。 | `1.5` |
|
||||
| `MERMAID_OPTIMIZE_LAYOUT` | 优化 Mermaid 布局: 自动将 LR (左右) 转换为 TD (上下) 以适应页面。 | `True` |
|
||||
| `MERMAID_CAPTIONS_ENABLE` | 启用/禁用 Mermaid 图表的图注。 | `True` |
|
||||
|
||||
---
|
||||
|
||||
@@ -47,23 +58,27 @@ Export to Word 插件会把聊天消息从 Markdown 转成精致的 Word 文档
|
||||
|
||||
---
|
||||
|
||||
## 支持的 Markdown
|
||||
## 支持的 Markdown 语法
|
||||
|
||||
| 语法 | Word 效果 |
|
||||
| :-------------------------- | :------------------ |
|
||||
| `# 标题1` 到 `###### 标题6` | 标题级别 1-6 |
|
||||
| `**粗体**` / `__粗体__` | 粗体文本 |
|
||||
| `*斜体*` / `_斜体_` | 斜体文本 |
|
||||
| `***粗斜体***` | 粗体 + 斜体 |
|
||||
| `` `行内代码` `` | 等宽字体 + 灰色背景 |
|
||||
| <code>``` 代码块 ```</code> | 语法高亮代码块 |
|
||||
| `> 引用文本` | 左侧边框的灰色斜体 |
|
||||
| `[链接](url)` | 蓝色下划线链接 |
|
||||
| `~~删除线~~` | 删除线 |
|
||||
| `- 项目` / `* 项目` | 无序列表 |
|
||||
| `1. 项目` | 有序列表 |
|
||||
| Markdown 表格 | 带边框表格 |
|
||||
| `---` / `***` | 水平分割线 |
|
||||
| 语法 | Word 效果 |
|
||||
| :--- | :--- |
|
||||
| `# 标题1` 到 `###### 标题6` | 标题级别 1-6 |
|
||||
| `**粗体**` / `__粗体__` | 粗体文本 |
|
||||
| `*斜体*` / `_斜体_` | 斜体文本 |
|
||||
| `***粗斜体***` | 粗体 + 斜体 |
|
||||
| `` `行内代码` `` | 等宽字体 + 灰色背景 |
|
||||
| <code>``` 代码块 ```</code> | 语法高亮代码块 |
|
||||
| `> 引用文本` | 左侧边框的灰色斜体 |
|
||||
| `[链接](url)` | 蓝色下划线链接 |
|
||||
| `~~删除线~~` | 删除线 |
|
||||
| `- 项目` / `* 项目` | 无序列表 |
|
||||
| `1. 项目` | 有序列表 |
|
||||
| Markdown 表格 | **增强表格**(智能列宽) |
|
||||
| `---` / `***` | 水平分割线 |
|
||||
| `$$LaTeX$$` 或 `\[LaTeX\]` | **原生 Word 公式**(块级) |
|
||||
| `$LaTeX$` 或 `\(LaTeX\)` | **原生 Word 公式**(行内) |
|
||||
| ` ```mermaid ... ``` ` | **Mermaid 图表**(图片形式) |
|
||||
| `[1]` 引用标记 | **可点击链接**到参考资料 |
|
||||
|
||||
---
|
||||
|
||||
@@ -71,7 +86,9 @@ Export to Word 插件会把聊天消息从 Markdown 转成精致的 Word 文档
|
||||
|
||||
!!! note "前置条件"
|
||||
- `python-docx==1.1.2`(文档生成)
|
||||
- `Pygments>=2.15.0`(语法高亮,建议安装)
|
||||
- `Pygments>=2.15.0`(语法高亮)
|
||||
- `latex2mathml`(LaTeX 转 MathML)
|
||||
- `mathml2omml`(MathML 转 Office Math)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -53,17 +53,17 @@ Actions are interactive plugins that:
|
||||
|
||||
Export chat conversations to Excel spreadsheet format for analysis and archiving.
|
||||
|
||||
**Version:** 0.3.4
|
||||
**Version:** 0.3.7
|
||||
|
||||
[:octicons-arrow-right-24: Documentation](export-to-excel.md)
|
||||
|
||||
- :material-file-word-box:{ .lg .middle } **Export to Word**
|
||||
- :material-file-word-box:{ .lg .middle } **Export to Word (Enhanced Formatting)**
|
||||
|
||||
---
|
||||
|
||||
Export chat content as Word (.docx) with Markdown formatting and syntax highlighting.
|
||||
|
||||
**Version:** 0.1.0
|
||||
Export the current conversation to a formatted Word doc with **syntax highlighting**, **native math equations**, **Mermaid diagrams**, **citations**, and **enhanced table formatting**.
|
||||
|
||||
**Version:** 0.2.0
|
||||
|
||||
[:octicons-arrow-right-24: Documentation](export-to-word.md)
|
||||
|
||||
|
||||
@@ -53,17 +53,17 @@ Actions 是交互式插件,能够:
|
||||
|
||||
将聊天记录导出为 Excel 电子表格,方便分析或归档。
|
||||
|
||||
**版本:** 0.3.4
|
||||
**版本:** 0.3.7
|
||||
|
||||
[:octicons-arrow-right-24: 查看文档](export-to-excel.md)
|
||||
|
||||
- :material-file-word-box:{ .lg .middle } **Export to Word**
|
||||
- :material-file-word-box:{ .lg .middle } **Word 导出 (格式增强)**
|
||||
|
||||
---
|
||||
|
||||
将聊天内容按 Markdown 格式导出为 Word (.docx),支持语法高亮。
|
||||
将当前对话导出为完美格式的 Word 文档,支持**代码语法高亮**、**原生数学公式**、**Mermaid 图表**、**引用资料**以及**增强表格**渲染。
|
||||
|
||||
**版本:** 0.1.0
|
||||
**版本:** 0.2.0
|
||||
|
||||
[:octicons-arrow-right-24: 查看文档](export-to-word.md)
|
||||
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
# Export to Word
|
||||
|
||||
Export current conversation from Markdown to Word (.docx) with **syntax highlighting**, **blockquote support**, and smarter filenames.
|
||||
Export conversation to Word (.docx) with **syntax highlighting**, **native math equations**, **Mermaid diagrams**, **citations**, and **enhanced table formatting**.
|
||||
|
||||
## 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).
|
||||
- **Native Math Equations**: LaTeX math (`$$...$$`, `\[...\]`, `$...$`, `\(...\)`) converted to editable Word equations.
|
||||
- **Mermaid Diagrams**: Mermaid flowcharts and sequence diagrams rendered as images in the document.
|
||||
- **Citations & References**: Auto-generates a References section from OpenWebUI sources with clickable citation links.
|
||||
- **Reasoning Stripping**: Automatically removes AI thinking blocks (`<think>`, `<analysis>`) from exports.
|
||||
- **Enhanced Tables**: Smart column widths, column alignment (`:---`, `---:`, `:---:`), header row repeat across pages.
|
||||
- **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.
|
||||
- **Multi-language Support**: Properly handles both Chinese and English text.
|
||||
- **Smarter Filenames**: Configurable title source (Chat Title, AI Generated, or Markdown Title).
|
||||
|
||||
## Configuration
|
||||
@@ -19,24 +24,33 @@ You can configure the following settings via the **Valves** button in the plugin
|
||||
- `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.
|
||||
- **MERMAID_JS_URL**: URL for the Mermaid.js library (for diagram rendering).
|
||||
- **MERMAID_PNG_SCALE**: Scale factor for Mermaid PNG generation (Resolution). Default: `3.0`.
|
||||
- **MERMAID_DISPLAY_SCALE**: Scale factor for Mermaid visual size in Word. Default: `1.5`.
|
||||
- **MERMAID_OPTIMIZE_LAYOUT**: Automatically convert LR (Left-Right) flowcharts to TD (Top-Down). Default: `True`.
|
||||
- **MERMAID_CAPTIONS_ENABLE**: Enable/disable figure captions for Mermaid diagrams.
|
||||
|
||||
## 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 ``` ` | **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 |
|
||||
| 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 | **Enhanced table** with smart widths |
|
||||
| `---` or `***` | Horizontal rule |
|
||||
| `$$LaTeX$$` or `\[LaTeX\]` | **Native Word equation** (display) |
|
||||
| `$LaTeX$` or `\(LaTeX\)` | **Native Word equation** (inline) |
|
||||
| ` ```mermaid ... ``` ` | **Mermaid diagram** as image |
|
||||
| `[1]` citation markers | **Clickable links** to References |
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -44,19 +58,14 @@ You can configure the following settings via the **Valves** button in the plugin
|
||||
2. In any chat, click the "Export to Word" button.
|
||||
3. The .docx file will be automatically downloaded to your device.
|
||||
|
||||
|
||||
### Notes
|
||||
|
||||
- Title detection only considers h1/h2 headings.
|
||||
- If the request carries `chat_id` (body or metadata), the plugin will fetch the chat title from the database when the body lacks one.
|
||||
- Default fonts: Times New Roman (en), SimSun/SimHei (zh), Consolas (code).
|
||||
|
||||
### Requirements
|
||||
## Requirements
|
||||
|
||||
- `python-docx==1.1.2` - Word document generation
|
||||
- `Pygments>=2.15.0` - Syntax highlighting (optional but recommended)
|
||||
- `Pygments>=2.15.0` - Syntax highlighting
|
||||
- `latex2mathml` - LaTeX to MathML conversion
|
||||
- `mathml2omml` - MathML to Office Math (OMML) conversion
|
||||
|
||||
Both are declared in the plugin docstring; ensure they are installed in your environment.
|
||||
All dependencies are declared in the plugin docstring.
|
||||
|
||||
## Font Configuration
|
||||
|
||||
@@ -64,6 +73,18 @@ Both are declared in the plugin docstring; ensure they are installed in your env
|
||||
- **Chinese Text**: SimSun (宋体) for body, SimHei (黑体) for headings
|
||||
- **Code**: Consolas
|
||||
|
||||
## Changelog
|
||||
|
||||
### v0.2.0
|
||||
- Added native math equation support (LaTeX → OMML)
|
||||
- Added Mermaid diagram rendering
|
||||
- Added citations and references section generation
|
||||
- Added automatic reasoning block stripping
|
||||
- Enhanced table formatting with smart column widths and alignment
|
||||
|
||||
### v0.1.1
|
||||
- Initial release with basic Markdown to Word conversion
|
||||
|
||||
## Author
|
||||
|
||||
Fu-Jie
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
# 导出为 Word
|
||||
|
||||
将当前对话内容从 Markdown 转换并导出为 Word (.docx) 文件,支持**代码语法高亮**、**引用块样式**和更智能的文件命名。
|
||||
将对话导出为 Word (.docx),支持**代码语法高亮**、**原生数学公式**、**Mermaid 图表**、**引用参考**和**增强表格格式**。
|
||||
|
||||
## 功能特点
|
||||
|
||||
- **一键导出**:在聊天界面添加“导出为 Word”动作按钮。
|
||||
- **一键导出**:在聊天界面添加"导出为 Word"动作按钮。
|
||||
- **Markdown 转换**:将 Markdown 语法转换为 Word 格式(标题、粗体、斜体、代码、表格、列表)。
|
||||
- **代码语法高亮**:使用 Pygments 库为代码块添加语法高亮(支持 500+ 种语言)。
|
||||
- **引用块支持**:Markdown 引用块会渲染为带左侧边框的灰色斜体样式。
|
||||
- **原生数学公式**:LaTeX 公式(`$$...$$`、`\[...\]`、`$...$`、`\(...\)`)转换为可编辑的 Word 公式。
|
||||
- **Mermaid 图表**:Mermaid 流程图和时序图渲染为文档中的图片。
|
||||
- **引用与参考**:自动从 OpenWebUI 来源生成参考资料章节,支持可点击的引用链接。
|
||||
- **移除思考过程**:自动移除 AI 思考块(`<think>`、`<analysis>`)。
|
||||
- **增强表格**:智能列宽、列对齐(`:---`、`---:`、`:---:`)、表头跨页重复。
|
||||
- **引用块支持**:Markdown 引用块渲染为带左侧边框的灰色斜体样式。
|
||||
- **多语言支持**:正确处理中文和英文文本,无乱码问题。
|
||||
- **更智能的文件名**:可配置标题来源(对话标题、AI 生成或 Markdown 标题)。
|
||||
- **智能文件名**:可配置标题来源(对话标题、AI 生成或 Markdown 标题)。
|
||||
|
||||
## 配置 (Configuration)
|
||||
## 配置
|
||||
|
||||
您可以通过插件设置中的 **Valves** 按钮配置以下选项:
|
||||
|
||||
@@ -19,24 +24,33 @@
|
||||
- `chat_title`:使用对话标题(默认)。
|
||||
- `ai_generated`:使用 AI 根据内容生成简短标题。
|
||||
- `markdown_title`:从 Markdown 内容中提取第一个一级或二级标题。
|
||||
- **MERMAID_JS_URL**:Mermaid.js 库的 URL(用于图表渲染)。
|
||||
- **MERMAID_PNG_SCALE**:Mermaid PNG 生成缩放比例(分辨率)。默认:`3.0`。
|
||||
- **MERMAID_DISPLAY_SCALE**:Mermaid 在 Word 中的显示比例(视觉大小)。默认:`1.5`。
|
||||
- **MERMAID_OPTIMIZE_LAYOUT**:自动将 LR(左右)流程图转换为 TD(上下)。默认:`True`。
|
||||
- **MERMAID_CAPTIONS_ENABLE**:启用/禁用 Mermaid 图表的图注。
|
||||
|
||||
## 支持的 Markdown 语法
|
||||
|
||||
| 语法 | Word 效果 |
|
||||
| :-------------------------- | :----------------------- |
|
||||
| `# 标题1` 到 `###### 标题6` | 标题级别 1-6 |
|
||||
| `**粗体**` 或 `__粗体__` | 粗体文本 |
|
||||
| `*斜体*` 或 `_斜体_` | 斜体文本 |
|
||||
| `***粗斜体***` | 粗体 + 斜体 |
|
||||
| `` `行内代码` `` | 等宽字体 + 灰色背景 |
|
||||
| ` ``` 代码块 ``` ` | **语法高亮**的代码块 |
|
||||
| `> 引用文本` | 带左侧边框的灰色斜体文本 |
|
||||
| `[链接](url)` | 蓝色下划线链接文本 |
|
||||
| `~~删除线~~` | 删除线文本 |
|
||||
| `- 项目` 或 `* 项目` | 无序列表 |
|
||||
| `1. 项目` | 有序列表 |
|
||||
| Markdown 表格 | 带边框表格 |
|
||||
| `---` 或 `***` | 水平分割线 |
|
||||
| 语法 | Word 效果 |
|
||||
| :---------------------------- | :-------------------------------- |
|
||||
| `# 标题1` 到 `###### 标题6` | 标题级别 1-6 |
|
||||
| `**粗体**` 或 `__粗体__` | 粗体文本 |
|
||||
| `*斜体*` 或 `_斜体_` | 斜体文本 |
|
||||
| `***粗斜体***` | 粗体 + 斜体 |
|
||||
| `` `行内代码` `` | 等宽字体 + 灰色背景 |
|
||||
| ` ``` 代码块 ``` ` | **语法高亮**的代码块 |
|
||||
| `> 引用文本` | 带左侧边框的灰色斜体文本 |
|
||||
| `[链接](url)` | 蓝色下划线链接文本 |
|
||||
| `~~删除线~~` | 删除线文本 |
|
||||
| `- 项目` 或 `* 项目` | 无序列表 |
|
||||
| `1. 项目` | 有序列表 |
|
||||
| Markdown 表格 | **增强表格**(智能列宽) |
|
||||
| `---` 或 `***` | 水平分割线 |
|
||||
| `$$LaTeX$$` 或 `\[LaTeX\]` | **原生 Word 公式**(块级) |
|
||||
| `$LaTeX$` 或 `\(LaTeX\)` | **原生 Word 公式**(行内) |
|
||||
| ` ```mermaid ... ``` ` | **Mermaid 图表**(图片形式) |
|
||||
| `[1]` 引用标记 | **可点击链接**到参考资料 |
|
||||
|
||||
## 使用方法
|
||||
|
||||
@@ -44,18 +58,14 @@
|
||||
2. 在任意对话中,点击"导出为 Word"按钮。
|
||||
3. .docx 文件将自动下载到你的设备。
|
||||
|
||||
### 说明
|
||||
|
||||
- 标题检测仅考虑一级/二级标题(h1/h2)。
|
||||
- 若请求体或 metadata 提供 `chat_id`,当正文缺少标题时会从数据库查询对话标题。
|
||||
- 默认字体:英文 Times New Roman,中文宋体/黑体,代码 Consolas。
|
||||
|
||||
### 依赖
|
||||
## 依赖
|
||||
|
||||
- `python-docx==1.1.2` - Word 文档生成
|
||||
- `Pygments>=2.15.0` - 语法高亮(可选但建议安装)
|
||||
- `Pygments>=2.15.0` - 语法高亮
|
||||
- `latex2mathml` - LaTeX 转 MathML
|
||||
- `mathml2omml` - MathML 转 Office Math (OMML)
|
||||
|
||||
两者已在插件文档字符串中声明,请确保环境已安装。
|
||||
所有依赖已在插件文档字符串中声明。
|
||||
|
||||
## 字体配置
|
||||
|
||||
@@ -63,6 +73,18 @@
|
||||
- **中文文本**:宋体(正文)、黑体(标题)
|
||||
- **代码**:Consolas
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v0.2.0
|
||||
- 新增原生数学公式支持(LaTeX → OMML)
|
||||
- 新增 Mermaid 图表渲染
|
||||
- 新增引用与参考资料章节生成
|
||||
- 新增自动移除 AI 思考块
|
||||
- 增强表格格式(智能列宽、对齐)
|
||||
|
||||
### v0.1.1
|
||||
- 初始版本,支持基本 Markdown 转 Word
|
||||
|
||||
## 作者
|
||||
|
||||
Fu-Jie
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1840
plugins/actions/export_to_docx/export_to_word_cn.py
Normal file
1840
plugins/actions/export_to_docx/export_to_word_cn.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,882 +0,0 @@
|
||||
"""
|
||||
title: 导出为 Word
|
||||
author: Fu-Jie
|
||||
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, Pygments>=2.15.0
|
||||
description: 将当前对话内容从 Markdown 转换并导出为 Word (.docx) 文件,支持代码语法高亮和引用块。
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import base64
|
||||
import datetime
|
||||
import io
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional, Callable, Awaitable, Any, List, Tuple
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor, Cm
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
|
||||
from docx.enum.table import WD_TABLE_ALIGNMENT
|
||||
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:
|
||||
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,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
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):
|
||||
self.valves = self.Valves()
|
||||
|
||||
async def _send_notification(self, emitter: Callable, type: str, content: str):
|
||||
await emitter(
|
||||
{"type": "notification", "data": {"type": type, "content": content}}
|
||||
)
|
||||
|
||||
async def action(
|
||||
self,
|
||||
body: dict,
|
||||
__user__=None,
|
||||
__event_emitter__=None,
|
||||
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
|
||||
__metadata__: Optional[dict] = None,
|
||||
__request__: Optional[Any] = None,
|
||||
):
|
||||
logger.info(f"action:{__name__}")
|
||||
|
||||
# 解析用户信息
|
||||
if isinstance(__user__, (list, tuple)):
|
||||
user_language = (
|
||||
__user__[0].get("language", "zh-CN") if __user__ else "zh-CN"
|
||||
)
|
||||
user_name = __user__[0].get("name", "用户") if __user__[0] else "用户"
|
||||
user_id = (
|
||||
__user__[0]["id"]
|
||||
if __user__ and "id" in __user__[0]
|
||||
else "unknown_user"
|
||||
)
|
||||
elif isinstance(__user__, dict):
|
||||
user_language = __user__.get("language", "zh-CN")
|
||||
user_name = __user__.get("name", "用户")
|
||||
user_id = __user__.get("id", "unknown_user")
|
||||
|
||||
if __event_emitter__:
|
||||
last_assistant_message = body["messages"][-1]
|
||||
|
||||
await __event_emitter__(
|
||||
{
|
||||
"type": "status",
|
||||
"data": {"description": "正在转换为 Word 文档...", "done": False},
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
message_content = last_assistant_message["content"]
|
||||
|
||||
if not message_content or not message_content.strip():
|
||||
await self._send_notification(
|
||||
__event_emitter__, "error", "没有找到可导出的内容!"
|
||||
)
|
||||
return
|
||||
|
||||
# 生成文件名
|
||||
title = ""
|
||||
chat_id = self.extract_chat_id(body, __metadata__)
|
||||
|
||||
# 直接通过 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":
|
||||
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 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=top_heading, has_h1=has_h1
|
||||
)
|
||||
|
||||
# 保存到内存
|
||||
doc_buffer = io.BytesIO()
|
||||
doc.save(doc_buffer)
|
||||
doc_buffer.seek(0)
|
||||
file_content = doc_buffer.read()
|
||||
base64_blob = base64.b64encode(file_content).decode("utf-8")
|
||||
|
||||
# 触发文件下载
|
||||
if __event_call__:
|
||||
await __event_call__(
|
||||
{
|
||||
"type": "execute",
|
||||
"data": {
|
||||
"code": f"""
|
||||
try {{
|
||||
const base64Data = "{base64_blob}";
|
||||
const binaryData = atob(base64Data);
|
||||
const arrayBuffer = new Uint8Array(binaryData.length);
|
||||
for (let i = 0; i < binaryData.length; i++) {{
|
||||
arrayBuffer[i] = binaryData.charCodeAt(i);
|
||||
}}
|
||||
const blob = new Blob([arrayBuffer], {{ type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }});
|
||||
const filename = "{filename}";
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.style.display = "none";
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
}} catch (error) {{
|
||||
console.error('触发下载时出错:', error);
|
||||
}}
|
||||
"""
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
await __event_emitter__(
|
||||
{
|
||||
"type": "status",
|
||||
"data": {"description": "Word 文档已导出", "done": True},
|
||||
}
|
||||
)
|
||||
|
||||
await self._send_notification(
|
||||
__event_emitter__, "success", f"已成功导出为 {filename}"
|
||||
)
|
||||
|
||||
return {"message": "下载事件已触发"}
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error exporting to Word: {str(e)}")
|
||||
await __event_emitter__(
|
||||
{
|
||||
"type": "status",
|
||||
"data": {
|
||||
"description": f"导出失败: {str(e)}",
|
||||
"done": True,
|
||||
},
|
||||
}
|
||||
)
|
||||
await self._send_notification(
|
||||
__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")
|
||||
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_title(self, body: dict) -> str:
|
||||
"""从请求体中提取会话标题"""
|
||||
if not isinstance(body, dict):
|
||||
return ""
|
||||
|
||||
candidates = []
|
||||
|
||||
for key in ("chat", "conversation"):
|
||||
if isinstance(body.get(key), dict):
|
||||
candidates.append(body.get(key, {}).get("title", ""))
|
||||
|
||||
for key in ("title", "chat_title"):
|
||||
value = body.get(key)
|
||||
if isinstance(value, str):
|
||||
candidates.append(value)
|
||||
|
||||
for candidate in candidates:
|
||||
if candidate and isinstance(candidate, str):
|
||||
return candidate.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:
|
||||
logger.warning(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 clean_filename(self, name: str) -> str:
|
||||
"""清理文件名中的非法字符"""
|
||||
return re.sub(r'[\\/*?:"<>|]', "", name).strip()[:50]
|
||||
|
||||
def markdown_to_docx(
|
||||
self, markdown_text: str, top_heading: str = "", has_h1: bool = False
|
||||
) -> Document:
|
||||
"""
|
||||
将 Markdown 文本转换为 Word 文档
|
||||
支持:标题、段落、粗体、斜体、代码块、列表、表格、链接
|
||||
"""
|
||||
doc = Document()
|
||||
|
||||
# 设置默认中文字体
|
||||
self.set_document_default_font(doc)
|
||||
|
||||
# 若正文无一级标题且有对话标题,则作为一级标题写入
|
||||
if top_heading and not has_h1:
|
||||
self.add_heading(doc, top_heading, 1)
|
||||
|
||||
lines = markdown_text.split("\n")
|
||||
i = 0
|
||||
in_code_block = False
|
||||
code_block_content = []
|
||||
code_block_lang = ""
|
||||
in_list = False
|
||||
list_items = []
|
||||
list_type = None # 'ordered' or 'unordered'
|
||||
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
|
||||
# 处理代码块
|
||||
if line.strip().startswith("```"):
|
||||
if not in_code_block:
|
||||
# 先处理之前积累的列表
|
||||
if in_list and list_items:
|
||||
self.add_list_to_doc(doc, list_items, list_type)
|
||||
list_items = []
|
||||
in_list = False
|
||||
|
||||
in_code_block = True
|
||||
code_block_lang = line.strip()[3:].strip()
|
||||
code_block_content = []
|
||||
else:
|
||||
# 代码块结束
|
||||
in_code_block = False
|
||||
self.add_code_block(
|
||||
doc, "\n".join(code_block_content), code_block_lang
|
||||
)
|
||||
code_block_content = []
|
||||
code_block_lang = ""
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if in_code_block:
|
||||
code_block_content.append(line)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# 处理表格
|
||||
if line.strip().startswith("|") and line.strip().endswith("|"):
|
||||
# 先处理之前积累的列表
|
||||
if in_list and list_items:
|
||||
self.add_list_to_doc(doc, list_items, list_type)
|
||||
list_items = []
|
||||
in_list = False
|
||||
|
||||
table_lines = []
|
||||
while i < len(lines) and lines[i].strip().startswith("|"):
|
||||
table_lines.append(lines[i])
|
||||
i += 1
|
||||
self.add_table(doc, table_lines)
|
||||
continue
|
||||
|
||||
# 处理标题
|
||||
header_match = re.match(r"^(#{1,6})\s+(.+)$", line.strip())
|
||||
if header_match:
|
||||
# 先处理之前积累的列表
|
||||
if in_list and list_items:
|
||||
self.add_list_to_doc(doc, list_items, list_type)
|
||||
list_items = []
|
||||
in_list = False
|
||||
|
||||
level = len(header_match.group(1))
|
||||
text = header_match.group(2)
|
||||
self.add_heading(doc, text, level)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# 处理无序列表
|
||||
unordered_match = re.match(r"^(\s*)[-*+]\s+(.+)$", line)
|
||||
if unordered_match:
|
||||
if not in_list or list_type != "unordered":
|
||||
if in_list and list_items:
|
||||
self.add_list_to_doc(doc, list_items, list_type)
|
||||
list_items = []
|
||||
in_list = True
|
||||
list_type = "unordered"
|
||||
indent = len(unordered_match.group(1)) // 2
|
||||
list_items.append((indent, unordered_match.group(2)))
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# 处理有序列表
|
||||
ordered_match = re.match(r"^(\s*)\d+[.)]\s+(.+)$", line)
|
||||
if ordered_match:
|
||||
if not in_list or list_type != "ordered":
|
||||
if in_list and list_items:
|
||||
self.add_list_to_doc(doc, list_items, list_type)
|
||||
list_items = []
|
||||
in_list = True
|
||||
list_type = "ordered"
|
||||
indent = len(ordered_match.group(1)) // 2
|
||||
list_items.append((indent, ordered_match.group(2)))
|
||||
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()):
|
||||
# 先处理之前积累的列表
|
||||
if in_list and list_items:
|
||||
self.add_list_to_doc(doc, list_items, list_type)
|
||||
list_items = []
|
||||
in_list = False
|
||||
|
||||
self.add_horizontal_rule(doc)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# 处理空行
|
||||
if not line.strip():
|
||||
# 列表结束
|
||||
if in_list and list_items:
|
||||
self.add_list_to_doc(doc, list_items, list_type)
|
||||
list_items = []
|
||||
in_list = False
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# 处理普通段落
|
||||
if in_list and list_items:
|
||||
self.add_list_to_doc(doc, list_items, list_type)
|
||||
list_items = []
|
||||
in_list = False
|
||||
|
||||
self.add_paragraph(doc, line)
|
||||
i += 1
|
||||
|
||||
# 处理剩余的列表
|
||||
if in_list and list_items:
|
||||
self.add_list_to_doc(doc, list_items, list_type)
|
||||
|
||||
return doc
|
||||
|
||||
def set_document_default_font(self, doc: Document):
|
||||
"""设置文档默认字体,确保中英文都正常显示"""
|
||||
# 设置正文样式
|
||||
style = doc.styles["Normal"]
|
||||
font = style.font
|
||||
font.name = "Times New Roman" # 英文字体
|
||||
font.size = Pt(11)
|
||||
|
||||
# 设置中文字体
|
||||
style._element.rPr.rFonts.set(qn("w:eastAsia"), "宋体")
|
||||
|
||||
# 设置段落格式
|
||||
paragraph_format = style.paragraph_format
|
||||
paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
paragraph_format.space_after = Pt(6)
|
||||
|
||||
def add_heading(self, doc: Document, text: str, level: int):
|
||||
"""添加标题"""
|
||||
# Word 标题级别从 0 开始,Markdown 从 1 开始
|
||||
heading_level = min(level, 9) # Word 最多支持 Heading 9
|
||||
heading = doc.add_heading(level=heading_level)
|
||||
|
||||
# 解析并添加格式化文本
|
||||
self.add_formatted_text(heading, text)
|
||||
|
||||
# 设置中文字体
|
||||
for run in heading.runs:
|
||||
run.font.name = "Times New Roman"
|
||||
run._element.rPr.rFonts.set(qn("w:eastAsia"), "黑体")
|
||||
run.font.color.rgb = RGBColor(0, 0, 0)
|
||||
|
||||
def add_paragraph(self, doc: Document, text: str):
|
||||
"""添加段落,支持内联格式"""
|
||||
paragraph = doc.add_paragraph()
|
||||
self.add_formatted_text(paragraph, text)
|
||||
|
||||
# 设置中文字体
|
||||
for run in paragraph.runs:
|
||||
run.font.name = "Times New Roman"
|
||||
run._element.rPr.rFonts.set(qn("w:eastAsia"), "宋体")
|
||||
|
||||
def add_formatted_text(self, paragraph, text: str):
|
||||
"""
|
||||
解析 Markdown 内联格式并添加到段落
|
||||
支持:粗体、斜体、行内代码、链接、删除线
|
||||
"""
|
||||
# 定义格式化模式
|
||||
patterns = [
|
||||
# 粗斜体 ***text*** 或 ___text___
|
||||
(r"\*\*\*(.+?)\*\*\*|___(.+?)___", {"bold": True, "italic": True}),
|
||||
# 粗体 **text** 或 __text__
|
||||
(r"\*\*(.+?)\*\*|__(.+?)__", {"bold": True}),
|
||||
# 斜体 *text* 或 _text_
|
||||
(
|
||||
r"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)|(?<!_)_(?!_)(.+?)(?<!_)_(?!_)",
|
||||
{"italic": True},
|
||||
),
|
||||
# 行内代码 `code`
|
||||
(r"`([^`]+)`", {"code": True}),
|
||||
# 链接 [text](url)
|
||||
(r"\[([^\]]+)\]\(([^)]+)\)", {"link": True}),
|
||||
# 删除线 ~~text~~
|
||||
(r"~~(.+?)~~", {"strike": True}),
|
||||
]
|
||||
|
||||
# 简化处理:逐段解析
|
||||
remaining = text
|
||||
last_end = 0
|
||||
|
||||
# 合并所有匹配项
|
||||
all_matches = []
|
||||
|
||||
for pattern, style in patterns:
|
||||
for match in re.finditer(pattern, text):
|
||||
# 获取匹配的文本内容
|
||||
groups = match.groups()
|
||||
matched_text = next((g for g in groups if g is not None), "")
|
||||
all_matches.append(
|
||||
{
|
||||
"start": match.start(),
|
||||
"end": match.end(),
|
||||
"text": matched_text,
|
||||
"style": style,
|
||||
"full_match": match.group(0),
|
||||
"url": (
|
||||
groups[1] if style.get("link") and len(groups) > 1 else None
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
# 按位置排序
|
||||
all_matches.sort(key=lambda x: x["start"])
|
||||
|
||||
# 移除重叠的匹配
|
||||
filtered_matches = []
|
||||
last_end = 0
|
||||
for m in all_matches:
|
||||
if m["start"] >= last_end:
|
||||
filtered_matches.append(m)
|
||||
last_end = m["end"]
|
||||
|
||||
# 构建最终文本
|
||||
pos = 0
|
||||
for match in filtered_matches:
|
||||
# 添加匹配前的普通文本
|
||||
if match["start"] > pos:
|
||||
plain_text = text[pos : match["start"]]
|
||||
if plain_text:
|
||||
paragraph.add_run(plain_text)
|
||||
|
||||
# 添加格式化文本
|
||||
style = match["style"]
|
||||
run_text = match["text"]
|
||||
|
||||
if style.get("link"):
|
||||
# 链接处理
|
||||
run = paragraph.add_run(run_text)
|
||||
run.font.color.rgb = RGBColor(0, 0, 255)
|
||||
run.font.underline = True
|
||||
elif style.get("code"):
|
||||
# 行内代码
|
||||
run = paragraph.add_run(run_text)
|
||||
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"), "E8E8E8")
|
||||
run._element.rPr.append(shading)
|
||||
else:
|
||||
run = paragraph.add_run(run_text)
|
||||
if style.get("bold"):
|
||||
run.bold = True
|
||||
if style.get("italic"):
|
||||
run.italic = True
|
||||
if style.get("strike"):
|
||||
run.font.strike = True
|
||||
|
||||
pos = match["end"]
|
||||
|
||||
# 添加剩余的普通文本
|
||||
if pos < len(text):
|
||||
paragraph.add_run(text[pos:])
|
||||
|
||||
def add_code_block(self, doc: Document, code: str, language: str = ""):
|
||||
"""添加代码块,支持语法高亮"""
|
||||
# 语法高亮颜色映射 (基于常见的 IDE 配色)
|
||||
TOKEN_COLORS = {
|
||||
Token.Keyword: RGBColor(0, 92, 197), # macOS 风格蓝 - 关键字
|
||||
Token.Keyword.Constant: RGBColor(0, 92, 197),
|
||||
Token.Keyword.Declaration: RGBColor(0, 92, 197),
|
||||
Token.Keyword.Namespace: RGBColor(0, 92, 197),
|
||||
Token.Keyword.Type: RGBColor(0, 92, 197),
|
||||
Token.Name.Function: RGBColor(0, 0, 0), # 函数名保持黑色
|
||||
Token.Name.Class: RGBColor(38, 82, 120), # 深青蓝 - 类名
|
||||
Token.Name.Decorator: RGBColor(170, 51, 0), # 暖橙 - 装饰器
|
||||
Token.Name.Builtin: RGBColor(0, 110, 71), # 墨绿 - 内置
|
||||
Token.String: RGBColor(196, 26, 22), # 红色 - 字符串
|
||||
Token.String.Doc: RGBColor(109, 120, 133), # 灰 - 文档字符串
|
||||
Token.Comment: RGBColor(109, 120, 133), # 灰 - 注释
|
||||
Token.Comment.Single: RGBColor(109, 120, 133),
|
||||
Token.Comment.Multiline: RGBColor(109, 120, 133),
|
||||
Token.Number: RGBColor(28, 0, 207), # 靛蓝 - 数字
|
||||
Token.Number.Integer: RGBColor(28, 0, 207),
|
||||
Token.Number.Float: RGBColor(28, 0, 207),
|
||||
Token.Operator: RGBColor(90, 99, 120), # 灰蓝 - 运算符
|
||||
Token.Punctuation: RGBColor(0, 0, 0), # 黑色 - 标点
|
||||
}
|
||||
|
||||
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(3) if language else Pt(6)
|
||||
paragraph.paragraph_format.space_after = Pt(6)
|
||||
|
||||
# 添加浅灰色背景
|
||||
shading = OxmlElement("w:shd")
|
||||
shading.set(qn("w:fill"), "F7F7F7")
|
||||
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:
|
||||
return
|
||||
|
||||
def _set_cell_shading(cell, fill: str):
|
||||
tc_pr = cell._element.get_or_add_tcPr()
|
||||
shd = OxmlElement("w:shd")
|
||||
shd.set(qn("w:fill"), fill)
|
||||
tc_pr.append(shd)
|
||||
|
||||
header_fill = "F2F2F2"
|
||||
zebra_fill = "FBFBFB"
|
||||
|
||||
# 解析表格数据
|
||||
rows = []
|
||||
for line in table_lines:
|
||||
cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
|
||||
# 跳过分隔行
|
||||
if all(re.fullmatch(r"[-:]+", cell) for cell in cells):
|
||||
continue
|
||||
rows.append(cells)
|
||||
|
||||
if not rows:
|
||||
return
|
||||
|
||||
# 确定列数
|
||||
num_cols = max(len(row) for row in rows)
|
||||
|
||||
# 创建表格
|
||||
table = doc.add_table(rows=len(rows), cols=num_cols)
|
||||
table.style = "Table Grid"
|
||||
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||||
|
||||
# 填充表格
|
||||
for row_idx, row_data in enumerate(rows):
|
||||
row = table.rows[row_idx]
|
||||
for col_idx, cell_text in enumerate(row_data):
|
||||
if col_idx < num_cols:
|
||||
cell = row.cells[col_idx]
|
||||
# 清除默认段落
|
||||
cell.paragraphs[0].clear()
|
||||
para = cell.paragraphs[0]
|
||||
para.paragraph_format.space_after = Pt(3)
|
||||
para.paragraph_format.space_before = Pt(1)
|
||||
para.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
|
||||
self.add_formatted_text(para, cell_text)
|
||||
|
||||
# 设置单元格字体
|
||||
for run in para.runs:
|
||||
run.font.name = "Times New Roman"
|
||||
run._element.rPr.rFonts.set(qn("w:eastAsia"), "宋体")
|
||||
run.font.size = Pt(10)
|
||||
|
||||
# 表头加粗并填充底色
|
||||
if row_idx == 0:
|
||||
for run in para.runs:
|
||||
run.bold = True
|
||||
_set_cell_shading(cell, header_fill)
|
||||
# 隔行底色
|
||||
elif row_idx % 2 == 1:
|
||||
_set_cell_shading(cell, zebra_fill)
|
||||
|
||||
# 统一列对齐为左对齐,避免居中导致阅读困难
|
||||
for row in table.rows:
|
||||
for cell in row.cells:
|
||||
for para in cell.paragraphs:
|
||||
para.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
|
||||
def add_list_to_doc(
|
||||
self, doc: Document, items: List[Tuple[int, str]], list_type: str
|
||||
):
|
||||
"""添加列表"""
|
||||
for indent, text in items:
|
||||
paragraph = doc.add_paragraph()
|
||||
|
||||
if list_type == "unordered":
|
||||
# 无序列表使用项目符号
|
||||
paragraph.style = "List Bullet"
|
||||
else:
|
||||
# 有序列表使用编号
|
||||
paragraph.style = "List Number"
|
||||
|
||||
# 设置缩进
|
||||
paragraph.paragraph_format.left_indent = Cm(0.5 * (indent + 1))
|
||||
|
||||
# 添加格式化文本
|
||||
self.add_formatted_text(paragraph, text)
|
||||
|
||||
# 设置字体
|
||||
for run in paragraph.runs:
|
||||
run.font.name = "Times New Roman"
|
||||
run._element.rPr.rFonts.set(qn("w:eastAsia"), "宋体")
|
||||
|
||||
def add_horizontal_rule(self, doc: Document):
|
||||
"""添加水平分割线"""
|
||||
paragraph = doc.add_paragraph()
|
||||
paragraph.paragraph_format.space_before = Pt(12)
|
||||
paragraph.paragraph_format.space_after = Pt(12)
|
||||
|
||||
# 添加底部边框作为分割线
|
||||
pPr = paragraph._element.get_or_add_pPr()
|
||||
pBdr = OxmlElement("w:pBdr")
|
||||
bottom = OxmlElement("w:bottom")
|
||||
bottom.set(qn("w:val"), "single")
|
||||
bottom.set(qn("w:sz"), "6")
|
||||
bottom.set(qn("w:space"), "1")
|
||||
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
|
||||
@@ -2,10 +2,19 @@
|
||||
|
||||
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
|
||||
## What's New in v0.3.6
|
||||
|
||||
- **Smart Filename Generation**: Now supports generating filenames based on Chat Title, AI Summary, or Markdown Headers.
|
||||
- **OpenWebUI-Style Theme**: Modern dark header (#1f2937) with light gray zebra striping for better readability.
|
||||
- **Zebra Striping**: Alternating row colors (#ffffff / #f3f4f6) for improved visual scanning.
|
||||
- **Smart Data Type Conversion**: Automatically converts columns to numeric or datetime types with fallback to string.
|
||||
- **Full Cell Bold/Italic**: Supports full cell Markdown bold (`**text**`) and italic (`*text*`) formatting in Excel.
|
||||
- **Partial Markdown Cleanup**: Automatically removes partial Markdown formatting symbols (e.g., `Some **bold** text` → `Some bold text`) for cleaner Excel output.
|
||||
- **Export Scope**: Added `EXPORT_SCOPE` valve to choose between exporting tables from the "Last Message" (default) or "All Messages".
|
||||
- **Smart Sheet Naming**: Automatically names sheets based on Markdown headers, AI titles (if enabled), or message index (e.g., `Msg1-Tab1`).
|
||||
- **Multiple Tables Support**: Improved handling of multiple tables within single or multiple messages.
|
||||
- **Smart Filename Generation**: Supports generating filenames based on Chat Title, AI Summary, or Markdown Headers.
|
||||
- **Configuration Options**: Added `TITLE_SOURCE` setting to control filename generation strategy.
|
||||
- **AI Title Generation**: Added `MODEL_ID` setting to specify the model for AI title generation, with progress notifications.
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
@@ -2,14 +2,23 @@
|
||||
|
||||
此插件允许你直接从聊天界面将对话历史导出为 Excel (.xlsx) 文件。
|
||||
|
||||
## v0.3.4 更新内容
|
||||
## v0.3.6 更新内容
|
||||
|
||||
- **OpenWebUI 风格主题**:现代深灰表头 (#1f2937),搭配浅灰斑马纹,提升可读性。
|
||||
- **斑马纹效果**:隔行变色(#ffffff / #f3f4f6),方便视觉扫描。
|
||||
- **智能数据类型转换**:自动将列转换为数字或日期类型,无法转换时保持字符串。
|
||||
- **全单元格粗体/斜体**:支持 Excel 中的全单元格 Markdown 粗体 (`**text**`) 和斜体 (`*text*`) 格式。
|
||||
- **部分 Markdown 清理**:自动移除部分 Markdown 格式符号(如 `部分**加粗**文本` → `部分加粗文本`),使 Excel 输出更整洁。
|
||||
- **导出范围**: 新增 `EXPORT_SCOPE` 配置项,可选择导出"最后一条消息"(默认)或"所有消息"中的表格。
|
||||
- **智能 Sheet 命名**: 根据 Markdown 标题、AI 标题(如启用)或消息索引(如 `消息1-表1`)自动命名 Sheet。
|
||||
- **多表格支持**: 优化了对单条或多条消息中包含多个表格的处理。
|
||||
- **智能文件名生成**:支持根据对话标题、AI 总结或 Markdown 标题生成文件名。
|
||||
- **配置选项**:新增 `TITLE_SOURCE` 设置,用于控制文件名生成策略。
|
||||
- **AI 标题生成**:新增 `MODEL_ID` 设置用于指定 AI 标题生成模型,并支持生成进度通知。
|
||||
|
||||
## 功能特点
|
||||
|
||||
- **一键导出**:在聊天界面添加“导出为 Excel”按钮。
|
||||
- **一键导出**:在聊天界面添加"导出为 Excel"按钮。
|
||||
- **自动表头提取**:智能识别聊天内容中的表格标题。
|
||||
- **多表支持**:支持处理单次对话中的多个表格。
|
||||
|
||||
@@ -23,7 +32,7 @@
|
||||
## 使用方法
|
||||
|
||||
1. 安装插件。
|
||||
2. 在任意对话中,点击“导出为 Excel”按钮。
|
||||
2. 在任意对话中,点击"导出为 Excel"按钮。
|
||||
3. 文件将自动下载到你的设备。
|
||||
|
||||
## 作者
|
||||
|
||||
@@ -3,9 +3,9 @@ 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.4
|
||||
version: 0.3.7
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNSAySDZhMiAyIDAgMCAwLTIgMnYxNmEyIDIgMCAwIDAgMiAyaDEyYTIgMiAwIDAgMCAyLTJWN1oiLz48cGF0aCBkPSJNMTQgMnY0YTIgMiAwIDAgMCAyIDJoNCIvPjxwYXRoIGQ9Ik04IDEzaDIiLz48cGF0aCBkPSJNMTQgMTNoMiIvPjxwYXRoIGQ9Ik04IDE3aDIiLz48cGF0aCBkPSJNMTQgMTdoMiIvPjwvc3ZnPg==
|
||||
description: Exports the current chat history to an Excel (.xlsx) file, with automatic header extraction.
|
||||
description: Extracts tables from chat messages and exports them to Excel (.xlsx) files with smart formatting.
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -20,16 +20,25 @@ 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
|
||||
from typing import Literal
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Action:
|
||||
class Valves(BaseModel):
|
||||
TITLE_SOURCE: str = Field(
|
||||
TITLE_SOURCE: Literal["chat_title", "ai_generated", "markdown_title"] = Field(
|
||||
default="chat_title",
|
||||
description="Title Source: 'chat_title' (Chat Title), 'ai_generated' (AI Generated), 'markdown_title' (Markdown Title)",
|
||||
)
|
||||
EXPORT_SCOPE: Literal["last_message", "all_messages"] = Field(
|
||||
default="last_message",
|
||||
description="Export Scope: 'last_message' (Last Message Only), 'all_messages' (All Messages)",
|
||||
)
|
||||
MODEL_ID: str = Field(
|
||||
default="",
|
||||
description="Model ID for AI title generation. Leave empty to use the current chat model.",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.valves = self.Valves()
|
||||
@@ -64,8 +73,6 @@ class Action:
|
||||
user_id = __user__.get("id", "unknown_user")
|
||||
|
||||
if __event_emitter__:
|
||||
last_assistant_message = body["messages"][-1]
|
||||
|
||||
await __event_emitter__(
|
||||
{
|
||||
"type": "status",
|
||||
@@ -74,19 +81,125 @@ class Action:
|
||||
)
|
||||
|
||||
try:
|
||||
message_content = last_assistant_message["content"]
|
||||
tables = self.extract_tables_from_message(message_content)
|
||||
messages = body.get("messages", [])
|
||||
if not messages:
|
||||
raise HTTPException(status_code=400, detail="No messages found.")
|
||||
|
||||
if not tables:
|
||||
raise HTTPException(status_code=400, detail="No tables found.")
|
||||
# Determine messages to process based on scope
|
||||
target_messages = []
|
||||
if self.valves.EXPORT_SCOPE == "all_messages":
|
||||
target_messages = messages
|
||||
else:
|
||||
target_messages = [messages[-1]]
|
||||
|
||||
# Generate filename
|
||||
all_tables = []
|
||||
all_sheet_names = []
|
||||
|
||||
# Process messages
|
||||
for msg_index, msg in enumerate(target_messages):
|
||||
content = msg.get("content", "")
|
||||
tables = self.extract_tables_from_message(content)
|
||||
|
||||
if not tables:
|
||||
continue
|
||||
|
||||
# Generate sheet names for this message's tables
|
||||
# If multiple messages, we need to ensure uniqueness across the whole workbook
|
||||
# We'll generate base names here and deduplicate later if needed,
|
||||
# or better: generate unique names on the fly.
|
||||
|
||||
# Extract headers for this message
|
||||
headers = []
|
||||
lines = content.split("\n")
|
||||
for i, line in enumerate(lines):
|
||||
if re.match(r"^#{1,6}\s+", line):
|
||||
headers.append(
|
||||
{
|
||||
"text": re.sub(r"^#{1,6}\s+", "", line).strip(),
|
||||
"line_num": i,
|
||||
}
|
||||
)
|
||||
|
||||
for table_index, table in enumerate(tables):
|
||||
sheet_name = ""
|
||||
|
||||
# 1. Try Markdown Header (closest above)
|
||||
table_start_line = table["start_line"] - 1
|
||||
closest_header_text = None
|
||||
candidate_headers = [
|
||||
h for h in headers if h["line_num"] < table_start_line
|
||||
]
|
||||
if candidate_headers:
|
||||
closest_header = max(
|
||||
candidate_headers, key=lambda x: x["line_num"]
|
||||
)
|
||||
closest_header_text = closest_header["text"]
|
||||
|
||||
if closest_header_text:
|
||||
sheet_name = self.clean_sheet_name(closest_header_text)
|
||||
|
||||
# 2. AI Generated (Only if explicitly enabled and we have a request object)
|
||||
# Note: Generating titles for EVERY table in all messages might be too slow/expensive.
|
||||
# We'll skip this for 'all_messages' scope to avoid timeout, unless it's just one message.
|
||||
if (
|
||||
not sheet_name
|
||||
and self.valves.TITLE_SOURCE == "ai_generated"
|
||||
and len(target_messages) == 1
|
||||
):
|
||||
# Logic for AI generation (simplified for now, reusing existing flow if possible)
|
||||
pass
|
||||
|
||||
# 3. Fallback: Message Index
|
||||
if not sheet_name:
|
||||
if len(target_messages) > 1:
|
||||
# Use global message index (from original list if possible, but here we iterate target_messages)
|
||||
# Let's use the loop index.
|
||||
# If multiple tables in one message: "Msg 1 - Table 1"
|
||||
if len(tables) > 1:
|
||||
sheet_name = f"Msg{msg_index+1}-Tab{table_index+1}"
|
||||
else:
|
||||
sheet_name = f"Msg{msg_index+1}"
|
||||
else:
|
||||
# Single message (last_message scope)
|
||||
if len(tables) > 1:
|
||||
sheet_name = f"Table {table_index+1}"
|
||||
else:
|
||||
sheet_name = "Sheet1"
|
||||
|
||||
all_tables.append(table)
|
||||
all_sheet_names.append(sheet_name)
|
||||
|
||||
if not all_tables:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="No tables found in the selected scope."
|
||||
)
|
||||
|
||||
# Deduplicate sheet names
|
||||
final_sheet_names = []
|
||||
seen_names = {}
|
||||
for name in all_sheet_names:
|
||||
base_name = name
|
||||
counter = 1
|
||||
while name in seen_names:
|
||||
name = f"{base_name} ({counter})"
|
||||
counter += 1
|
||||
seen_names[name] = True
|
||||
final_sheet_names.append(name)
|
||||
|
||||
# Notify user about the number of tables found
|
||||
table_count = len(all_tables)
|
||||
if self.valves.EXPORT_SCOPE == "all_messages":
|
||||
await self._send_notification(
|
||||
__event_emitter__,
|
||||
"info",
|
||||
f"Found {table_count} table(s) in all messages.",
|
||||
)
|
||||
# Wait a moment for user to see the notification before download dialog
|
||||
await asyncio.sleep(1.5)
|
||||
# Generate Workbook Title (Filename)
|
||||
# Use the title of the chat, or the first header of the first message with tables
|
||||
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_id = self.extract_chat_id(body, None)
|
||||
chat_title = ""
|
||||
if chat_id:
|
||||
chat_title = await self.fetch_chat_title(chat_id, user_id)
|
||||
@@ -96,44 +209,48 @@ class Action:
|
||||
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
|
||||
# Use AI to generate a title based on message content
|
||||
if target_messages and __request__:
|
||||
# Get content from the first message with tables
|
||||
content_for_title = ""
|
||||
for msg in target_messages:
|
||||
msg_content = msg.get("content", "")
|
||||
if msg_content:
|
||||
content_for_title = msg_content
|
||||
break
|
||||
if content_for_title:
|
||||
title = await self.generate_title_using_ai(
|
||||
body,
|
||||
content_for_title,
|
||||
user_id,
|
||||
__request__,
|
||||
__event_emitter__,
|
||||
)
|
||||
elif self.valves.TITLE_SOURCE == "markdown_title":
|
||||
# Try to find first header in the first message that has content
|
||||
for msg in target_messages:
|
||||
extracted = self.extract_title(msg.get("content", ""))
|
||||
if extracted:
|
||||
title = extracted
|
||||
break
|
||||
|
||||
# Get dynamic filename and sheet names
|
||||
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
|
||||
# Fallback for filename
|
||||
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:
|
||||
if chat_title:
|
||||
title = chat_title
|
||||
else:
|
||||
# Try extracting from content again if not already tried
|
||||
if self.valves.TITLE_SOURCE != "markdown_title":
|
||||
for msg in target_messages:
|
||||
extracted = self.extract_title(msg.get("content", ""))
|
||||
if extracted:
|
||||
title = extracted
|
||||
break
|
||||
|
||||
# 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 title:
|
||||
workbook_name = f"{user_name}_{formatted_date}"
|
||||
else:
|
||||
@@ -146,8 +263,10 @@ class Action:
|
||||
|
||||
os.makedirs(os.path.dirname(excel_file_path), exist_ok=True)
|
||||
|
||||
# Save tables to Excel (using enhanced formatting)
|
||||
self.save_tables_to_excel_enhanced(tables, excel_file_path, sheet_names)
|
||||
# Save tables to Excel
|
||||
self.save_tables_to_excel_enhanced(
|
||||
all_tables, excel_file_path, final_sheet_names
|
||||
)
|
||||
|
||||
# Trigger file download
|
||||
if __event_call__:
|
||||
@@ -230,32 +349,93 @@ class Action:
|
||||
)
|
||||
|
||||
async def generate_title_using_ai(
|
||||
self, body: dict, content: str, user_id: str, request: Any
|
||||
self,
|
||||
body: dict,
|
||||
content: str,
|
||||
user_id: str,
|
||||
request: Any,
|
||||
event_emitter: Callable = None,
|
||||
) -> str:
|
||||
if not request:
|
||||
return ""
|
||||
|
||||
try:
|
||||
user_obj = Users.get_user_by_id(user_id)
|
||||
model = body.get("model")
|
||||
# Use configured MODEL_ID or fallback to current chat model
|
||||
model = (
|
||||
self.valves.MODEL_ID.strip()
|
||||
if self.valves.MODEL_ID
|
||||
else 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.",
|
||||
"content": "You are a helpful assistant. Generate a short, concise filename (max 10 words) for an Excel export based on the following content. Do not use quotes or file extensions. Avoid special characters that are invalid in filenames. Only output the filename.",
|
||||
},
|
||||
{"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()
|
||||
# Define the generation task
|
||||
async def generate_task():
|
||||
return await generate_chat_completion(request, payload, user_obj)
|
||||
|
||||
# Define the notification task
|
||||
async def notification_task():
|
||||
# Send initial notification immediately
|
||||
if event_emitter:
|
||||
await self._send_notification(
|
||||
event_emitter,
|
||||
"info",
|
||||
"AI is generating a filename for your Excel file...",
|
||||
)
|
||||
|
||||
# Subsequent notifications every 5 seconds
|
||||
while True:
|
||||
await asyncio.sleep(5)
|
||||
if event_emitter:
|
||||
await self._send_notification(
|
||||
event_emitter,
|
||||
"info",
|
||||
"Still generating filename, please be patient...",
|
||||
)
|
||||
|
||||
# Run tasks concurrently
|
||||
gen_future = asyncio.ensure_future(generate_task())
|
||||
notify_future = asyncio.ensure_future(notification_task())
|
||||
|
||||
done, pending = await asyncio.wait(
|
||||
[gen_future, notify_future], return_when=asyncio.FIRST_COMPLETED
|
||||
)
|
||||
|
||||
# Cancel notification task if generation is done
|
||||
if not notify_future.done():
|
||||
notify_future.cancel()
|
||||
|
||||
# Get result
|
||||
if gen_future in done:
|
||||
response = gen_future.result()
|
||||
if response and "choices" in response:
|
||||
return response["choices"][0]["message"]["content"].strip()
|
||||
else:
|
||||
# Should not happen if return_when=FIRST_COMPLETED and we cancel notify
|
||||
await gen_future
|
||||
response = gen_future.result()
|
||||
if response and "choices" in response:
|
||||
return response["choices"][0]["message"]["content"].strip()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error generating title: {e}")
|
||||
if event_emitter:
|
||||
await self._send_notification(
|
||||
event_emitter,
|
||||
"warning",
|
||||
f"AI title generation failed, using default title. Error: {str(e)}",
|
||||
)
|
||||
|
||||
return ""
|
||||
|
||||
@@ -595,24 +775,51 @@ class Action:
|
||||
with pd.ExcelWriter(file_path, engine="xlsxwriter") as writer:
|
||||
workbook = writer.book
|
||||
|
||||
# OpenWebUI-style theme colors
|
||||
HEADER_BG = "#1f2937" # Dark gray (matches OpenWebUI sidebar)
|
||||
HEADER_FG = "#ffffff" # White text
|
||||
ROW_ODD_BG = "#ffffff" # White for odd rows
|
||||
ROW_EVEN_BG = "#f3f4f6" # Light gray for even rows (zebra striping)
|
||||
BORDER_COLOR = "#e5e7eb" # Light border
|
||||
|
||||
# Define header style - Center aligned
|
||||
header_format = workbook.add_format(
|
||||
{
|
||||
"bold": True,
|
||||
"font_size": 12,
|
||||
"font_color": "white",
|
||||
"bg_color": "#00abbd",
|
||||
"font_size": 11,
|
||||
"font_name": "Arial",
|
||||
"font_color": HEADER_FG,
|
||||
"bg_color": HEADER_BG,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"align": "center",
|
||||
"valign": "vcenter",
|
||||
"text_wrap": True,
|
||||
}
|
||||
)
|
||||
|
||||
# Text cell style - Left aligned
|
||||
# Text cell style - Left aligned (odd rows)
|
||||
text_format = workbook.add_format(
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_ODD_BG,
|
||||
"align": "left",
|
||||
"valign": "vcenter",
|
||||
"text_wrap": True,
|
||||
}
|
||||
)
|
||||
|
||||
# Text cell style - Left aligned (even rows - zebra)
|
||||
text_format_alt = workbook.add_format(
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_EVEN_BG,
|
||||
"align": "left",
|
||||
"valign": "vcenter",
|
||||
"text_wrap": True,
|
||||
@@ -621,14 +828,51 @@ class Action:
|
||||
|
||||
# Number cell style - Right aligned
|
||||
number_format = workbook.add_format(
|
||||
{"border": 1, "align": "right", "valign": "vcenter"}
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_ODD_BG,
|
||||
"align": "right",
|
||||
"valign": "vcenter",
|
||||
}
|
||||
)
|
||||
|
||||
number_format_alt = workbook.add_format(
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_EVEN_BG,
|
||||
"align": "right",
|
||||
"valign": "vcenter",
|
||||
}
|
||||
)
|
||||
|
||||
# Integer format - Right aligned
|
||||
integer_format = workbook.add_format(
|
||||
{
|
||||
"num_format": "0",
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_ODD_BG,
|
||||
"align": "right",
|
||||
"valign": "vcenter",
|
||||
}
|
||||
)
|
||||
|
||||
integer_format_alt = workbook.add_format(
|
||||
{
|
||||
"num_format": "0",
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_EVEN_BG,
|
||||
"align": "right",
|
||||
"valign": "vcenter",
|
||||
}
|
||||
@@ -638,7 +882,24 @@ class Action:
|
||||
decimal_format = workbook.add_format(
|
||||
{
|
||||
"num_format": "0.00",
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_ODD_BG,
|
||||
"align": "right",
|
||||
"valign": "vcenter",
|
||||
}
|
||||
)
|
||||
|
||||
decimal_format_alt = workbook.add_format(
|
||||
{
|
||||
"num_format": "0.00",
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_EVEN_BG,
|
||||
"align": "right",
|
||||
"valign": "vcenter",
|
||||
}
|
||||
@@ -647,7 +908,24 @@ class Action:
|
||||
# Date format - Center aligned
|
||||
date_format = workbook.add_format(
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_ODD_BG,
|
||||
"align": "center",
|
||||
"valign": "vcenter",
|
||||
"text_wrap": True,
|
||||
}
|
||||
)
|
||||
|
||||
date_format_alt = workbook.add_format(
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_EVEN_BG,
|
||||
"align": "center",
|
||||
"valign": "vcenter",
|
||||
"text_wrap": True,
|
||||
@@ -657,12 +935,114 @@ class Action:
|
||||
# Sequence format - Center aligned
|
||||
sequence_format = workbook.add_format(
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_ODD_BG,
|
||||
"align": "center",
|
||||
"valign": "vcenter",
|
||||
}
|
||||
)
|
||||
|
||||
sequence_format_alt = workbook.add_format(
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_EVEN_BG,
|
||||
"align": "center",
|
||||
"valign": "vcenter",
|
||||
}
|
||||
)
|
||||
|
||||
# Bold cell style (for full cell bolding)
|
||||
text_bold_format = workbook.add_format(
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_ODD_BG,
|
||||
"align": "left",
|
||||
"valign": "vcenter",
|
||||
"text_wrap": True,
|
||||
"bold": True,
|
||||
}
|
||||
)
|
||||
|
||||
text_bold_format_alt = workbook.add_format(
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_EVEN_BG,
|
||||
"align": "left",
|
||||
"valign": "vcenter",
|
||||
"text_wrap": True,
|
||||
"bold": True,
|
||||
}
|
||||
)
|
||||
|
||||
# Italic cell style (for full cell italics)
|
||||
text_italic_format = workbook.add_format(
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_ODD_BG,
|
||||
"align": "left",
|
||||
"valign": "vcenter",
|
||||
"text_wrap": True,
|
||||
"italic": True,
|
||||
}
|
||||
)
|
||||
|
||||
text_italic_format_alt = workbook.add_format(
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_EVEN_BG,
|
||||
"align": "left",
|
||||
"valign": "vcenter",
|
||||
"text_wrap": True,
|
||||
"italic": True,
|
||||
}
|
||||
)
|
||||
|
||||
# Code cell style (for inline code with highlight background)
|
||||
CODE_BG = "#f0f0f0" # Light gray background for code
|
||||
text_code_format = workbook.add_format(
|
||||
{
|
||||
"font_name": "Consolas",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": CODE_BG,
|
||||
"align": "left",
|
||||
"valign": "vcenter",
|
||||
"text_wrap": True,
|
||||
}
|
||||
)
|
||||
|
||||
text_code_format_alt = workbook.add_format(
|
||||
{
|
||||
"font_name": "Consolas",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": CODE_BG,
|
||||
"align": "left",
|
||||
"valign": "vcenter",
|
||||
"text_wrap": True,
|
||||
}
|
||||
)
|
||||
|
||||
for i, table in enumerate(tables):
|
||||
try:
|
||||
table_data = table["data"]
|
||||
@@ -704,12 +1084,18 @@ class Action:
|
||||
|
||||
print(f"DataFrame created with columns: {list(df.columns)}")
|
||||
|
||||
# Fix pandas FutureWarning
|
||||
# Smart data type conversion using pandas infer_objects
|
||||
for col in df.columns:
|
||||
# Try numeric conversion first
|
||||
try:
|
||||
df[col] = pd.to_numeric(df[col])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
# Try datetime conversion
|
||||
try:
|
||||
df[col] = pd.to_datetime(df[col], errors="raise")
|
||||
except (ValueError, TypeError):
|
||||
# Keep as string, use infer_objects for optimization
|
||||
df[col] = df[col].infer_objects()
|
||||
|
||||
# Write data first (without header)
|
||||
df.to_excel(
|
||||
@@ -721,19 +1107,25 @@ class Action:
|
||||
)
|
||||
worksheet = writer.sheets[sheet_name]
|
||||
|
||||
# Apply enhanced formatting
|
||||
# Apply enhanced formatting with zebra striping
|
||||
formats = {
|
||||
"header": header_format,
|
||||
"text": [text_format, text_format_alt],
|
||||
"number": [number_format, number_format_alt],
|
||||
"integer": [integer_format, integer_format_alt],
|
||||
"decimal": [decimal_format, decimal_format_alt],
|
||||
"date": [date_format, date_format_alt],
|
||||
"sequence": [sequence_format, sequence_format_alt],
|
||||
"bold": [text_bold_format, text_bold_format_alt],
|
||||
"italic": [text_italic_format, text_italic_format_alt],
|
||||
"code": [text_code_format, text_code_format_alt],
|
||||
}
|
||||
self.apply_enhanced_formatting(
|
||||
worksheet,
|
||||
df,
|
||||
headers,
|
||||
workbook,
|
||||
header_format,
|
||||
text_format,
|
||||
number_format,
|
||||
integer_format,
|
||||
decimal_format,
|
||||
date_format,
|
||||
sequence_format,
|
||||
formats,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -750,23 +1142,22 @@ class Action:
|
||||
df,
|
||||
headers,
|
||||
workbook,
|
||||
header_format,
|
||||
text_format,
|
||||
number_format,
|
||||
integer_format,
|
||||
decimal_format,
|
||||
date_format,
|
||||
sequence_format,
|
||||
formats,
|
||||
):
|
||||
"""
|
||||
Apply enhanced formatting
|
||||
- Header: Center aligned
|
||||
Apply enhanced formatting with zebra striping
|
||||
- Header: Center aligned (dark background)
|
||||
- Number: Right aligned
|
||||
- Text: Left aligned
|
||||
- Date: Center aligned
|
||||
- Sequence: Center aligned
|
||||
- Zebra striping: alternating row colors
|
||||
- Supports full cell Markdown bold (**text**) and italic (*text*)
|
||||
"""
|
||||
try:
|
||||
# Extract format from formats dict
|
||||
header_format = formats["header"]
|
||||
|
||||
# 1. Write headers (Center aligned)
|
||||
print(f"Writing headers with enhanced alignment: {headers}")
|
||||
for col_idx, header in enumerate(headers):
|
||||
@@ -790,43 +1181,99 @@ class Action:
|
||||
else:
|
||||
column_types[col_idx] = "text"
|
||||
|
||||
# 3. Write and format data
|
||||
# 3. Write and format data with zebra striping
|
||||
for row_idx, row in df.iterrows():
|
||||
# Determine if odd or even row (0-indexed, so row 0 is odd visually as row 1)
|
||||
is_alt_row = (
|
||||
row_idx % 2 == 1
|
||||
) # Even index = odd visual row, use alt format
|
||||
|
||||
for col_idx, value in enumerate(row):
|
||||
content_type = column_types.get(col_idx, "text")
|
||||
|
||||
# Select format based on content type
|
||||
# Select format based on content type and zebra striping
|
||||
fmt_idx = 1 if is_alt_row else 0
|
||||
|
||||
if content_type == "number":
|
||||
# Number - Right aligned
|
||||
if pd.api.types.is_numeric_dtype(df.iloc[:, col_idx]):
|
||||
if pd.api.types.is_integer_dtype(df.iloc[:, col_idx]):
|
||||
current_format = integer_format
|
||||
current_format = formats["integer"][fmt_idx]
|
||||
else:
|
||||
try:
|
||||
numeric_value = float(value)
|
||||
if numeric_value.is_integer():
|
||||
current_format = integer_format
|
||||
current_format = formats["integer"][fmt_idx]
|
||||
value = int(numeric_value)
|
||||
else:
|
||||
current_format = decimal_format
|
||||
current_format = formats["decimal"][fmt_idx]
|
||||
except (ValueError, TypeError):
|
||||
current_format = decimal_format
|
||||
current_format = formats["decimal"][fmt_idx]
|
||||
else:
|
||||
current_format = number_format
|
||||
current_format = formats["number"][fmt_idx]
|
||||
|
||||
elif content_type == "date":
|
||||
# Date - Center aligned
|
||||
current_format = date_format
|
||||
current_format = formats["date"][fmt_idx]
|
||||
|
||||
elif content_type == "sequence":
|
||||
# Sequence - Center aligned
|
||||
current_format = sequence_format
|
||||
current_format = formats["sequence"][fmt_idx]
|
||||
|
||||
else:
|
||||
# Text - Left aligned
|
||||
current_format = text_format
|
||||
current_format = formats["text"][fmt_idx]
|
||||
|
||||
worksheet.write(row_idx + 1, col_idx, value, current_format)
|
||||
if content_type == "text" and isinstance(value, str):
|
||||
# Check for full cell bold (**text**)
|
||||
match_bold = re.fullmatch(r"\*\*(.+)\*\*", value.strip())
|
||||
# Check for full cell italic (*text*)
|
||||
match_italic = re.fullmatch(r"\*(.+)\*", value.strip())
|
||||
# Check for full cell code (`text`)
|
||||
match_code = re.fullmatch(r"`(.+)`", value.strip())
|
||||
|
||||
if match_bold:
|
||||
# Extract content and apply bold format
|
||||
clean_value = match_bold.group(1)
|
||||
worksheet.write(
|
||||
row_idx + 1,
|
||||
col_idx,
|
||||
clean_value,
|
||||
formats["bold"][fmt_idx],
|
||||
)
|
||||
elif match_italic:
|
||||
# Extract content and apply italic format
|
||||
clean_value = match_italic.group(1)
|
||||
worksheet.write(
|
||||
row_idx + 1,
|
||||
col_idx,
|
||||
clean_value,
|
||||
formats["italic"][fmt_idx],
|
||||
)
|
||||
elif match_code:
|
||||
# Extract content and apply code format (highlighted)
|
||||
clean_value = match_code.group(1)
|
||||
worksheet.write(
|
||||
row_idx + 1,
|
||||
col_idx,
|
||||
clean_value,
|
||||
formats["code"][fmt_idx],
|
||||
)
|
||||
else:
|
||||
# Remove partial markdown formatting symbols (can't render partial formatting in Excel)
|
||||
# Remove bold markers **text** -> text
|
||||
clean_value = re.sub(r"\*\*(.+?)\*\*", r"\1", value)
|
||||
# Remove italic markers *text* -> text (but not inside **)
|
||||
clean_value = re.sub(
|
||||
r"(?<!\*)\*([^*]+)\*(?!\*)", r"\1", clean_value
|
||||
)
|
||||
# Remove code markers `text` -> text
|
||||
clean_value = re.sub(r"`(.+?)`", r"\1", clean_value)
|
||||
worksheet.write(
|
||||
row_idx + 1, col_idx, clean_value, current_format
|
||||
)
|
||||
else:
|
||||
worksheet.write(row_idx + 1, col_idx, value, current_format)
|
||||
|
||||
# 4. Auto-adjust column width
|
||||
for col_idx, column in enumerate(headers):
|
||||
@@ -916,3 +1363,6 @@ class Action:
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in basic formatting: {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in basic formatting: {str(e)}")
|
||||
|
||||
@@ -3,9 +3,9 @@ title: 导出为 Excel
|
||||
author: Fu-Jie
|
||||
author_url: https://github.com/Fu-Jie
|
||||
funding_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||
version: 0.3.4
|
||||
version: 0.3.7
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNSAySDZhMiAyIDAgMCAwLTIgMnYxNmEyIDIgMCAwIDAgMiAyaDEyYTIgMiAwIDAgMCAyLTJWN1oiLz48cGF0aCBkPSJNMTQgMnY0YTIgMiAwIDAgMCAyIDJoNCIvPjxwYXRoIGQ9Ik04IDEzaDIiLz48cGF0aCBkPSJNMTQgMTNoMiIvPjxwYXRoIGQ9Ik04IDE3aDIiLz48cGF0aCBkPSJNMTQgMTdoMiIvPjwvc3ZnPg==
|
||||
description: 将当前对话历史导出为 Excel (.xlsx) 文件,支持自动提取表头。
|
||||
description: 从聊天消息中提取表格并导出为 Excel (.xlsx) 文件,支持智能格式化。
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -20,15 +20,24 @@ 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
|
||||
from typing import Literal
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Action:
|
||||
class Valves(BaseModel):
|
||||
TITLE_SOURCE: str = Field(
|
||||
TITLE_SOURCE: Literal["chat_title", "ai_generated", "markdown_title"] = Field(
|
||||
default="chat_title",
|
||||
description="标题来源:'chat_title' (对话标题), 'ai_generated' (AI生成), 'markdown_title' (Markdown标题)",
|
||||
description="标题来源: 'chat_title' (对话标题), 'ai_generated' (AI生成), 'markdown_title' (Markdown标题)",
|
||||
)
|
||||
EXPORT_SCOPE: Literal["last_message", "all_messages"] = Field(
|
||||
default="last_message",
|
||||
description="导出范围: 'last_message' (仅最后一条消息), 'all_messages' (所有消息)",
|
||||
)
|
||||
MODEL_ID: str = Field(
|
||||
default="",
|
||||
description="AI 标题生成模型 ID。留空则使用当前对话模型。",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
@@ -50,43 +59,138 @@ class Action:
|
||||
print(f"action:{__name__}")
|
||||
if isinstance(__user__, (list, tuple)):
|
||||
user_language = (
|
||||
__user__[0].get("language", "zh-CN") if __user__ else "zh-CN"
|
||||
__user__[0].get("language", "en-US") if __user__ else "en-US"
|
||||
)
|
||||
user_name = __user__[0].get("name", "用户") if __user__[0] else "用户"
|
||||
user_name = __user__[0].get("name", "User") if __user__[0] else "User"
|
||||
user_id = (
|
||||
__user__[0]["id"]
|
||||
if __user__ and "id" in __user__[0]
|
||||
else "unknown_user"
|
||||
)
|
||||
elif isinstance(__user__, dict):
|
||||
user_language = __user__.get("language", "zh-CN")
|
||||
user_name = __user__.get("name", "用户")
|
||||
user_language = __user__.get("language", "en-US")
|
||||
user_name = __user__.get("name", "User")
|
||||
user_id = __user__.get("id", "unknown_user")
|
||||
|
||||
if __event_emitter__:
|
||||
last_assistant_message = body["messages"][-1]
|
||||
|
||||
await __event_emitter__(
|
||||
{
|
||||
"type": "status",
|
||||
"data": {"description": "正在保存到文件...", "done": False},
|
||||
"data": {"description": "正在保存文件...", "done": False},
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
message_content = last_assistant_message["content"]
|
||||
tables = self.extract_tables_from_message(message_content)
|
||||
messages = body.get("messages", [])
|
||||
if not messages:
|
||||
raise HTTPException(status_code=400, detail="未找到消息。")
|
||||
|
||||
if not tables:
|
||||
raise HTTPException(status_code=400, detail="未找到任何表格。")
|
||||
# Determine messages to process based on scope
|
||||
target_messages = []
|
||||
if self.valves.EXPORT_SCOPE == "all_messages":
|
||||
target_messages = messages
|
||||
else:
|
||||
target_messages = [messages[-1]]
|
||||
|
||||
# 生成文件名
|
||||
all_tables = []
|
||||
all_sheet_names = []
|
||||
|
||||
# Process messages
|
||||
for msg_index, msg in enumerate(target_messages):
|
||||
content = msg.get("content", "")
|
||||
tables = self.extract_tables_from_message(content)
|
||||
|
||||
if not tables:
|
||||
continue
|
||||
|
||||
# Generate sheet names for this message's tables
|
||||
|
||||
# Extract headers for this message
|
||||
headers = []
|
||||
lines = content.split("\n")
|
||||
for i, line in enumerate(lines):
|
||||
if re.match(r"^#{1,6}\s+", line):
|
||||
headers.append(
|
||||
{
|
||||
"text": re.sub(r"^#{1,6}\s+", "", line).strip(),
|
||||
"line_num": i,
|
||||
}
|
||||
)
|
||||
|
||||
for table_index, table in enumerate(tables):
|
||||
sheet_name = ""
|
||||
|
||||
# 1. Try Markdown Header (closest above)
|
||||
table_start_line = table["start_line"] - 1
|
||||
closest_header_text = None
|
||||
candidate_headers = [
|
||||
h for h in headers if h["line_num"] < table_start_line
|
||||
]
|
||||
if candidate_headers:
|
||||
closest_header = max(
|
||||
candidate_headers, key=lambda x: x["line_num"]
|
||||
)
|
||||
closest_header_text = closest_header["text"]
|
||||
|
||||
if closest_header_text:
|
||||
sheet_name = self.clean_sheet_name(closest_header_text)
|
||||
|
||||
# 2. AI Generated (Only if explicitly enabled and we have a request object)
|
||||
if (
|
||||
not sheet_name
|
||||
and self.valves.TITLE_SOURCE == "ai_generated"
|
||||
and len(target_messages) == 1
|
||||
):
|
||||
pass
|
||||
|
||||
# 3. Fallback: Message Index
|
||||
if not sheet_name:
|
||||
if len(target_messages) > 1:
|
||||
if len(tables) > 1:
|
||||
sheet_name = f"消息{msg_index+1}-表{table_index+1}"
|
||||
else:
|
||||
sheet_name = f"消息{msg_index+1}"
|
||||
else:
|
||||
# Single message (last_message scope)
|
||||
if len(tables) > 1:
|
||||
sheet_name = f"表{table_index+1}"
|
||||
else:
|
||||
sheet_name = "Sheet1"
|
||||
|
||||
all_tables.append(table)
|
||||
all_sheet_names.append(sheet_name)
|
||||
|
||||
if not all_tables:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="在选定范围内未找到表格。"
|
||||
)
|
||||
|
||||
# Deduplicate sheet names
|
||||
final_sheet_names = []
|
||||
seen_names = {}
|
||||
for name in all_sheet_names:
|
||||
base_name = name
|
||||
counter = 1
|
||||
while name in seen_names:
|
||||
name = f"{base_name} ({counter})"
|
||||
counter += 1
|
||||
seen_names[name] = True
|
||||
final_sheet_names.append(name)
|
||||
|
||||
# 通知用户提取到的表格数量
|
||||
table_count = len(all_tables)
|
||||
if self.valves.EXPORT_SCOPE == "all_messages":
|
||||
await self._send_notification(
|
||||
__event_emitter__,
|
||||
"info",
|
||||
f"从所有消息中提取到 {table_count} 个表格。",
|
||||
)
|
||||
# 等待片刻让用户看到通知,再触发下载
|
||||
await asyncio.sleep(1.5)
|
||||
|
||||
# Generate Workbook Title (Filename)
|
||||
title = ""
|
||||
chat_id = self.extract_chat_id(
|
||||
body, None
|
||||
) # metadata 在此版本 action 签名中不可用,但通常在 body 中
|
||||
|
||||
# 直接通过 chat_id 获取对话标题,因为 body 中通常缺少该信息
|
||||
chat_id = self.extract_chat_id(body, None)
|
||||
chat_title = ""
|
||||
if chat_id:
|
||||
chat_title = await self.fetch_chat_title(chat_id, user_id)
|
||||
@@ -96,38 +200,46 @@ class Action:
|
||||
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
|
||||
# 使用 AI 根据消息内容生成标题
|
||||
if target_messages and __request__:
|
||||
# 获取第一条有表格的消息内容
|
||||
content_for_title = ""
|
||||
for msg in target_messages:
|
||||
msg_content = msg.get("content", "")
|
||||
if msg_content:
|
||||
content_for_title = msg_content
|
||||
break
|
||||
if content_for_title:
|
||||
title = await self.generate_title_using_ai(
|
||||
body,
|
||||
content_for_title,
|
||||
user_id,
|
||||
__request__,
|
||||
__event_emitter__,
|
||||
)
|
||||
elif self.valves.TITLE_SOURCE == "markdown_title":
|
||||
for msg in target_messages:
|
||||
extracted = self.extract_title(msg.get("content", ""))
|
||||
if extracted:
|
||||
title = extracted
|
||||
break
|
||||
|
||||
# 获取动态文件名和sheet名称
|
||||
workbook_name_from_content, sheet_names = (
|
||||
self.generate_names_from_content(message_content, tables)
|
||||
)
|
||||
|
||||
# 标题回退逻辑
|
||||
# Fallback for filename
|
||||
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:
|
||||
if chat_title:
|
||||
title = chat_title
|
||||
else:
|
||||
if self.valves.TITLE_SOURCE != "markdown_title":
|
||||
for msg in target_messages:
|
||||
extracted = self.extract_title(msg.get("content", ""))
|
||||
if extracted:
|
||||
title = extracted
|
||||
break
|
||||
|
||||
# 使用优化后的文件名生成逻辑
|
||||
current_datetime = datetime.datetime.now()
|
||||
formatted_date = current_datetime.strftime("%Y%m%d")
|
||||
|
||||
# 如果没找到标题则使用 user_yyyymmdd 格式
|
||||
if not title:
|
||||
workbook_name = f"{user_name}_{formatted_date}"
|
||||
else:
|
||||
@@ -140,10 +252,12 @@ class Action:
|
||||
|
||||
os.makedirs(os.path.dirname(excel_file_path), exist_ok=True)
|
||||
|
||||
# 保存表格到Excel(使用符合中国规范的格式化功能)
|
||||
self.save_tables_to_excel_enhanced(tables, excel_file_path, sheet_names)
|
||||
# Save tables to Excel
|
||||
self.save_tables_to_excel_enhanced(
|
||||
all_tables, excel_file_path, final_sheet_names
|
||||
)
|
||||
|
||||
# 触发文件下载
|
||||
# Trigger file download
|
||||
if __event_call__:
|
||||
with open(excel_file_path, "rb") as file:
|
||||
file_content = file.read()
|
||||
@@ -174,7 +288,7 @@ class Action:
|
||||
URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
}} catch (error) {{
|
||||
console.error('触发下载时出错:', error);
|
||||
console.error('Error triggering download:', error);
|
||||
}}
|
||||
"""
|
||||
},
|
||||
@@ -183,15 +297,15 @@ class Action:
|
||||
await __event_emitter__(
|
||||
{
|
||||
"type": "status",
|
||||
"data": {"description": "输出已保存", "done": True},
|
||||
"data": {"description": "文件已保存", "done": True},
|
||||
}
|
||||
)
|
||||
|
||||
# 清理临时文件
|
||||
# Clean up temp file
|
||||
if os.path.exists(excel_file_path):
|
||||
os.remove(excel_file_path)
|
||||
|
||||
return {"message": "下载事件已触发"}
|
||||
return {"message": "下载已触发"}
|
||||
|
||||
except HTTPException as e:
|
||||
print(f"Error processing tables: {str(e.detail)}")
|
||||
@@ -199,13 +313,13 @@ class Action:
|
||||
{
|
||||
"type": "status",
|
||||
"data": {
|
||||
"description": f"保存文件时出错: {e.detail}",
|
||||
"description": f"保存文件错误: {e.detail}",
|
||||
"done": True,
|
||||
},
|
||||
}
|
||||
)
|
||||
await self._send_notification(
|
||||
__event_emitter__, "error", "没有找到可以导出的表格!"
|
||||
__event_emitter__, "error", "未找到可导出的表格!"
|
||||
)
|
||||
raise e
|
||||
except Exception as e:
|
||||
@@ -214,42 +328,103 @@ class Action:
|
||||
{
|
||||
"type": "status",
|
||||
"data": {
|
||||
"description": f"保存文件时出错: {str(e)}",
|
||||
"description": f"保存文件错误: {str(e)}",
|
||||
"done": True,
|
||||
},
|
||||
}
|
||||
)
|
||||
await self._send_notification(
|
||||
__event_emitter__, "error", "没有找到可以导出的表格!"
|
||||
__event_emitter__, "error", "未找到可导出的表格!"
|
||||
)
|
||||
|
||||
async def generate_title_using_ai(
|
||||
self, body: dict, content: str, user_id: str, request: Any
|
||||
self,
|
||||
body: dict,
|
||||
content: str,
|
||||
user_id: str,
|
||||
request: Any,
|
||||
event_emitter: Callable = None,
|
||||
) -> str:
|
||||
if not request:
|
||||
return ""
|
||||
|
||||
try:
|
||||
user_obj = Users.get_user_by_id(user_id)
|
||||
model = body.get("model")
|
||||
# 使用配置的 MODEL_ID 或回退到当前对话模型
|
||||
model = (
|
||||
self.valves.MODEL_ID.strip()
|
||||
if self.valves.MODEL_ID
|
||||
else body.get("model")
|
||||
)
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "你是一个乐于助人的助手。请为以下文本生成一个简短、简洁的标题(最多10个字)。不要使用引号。只输出标题。",
|
||||
"content": "你是一个乐于助人的助手。请根据以下内容为 Excel 导出文件生成一个简短、简洁的文件名(最多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()
|
||||
# 定义生成任务
|
||||
async def generate_task():
|
||||
return await generate_chat_completion(request, payload, user_obj)
|
||||
|
||||
# 定义通知任务
|
||||
async def notification_task():
|
||||
# 立即发送首次通知
|
||||
if event_emitter:
|
||||
await self._send_notification(
|
||||
event_emitter,
|
||||
"info",
|
||||
"AI 正在为您生成文件名,请稍候...",
|
||||
)
|
||||
|
||||
# 之后每5秒通知一次
|
||||
while True:
|
||||
await asyncio.sleep(5)
|
||||
if event_emitter:
|
||||
await self._send_notification(
|
||||
event_emitter,
|
||||
"info",
|
||||
"文件名生成中,请耐心等待...",
|
||||
)
|
||||
|
||||
# 并发运行任务
|
||||
gen_future = asyncio.ensure_future(generate_task())
|
||||
notify_future = asyncio.ensure_future(notification_task())
|
||||
|
||||
done, pending = await asyncio.wait(
|
||||
[gen_future, notify_future], return_when=asyncio.FIRST_COMPLETED
|
||||
)
|
||||
|
||||
# 如果生成完成,取消通知任务
|
||||
if not notify_future.done():
|
||||
notify_future.cancel()
|
||||
|
||||
# 获取结果
|
||||
if gen_future in done:
|
||||
response = gen_future.result()
|
||||
if response and "choices" in response:
|
||||
return response["choices"][0]["message"]["content"].strip()
|
||||
else:
|
||||
# 理论上不会发生,因为是 FIRST_COMPLETED 且我们取消了 notify
|
||||
await gen_future
|
||||
response = gen_future.result()
|
||||
if response and "choices" in response:
|
||||
return response["choices"][0]["message"]["content"].strip()
|
||||
|
||||
except Exception as e:
|
||||
print(f"生成标题时出错: {e}")
|
||||
if event_emitter:
|
||||
await self._send_notification(
|
||||
event_emitter,
|
||||
"warning",
|
||||
f"AI 文件名生成失败,将使用默认名称。错误: {str(e)}",
|
||||
)
|
||||
|
||||
return ""
|
||||
|
||||
@@ -606,25 +781,52 @@ class Action:
|
||||
with pd.ExcelWriter(file_path, engine="xlsxwriter") as writer:
|
||||
workbook = writer.book
|
||||
|
||||
# 定义表头样式 - 居中对齐(符合中国规范)
|
||||
# OpenWebUI 风格主题配色
|
||||
HEADER_BG = "#1f2937" # 深灰色 (匹配 OpenWebUI 侧边栏)
|
||||
HEADER_FG = "#ffffff" # 白色文字
|
||||
ROW_ODD_BG = "#ffffff" # 奇数行白色
|
||||
ROW_EVEN_BG = "#f3f4f6" # 偶数行浅灰 (斑马纹)
|
||||
BORDER_COLOR = "#e5e7eb" # 浅色边框
|
||||
|
||||
# 表头样式 - 居中对齐
|
||||
header_format = workbook.add_format(
|
||||
{
|
||||
"bold": True,
|
||||
"font_size": 12,
|
||||
"font_color": "white",
|
||||
"bg_color": "#00abbd",
|
||||
"font_size": 11,
|
||||
"font_name": "Arial",
|
||||
"font_color": HEADER_FG,
|
||||
"bg_color": HEADER_BG,
|
||||
"border": 1,
|
||||
"align": "center", # 表头居中
|
||||
"border_color": BORDER_COLOR,
|
||||
"align": "center",
|
||||
"valign": "vcenter",
|
||||
"text_wrap": True,
|
||||
}
|
||||
)
|
||||
|
||||
# 文本单元格样式 - 左对齐
|
||||
# 文本单元格样式 - 左对齐 (奇数行)
|
||||
text_format = workbook.add_format(
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"align": "left", # 文本左对齐
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_ODD_BG,
|
||||
"align": "left",
|
||||
"valign": "vcenter",
|
||||
"text_wrap": True,
|
||||
}
|
||||
)
|
||||
|
||||
# 文本单元格样式 - 左对齐 (偶数行 - 斑马纹)
|
||||
text_format_alt = workbook.add_format(
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_EVEN_BG,
|
||||
"align": "left",
|
||||
"valign": "vcenter",
|
||||
"text_wrap": True,
|
||||
}
|
||||
@@ -632,15 +834,52 @@ class Action:
|
||||
|
||||
# 数值单元格样式 - 右对齐
|
||||
number_format = workbook.add_format(
|
||||
{"border": 1, "align": "right", "valign": "vcenter"} # 数值右对齐
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_ODD_BG,
|
||||
"align": "right",
|
||||
"valign": "vcenter",
|
||||
}
|
||||
)
|
||||
|
||||
number_format_alt = workbook.add_format(
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_EVEN_BG,
|
||||
"align": "right",
|
||||
"valign": "vcenter",
|
||||
}
|
||||
)
|
||||
|
||||
# 整数格式 - 右对齐
|
||||
integer_format = workbook.add_format(
|
||||
{
|
||||
"num_format": "0",
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"align": "right", # 整数右对齐
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_ODD_BG,
|
||||
"align": "right",
|
||||
"valign": "vcenter",
|
||||
}
|
||||
)
|
||||
|
||||
integer_format_alt = workbook.add_format(
|
||||
{
|
||||
"num_format": "0",
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_EVEN_BG,
|
||||
"align": "right",
|
||||
"valign": "vcenter",
|
||||
}
|
||||
)
|
||||
@@ -649,8 +888,25 @@ class Action:
|
||||
decimal_format = workbook.add_format(
|
||||
{
|
||||
"num_format": "0.00",
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"align": "right", # 小数右对齐
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_ODD_BG,
|
||||
"align": "right",
|
||||
"valign": "vcenter",
|
||||
}
|
||||
)
|
||||
|
||||
decimal_format_alt = workbook.add_format(
|
||||
{
|
||||
"num_format": "0.00",
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_EVEN_BG,
|
||||
"align": "right",
|
||||
"valign": "vcenter",
|
||||
}
|
||||
)
|
||||
@@ -658,8 +914,25 @@ class Action:
|
||||
# 日期格式 - 居中对齐
|
||||
date_format = workbook.add_format(
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"align": "center", # 日期居中对齐
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_ODD_BG,
|
||||
"align": "center",
|
||||
"valign": "vcenter",
|
||||
"text_wrap": True,
|
||||
}
|
||||
)
|
||||
|
||||
date_format_alt = workbook.add_format(
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_EVEN_BG,
|
||||
"align": "center",
|
||||
"valign": "vcenter",
|
||||
"text_wrap": True,
|
||||
}
|
||||
@@ -668,12 +941,114 @@ class Action:
|
||||
# 序号格式 - 居中对齐
|
||||
sequence_format = workbook.add_format(
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"align": "center", # 序号居中对齐
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_ODD_BG,
|
||||
"align": "center",
|
||||
"valign": "vcenter",
|
||||
}
|
||||
)
|
||||
|
||||
sequence_format_alt = workbook.add_format(
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_EVEN_BG,
|
||||
"align": "center",
|
||||
"valign": "vcenter",
|
||||
}
|
||||
)
|
||||
|
||||
# 粗体单元格样式 (用于全单元格加粗)
|
||||
text_bold_format = workbook.add_format(
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_ODD_BG,
|
||||
"align": "left",
|
||||
"valign": "vcenter",
|
||||
"text_wrap": True,
|
||||
"bold": True,
|
||||
}
|
||||
)
|
||||
|
||||
text_bold_format_alt = workbook.add_format(
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_EVEN_BG,
|
||||
"align": "left",
|
||||
"valign": "vcenter",
|
||||
"text_wrap": True,
|
||||
"bold": True,
|
||||
}
|
||||
)
|
||||
|
||||
# 斜体单元格样式 (用于全单元格斜体)
|
||||
text_italic_format = workbook.add_format(
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_ODD_BG,
|
||||
"align": "left",
|
||||
"valign": "vcenter",
|
||||
"text_wrap": True,
|
||||
"italic": True,
|
||||
}
|
||||
)
|
||||
|
||||
text_italic_format_alt = workbook.add_format(
|
||||
{
|
||||
"font_name": "Arial",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": ROW_EVEN_BG,
|
||||
"align": "left",
|
||||
"valign": "vcenter",
|
||||
"text_wrap": True,
|
||||
"italic": True,
|
||||
}
|
||||
)
|
||||
|
||||
# 代码单元格样式 (用于行内代码高亮显示)
|
||||
CODE_BG = "#f0f0f0" # 代码浅灰背景
|
||||
text_code_format = workbook.add_format(
|
||||
{
|
||||
"font_name": "Consolas",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": CODE_BG,
|
||||
"align": "left",
|
||||
"valign": "vcenter",
|
||||
"text_wrap": True,
|
||||
}
|
||||
)
|
||||
|
||||
text_code_format_alt = workbook.add_format(
|
||||
{
|
||||
"font_name": "Consolas",
|
||||
"font_size": 10,
|
||||
"border": 1,
|
||||
"border_color": BORDER_COLOR,
|
||||
"bg_color": CODE_BG,
|
||||
"align": "left",
|
||||
"valign": "vcenter",
|
||||
"text_wrap": True,
|
||||
}
|
||||
)
|
||||
|
||||
for i, table in enumerate(tables):
|
||||
try:
|
||||
table_data = table["data"]
|
||||
@@ -715,12 +1090,18 @@ class Action:
|
||||
|
||||
print(f"DataFrame created with columns: {list(df.columns)}")
|
||||
|
||||
# 修复pandas FutureWarning - 使用try-except替代errors='ignore'
|
||||
# 智能数据类型转换
|
||||
for col in df.columns:
|
||||
# 先尝试数字转换
|
||||
try:
|
||||
df[col] = pd.to_numeric(df[col])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
# 尝试日期转换
|
||||
try:
|
||||
df[col] = pd.to_datetime(df[col], errors="raise")
|
||||
except (ValueError, TypeError):
|
||||
# 保持为字符串,使用 infer_objects 优化
|
||||
df[col] = df[col].infer_objects()
|
||||
|
||||
# 先写入数据(不包含表头)
|
||||
df.to_excel(
|
||||
@@ -732,19 +1113,25 @@ class Action:
|
||||
)
|
||||
worksheet = writer.sheets[sheet_name]
|
||||
|
||||
# 应用符合中国规范的格式化
|
||||
# 应用符合中国规范的格式化 (带斑马纹)
|
||||
formats = {
|
||||
"header": header_format,
|
||||
"text": [text_format, text_format_alt],
|
||||
"number": [number_format, number_format_alt],
|
||||
"integer": [integer_format, integer_format_alt],
|
||||
"decimal": [decimal_format, decimal_format_alt],
|
||||
"date": [date_format, date_format_alt],
|
||||
"sequence": [sequence_format, sequence_format_alt],
|
||||
"bold": [text_bold_format, text_bold_format_alt],
|
||||
"italic": [text_italic_format, text_italic_format_alt],
|
||||
"code": [text_code_format, text_code_format_alt],
|
||||
}
|
||||
self.apply_chinese_standard_formatting(
|
||||
worksheet,
|
||||
df,
|
||||
headers,
|
||||
workbook,
|
||||
header_format,
|
||||
text_format,
|
||||
number_format,
|
||||
integer_format,
|
||||
decimal_format,
|
||||
date_format,
|
||||
sequence_format,
|
||||
formats,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -761,23 +1148,22 @@ class Action:
|
||||
df,
|
||||
headers,
|
||||
workbook,
|
||||
header_format,
|
||||
text_format,
|
||||
number_format,
|
||||
integer_format,
|
||||
decimal_format,
|
||||
date_format,
|
||||
sequence_format,
|
||||
formats,
|
||||
):
|
||||
"""
|
||||
应用符合中国官方表格规范的格式化
|
||||
- 表头: 居中对齐
|
||||
应用符合中国官方表格规范的格式化 (带斑马纹)
|
||||
- 表头: 居中对齐 (深色背景)
|
||||
- 数值: 右对齐
|
||||
- 文本: 左对齐
|
||||
- 日期: 居中对齐
|
||||
- 序号: 居中对齐
|
||||
- 斑马纹: 隔行变色
|
||||
- 支持全单元格 Markdown 粗体 (**text**) 和斜体 (*text*)
|
||||
"""
|
||||
try:
|
||||
# 从 formats 字典提取格式
|
||||
header_format = formats["header"]
|
||||
|
||||
# 1. 写入表头(居中对齐)
|
||||
print(f"Writing headers with Chinese standard alignment: {headers}")
|
||||
for col_idx, header in enumerate(headers):
|
||||
@@ -801,43 +1187,97 @@ class Action:
|
||||
else:
|
||||
column_types[col_idx] = "text"
|
||||
|
||||
# 3. 写入并格式化数据(根据类型使用不同对齐方式)
|
||||
# 3. 写入并格式化数据(带斑马纹)
|
||||
for row_idx, row in df.iterrows():
|
||||
# 确定奇偶行 (0-indexed, 所以 row 0 视觉上是第 1 行)
|
||||
is_alt_row = row_idx % 2 == 1 # 偶数索引 = 奇数行, 使用 alt 格式
|
||||
|
||||
for col_idx, value in enumerate(row):
|
||||
content_type = column_types.get(col_idx, "text")
|
||||
|
||||
# 根据内容类型选择格式
|
||||
# 根据内容类型和斑马纹选择格式
|
||||
fmt_idx = 1 if is_alt_row else 0
|
||||
|
||||
if content_type == "number":
|
||||
# 数值类型 - 右对齐
|
||||
if pd.api.types.is_numeric_dtype(df.iloc[:, col_idx]):
|
||||
if pd.api.types.is_integer_dtype(df.iloc[:, col_idx]):
|
||||
current_format = integer_format
|
||||
current_format = formats["integer"][fmt_idx]
|
||||
else:
|
||||
try:
|
||||
numeric_value = float(value)
|
||||
if numeric_value.is_integer():
|
||||
current_format = integer_format
|
||||
current_format = formats["integer"][fmt_idx]
|
||||
value = int(numeric_value)
|
||||
else:
|
||||
current_format = decimal_format
|
||||
current_format = formats["decimal"][fmt_idx]
|
||||
except (ValueError, TypeError):
|
||||
current_format = decimal_format
|
||||
current_format = formats["decimal"][fmt_idx]
|
||||
else:
|
||||
current_format = number_format
|
||||
current_format = formats["number"][fmt_idx]
|
||||
|
||||
elif content_type == "date":
|
||||
# 日期类型 - 居中对齐
|
||||
current_format = date_format
|
||||
current_format = formats["date"][fmt_idx]
|
||||
|
||||
elif content_type == "sequence":
|
||||
# 序号类型 - 居中对齐
|
||||
current_format = sequence_format
|
||||
current_format = formats["sequence"][fmt_idx]
|
||||
|
||||
else:
|
||||
# 文本类型 - 左对齐
|
||||
current_format = text_format
|
||||
current_format = formats["text"][fmt_idx]
|
||||
|
||||
worksheet.write(row_idx + 1, col_idx, value, current_format)
|
||||
if content_type == "text" and isinstance(value, str):
|
||||
# 检查是否全单元格加粗 (**text**)
|
||||
match_bold = re.fullmatch(r"\*\*(.+)\*\*", value.strip())
|
||||
# 检查是否全单元格斜体 (*text*)
|
||||
match_italic = re.fullmatch(r"\*(.+)\*", value.strip())
|
||||
# 检查是否全单元格代码 (`text`)
|
||||
match_code = re.fullmatch(r"`(.+)`", value.strip())
|
||||
|
||||
if match_bold:
|
||||
# 提取内容并应用粗体格式
|
||||
clean_value = match_bold.group(1)
|
||||
worksheet.write(
|
||||
row_idx + 1,
|
||||
col_idx,
|
||||
clean_value,
|
||||
formats["bold"][fmt_idx],
|
||||
)
|
||||
elif match_italic:
|
||||
# 提取内容并应用斜体格式
|
||||
clean_value = match_italic.group(1)
|
||||
worksheet.write(
|
||||
row_idx + 1,
|
||||
col_idx,
|
||||
clean_value,
|
||||
formats["italic"][fmt_idx],
|
||||
)
|
||||
elif match_code:
|
||||
# 提取内容并应用代码格式 (高亮显示)
|
||||
clean_value = match_code.group(1)
|
||||
worksheet.write(
|
||||
row_idx + 1,
|
||||
col_idx,
|
||||
clean_value,
|
||||
formats["code"][fmt_idx],
|
||||
)
|
||||
else:
|
||||
# 移除部分 Markdown 格式符号 (Excel 无法渲染部分格式)
|
||||
# 移除粗体标记 **text** -> text
|
||||
clean_value = re.sub(r"\*\*(.+?)\*\*", r"\1", value)
|
||||
# 移除斜体标记 *text* -> text (但不影响 ** 内部的内容)
|
||||
clean_value = re.sub(
|
||||
r"(?<!\*)\*([^*]+)\*(?!\*)", r"\1", clean_value
|
||||
)
|
||||
# 移除代码标记 `text` -> text
|
||||
clean_value = re.sub(r"`(.+?)`", r"\1", clean_value)
|
||||
worksheet.write(
|
||||
row_idx + 1, col_idx, clean_value, current_format
|
||||
)
|
||||
else:
|
||||
worksheet.write(row_idx + 1, col_idx, value, current_format)
|
||||
|
||||
# 4. 自动调整列宽
|
||||
for col_idx, column in enumerate(headers):
|
||||
@@ -937,3 +1377,6 @@ class Action:
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Even basic formatting failed: {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Even basic formatting failed: {str(e)}")
|
||||
@@ -48,3 +48,9 @@ GitHub: [Fu-Jie/awesome-openwebui](https://github.com/Fu-Jie/awesome-openwebui)
|
||||
## License
|
||||
|
||||
MIT License
|
||||
|
||||
## Changelog
|
||||
|
||||
### v0.2.4
|
||||
|
||||
- Removed debug messages from output
|
||||
|
||||
@@ -48,3 +48,9 @@ GitHub: [Fu-Jie/awesome-openwebui](https://github.com/Fu-Jie/awesome-openwebui)
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v0.2.4
|
||||
|
||||
- 移除输出中的调试信息
|
||||
|
||||
@@ -3,7 +3,7 @@ title: Flash Card
|
||||
author: Fu-Jie
|
||||
author_url: https://github.com/Fu-Jie
|
||||
funding_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||
version: 0.2.2
|
||||
version: 0.2.4
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwb2x5Z29uIHBvaW50cz0iMTIgMiAyIDcgMTIgMTIgMjIgNyAxMiAyIi8+PHBvbHlsaW5lIHBvaW50cz0iMiAxNyAxMiAyMiAyMiAxNyIvPjxwb2x5bGluZSBwb2ludHM9IjIgMTIgMTIgMTcgMjIgMTIiLz48L3N2Zz4=
|
||||
description: Quickly generates beautiful flashcards from text, extracting key points and categories.
|
||||
"""
|
||||
@@ -147,7 +147,7 @@ class Action:
|
||||
if role == "user"
|
||||
else "Assistant" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(f"[{role_label} Message {i}]\n{text_content}")
|
||||
aggregated_parts.append(f"{text_content}")
|
||||
|
||||
if not aggregated_parts:
|
||||
return body
|
||||
|
||||
@@ -3,7 +3,7 @@ title: 闪记卡 (Flash Card)
|
||||
author: Fu-Jie
|
||||
author_url: https://github.com/Fu-Jie
|
||||
funding_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||
version: 0.2.2
|
||||
version: 0.2.4
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwb2x5Z29uIHBvaW50cz0iMTIgMiAyIDcgMTIgMTIgMjIgNyAxMiAyIi8+PHBvbHlsaW5lIHBvaW50cz0iMiAxNyAxMiAyMiAyMiAxNyIvPjxwb2x5bGluZSBwb2ludHM9IjIgMTIgMTIgMTcgMjIgMTIiLz48L3N2Zz4=
|
||||
description: 快速将文本提炼为精美的学习记忆卡片,支持核心要点提取与分类。
|
||||
"""
|
||||
@@ -144,7 +144,7 @@ class Action:
|
||||
if role == "user"
|
||||
else "助手" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(f"[{role_label} 消息 {i}]\n{text_content}")
|
||||
aggregated_parts.append(f"{text_content}")
|
||||
|
||||
if not aggregated_parts:
|
||||
return body
|
||||
@@ -63,3 +63,9 @@ data
|
||||
## 📄 License
|
||||
|
||||
MIT License
|
||||
|
||||
## Changelog
|
||||
|
||||
### v1.3.2
|
||||
|
||||
- Removed debug messages from output
|
||||
|
||||
@@ -63,3 +63,9 @@ data
|
||||
## 📄 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.3.2
|
||||
|
||||
- 移除输出中的调试信息
|
||||
|
||||
@@ -3,7 +3,7 @@ title: 📊 Smart Infographic (AntV)
|
||||
author: jeff
|
||||
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogIDxsaW5lIHgxPSIxMiIgeTE9IjIwIiB4Mj0iMTIiIHkyPSIxMCIgLz4KICA8bGluZSB4MT0iMTgiIHkxPSIyMCIgeDI9IjE4IiB5Mj0iNCIgLz4KICA8bGluZSB4MT0iNiIgeTE9IjIwIiB4Mj0iNiIgeTI9IjE2IiAvPgo8L3N2Zz4=
|
||||
version: 1.3.0
|
||||
version: 1.3.2
|
||||
description: AI-powered infographic generator based on AntV Infographic. Supports professional templates, auto-icon matching, and SVG/PNG downloads.
|
||||
"""
|
||||
|
||||
@@ -961,9 +961,7 @@ class Action:
|
||||
if role == "user"
|
||||
else "Assistant" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(
|
||||
f"[{role_label} Message {i}]\n{text_content}"
|
||||
)
|
||||
aggregated_parts.append(f"{text_content}")
|
||||
|
||||
if not aggregated_parts:
|
||||
raise ValueError("Unable to get valid user message content.")
|
||||
|
||||
@@ -3,7 +3,7 @@ title: 📊 智能信息图 (AntV Infographic)
|
||||
author: jeff
|
||||
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogIDxsaW5lIHgxPSIxMiIgeTE9IjIwIiB4Mj0iMTIiIHkyPSIxMCIgLz4KICA8bGluZSB4MT0iMTgiIHkxPSIyMCIgeDI9IjE4IiB5Mj0iNCIgLz4KICA8bGluZSB4MT0iNiIgeTE9IjIwIiB4Mj0iNiIgeTI9IjE2IiAvPgo8L3N2Zz4=
|
||||
version: 1.3.0
|
||||
version: 1.3.2
|
||||
description: 基于 AntV Infographic 的智能信息图生成插件。支持多种专业模板,自动图标匹配,并提供 SVG/PNG 下载功能。
|
||||
"""
|
||||
|
||||
@@ -1026,7 +1026,7 @@ class Action:
|
||||
if role == "user"
|
||||
else "助手" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(f"[{role_label} 消息 {i}]\n{text_content}")
|
||||
aggregated_parts.append(f"{text_content}")
|
||||
|
||||
if not aggregated_parts:
|
||||
raise ValueError("无法获取有效的用户消息内容。")
|
||||
@@ -24,7 +24,7 @@ if not API_KEY or not BASE_URL:
|
||||
sys.exit(1)
|
||||
|
||||
# =================================================================
|
||||
# Prompts (Extracted from 信息图.py)
|
||||
# Prompts (Extracted from infographic_cn.py)
|
||||
# =================================================================
|
||||
|
||||
SYSTEM_PROMPT_INFOGRAPHIC_ASSISTANT = """
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Smart Mind Map - Mind Mapping Generation Plugin
|
||||
|
||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.8.0 | **License:** MIT
|
||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.8.2 | **License:** MIT
|
||||
|
||||
> **Important**: To ensure the maintainability and usability of all plugins, each plugin should be accompanied by clear and comprehensive documentation to ensure its functionality, configuration, and usage are well explained.
|
||||
|
||||
@@ -39,7 +39,7 @@ Smart Mind Map is a powerful OpenWebUI action plugin that intelligently analyzes
|
||||
|
||||
### 1. Plugin Installation
|
||||
|
||||
1. Download the `思维导图.py` file to your local computer
|
||||
1. Download the `smart_mind_map_cn.py` file to your local computer
|
||||
2. In OpenWebUI Admin Settings, find the "Plugins" section
|
||||
3. Select "Actions" type
|
||||
4. Upload the downloaded file
|
||||
@@ -277,7 +277,11 @@ This plugin uses only OpenWebUI's built-in dependencies. **No additional package
|
||||
|
||||
## Changelog
|
||||
|
||||
### v0.8.0 (Current Version)
|
||||
### v0.8.2
|
||||
|
||||
- Removed debug messages from output
|
||||
|
||||
### v0.8.0 (Previous Version)
|
||||
|
||||
**Major Features:**
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 思维导图 - 思维导图生成插件
|
||||
|
||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie) | **版本:** 0.8.0 | **许可证:** MIT
|
||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie) | **版本:** 0.8.2 | **许可证:** MIT
|
||||
|
||||
> **重要提示**:为了确保所有插件的可维护性和易用性,每个插件都应附带清晰、完整的文档,以确保其功能、配置和使用方法得到充分说明。
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
### 1. 插件安装
|
||||
|
||||
1. 下载 `思维导图.py` 文件到本地
|
||||
1. 下载 `smart_mind_map_cn.py` 文件到本地
|
||||
2. 在 OpenWebUI 管理员设置中找到"插件"(Plugins)部分
|
||||
3. 选择"动作"(Actions)类型
|
||||
4. 上传下载的文件
|
||||
@@ -277,7 +277,11 @@
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v0.8.0(当前版本)
|
||||
### v0.8.2
|
||||
|
||||
- 移除输出中的调试信息
|
||||
|
||||
### v0.8.0 (Previous Version)
|
||||
|
||||
**主要功能:**
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ title: Smart Mind Map
|
||||
author: Fu-Jie
|
||||
author_url: https://github.com/Fu-Jie
|
||||
funding_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||
version: 0.8.0
|
||||
version: 0.8.2
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4=
|
||||
description: Intelligently analyzes text content and generates interactive mind maps to help users structure and visualize knowledge.
|
||||
"""
|
||||
@@ -960,7 +960,7 @@ class Action:
|
||||
if role == "user"
|
||||
else "Assistant" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(f"[{role_label} Message {i}]\n{text_content}")
|
||||
aggregated_parts.append(f"{text_content}")
|
||||
|
||||
if not aggregated_parts:
|
||||
error_message = "Unable to retrieve valid user message content."
|
||||
|
||||
@@ -3,7 +3,7 @@ title: 思维导图
|
||||
author: Fu-Jie
|
||||
author_url: https://github.com/Fu-Jie
|
||||
funding_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||
version: 0.8.0
|
||||
version: 0.8.2
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4=
|
||||
description: 智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。
|
||||
"""
|
||||
@@ -957,7 +957,7 @@ class Action:
|
||||
if role == "user"
|
||||
else "助手" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(f"[{role_label} 消息 {i}]\n{text_content}")
|
||||
aggregated_parts.append(f"{text_content}")
|
||||
|
||||
if not aggregated_parts:
|
||||
error_message = "无法获取有效的用户消息内容。"
|
||||
@@ -22,3 +22,9 @@ GitHub: [Fu-Jie/awesome-openwebui](https://github.com/Fu-Jie/awesome-openwebui)
|
||||
## License
|
||||
|
||||
MIT License
|
||||
|
||||
## Changelog
|
||||
|
||||
### v0.1.2
|
||||
|
||||
- Removed debug messages from output
|
||||
|
||||
@@ -22,3 +22,9 @@ GitHub: [Fu-Jie/awesome-openwebui](https://github.com/Fu-Jie/awesome-openwebui)
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v0.1.2
|
||||
|
||||
- 移除输出中的调试信息
|
||||
|
||||
@@ -3,7 +3,7 @@ title: Deep Reading & Summary
|
||||
author: Fu-Jie
|
||||
author_url: https://github.com/Fu-Jie
|
||||
funding_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||
version: 0.1.0
|
||||
version: 0.1.2
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNSAxMmgtNSIvPjxwYXRoIGQ9Ik0xNSA4aC01Ii8+PHBhdGggZD0iTTE5IDE3VjVhMiAyIDAgMCAwLTItMkg0Ii8+PHBhdGggZD0iTTggMjFoMTJhMiAyIDAgMCAwIDItMnYtMWExIDEgMCAwIDAtMS0xSDExYTEgMSAwIDAgMC0xIDF2MWEyIDIgMCAxIDEtNCAwVjVhMiAyIDAgMSAwLTQgMHYyYTEgMSAwIDAgMCAxIDFoMyIvPjwvc3ZnPg==
|
||||
description: Provides deep reading analysis and summarization for long texts.
|
||||
requirements: jinja2, markdown
|
||||
@@ -529,9 +529,7 @@ class Action:
|
||||
if role == "user"
|
||||
else "Assistant" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(
|
||||
f"[{role_label} Message {i}]\n{text_content}"
|
||||
)
|
||||
aggregated_parts.append(f"{text_content}")
|
||||
|
||||
if not aggregated_parts:
|
||||
raise ValueError("Unable to get valid user message content.")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
title: 精读 (Deep Reading)
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImciIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIxIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjNDI4NWY0Ii8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMWU4OGU1Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTYgMmg4bDYgNnYxMmEyIDIgMCAwIDEtMiAySDZhMiAyIDAgMCAxLTItMlY0YTIgMiAwIDAgMSAyLTJ6IiBmaWxsPSJ1cmwoI2cpIi8+PHBhdGggZD0iTTE0IDJsNiA2aC02eiIgZmlsbD0iIzFlODhlNSIgb3BhY2l0eT0iMC42Ii8+PGxpbmUgeDE9IjgiIHkxPSIxMyIgeDI9IjE2IiB5Mj0iMTMiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiLz48bGluZSB4MT0iOCIgeTE9IjE3IiB4Mj0iMTQiIHkyPSIxNyIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEuNSIvPjxjaXJjbGUgY3g9IjE2IiBjeT0iMTgiIHI9IjMiIGZpbGw9IiNmZmQ3MDAiLz48cGF0aCBkPSJNMTYgMTZsMS41IDEuNSIgc3Ryb2tlPSIjNDI4NWY0IiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvc3ZnPg==
|
||||
version: 0.1.0
|
||||
version: 0.1.2
|
||||
description: 深度分析长篇文本,提炼详细摘要、关键信息点和可执行的行动建议,适合工作和学习场景。
|
||||
requirements: jinja2, markdown
|
||||
"""
|
||||
@@ -528,7 +528,7 @@ class Action:
|
||||
if role == "user"
|
||||
else "助手" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(f"[{role_label} 消息 {i}]\n{text_content}")
|
||||
aggregated_parts.append(f"{text_content}")
|
||||
|
||||
if not aggregated_parts:
|
||||
raise ValueError("无法获取有效的用户消息内容。")
|
||||
@@ -41,19 +41,19 @@
|
||||
|
||||
## Actions (动作插件)
|
||||
|
||||
1. **📊 智能信息图 (infographic/信息图.py)** - 基于 AntV Infographic 的智能信息图生成插件,支持多种专业模板与 SVG/PNG 下载
|
||||
1. **📊 智能信息图 (infographic/infographic_cn.py)** - 基于 AntV Infographic 的智能信息图生成插件,支持多种专业模板与 SVG/PNG 下载
|
||||
|
||||
2. **🧠 思维导图 (smart-mind-map/思维导图.py)** - 智能分析文本内容生成交互式思维导图,帮助用户结构化和可视化知识
|
||||
2. **🧠 思维导图 (smart-mind-map/smart_mind_map_cn.py)** - 智能分析文本内容生成交互式思维导图,帮助用户结构化和可视化知识
|
||||
|
||||
3. **📊 导出为 Excel (export_to_excel/导出为Excel.py)** - 将对话历史中的 Markdown 表格导出为符合中国规范的 Excel 文件
|
||||
3. **📊 导出为 Excel (export_to_excel/export_to_excel_cn.py)** - 将对话历史中的 Markdown 表格导出为符合中国规范的 Excel 文件
|
||||
|
||||
4. **⚡ 闪记卡 (knowledge-card/闪记卡.py)** - 快速将文本提炼为精美的学习记忆卡片,支持核心要点提取与分类
|
||||
4. **⚡ 闪记卡 (flash-card/flash_card_cn.py)** - 快速将文本提炼为精美的学习记忆卡片,支持核心要点提取与分类
|
||||
|
||||
5. **📖 精读 (summary/精读.py)** - 深度分析长篇文本,提炼详细摘要、关键信息点和可执行的行动建议
|
||||
5. **📖 精读 (summary/summary_cn.py)** - 深度分析长篇文本,提炼详细摘要、关键信息点和可执行的行动建议
|
||||
|
||||
## Filters (过滤器插件)
|
||||
|
||||
1. **🔄 异步上下文压缩 (async-context-compression/异步上下文压缩.py)** - 异步生成摘要并压缩对话历史,支持数据库持久化存储
|
||||
1. **🔄 异步上下文压缩 (async-context-compression/async_context_compression_cn.py)** - 异步生成摘要并压缩对话历史,支持数据库持久化存储
|
||||
|
||||
2. **✨ 上下文增强过滤器 (context_enhancement_filter/context_enhancement_filter.py)** - 增强请求上下文和优化模型功能,包含环境变量管理、模型功能适配和内容清洗
|
||||
|
||||
|
||||
@@ -295,6 +295,8 @@ def main():
|
||||
if args.output:
|
||||
with open(args.output, "w", encoding="utf-8") as f:
|
||||
f.write(output)
|
||||
if not output.endswith("\n"):
|
||||
f.write("\n")
|
||||
print(f"Output written to {args.output}")
|
||||
else:
|
||||
print(output)
|
||||
|
||||
Reference in New Issue
Block a user