Compare commits

..

40 Commits

Author SHA1 Message Date
fujie
28d55c1469 feat: 添加 JavaScript 渲染 PoC,支持通过 API 更新消息内容 2026-01-05 09:01:42 +08:00
fujie
59933e9361 docs: 更新插件安装指南,增加OpenWebUI社区推荐安装方式。 2026-01-05 00:31:18 +08:00
fujie
7cbd0e2920 chore: release export-to-word v0.3.0 2026-01-04 03:17:35 +08:00
fujie
88038b35cc chore: release plugins (remove debug messages) 2026-01-04 03:14:28 +08:00
fujie
1fd7d90284 fix: sync mermaid layout optimization to cn plugin 2026-01-04 02:44:33 +08:00
fujie
aee9c93bfb docs: update documentation for Export to Word plugin (v0.2.0) 2026-01-04 02:40:46 +08:00
fujie
3951f7f91d feat: 增强 Word 导出插件,支持原生数学公式、Mermaid 图表、引用、高级表格格式及剥离推理块。 2026-01-04 02:24:46 +08:00
fujie
3680fcf39f feat: 更新了多个插件版本,并同步更新了中英文文档和相关说明。 2026-01-03 18:43:22 +08:00
fujie
593a9ce22b feat: 升级Excel导出插件,增加AI生成文件名、导出所有消息选项并优化样式 2026-01-03 17:57:27 +08:00
fujie
fe497cccb7 refactor: 将代理的 Git 操作规则引用到 copilot-instructions.md 2026-01-03 16:27:37 +08:00
fujie
88aa7e156a fix: check for files before gh release upload to prevent empty file error 2026-01-03 16:13:22 +08:00
fujie
dbfce27986 fix: explicitly add newlines before EOF in release workflow 2026-01-03 16:04:21 +08:00
fujie
9be6fe08fa fix: ensure extract_plugin_versions.py output ends with newline to prevent GH Actions EOF error 2026-01-03 16:00:50 +08:00
fujie
782378eed8 fix: quote GITHUB_OUTPUT delimiter to prevent EOF error 2026-01-03 14:18:50 +08:00
fujie
4e59bb6518 feat: support full-cell markdown italic formatting in excel export 2026-01-03 14:18:32 +08:00
fujie
3e73fcb3f0 feat: refine excel export to apply bold formatting only to fully bolded cells 2026-01-03 14:16:00 +08:00
fujie
c460337c43 feat: support markdown italic formatting and refine bold parsing 2026-01-03 14:12:53 +08:00
fujie
e775b23503 feat: support markdown bold formatting in excel export 2026-01-03 14:10:11 +08:00
fujie
b3cdb8e26e fix: use gh cli for asset upload to support chinese filenames 2026-01-03 13:52:25 +08:00
fujie
0e6f902d16 chore: add debug steps and artifact upload to release workflow 2026-01-03 13:38:59 +08:00
fujie
c15c73897f fix: enforce utf-8 and disable git path quoting in release workflow to support chinese filenames 2026-01-03 13:25:15 +08:00
fujie
035439ce02 docs: forbid agents from auto-pushing to remote main branch 2026-01-03 13:21:31 +08:00
fujie
b84ff4a3a2 chore: disable auto-release on push to main, use workflow_dispatch only 2026-01-03 13:18:36 +08:00
fujie
e22744abd0 feat: add export scope option and smart sheet naming to export to excel plugin (v0.3.5) 2026-01-03 13:15:13 +08:00
fujie
54c90238f7 fix: enforce utf-8 locale in release workflow to support chinese filenames 2026-01-03 12:42:26 +08:00
fujie
40d77121bd docs: update plugin development standards for readme requirements 2026-01-03 12:37:05 +08:00
fujie
3795976a79 feat: optimize export to excel filename generation and update docs 2026-01-03 12:37:05 +08:00
fujie
f5e5e5caa4 feat: 添加版本一致性检查脚本以确保插件和文档版本同步 2026-01-03 12:25:04 +08:00
fujie
0c893ce61f docs: 新增插件开发工作流程文档并更新了多个插件的版本信息 2026-01-03 12:22:14 +08:00
fujie
8f4ce8f084 docs: 更新异步上下文压缩的图标。 2026-01-03 12:00:18 +08:00
fujie
ac2cf00807 feat: 更新了异步上下文压缩插件版本,并优化了发布工作流以直接上传插件文件,同时排除了新的插件模板文件。 2026-01-03 11:54:17 +08:00
fujie
b9d8100cdb docs: add consistency maintenance section to copilot instructions 2026-01-03 11:18:39 +08:00
fujie
bb1cc0d966 docs: update copilot instructions for consistency and sync READMEs 2026-01-03 11:17:54 +08:00
fujie
2e238c5b5d 移除多模型上下文合并器插件的描述。 2026-01-03 11:17:48 +08:00
fujie
b56e7cb41e docs: update documentation versions, fix rendering issues, and update READMEs 2026-01-03 11:16:24 +08:00
fujie
236ae43c0c fix: 提取插件版本时跳过 gemini_manifold.pygemini_manifold_companion.py 文件 2026-01-03 11:00:14 +08:00
fujie
a4e8cc52f9 docs: 更新 Copilot 指令。 2026-01-03 10:57:21 +08:00
fujie
c8e8434bc6 feat: 引入选项以在发布说明中忽略已移除的插件。 2026-01-03 10:55:05 +08:00
fujie
3ee00bb083 docs(copilot-instructions): add release workflow guidelines
- Add release workflow section with automatic release process
- Document version numbering rules (semver)
- Add three release methods: direct push, PR, manual trigger
- Include commit message convention (Conventional Commits)
- Add release checklist
2026-01-01 04:23:22 +08:00
fujie
0a9549da34 feat(flash-card): add _get_user_context for safer user info retrieval
- Add _get_user_context method to handle various __user__ types (dict, list, tuple)
- Prevent AttributeError when __user__ is not a dict
- Update version to 0.2.2 for both English and Chinese versions
2026-01-01 04:19:22 +08:00
63 changed files with 6759 additions and 2094 deletions

View File

@@ -0,0 +1,103 @@
---
description: OpenWebUI Plugin Development & Release Workflow
---
# OpenWebUI Plugin Development Workflow
This workflow outlines the standard process for developing, documenting, and releasing plugins for OpenWebUI, ensuring compliance with project standards and CI/CD requirements.
## 1. Development Standards
Reference: `.github/copilot-instructions.md`
### Bilingual Requirement
Every plugin **MUST** have bilingual versions for both code and documentation:
- **Code**:
- English: `plugins/{type}/{name}/{name}.py`
- Chinese: `plugins/{type}/{name}/{name_cn}.py` (or `中文名.py`)
- **README**:
- English: `plugins/{type}/{name}/README.md`
- Chinese: `plugins/{type}/{name}/README_CN.md`
### Code Structure
- **Docstring**: Must include `title`, `author`, `version`, `description`, etc.
- **Valves**: Use `pydantic` for configuration.
- **Database**: Re-use `open_webui.internal.db` shared connection.
- **User Context**: Use `_get_user_context` helper method.
### Commit Messages
- **Language**: **English ONLY**. Do not use Chinese in commit messages.
- **Format**: Conventional Commits (e.g., `feat:`, `fix:`, `docs:`).
## 2. Documentation Updates
When adding or updating a plugin, you **MUST** update the following documentation files to maintain consistency:
### Plugin Directory
- `README.md`: Update version, description, and usage. **Explicitly describe new features.**
- `README_CN.md`: Update version, description, and usage. **Explicitly describe new features.**
### Global Documentation (`docs/`)
- **Index Pages**:
- `docs/plugins/{type}/index.md`: Add/Update list item with **correct version**.
- `docs/plugins/{type}/index.zh.md`: Add/Update list item with **correct version**.
- **Detail Pages**:
- `docs/plugins/{type}/{name}.md`: Ensure content matches README.
- `docs/plugins/{type}/{name}.zh.md`: Ensure content matches README_CN.
### Root README
- `README.md`: Add to "Featured Plugins" if applicable.
- `README_CN.md`: Add to "Featured Plugins" if applicable.
## 3. Version Control & Release
Reference: `.github/workflows/release.yml`
### Version Bumping
- **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`).
- **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`)
4. Chinese README (`README_CN.md`)
5. Docs Index (`docs/.../index.md`)
6. Docs Index CN (`docs/.../index.zh.md`)
7. Docs Detail (`docs/.../{name}.md`)
8. Docs Detail CN (`docs/.../{name}.zh.md`)
### Automated Release Process
1. **Trigger**: Push to `main` branch with changes in `plugins/**/*.py`.
2. **Detection**: `scripts/extract_plugin_versions.py` detects changed plugins and compares versions.
3. **Release**:
- Generates release notes based on changes.
- Creates a GitHub Release tag (e.g., `v2024.01.01-1`).
- Uploads individual `.py` files of **changed plugins only** as assets.
### Pull Request Check
- Workflow: `.github/workflows/plugin-version-check.yml`
- Checks if plugin files are modified.
- **Fails** if version number is not updated.
- **Fails** if PR description is too short (< 20 chars).
## 4. Verification Checklist
Before committing:
- [ ] Code is bilingual and functional?
- [ ] Docstrings have updated version?
- [ ] READMEs are updated and bilingual?
- [ ] `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.

View File

@@ -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
```
@@ -37,7 +37,9 @@ README 文件应包含以下内容:
- 安装和设置说明 / Installation and setup instructions
- 使用示例 / Usage examples
- 故障排除指南 / Troubleshooting guide
- 故障排除指南 / Troubleshooting guide
- 版本和作者信息 / Version and author information
- **新增功能 / New Features**: 如果是更新现有插件,必须明确列出并描述新增功能(发布到官方市场的重要要求)。/ If updating an existing plugin, explicitly list and describe new features (Critical for official market release).
### 官方文档 (Official Documentation)
@@ -795,10 +797,212 @@ For iframe plugins to access parent document theme information, users need to co
- [ ] 实现 Valves 配置
- [ ] 使用 logging 而非 print
- [ ] 测试双语界面
- [ ] **一致性检查 (Consistency Check)**:
---
## 📚 参考资源 (Reference Resources)
## 🚀 高级开发模式 (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/` 下的对应文档
- [ ] 确保文档版本号与代码一致
---
## 🔄 一致性维护 (Consistency Maintenance)
任何插件的**新增、修改或移除**,必须同时更新以下三个位置,保持完全一致:
1. **插件代码 (Plugin Code)**: 更新 `version` 和功能实现。
2. **项目文档 (Docs)**: 更新 `docs/` 下对应的文档文件(版本号、功能描述)。
3. **自述文件 (README)**: 更新根目录下的 `README.md``README_CN.md` 中的插件列表。
> [!IMPORTANT]
> 提交 PR 前,请务必检查这三处是否同步。例如:如果删除了一个插件,必须同时从 README 列表中移除,并删除对应的 docs 文档。
---
## <20> 发布工作流 (Release Workflow)
### 自动发布 (Automatic Release)
当插件更新推送到 `main` 分支时,会**自动触发**发布流程:
1. 🔍 检测版本变化(与上次 release 对比)
2. 📝 生成发布说明(包含更新内容和提交记录)
3. 📦 创建 GitHub Release包含可下载的插件文件
4. 🏷️ 自动生成版本号(格式:`vYYYY.MM.DD-运行号`
**注意**:仅**移除插件**(删除文件)**不会触发**自动发布。只有新增或修改插件(且更新了版本号)才会触发发布。移除的插件将不会出现在发布日志中。
### 发布前必须完成 (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 # <- 发布时更新这里!
...
"""
```
### 版本编号规则 (Versioning)
遵循[语义化版本](https://semver.org/lang/zh-CN/)
| 变更类型 | 版本变化 | 示例 |
|---------|---------|------|
| Bug 修复 | PATCH +1 | 0.1.0 → 0.1.1 |
| 新功能 | MINOR +1 | 0.1.1 → 0.2.0 |
| 不兼容变更 | MAJOR +1 | 0.2.0 → 1.0.0 |
### 发布方式 (Release Methods)
**方式 A直接推送到 main推荐**
```bash
# 1. 暂存更改
git add plugins/actions/my-plugin/
# 2. 提交(使用规范的 commit message
git commit -m "feat(my-plugin): add new feature X
- Add feature X for better user experience
- Fix bug Y
- Update version to 0.2.0"
# 3. 推送到 main
git push origin main
# GitHub Actions 会自动创建 Release
```
**方式 B创建 PR团队协作**
```bash
# 1. 创建功能分支
git checkout -b feature/my-plugin-v0.2.0
# 2. 提交更改
git commit -m "feat(my-plugin): add new feature X"
# 3. 推送并创建 PR
git push origin feature/my-plugin-v0.2.0
# 4. PR 合并后自动触发发布
```
**方式 C手动触发发布**
1. 前往 GitHub Actions → "Plugin Release / 插件发布"
2. 点击 "Run workflow"
3. 填写版本号和发布说明
### Commit Message 规范 (Commit Convention)
使用 [Conventional Commits](https://www.conventionalcommits.org/) 格式:
```
<type>(<scope>): <description>
[optional body]
[optional footer]
```
常用类型:
- `feat`: 新功能
- `fix`: Bug 修复
- `docs`: 文档更新
- `refactor`: 代码重构
- `style`: 代码格式调整
- `perf`: 性能优化
示例:
```
feat(flash-card): add _get_user_context for safer user info retrieval
- Add _get_user_context method to handle various __user__ types
- Prevent AttributeError when __user__ is not a dict
- Update version to 0.2.2 for both English and Chinese versions
```
### 发布检查清单 (Release Checklist)
发布前确保完成以下检查:
- [ ] 更新插件版本号(英文版 + 中文版)
- [ ] 测试插件功能正常
- [ ] 确保代码通过格式检查
- [ ] 编写清晰的 commit message
- [ ] 推送到 main 分支或合并 PR
---
## <20>📚 参考资源 (Reference Resources)
- [Action 插件模板 (英文)](plugins/actions/ACTION_PLUGIN_TEMPLATE.py)
- [Action 插件模板 (中文)](plugins/actions/ACTION_PLUGIN_TEMPLATE_CN.py)
@@ -816,3 +1020,112 @@ GitHub: [Fu-Jie/awesome-openwebui](https://github.com/Fu-Jie/awesome-openwebui)
## License
MIT License
---
## 📝 Commit Message Guidelines
**Commit messages MUST be in English.** Do not use Chinese.
### Format
Follow the [Conventional Commits](https://www.conventionalcommits.org/) specification:
- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation only changes
- `style`: Changes that do not affect the meaning of the code (white-space, formatting, etc)
- `refactor`: A code change that neither fixes a bug nor adds a feature
- `perf`: A code change that improves performance
- `test`: Adding missing tests or correcting existing tests
- `chore`: Changes to the build process or auxiliary tools and libraries such as documentation generation
### Examples
**Good:**
- `feat: add new export to pdf plugin`
- `fix: resolve icon rendering issue in documentation`
- `docs: update README with installation steps`
**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()
```

View File

@@ -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:
@@ -101,7 +110,7 @@ jobs:
fi
# Compare versions and generate release notes
python scripts/extract_plugin_versions.py --compare old_versions.json --output changes.md
python scripts/extract_plugin_versions.py --compare old_versions.json --ignore-removed --output changes.md
python scripts/extract_plugin_versions.py --compare old_versions.json --json --output changes.json
echo "=== Version Changes ==="
@@ -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:
@@ -175,10 +196,7 @@ jobs:
id: plugins
run: |
python scripts/extract_plugin_versions.py --json --output plugin_versions.json
python scripts/extract_plugin_versions.py --markdown --output plugin_table.md
echo "=== Plugin Versions ==="
cat plugin_table.md
python scripts/extract_plugin_versions.py --json --output plugin_versions.json
- name: Collect plugin files for release
id: collect_files
@@ -198,32 +216,27 @@ jobs:
fi
done
else
echo "Collecting all plugin files..."
find plugins -name "*.py" -type f ! -name "__*" | while read -r file; do
dir=$(dirname "$file")
mkdir -p "release_plugins/$dir"
cp "$file" "release_plugins/$file"
done
echo "No changed plugins detected. Skipping file collection."
fi
# Create a zip file with error handling
cd release_plugins
if [ -n "$(ls -A . 2>/dev/null)" ]; then
if zip -r ../plugins_release.zip .; then
echo "Successfully created plugins_release.zip"
else
echo "Warning: Failed to create zip file, creating empty placeholder"
touch ../plugins_release.zip
fi
else
echo "No plugin files to zip, creating empty placeholder"
touch ../plugins_release.zip
fi
cd ..
# cd release_plugins
# Zip step removed as per user request
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'
@@ -239,8 +252,9 @@ jobs:
{
echo 'commits<<EOF'
echo "$COMMITS"
echo ""
echo 'EOF'
} >> $GITHUB_OUTPUT
} >> "$GITHUB_OUTPUT"
- name: Generate release notes
id: notes
@@ -280,16 +294,13 @@ jobs:
echo "" >> release_notes.md
fi
echo "## All Plugin Versions / 所有插件版本" >> release_notes.md
echo "" >> release_notes.md
cat plugin_table.md >> release_notes.md
echo "" >> release_notes.md
cat >> release_notes.md << 'EOF'
## Download / 下载
📦 **plugins_release.zip** - 包含本次更新的所有插件文件 / Contains all updated plugin files
📦 **Download the updated plugin files below** / 请在下方下载更新的插件文件
### Installation / 安装
@@ -323,10 +334,21 @@ jobs:
prerelease: ${{ github.event.inputs.prerelease || false }}
files: |
plugin_versions.json
plugins_release.zip
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
@@ -336,5 +358,4 @@ jobs:
echo "### Updated Plugins" >> $GITHUB_STEP_SUMMARY
echo "${{ needs.check-changes.outputs.release_notes }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### All Plugin Versions" >> $GITHUB_STEP_SUMMARY
cat plugin_table.md >> $GITHUB_STEP_SUMMARY

View File

@@ -14,15 +14,17 @@ Located in the `plugins/` directory, containing Python-based enhancements:
#### Actions
- **Smart Mind Map** (`smart-mind-map`): Generates interactive mind maps from text.
- **Smart Infographic** (`infographic`): Transforms text into professional infographics using AntV.
- **Knowledge Card** (`knowledge-card`): Creates beautiful flashcards for learning.
- **Export to Excel** (`export_to_excel`): Exports chat history to Excel files.
- **Export to Word** (`export_to_docx`): Exports chat history to Word documents.
- **Summary** (`summary`): Text summarization tool.
#### Filters
- **Async Context Compression** (`async-context-compression`): Optimizes token usage via context compression.
- **Context Enhancement** (`context_enhancement_filter`): Enhances chat context.
- **Gemini Manifold Companion** (`gemini_manifold_companion`): Companion filter for Gemini Manifold.
- **Multi-Model Context Merger** (`multi_model_context_merger`): Merges context from multiple models.
#### Pipes
- **Gemini Manifold** (`gemini_mainfold`): Pipeline for Gemini model integration.
@@ -58,10 +60,16 @@ This project is a collection of resources and does not require a Python environm
### Using Plugins
1. Browse the `/plugins` directory and download the plugin file (`.py`) you need.
2. Go to OpenWebUI **Admin Panel** -> **Settings** -> **Plugins**.
3. Click the upload button and select the `.py` file you just downloaded.
4. Once uploaded, refresh the page to enable the plugin in your chat settings or toolbar.
1. **Install from OpenWebUI Community (Recommended)**:
- Visit my profile: [Fu-Jie's Profile](https://openwebui.com/u/Fu-Jie)
- Browse the plugins and select the one you like.
- Click "Get" to import it directly into your OpenWebUI instance.
2. **Manual Installation**:
- Browse the `/plugins` directory and download the plugin file (`.py`) you need.
- Go to OpenWebUI **Admin Panel** -> **Settings** -> **Plugins**.
- Click the upload button and select the `.py` file you just downloaded.
- Once uploaded, refresh the page to enable the plugin in your chat settings or toolbar.
### Contributing

View File

@@ -8,15 +8,17 @@ OpenWebUI 增强功能集合。包含个人开发与收集的### 🧩 插件 (Pl
#### Actions (交互增强)
- **Smart Mind Map** (`smart-mind-map`): 智能分析文本并生成交互式思维导图。
- **Smart Infographic** (`infographic`): 基于 AntV 的智能信息图生成工具。
- **Knowledge Card** (`knowledge-card`): 快速生成精美的学习记忆卡片。
- **Export to Excel** (`export_to_excel`): 将对话内容导出为 Excel 文件。
- **Export to Word** (`export_to_docx`): 将对话内容导出为 Word 文档。
- **Summary** (`summary`): 文本摘要生成工具。
#### Filters (消息处理)
- **Async Context Compression** (`async-context-compression`): 异步上下文压缩,优化 Token 使用。
- **Context Enhancement** (`context_enhancement_filter`): 上下文增强过滤器。
- **Gemini Manifold Companion** (`gemini_manifold_companion`): Gemini Manifold 配套增强。
- **Multi-Model Context Merger** (`multi_model_context_merger`): 多模型上下文合并。
#### Pipes (模型管道)
- **Gemini Manifold** (`gemini_mainfold`): 集成 Gemini 模型的管道。
@@ -85,10 +87,16 @@ OpenWebUI 增强功能集合。包含个人开发与收集的### 🧩 插件 (Pl
### 使用插件 (Plugins)
1. `/plugins` 目录中浏览并下载你需要的插件文件 (`.py`)。
2. 打开 OpenWebUI 的 **管理员面板 (Admin Panel)** -> **设置 (Settings)** -> **插件 (Plugins)**
3. 点击上传按钮,选择刚才下载的 `.py`件。
4. 上传成功后,刷新页面,你就可以在聊天设置或工具栏中启用该插件了
1. **从 OpenWebUI 社区安装 (推荐)**:
- 访问我的主页: [Fu-Jie's Profile](https://openwebui.com/u/Fu-Jie)
- 浏览插件列表,选择你喜欢的插件。
- 点击 "Get" 按钮,将其直接导入到你的 OpenWebUI 实例中
2. **手动安装**:
-`/plugins` 目录中浏览并下载你需要的插件文件 (`.py`)。
- 打开 OpenWebUI 的 **管理员面板 (Admin Panel)** -> **设置 (Settings)** -> **插件 (Plugins)**
- 点击上传按钮,选择刚才下载的 `.py` 文件。
- 上传成功后,刷新页面,你就可以在聊天设置或工具栏中启用该插件了。
### 贡献代码

View File

@@ -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(

View File

@@ -104,10 +104,16 @@ hide:
### Using Plugins
1. Browse the [Plugin Center](plugins/index.md) and download the plugin file (`.py`)
2. Open OpenWebUI **Admin Panel****Settings****Plugins**
3. Click the upload button and select the `.py` file
4. Refresh the page and enable the plugin in your chat settings
1. **Install from OpenWebUI Community (Recommended)**:
- Visit my profile: [Fu-Jie's Profile](https://openwebui.com/u/Fu-Jie)
- Browse the plugins and select the one you like.
- Click "Get" to import it directly into your OpenWebUI instance.
2. **Manual Installation**:
- Browse the [Plugin Center](plugins/index.md) and download the plugin file (`.py`)
- Open OpenWebUI **Admin Panel****Settings****Plugins**
- Click the upload button and select the `.py` file
- Refresh the page and enable the plugin in your chat settings
---

View File

@@ -104,10 +104,16 @@ hide:
### 使用插件
1. 浏览[插件中心](plugins/index.md)并下载插件文件(`.py`
2. 打开 OpenWebUI **管理面板****设置****插件**
3. 点击上传按钮并选择 `.py` 文件
4. 刷新页面并在聊天设置中启用插件
1. **从 OpenWebUI 社区安装 (推荐)**:
- 访问我的主页: [Fu-Jie's Profile](https://openwebui.com/u/Fu-Jie)
- 浏览插件列表,选择你喜欢的插件。
- 点击 "Get" 按钮,将其直接导入到你的 OpenWebUI 实例中。
2. **手动安装**:
- 浏览[插件中心](plugins/index.md)并下载插件文件(`.py`
- 打开 OpenWebUI **管理面板****设置****插件**
- 点击上传按钮并选择 `.py` 文件
- 刷新页面并在聊天设置中启用插件
---

View File

@@ -0,0 +1,290 @@
# 使用 JavaScript 生成可视化内容的技术方案
## 概述
本文档描述了在 OpenWebUI Action 插件中使用浏览器端 JavaScript 代码生成可视化内容(如思维导图、信息图等)并将结果保存到消息中的技术方案。
## 核心架构
```mermaid
sequenceDiagram
participant Plugin as Python 插件
participant EventCall as __event_call__
participant Browser as 浏览器 (JS)
participant API as OpenWebUI API
participant DB as 数据库
Plugin->>EventCall: 1. 发送 execute 事件 (含 JS 代码)
EventCall->>Browser: 2. 执行 JS 代码
Browser->>Browser: 3. 加载可视化库 (D3/Markmap/AntV)
Browser->>Browser: 4. 渲染可视化内容
Browser->>Browser: 5. 转换为 Base64 Data URI
Browser->>API: 6. GET 获取当前消息内容
API-->>Browser: 7. 返回消息数据
Browser->>API: 8. POST 追加 Markdown 图片到消息
API->>DB: 9. 保存更新后的消息
```
## 关键步骤
### 1. Python 端通过 `__event_call__` 执行 JS
Python 插件**不直接修改 `body["messages"]`**,而是通过 `__event_call__` 发送 JS 代码让浏览器执行:
```python
async def action(
self,
body: dict,
__user__: dict = None,
__event_emitter__=None,
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
__metadata__: Optional[dict] = None,
__request__: Request = None,
) -> dict:
# 从 body 获取 chat_id 和 message_id
chat_id = body.get("chat_id", "")
message_id = body.get("id", "") # 注意body["id"] 是 message_id
# 通过 __event_call__ 执行 JS 代码
if __event_call__:
await __event_call__({
"type": "execute",
"data": {
"code": f"""
(async function() {{
const chatId = "{chat_id}";
const messageId = "{message_id}";
// ... JS 渲染和 API 更新逻辑 ...
}})();
"""
},
})
# 不修改 body直接返回
return body
```
### 2. JavaScript 加载可视化库
在浏览器端动态加载所需的 JS 库:
```javascript
// 加载 D3.js
if (!window.d3) {
await new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/d3@7';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
// 加载 Markmap (思维导图)
if (!window.markmap) {
await loadScript('https://cdn.jsdelivr.net/npm/markmap-lib@0.17');
await loadScript('https://cdn.jsdelivr.net/npm/markmap-view@0.17');
}
```
### 3. 渲染并转换为 Data URI
```javascript
// 创建 SVG 元素
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '800');
svg.setAttribute('height', '600');
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
// ... 执行渲染逻辑 (添加图形元素) ...
// 转换为 Base64 Data URI
const svgData = new XMLSerializer().serializeToString(svg);
const base64 = btoa(unescape(encodeURIComponent(svgData)));
const dataUri = 'data:image/svg+xml;base64,' + base64;
```
### 4. 获取当前消息内容
由于 Python 端不传递原始内容JS 需要通过 API 获取:
```javascript
const token = localStorage.getItem('token');
// 获取当前聊天数据
const getResponse = await fetch(`/api/v1/chats/${chatId}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});
const chatData = await getResponse.json();
// 查找目标消息
let originalContent = '';
if (chatData.chat && chatData.chat.messages) {
const targetMsg = chatData.chat.messages.find(m => m.id === messageId);
if (targetMsg && targetMsg.content) {
originalContent = targetMsg.content;
}
}
```
### 5. 调用 API 更新消息
```javascript
// 构造新内容:原始内容 + Markdown 图片
const markdownImage = `![可视化图片](${dataUri})`;
const newContent = originalContent + '\n\n' + markdownImage;
// 调用 API 更新消息
const response = await fetch(`/api/v1/chats/${chatId}/messages/${messageId}/event`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
type: 'chat:message',
data: { content: newContent }
})
});
if (response.ok) {
console.log('消息更新成功!');
}
```
## 完整示例
参考 [js_render_poc.py](../plugins/actions/js-render-poc/js_render_poc.py) 获取完整的 PoC 实现。
## 事件类型
| 类型 | 用途 |
|------|------|
| `chat:message:delta` | 增量更新(追加文本) |
| `chat:message` | 完全替换消息内容 |
```javascript
// 增量更新
{ type: "chat:message:delta", data: { content: "追加的内容" } }
// 完全替换
{ type: "chat:message", data: { content: "完整的新内容" } }
```
## 关键数据来源
| 数据 | 来源 | 说明 |
|------|------|------|
| `chat_id` | `body["chat_id"]` | 聊天会话 ID |
| `message_id` | `body["id"]` | ⚠️ 注意:是 `body["id"]`,不是 `body["message_id"]` |
| `token` | `localStorage.getItem('token')` | 用户认证 Token |
| `originalContent` | 通过 API `GET /api/v1/chats/{chatId}` 获取 | 当前消息内容 |
## Python 端 API
| 参数 | 类型 | 说明 |
|------|------|------|
| `__event_emitter__` | Callable | 发送状态/通知事件 |
| `__event_call__` | Callable | 执行 JS 代码(用于可视化渲染) |
| `__metadata__` | dict | 元数据(可能为 None |
| `body` | dict | 请求体,包含 messages、chat_id、id 等 |
### body 结构示例
```json
{
"model": "gemini-3-flash-preview",
"messages": [...],
"chat_id": "ac2633a3-5731-4944-98e3-bf9b3f0ef0ab",
"id": "2e0bb7d4-dfc0-43d7-b028-fd9e06c6fdc8",
"session_id": "bX30sHI8r4_CKxCdAAAL"
}
```
### 常用事件
```python
# 发送状态更新
await __event_emitter__({
"type": "status",
"data": {"description": "正在渲染...", "done": False}
})
# 执行 JS 代码
await __event_call__({
"type": "execute",
"data": {"code": "console.log('Hello from Python!')"}
})
# 发送通知
await __event_emitter__({
"type": "notification",
"data": {"type": "success", "content": "渲染完成!"}
})
```
## 适用场景
- **思维导图** (Markmap)
- **信息图** (AntV Infographic)
- **流程图** (Mermaid)
- **数据图表** (ECharts, Chart.js)
- **任何需要 JS 渲染的可视化内容**
## 注意事项
### 1. 竞态条件问题
⚠️ **多次快速点击会导致内容覆盖问题**
由于 API 调用是异步的,如果用户快速多次触发 Action
- 第一次点击:获取原始内容 A → 渲染 → 更新为 A+图片1
- 第二次点击:可能获取到旧内容 A第一次还没保存完→ 更新为 A+图片2
结果图片1 被覆盖丢失!
**解决方案**
- 添加防抖debounce机制
- 使用锁/标志位防止重复执行
- 或使用 `chat:message:delta` 增量更新
### 2. 不要直接修改 `body["messages"]`
消息更新应由 JS 通过 API 完成,确保获取最新内容。
### 3. f-string 限制
Python f-string 内不能直接使用反斜杠,需要将转义字符串预先处理:
```python
# 转义 JSON 中的特殊字符
body_json = json.dumps(data, ensure_ascii=False)
escaped = body_json.replace("\\", "\\\\").replace("`", "\\`").replace("${", "\\${")
```
### 4. Data URI 大小限制
Base64 编码会增加约 33% 的体积,复杂图片可能导致消息过大。
### 5. 跨域问题
确保 CDN 资源支持 CORS。
### 6. API 权限
确保用户 token 有权限访问和更新目标消息。
## 与传统方式对比
| 特性 | 传统方式 (修改 body) | 新方式 (__event_call__) |
|------|---------------------|------------------------|
| 消息更新 | Python 直接修改 | JS 通过 API 更新 |
| 原始内容 | Python 传递给 JS | JS 通过 API 获取 |
| 灵活性 | 低 | 高 |
| 实时性 | 一次性 | 可多次更新 |
| 复杂度 | 简单 | 中等 |
| 竞态风险 | 低 | ⚠️ 需要处理 |

View File

@@ -1,12 +1,25 @@
# Export to Excel
<span class="category-badge action">Action</span>
<span class="version-badge">v1.0.0</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.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.
---
## Overview
The Export to Excel plugin allows you to download your chat conversations as Excel files. This is useful for:
@@ -23,6 +36,13 @@ The Export to Excel plugin allows you to download your chat conversations as Exc
- :material-download: **One-Click Download**: Instant file generation
- :material-history: **Full History**: Exports complete conversation
## Configuration
- **Title Source**: Choose how the filename is generated:
- `chat_title`: Use the chat title (default).
- `ai_generated`: Use AI to generate a concise title from the content.
- `markdown_title`: Extract the first H1/H2 header from the markdown content.
---
## Installation

View File

@@ -1,12 +1,25 @@
# Export to Excel导出到 Excel
<span class="category-badge action">Action</span>
<span class="version-badge">v1.0.0</span>
<span class="version-badge">v0.3.7</span>
将聊天记录导出为 Excel 表格,便于分析、归档和分享。
### v0.3.6 更新内容
- **OpenWebUI 风格主题**:现代深灰表头,搭配浅灰斑马纹,提升可读性。
- **斑马纹效果**:隔行变色,方便视觉扫描。
- **智能数据类型转换**:自动将列转换为数字或日期类型。
- **全单元格粗体/斜体**:支持 Markdown 粗体/斜体格式。
- **部分 Markdown 清理**:移除部分 Markdown 符号,输出更整洁。
- **导出范围**:可选择导出"最后一条消息"或"所有消息"。
- **智能 Sheet 命名**:根据 Markdown 标题或消息索引命名 Sheet。
- **智能文件名生成**支持对话标题、AI 总结或 Markdown 标题生成文件名。
- **AI 标题生成**:支持指定模型 (`MODEL_ID`) 生成标题,并提供生成进度通知。
---
## 概览
Export to Excel 插件可以把你的聊天记录下载为 Excel 文件,适用于:
@@ -23,6 +36,13 @@ Export to Excel 插件可以把你的聊天记录下载为 Excel 文件,适用
- :material-download: **一键下载**:即时生成文件
- :material-history: **完整历史**:导出完整会话内容
## 配置
- **标题来源 (Title Source)**:选择文件名的生成方式:
- `chat_title`:使用对话标题(默认)。
- `ai_generated`:使用 AI 根据内容生成简洁标题。
- `markdown_title`:提取 Markdown 内容中的第一个 H1/H2 标题。
---
## 安装

View File

@@ -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
---

View File

@@ -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
---

View File

@@ -33,7 +33,7 @@ Actions are interactive plugins that:
Transform text into professional infographics using AntV visualization engine with various templates.
**Version:** 1.0.0
**Version:** 1.3.0
[:octicons-arrow-right-24: Documentation](smart-infographic.md)
@@ -43,7 +43,7 @@ Actions are interactive plugins that:
Quickly generates beautiful learning memory cards, perfect for studying and memorization.
**Version:** 0.2.0
**Version:** 0.2.2
[:octicons-arrow-right-24: Documentation](knowledge-card.md)
@@ -53,17 +53,17 @@ Actions are interactive plugins that:
Export chat conversations to Excel spreadsheet format for analysis and archiving.
**Version:** 1.0.0
**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)
@@ -73,7 +73,7 @@ Actions are interactive plugins that:
Generate concise summaries of long text content with key points extraction.
**Version:** 1.0.0
**Version:** 0.1.0
[:octicons-arrow-right-24: Documentation](summary.md)

View File

@@ -33,7 +33,7 @@ Actions 是交互式插件,能够:
使用 AntV 可视化引擎,将文本转成专业的信息图。
**版本:** 1.0.0
**版本:** 1.3.0
[:octicons-arrow-right-24: 查看文档](smart-infographic.md)
@@ -43,7 +43,7 @@ Actions 是交互式插件,能够:
快速生成精美的学习记忆卡片,适合学习与记忆。
**版本:** 0.2.0
**版本:** 0.2.2
[:octicons-arrow-right-24: 查看文档](knowledge-card.md)
@@ -53,17 +53,17 @@ Actions 是交互式插件,能够:
将聊天记录导出为 Excel 电子表格,方便分析或归档。
**版本:** 1.0.0
**版本:** 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)
@@ -73,7 +73,7 @@ Actions 是交互式插件,能够:
对长文本进行精简总结,提取要点。
**版本:** 1.0.0
**版本:** 0.1.0
[:octicons-arrow-right-24: 查看文档](summary.md)

View File

@@ -1,7 +1,7 @@
# Knowledge Card
<span class="category-badge action">Action</span>
<span class="version-badge">v0.2.0</span>
<span class="version-badge">v0.2.2</span>
Quickly generates beautiful learning memory cards, perfect for studying and quick memorization.

View File

@@ -1,7 +1,7 @@
# Smart Infographic
<span class="category-badge action">Action</span>
<span class="version-badge">v1.0.0</span>
<span class="version-badge">v1.3.0</span>
An AntV Infographic engine powered plugin that transforms long text into professional, beautiful infographics with a single click.

View File

@@ -1,7 +1,7 @@
# Summary
<span class="category-badge action">Action</span>
<span class="version-badge">v1.0.0</span>
<span class="version-badge">v0.1.0</span>
Generate concise summaries of long text content with key points extraction.

View File

@@ -1,7 +1,7 @@
# Summary摘要
<span class="category-badge action">Action</span>
<span class="version-badge">v1.0.0</span>
<span class="version-badge">v0.1.0</span>
为长文本生成简洁摘要,并提取关键要点。

View File

@@ -1,7 +1,7 @@
# Async Context Compression
<span class="category-badge filter">Filter</span>
<span class="version-badge">v1.0.0</span>
<span class="version-badge">v1.1.0</span>
Reduces token consumption in long conversations through intelligent summarization while maintaining conversational coherence.

View File

@@ -1,7 +1,7 @@
# Async Context Compression异步上下文压缩
<span class="category-badge filter">Filter</span>
<span class="version-badge">v1.0.0</span>
<span class="version-badge">v1.1.0</span>
通过智能摘要减少长对话的 token 消耗,同时保持对话连贯。

View File

@@ -1,7 +1,7 @@
# Context Enhancement
<span class="category-badge filter">Filter</span>
<span class="version-badge">v1.0.0</span>
<span class="version-badge">v0.2</span>
Enhances chat context with additional information for improved LLM responses.

View File

@@ -1,7 +1,7 @@
# Context Enhancement上下文增强
<span class="category-badge filter">Filter</span>
<span class="version-badge">v1.0.0</span>
<span class="version-badge">v0.2</span>
为聊天自动补充上下文信息,让 LLM 回复更相关、更准确。

View File

@@ -1,7 +1,7 @@
# Gemini Manifold Companion
<span class="category-badge filter">Filter</span>
<span class="version-badge">v1.0.0</span>
<span class="version-badge">v0.3.2</span>
Companion filter for the Gemini Manifold pipe plugin, providing enhanced functionality.

View File

@@ -1,7 +1,7 @@
# Gemini Manifold Companion
<span class="category-badge filter">Filter</span>
<span class="version-badge">v1.0.0</span>
<span class="version-badge">v0.3.2</span>
Gemini Manifold Pipe 的伴随过滤器,用于增强 Gemini 集成的处理效果。

View File

@@ -16,13 +16,13 @@ Filters act as middleware in the message pipeline:
<div class="grid cards" markdown>
- :material-compress:{ .lg .middle } **Async Context Compression**
- :material-arrow-collapse-vertical:{ .lg .middle } **Async Context Compression**
---
Reduces token consumption in long conversations through intelligent summarization while maintaining coherence.
**Version:** 1.0.0
**Version:** 1.1.0
[:octicons-arrow-right-24: Documentation](async-context-compression.md)
@@ -32,7 +32,7 @@ Filters act as middleware in the message pipeline:
Enhances chat context with additional information for better responses.
**Version:** 1.0.0
**Version:** 0.2
[:octicons-arrow-right-24: Documentation](context-enhancement.md)
@@ -42,7 +42,7 @@ Filters act as middleware in the message pipeline:
Companion filter for the Gemini Manifold pipe plugin.
**Version:** 1.0.0
**Version:** 1.7.0
[:octicons-arrow-right-24: Documentation](gemini-manifold-companion.md)

View File

@@ -16,13 +16,13 @@ Filter 充当消息管线中的中间件:
<div class="grid cards" markdown>
- :material-compress:{ .lg .middle } **Async Context Compression**
- :material-arrow-collapse-vertical:{ .lg .middle } **Async Context Compression**
---
通过智能总结减少长对话的 token 消耗,同时保持连贯性。
**版本:** 1.0.0
**版本:** 1.1.0
[:octicons-arrow-right-24: 查看文档](async-context-compression.md)
@@ -32,7 +32,7 @@ Filter 充当消息管线中的中间件:
为聊天增加额外信息,提升回复质量。
**版本:** 1.0.0
**版本:** 0.2
[:octicons-arrow-right-24: 查看文档](context-enhancement.md)
@@ -42,7 +42,7 @@ Filter 充当消息管线中的中间件:
Gemini Manifold Pipe 插件的伴随过滤器。
**版本:** 1.0.0
**版本:** 1.7.0
[:octicons-arrow-right-24: 查看文档](gemini-manifold-companion.md)

View File

@@ -315,7 +315,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 # Or handle error

View File

@@ -326,7 +326,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 # 或者处理错误

View File

@@ -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,26 @@ 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.3.0
- **Mermaid Diagrams**: Native support for rendering Mermaid diagrams as images in Word.
- **Native Math**: Converts LaTeX equations to native Office MathML for editable equations.
- **Citations**: Automatic bibliography generation and citation linking.
- **Reasoning Removal**: Option to strip `<think>` blocks from the output.
- **Table Enhancements**: Improved table formatting with smart column widths.
### 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

View File

@@ -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,26 @@
- **中文文本**:宋体(正文)、黑体(标题)
- **代码**Consolas
## 更新日志
### v0.3.0
- **Mermaid 图表**: 原生支持将 Mermaid 图表渲染为 Word 中的图片。
- **原生公式**: 将 LaTeX 公式转换为原生 Office MathML支持在 Word 中编辑。
- **引用参考**: 自动生成参考文献列表并链接引用。
- **移除推理**: 选项支持从输出中移除 `<think>` 推理块。
- **表格增强**: 改进表格格式,支持智能列宽。
### v0.2.0
- 新增原生数学公式支持LaTeX → OMML
- 新增 Mermaid 图表渲染
- 新增引用与参考资料章节生成
- 新增自动移除 AI 思考块
- 增强表格格式(智能列宽、对齐)
### v0.1.1
- 初始版本,支持基本 Markdown 转 Word
## 作者
Fu-Jie

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -2,12 +2,33 @@
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.6
- **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
- **One-Click Export**: Adds an "Export to Excel" button to the chat.
- **Automatic Header Extraction**: Intelligently identifies table headers from the chat content.
- **Multi-Table Support**: Handles multiple tables within a single chat session.
## Configuration
- **Title Source**: Choose how the filename is generated:
- `chat_title`: Use the chat title (default).
- `ai_generated`: Use AI to generate a concise title from the content.
- `markdown_title`: Extract the first H1/H2 header from the markdown content.
## Usage
1. Install the plugin.

View File

@@ -2,16 +2,37 @@
此插件允许你直接从聊天界面将对话历史导出为 Excel (.xlsx) 文件。
## 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"按钮。
- **自动表头提取**:智能识别聊天内容中的表格标题。
- **多表支持**:支持处理单次对话中的多个表格。
## 配置
- **标题来源 (Title Source)**:选择文件名的生成方式:
- `chat_title`:使用对话标题(默认)。
- `ai_generated`:使用 AI 根据内容生成简洁标题。
- `markdown_title`:提取 Markdown 内容中的第一个 H1/H2 标题。
## 使用方法
1. 安装插件。
2. 在任意对话中,点击导出为 Excel按钮。
2. 在任意对话中,点击"导出为 Excel"按钮。
3. 文件将自动下载到你的设备。
## 作者

View File

@@ -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.3
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
@@ -15,14 +15,33 @@ import base64
from fastapi import FastAPI, HTTPException
from typing import Optional, Callable, Awaitable, Any, List, Dict
import datetime
import asyncio
from open_webui.models.chats import Chats
from open_webui.models.users import Users
from open_webui.utils.chat import generate_chat_completion
from pydantic import BaseModel, Field
from typing import Literal
app = FastAPI()
class Action:
class Valves(BaseModel):
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):
pass
self.valves = self.Valves()
async def _send_notification(self, emitter: Callable, type: str, content: str):
await emitter(
@@ -35,6 +54,7 @@ class Action:
__user__=None,
__event_emitter__=None,
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
__request__: Optional[Any] = None,
):
print(f"action:{__name__}")
if isinstance(__user__, (list, tuple)):
@@ -53,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",
@@ -63,24 +81,180 @@ 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]]
# Get dynamic filename and sheet names
workbook_name, sheet_names = self.generate_names_from_content(
message_content, tables
)
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)
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 == "ai_generated":
# 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
# Fallback for filename
if not 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 workbook_name:
if not title:
workbook_name = f"{user_name}_{formatted_date}"
else:
workbook_name = self.clean_filename(title)
filename = f"{workbook_name}.xlsx"
excel_file_path = os.path.join(
@@ -89,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__:
@@ -172,6 +348,149 @@ class Action:
__event_emitter__, "error", "No tables found to export!"
)
async def generate_title_using_ai(
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)
# 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 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,
}
# 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 ""
def extract_title(self, content: str) -> str:
"""Extract title from Markdown h1/h2 only"""
lines = content.split("\n")
for line in lines:
# Match h1-h2 headings only
match = re.match(r"^#{1,2}\s+(.+)$", line.strip())
if match:
return match.group(1).strip()
return ""
def extract_chat_id(self, body: dict, metadata: Optional[dict]) -> str:
"""Extract chat_id from body or metadata"""
if isinstance(body, dict):
chat_id = body.get("chat_id") or body.get("id")
if isinstance(chat_id, str) and chat_id.strip():
return chat_id.strip()
for key in ("chat", "conversation"):
nested = body.get(key)
if isinstance(nested, dict):
nested_id = nested.get("id") or nested.get("chat_id")
if isinstance(nested_id, str) and nested_id.strip():
return nested_id.strip()
if isinstance(metadata, dict):
chat_id = metadata.get("chat_id")
if isinstance(chat_id, str) and chat_id.strip():
return chat_id.strip()
return ""
async def fetch_chat_title(self, chat_id: str, user_id: str = "") -> str:
"""Fetch chat title from database by chat_id"""
if not chat_id:
return ""
def _load_chat():
if user_id:
return Chats.get_chat_by_id_and_user_id(id=chat_id, user_id=user_id)
return Chats.get_chat_by_id(chat_id)
try:
chat = await asyncio.to_thread(_load_chat)
except Exception as exc:
print(f"Failed to load chat {chat_id}: {exc}")
return ""
if not chat:
return ""
data = getattr(chat, "chat", {}) or {}
title = data.get("title") or getattr(chat, "title", "")
return title.strip() if isinstance(title, str) else ""
def extract_tables_from_message(self, message: str) -> List[Dict]:
"""
Extract Markdown tables and their positions from message text
@@ -456,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,
@@ -482,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",
}
@@ -499,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",
}
@@ -508,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,
@@ -518,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"]
@@ -565,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(
@@ -582,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:
@@ -611,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):
@@ -651,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):
@@ -777,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)}")

File diff suppressed because it is too large Load Diff

View File

@@ -1,806 +0,0 @@
"""
title: 导出为 Excel
author: Fu-Jie
author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui
version: 0.3.3
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNSAySDZhMiAyIDAgMCAwLTIgMnYxNmEyIDIgMCAwIDAgMiAyaDEyYTIgMiAwIDAgMCAyLTJWN1oiLz48cGF0aCBkPSJNMTQgMnY0YTIgMiAwIDAgMCAyIDJoNCIvPjxwYXRoIGQ9Ik04IDEzaDIiLz48cGF0aCBkPSJNMTQgMTNoMiIvPjxwYXRoIGQ9Ik04IDE3aDIiLz48cGF0aCBkPSJNMTQgMTdoMiIvPjwvc3ZnPg==
description: 将当前对话历史导出为 Excel (.xlsx) 文件,支持自动提取表头。
"""
import os
import pandas as pd
import re
import base64
from fastapi import FastAPI, HTTPException
from typing import Optional, Callable, Awaitable, Any, List, Dict
import datetime
app = FastAPI()
class Action:
def __init__(self):
pass
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,
):
print(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": "正在保存到文件...", "done": False},
}
)
try:
message_content = last_assistant_message["content"]
tables = self.extract_tables_from_message(message_content)
if not tables:
raise HTTPException(status_code=400, detail="未找到任何表格。")
# 获取动态文件名和sheet名称
workbook_name, sheet_names = self.generate_names_from_content(
message_content, tables
)
# 使用优化后的文件名生成逻辑
current_datetime = datetime.datetime.now()
formatted_date = current_datetime.strftime("%Y%m%d")
# 如果没找到标题则使用 user_yyyymmdd 格式
if not workbook_name:
workbook_name = f"{user_name}_{formatted_date}"
filename = f"{workbook_name}.xlsx"
excel_file_path = os.path.join(
"app", "backend", "data", "temp", filename
)
os.makedirs(os.path.dirname(excel_file_path), exist_ok=True)
# 保存表格到Excel使用符合中国规范的格式化功能
self.save_tables_to_excel_enhanced(tables, excel_file_path, sheet_names)
# 触发文件下载
if __event_call__:
with open(excel_file_path, "rb") as file:
file_content = file.read()
base64_blob = base64.b64encode(file_content).decode("utf-8")
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.spreadsheetml.sheet" }});
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": "输出已保存", "done": True},
}
)
# 清理临时文件
if os.path.exists(excel_file_path):
os.remove(excel_file_path)
return {"message": "下载事件已触发"}
except HTTPException as e:
print(f"Error processing tables: {str(e.detail)}")
await __event_emitter__(
{
"type": "status",
"data": {
"description": f"保存文件时出错: {e.detail}",
"done": True,
},
}
)
await self._send_notification(
__event_emitter__, "error", "没有找到可以导出的表格!"
)
raise e
except Exception as e:
print(f"Error processing tables: {str(e)}")
await __event_emitter__(
{
"type": "status",
"data": {
"description": f"保存文件时出错: {str(e)}",
"done": True,
},
}
)
await self._send_notification(
__event_emitter__, "error", "没有找到可以导出的表格!"
)
def extract_tables_from_message(self, message: str) -> List[Dict]:
"""
从消息文本中提取Markdown表格及位置信息
返回结构: [{
"data": 表格数据,
"start_line": 起始行号,
"end_line": 结束行号
}]
"""
table_row_pattern = r"^\s*\|.*\|.*\s*$"
rows = message.split("\n")
tables = []
current_table = []
start_line = None
current_line = 0
for row in rows:
current_line += 1
if re.search(table_row_pattern, row):
if start_line is None:
start_line = current_line # 记录表格起始行
# 处理表格行
cells = [cell.strip() for cell in row.strip().strip("|").split("|")]
# 跳过分隔行
is_separator_row = all(re.fullmatch(r"[:\-]+", cell) for cell in cells)
if not is_separator_row:
current_table.append(cells)
elif current_table:
# 表格结束
tables.append(
{
"data": current_table,
"start_line": start_line,
"end_line": current_line - 1,
}
)
current_table = []
start_line = None
# 处理最后一个表格
if current_table:
tables.append(
{
"data": current_table,
"start_line": start_line,
"end_line": current_line,
}
)
return tables
def generate_names_from_content(self, content: str, tables: List[Dict]) -> tuple:
"""
根据内容生成工作簿名称和sheet名称
- 忽略非空段落,只使用 markdown 标题 (h1-h6)。
- 单表格: 使用最近的标题作为工作簿和工作表名。
- 多表格: 使用文档第一个标题作为工作簿名,各表格最近的标题作为工作表名。
- 默认命名:
- 工作簿: 在主流程中处理 (user_yyyymmdd.xlsx)。
- 工作表: 表1, 表2, ...
"""
lines = content.split("\n")
workbook_name = ""
sheet_names = []
all_headers = []
# 1. 查找文档中所有 h1-h6 标题及其位置
for i, line in enumerate(lines):
if re.match(r"^#{1,6}\s+", line):
all_headers.append(
{"text": re.sub(r"^#{1,6}\s+", "", line).strip(), "line_num": i}
)
# 2. 为每个表格生成 sheet 名称
for i, table in enumerate(tables):
table_start_line = table["start_line"] - 1 # 转换为 0-based 索引
closest_header_text = None
# 查找当前表格上方最近的标题
candidate_headers = [
h for h in all_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_names.append(self.clean_sheet_name(closest_header_text))
else:
# 如果找不到标题,使用默认名称 "表{i+1}"
sheet_names.append(f"{i+1}")
# 3. 根据表格数量确定工作簿名称
if len(tables) == 1:
# 单个表格: 使用其工作表名作为工作簿名 (前提是该名称不是默认的 "表1")
if sheet_names[0] != "表1":
workbook_name = sheet_names[0]
elif len(tables) > 1:
# 多个表格: 使用文档中的第一个标题作为工作簿名
if all_headers:
# 找到所有标题中行号最小的,即为第一个标题
first_header = min(all_headers, key=lambda x: x["line_num"])
workbook_name = first_header["text"]
# 4. 清理工作簿名称 (如果为空,主流程会使用默认名称)
workbook_name = self.clean_filename(workbook_name) if workbook_name else ""
return workbook_name, sheet_names
def clean_filename(self, name: str) -> str:
"""清理文件名中的非法字符"""
return re.sub(r'[\\/*?:"<>|]', "", name).strip()
def clean_sheet_name(self, name: str) -> str:
"""清理sheet名称(限制31字符,去除非法字符)"""
name = re.sub(r"[\\/*?[\]:]", "", name).strip()
return name[:31] if len(name) > 31 else name
# ======================== 符合中国规范的格式化功能 ========================
def calculate_text_width(self, text: str) -> float:
"""
计算文本显示宽度,考虑中英文字符差异
中文字符按2个单位计算英文字符按1个单位计算
"""
if not text:
return 0
width = 0
for char in str(text):
# 判断是否为中文字符(包括中文标点)
if "\u4e00" <= char <= "\u9fff" or "\u3000" <= char <= "\u303f":
width += 2 # 中文字符占2个单位宽度
else:
width += 1 # 英文字符占1个单位宽度
return width
def calculate_text_height(self, text: str, max_width: int = 50) -> int:
"""
计算文本显示所需的行数
根据换行符和文本长度计算
"""
if not text:
return 1
text = str(text)
# 计算换行符导致的行数
explicit_lines = text.count("\n") + 1
# 计算因文本长度超出而需要的额外行数
text_width = self.calculate_text_width(text.replace("\n", ""))
wrapped_lines = max(
1, int(text_width / max_width) + (1 if text_width % max_width > 0 else 0)
)
return max(explicit_lines, wrapped_lines)
def get_column_letter(self, col_index: int) -> str:
"""
将列索引转换为Excel列字母 (A, B, C, ..., AA, AB, ...)
"""
result = ""
while col_index >= 0:
result = chr(65 + col_index % 26) + result
col_index = col_index // 26 - 1
return result
def determine_content_type(self, header: str, values: list) -> str:
"""
根据表头和内容智能判断数据类型,符合中国官方表格规范
返回: 'number', 'date', 'sequence', 'text'
"""
header_lower = str(header).lower().strip()
# 检查表头关键词
number_keywords = [
"数量",
"金额",
"价格",
"费用",
"成本",
"收入",
"支出",
"总计",
"小计",
"百分比",
"%",
"比例",
"",
"数值",
"分数",
"成绩",
"得分",
]
date_keywords = ["日期", "时间", "年份", "月份", "时刻", "date", "time"]
sequence_keywords = [
"序号",
"编号",
"号码",
"排序",
"次序",
"顺序",
"id",
"编码",
]
# 检查表头
for keyword in number_keywords:
if keyword in header_lower:
return "number"
for keyword in date_keywords:
if keyword in header_lower:
return "date"
for keyword in sequence_keywords:
if keyword in header_lower:
return "sequence"
# 检查数据内容
if not values:
return "text"
sample_values = [
str(v).strip() for v in values[:10] if str(v).strip()
] # 取前10个非空值作为样本
if not sample_values:
return "text"
numeric_count = 0
date_count = 0
sequence_count = 0
for value in sample_values:
# 检查是否为数字
try:
float(
value.replace(",", "")
.replace("", "")
.replace("%", "")
.replace("", "")
)
numeric_count += 1
continue
except ValueError:
pass
# 检查是否为日期格式
date_patterns = [
r"\d{4}[-/年]\d{1,2}[-/月]\d{1,2}日?",
r"\d{1,2}[-/]\d{1,2}[-/]\d{4}",
r"\d{4}\d{2}\d{2}",
]
for pattern in date_patterns:
if re.match(pattern, value):
date_count += 1
break
# 检查是否为序号格式
if (
re.match(r"^\d+$", value) and len(value) <= 4
): # 纯数字且不超过4位可能是序号
sequence_count += 1
total_count = len(sample_values)
# 根据比例判断类型
if numeric_count / total_count >= 0.7:
return "number"
elif date_count / total_count >= 0.7:
return "date"
elif sequence_count / total_count >= 0.8 and sequence_count > 2:
return "sequence"
else:
return "text"
def get_column_letter(self, col_index: int) -> str:
"""
将列索引转换为Excel列字母 (A, B, C, ..., AA, AB, ...)
"""
result = ""
while col_index >= 0:
result = chr(65 + col_index % 26) + result
col_index = col_index // 26 - 1
return result
def save_tables_to_excel_enhanced(
self, tables: List[Dict], file_path: str, sheet_names: List[str]
):
"""
符合中国官方表格规范的Excel保存功能
"""
try:
with pd.ExcelWriter(file_path, engine="xlsxwriter") as writer:
workbook = writer.book
# 定义表头样式 - 居中对齐(符合中国规范)
header_format = workbook.add_format(
{
"bold": True,
"font_size": 12,
"font_color": "white",
"bg_color": "#00abbd",
"border": 1,
"align": "center", # 表头居中
"valign": "vcenter",
"text_wrap": True,
}
)
# 文本单元格样式 - 左对齐
text_format = workbook.add_format(
{
"border": 1,
"align": "left", # 文本左对齐
"valign": "vcenter",
"text_wrap": True,
}
)
# 数值单元格样式 - 右对齐
number_format = workbook.add_format(
{"border": 1, "align": "right", "valign": "vcenter"} # 数值右对齐
)
# 整数格式 - 右对齐
integer_format = workbook.add_format(
{
"num_format": "0",
"border": 1,
"align": "right", # 整数右对齐
"valign": "vcenter",
}
)
# 小数格式 - 右对齐
decimal_format = workbook.add_format(
{
"num_format": "0.00",
"border": 1,
"align": "right", # 小数右对齐
"valign": "vcenter",
}
)
# 日期格式 - 居中对齐
date_format = workbook.add_format(
{
"border": 1,
"align": "center", # 日期居中对齐
"valign": "vcenter",
"text_wrap": True,
}
)
# 序号格式 - 居中对齐
sequence_format = workbook.add_format(
{
"border": 1,
"align": "center", # 序号居中对齐
"valign": "vcenter",
}
)
for i, table in enumerate(tables):
try:
table_data = table["data"]
if not table_data or len(table_data) < 1:
print(f"Skipping empty table at index {i}")
continue
print(f"Processing table {i+1} with {len(table_data)} rows")
# 获取sheet名称
sheet_name = (
sheet_names[i] if i < len(sheet_names) else f"{i+1}"
)
# 创建DataFrame
headers = [
str(cell).strip()
for cell in table_data[0]
if str(cell).strip()
]
if not headers:
print(f"Warning: No valid headers found for table {i+1}")
headers = [f"{j+1}" for j in range(len(table_data[0]))]
data_rows = []
if len(table_data) > 1:
max_cols = len(headers)
for row in table_data[1:]:
processed_row = []
for j in range(max_cols):
if j < len(row):
processed_row.append(str(row[j]))
else:
processed_row.append("")
data_rows.append(processed_row)
df = pd.DataFrame(data_rows, columns=headers)
else:
df = pd.DataFrame(columns=headers)
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
# 先写入数据(不包含表头)
df.to_excel(
writer,
sheet_name=sheet_name,
index=False,
header=False,
startrow=1,
)
worksheet = writer.sheets[sheet_name]
# 应用符合中国规范的格式化
self.apply_chinese_standard_formatting(
worksheet,
df,
headers,
workbook,
header_format,
text_format,
number_format,
integer_format,
decimal_format,
date_format,
sequence_format,
)
except Exception as e:
print(f"Error processing table {i+1}: {str(e)}")
continue
except Exception as e:
print(f"Error saving Excel file: {str(e)}")
raise
def apply_chinese_standard_formatting(
self,
worksheet,
df,
headers,
workbook,
header_format,
text_format,
number_format,
integer_format,
decimal_format,
date_format,
sequence_format,
):
"""
应用符合中国官方表格规范的格式化
- 表头: 居中对齐
- 数值: 右对齐
- 文本: 左对齐
- 日期: 居中对齐
- 序号: 居中对齐
"""
try:
# 1. 写入表头(居中对齐)
print(f"Writing headers with Chinese standard alignment: {headers}")
for col_idx, header in enumerate(headers):
if header and str(header).strip():
worksheet.write(0, col_idx, str(header).strip(), header_format)
else:
default_header = f"{col_idx+1}"
worksheet.write(0, col_idx, default_header, header_format)
# 2. 分析每列的数据类型并应用相应格式
column_types = {}
for col_idx, column in enumerate(headers):
if col_idx < len(df.columns):
column_values = df.iloc[:, col_idx].tolist()
column_types[col_idx] = self.determine_content_type(
column, column_values
)
print(
f"Column '{column}' determined as type: {column_types[col_idx]}"
)
else:
column_types[col_idx] = "text"
# 3. 写入并格式化数据(根据类型使用不同对齐方式)
for row_idx, row in df.iterrows():
for col_idx, value in enumerate(row):
content_type = column_types.get(col_idx, "text")
# 根据内容类型选择格式
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
else:
try:
numeric_value = float(value)
if numeric_value.is_integer():
current_format = integer_format
value = int(numeric_value)
else:
current_format = decimal_format
except (ValueError, TypeError):
current_format = decimal_format
else:
current_format = number_format
elif content_type == "date":
# 日期类型 - 居中对齐
current_format = date_format
elif content_type == "sequence":
# 序号类型 - 居中对齐
current_format = sequence_format
else:
# 文本类型 - 左对齐
current_format = text_format
worksheet.write(row_idx + 1, col_idx, value, current_format)
# 4. 自动调整列宽
for col_idx, column in enumerate(headers):
col_letter = self.get_column_letter(col_idx)
# 计算表头宽度
header_width = self.calculate_text_width(str(column))
# 计算数据列的最大宽度
max_data_width = 0
if not df.empty and col_idx < len(df.columns):
for value in df.iloc[:, col_idx]:
value_width = self.calculate_text_width(str(value))
max_data_width = max(max_data_width, value_width)
# 基础宽度:取表头和数据的最大宽度
base_width = max(header_width, max_data_width)
# 根据内容类型调整宽度
content_type = column_types.get(col_idx, "text")
if content_type == "sequence":
# 序号列通常比较窄
optimal_width = max(8, min(15, base_width + 2))
elif content_type == "number":
# 数值列需要额外空间显示数字
optimal_width = max(12, min(25, base_width + 3))
elif content_type == "date":
# 日期列需要固定宽度
optimal_width = max(15, min(20, base_width + 2))
else:
# 文本列根据内容调整
if base_width <= 10:
optimal_width = base_width + 3
elif base_width <= 20:
optimal_width = base_width + 4
else:
optimal_width = base_width + 5
optimal_width = max(10, min(60, optimal_width))
worksheet.set_column(f"{col_letter}:{col_letter}", optimal_width)
# 5. 自动调整行高
# 设置表头行高为35点
worksheet.set_row(0, 35)
# 设置数据行行高
for row_idx, row in df.iterrows():
max_row_height = 20 # 中国表格规范建议的最小行高
for col_idx, value in enumerate(row):
if col_idx < len(headers):
col_width = min(
60,
max(
10, self.calculate_text_width(str(headers[col_idx])) + 5
),
)
else:
col_width = 15
cell_lines = self.calculate_text_height(str(value), col_width)
cell_height = cell_lines * 20 # 每行20点高度符合中国规范
max_row_height = max(max_row_height, cell_height)
final_height = min(120, max_row_height)
worksheet.set_row(row_idx + 1, final_height)
print(f"Successfully applied Chinese standard formatting")
except Exception as e:
print(f"Warning: Failed to apply Chinese standard formatting: {str(e)}")
# 降级到基础格式化
self.apply_basic_formatting_fallback(worksheet, df)
def apply_basic_formatting_fallback(self, worksheet, df):
"""
基础格式化降级方案
"""
try:
# 基础列宽调整
for i, column in enumerate(df.columns):
column_width = (
max(
len(str(column)),
(df[column].astype(str).map(len).max() if not df.empty else 0),
)
+ 2
)
col_letter = self.get_column_letter(i)
worksheet.set_column(
f"{col_letter}:{col_letter}", min(60, max(10, column_width))
)
print("Applied basic formatting fallback")
except Exception as e:
print(f"Warning: Even basic formatting failed: {str(e)}")

View File

@@ -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

View File

@@ -48,3 +48,9 @@ GitHub: [Fu-Jie/awesome-openwebui](https://github.com/Fu-Jie/awesome-openwebui)
## 许可证
MIT License
## 更新日志
### v0.2.4
- 移除输出中的调试信息

View File

@@ -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.1
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

View File

@@ -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.1
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

View File

@@ -63,3 +63,9 @@ data
## 📄 License
MIT License
## Changelog
### v1.3.2
- Removed debug messages from output

View File

@@ -63,3 +63,9 @@ data
## 📄 许可证
MIT License
## 更新日志
### v1.3.2
- 移除输出中的调试信息

View File

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

View File

@@ -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("无法获取有效的用户消息内容。")

View File

@@ -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 = """

View File

@@ -0,0 +1,257 @@
"""
title: JS Render PoC
author: Fu-Jie
version: 0.6.0
description: Proof of concept for JS rendering + API write-back pattern. JS renders SVG and updates message via API.
"""
import time
import json
import logging
from typing import Optional, Callable, Awaitable, Any
from pydantic import BaseModel, Field
from fastapi import Request
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class Action:
class Valves(BaseModel):
pass
def __init__(self):
self.valves = self.Valves()
def _extract_chat_id(self, body: dict, metadata: Optional[dict]) -> str:
"""Extract chat_id from body or metadata"""
if isinstance(body, dict):
# body["chat_id"] 是 chat_id
chat_id = body.get("chat_id")
if isinstance(chat_id, str) and chat_id.strip():
return chat_id.strip()
body_metadata = body.get("metadata", {})
if isinstance(body_metadata, dict):
chat_id = body_metadata.get("chat_id")
if isinstance(chat_id, str) and chat_id.strip():
return chat_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 ""
def _extract_message_id(self, body: dict, metadata: Optional[dict]) -> str:
"""Extract message_id from body or metadata"""
if isinstance(body, dict):
# body["id"] 是 message_id
message_id = body.get("id")
if isinstance(message_id, str) and message_id.strip():
return message_id.strip()
body_metadata = body.get("metadata", {})
if isinstance(body_metadata, dict):
message_id = body_metadata.get("message_id")
if isinstance(message_id, str) and message_id.strip():
return message_id.strip()
if isinstance(metadata, dict):
message_id = metadata.get("message_id")
if isinstance(message_id, str) and message_id.strip():
return message_id.strip()
return ""
async def action(
self,
body: dict,
__user__: dict = None,
__event_emitter__=None,
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
__metadata__: Optional[dict] = None,
__request__: Request = None,
) -> dict:
"""
PoC: Use __event_call__ to execute JS that renders SVG and updates message via API.
"""
# 准备调试数据
body_for_log = {}
for k, v in body.items():
if k == "messages":
body_for_log[k] = f"[{len(v)} messages]"
else:
body_for_log[k] = v
body_json = json.dumps(body_for_log, ensure_ascii=False, default=str)
metadata_json = (
json.dumps(__metadata__, ensure_ascii=False, default=str)
if __metadata__
else "null"
)
# 转义 JSON 中的特殊字符以便嵌入 JS
body_json_escaped = (
body_json.replace("\\", "\\\\").replace("`", "\\`").replace("${", "\\${")
)
metadata_json_escaped = (
metadata_json.replace("\\", "\\\\")
.replace("`", "\\`")
.replace("${", "\\${")
)
chat_id = self._extract_chat_id(body, __metadata__)
message_id = self._extract_message_id(body, __metadata__)
unique_id = f"poc_{int(time.time() * 1000)}"
if __event_emitter__:
await __event_emitter__(
{
"type": "status",
"data": {"description": "🔄 正在渲染...", "done": False},
}
)
if __event_call__:
await __event_call__(
{
"type": "execute",
"data": {
"code": f"""
(async function() {{
const uniqueId = "{unique_id}";
const chatId = "{chat_id}";
const messageId = "{message_id}";
// ===== DEBUG: 输出 Python 端的数据 =====
console.log("[JS Render PoC] ===== DEBUG INFO (from Python) =====");
console.log("[JS Render PoC] body:", `{body_json_escaped}`);
console.log("[JS Render PoC] __metadata__:", `{metadata_json_escaped}`);
console.log("[JS Render PoC] Extracted: chatId=", chatId, "messageId=", messageId);
console.log("[JS Render PoC] =========================================");
try {{
console.log("[JS Render PoC] Starting SVG render...");
// Create SVG
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("width", "200");
svg.setAttribute("height", "200");
svg.setAttribute("viewBox", "0 0 200 200");
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
const gradient = document.createElementNS("http://www.w3.org/2000/svg", "linearGradient");
gradient.setAttribute("id", "grad-" + uniqueId);
gradient.innerHTML = `
<stop offset="0%" style="stop-color:#1e88e5;stop-opacity:1" />
<stop offset="100%" style="stop-color:#43a047;stop-opacity:1" />
`;
defs.appendChild(gradient);
svg.appendChild(defs);
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
circle.setAttribute("cx", "100");
circle.setAttribute("cy", "100");
circle.setAttribute("r", "80");
circle.setAttribute("fill", `url(#grad-${{uniqueId}})`);
svg.appendChild(circle);
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
text.setAttribute("x", "100");
text.setAttribute("y", "105");
text.setAttribute("text-anchor", "middle");
text.setAttribute("fill", "white");
text.setAttribute("font-size", "16");
text.setAttribute("font-weight", "bold");
text.textContent = "PoC Success!";
svg.appendChild(text);
// Convert to Base64 Data URI
const svgData = new XMLSerializer().serializeToString(svg);
const base64 = btoa(unescape(encodeURIComponent(svgData)));
const dataUri = "data:image/svg+xml;base64," + base64;
console.log("[JS Render PoC] SVG rendered, data URI length:", dataUri.length);
// Call API - 完全替换方案(更稳定)
if (chatId && messageId) {{
const token = localStorage.getItem("token");
// 1. 获取当前消息内容
const getResponse = await fetch(`/api/v1/chats/${{chatId}}`, {{
method: "GET",
headers: {{ "Authorization": `Bearer ${{token}}` }}
}});
if (!getResponse.ok) {{
throw new Error("Failed to get chat data: " + getResponse.status);
}}
const chatData = await getResponse.json();
console.log("[JS Render PoC] Got chat data");
let originalContent = "";
if (chatData.chat && chatData.chat.messages) {{
const targetMsg = chatData.chat.messages.find(m => m.id === messageId);
if (targetMsg && targetMsg.content) {{
originalContent = targetMsg.content;
console.log("[JS Render PoC] Found original content, length:", originalContent.length);
}}
}}
// 2. 移除已存在的 PoC 图片(如果有的话)
// 匹配 ![JS Render PoC 生成的 SVG](data:...) 格式
const pocImagePattern = /\\n*!\\[JS Render PoC[^\\]]*\\]\\(data:image\\/svg\\+xml;base64,[^)]+\\)/g;
let cleanedContent = originalContent.replace(pocImagePattern, "");
// 移除可能残留的多余空行
cleanedContent = cleanedContent.replace(/\\n{{3,}}/g, "\\n\\n").trim();
if (cleanedContent !== originalContent) {{
console.log("[JS Render PoC] Removed existing PoC image(s)");
}}
// 3. 添加新的 Markdown 图片
const markdownImage = `![JS Render PoC 生成的 SVG](${{dataUri}})`;
const newContent = cleanedContent + "\\n\\n" + markdownImage;
// 3. 使用 chat:message 完全替换
const updateResponse = await fetch(`/api/v1/chats/${{chatId}}/messages/${{messageId}}/event`, {{
method: "POST",
headers: {{
"Content-Type": "application/json",
"Authorization": `Bearer ${{token}}`
}},
body: JSON.stringify({{
type: "chat:message",
data: {{ content: newContent }}
}})
}});
if (updateResponse.ok) {{
console.log("[JS Render PoC] ✅ Message updated successfully!");
}} else {{
console.error("[JS Render PoC] API error:", updateResponse.status, await updateResponse.text());
}}
}} else {{
console.warn("[JS Render PoC] ⚠️ Missing chatId or messageId, cannot persist.");
}}
}} catch (error) {{
console.error("[JS Render PoC] Error:", error);
}}
}})();
"""
},
}
)
if __event_emitter__:
await __event_emitter__(
{"type": "status", "data": {"description": "✅ 渲染完成", "done": True}}
)
return body

View File

@@ -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:**

View File

@@ -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)
**主要功能:**

View File

@@ -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."

View File

@@ -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 = "无法获取有效的用户消息内容。"

View File

@@ -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

View File

@@ -22,3 +22,9 @@ GitHub: [Fu-Jie/awesome-openwebui](https://github.com/Fu-Jie/awesome-openwebui)
## 许可证
MIT License
## 更新日志
### v0.1.2
- 移除输出中的调试信息

View File

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

View File

@@ -1,7 +1,7 @@
"""
title: 精读 (Deep Reading)
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImciIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIxIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjNDI4NWY0Ii8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMWU4OGU1Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTYgMmg4bDYgNnYxMmEyIDIgMCAwIDEtMiAySDZhMiAyIDAgMCAxLTItMlY0YTIgMiAwIDAgMSAyLTJ6IiBmaWxsPSJ1cmwoI2cpIi8+PHBhdGggZD0iTTE0IDJsNiA2aC02eiIgZmlsbD0iIzFlODhlNSIgb3BhY2l0eT0iMC42Ii8+PGxpbmUgeDE9IjgiIHkxPSIxMyIgeDI9IjE2IiB5Mj0iMTMiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiLz48bGluZSB4MT0iOCIgeTE9IjE3IiB4Mj0iMTQiIHkyPSIxNyIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEuNSIvPjxjaXJjbGUgY3g9IjE2IiBjeT0iMTgiIHI9IjMiIGZpbGw9IiNmZmQ3MDAiLz48cGF0aCBkPSJNMTYgMTZsMS41IDEuNSIgc3Ryb2tlPSIjNDI4NWY0IiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvc3ZnPg==
version: 2.0.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("无法获取有效的用户消息内容。")

View File

@@ -1,6 +1,6 @@
# Async Context Compression Filter
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 1.2.0 | **License:** MIT
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 1.1.0 | **License:** MIT
This filter reduces token consumption in long conversations through intelligent summarization and message compression while keeping conversations coherent.

View File

@@ -1,6 +1,6 @@
# Example Pipe Plugin
**Author:** OpenWebUI Community | **Version:** 1.0.0 | **License:** MIT
**Author:** OpenWebUI Community | **Version:** 1.26.0 | **License:** MIT
This is a template/example for creating Pipe plugins in OpenWebUI.

View File

@@ -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)** - 增强请求上下文和优化模型功能,包含环境变量管理、模型功能适配和内容清洗

View File

@@ -0,0 +1,315 @@
#!/usr/bin/env python3
"""
Script to check and enforce version consistency across OpenWebUI plugins and documentation.
用于检查并强制 OpenWebUI 插件和文档之间版本一致性的脚本。
Usage:
python scripts/check_version_consistency.py # Check only
python scripts/check_version_consistency.py --fix # Check and fix
"""
import argparse
import os
import re
import sys
from pathlib import Path
from typing import Optional, List, Dict, Tuple
# ANSI colors
GREEN = "\033[92m"
RED = "\033[91m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
RESET = "\033[0m"
def log_info(msg):
print(f"{BLUE}[INFO]{RESET} {msg}")
def log_success(msg):
print(f"{GREEN}[OK]{RESET} {msg}")
def log_warning(msg):
print(f"{YELLOW}[WARN]{RESET} {msg}")
def log_error(msg):
print(f"{RED}[ERR]{RESET} {msg}")
class VersionChecker:
def __init__(self, root_dir: str, fix: bool = False):
self.root_dir = Path(root_dir)
self.plugins_dir = self.root_dir / "plugins"
self.docs_dir = self.root_dir / "docs" / "plugins"
self.fix = fix
self.issues_found = 0
self.fixed_count = 0
def extract_version_from_py(self, file_path: Path) -> Optional[str]:
"""Extract version from Python docstring."""
try:
content = file_path.read_text(encoding="utf-8")
match = re.search(r"version:\s*([\d\.]+)", content)
if match:
return match.group(1)
except Exception as e:
log_error(f"Failed to read {file_path}: {e}")
return None
def update_file_content(
self, file_path: Path, pattern: str, replacement: str, version: str
) -> bool:
"""Update file content with new version."""
try:
content = file_path.read_text(encoding="utf-8")
new_content = re.sub(pattern, replacement, content)
if content != new_content:
if self.fix:
file_path.write_text(new_content, encoding="utf-8")
log_success(
f"Fixed {file_path.relative_to(self.root_dir)}: -> {version}"
)
self.fixed_count += 1
return True
else:
log_error(
f"Mismatch in {file_path.relative_to(self.root_dir)}: Expected {version}"
)
self.issues_found += 1
return False
return True
except Exception as e:
log_error(f"Failed to update {file_path}: {e}")
return False
def check_plugin(self, plugin_type: str, plugin_dir: Path):
"""Check consistency for a single plugin."""
plugin_name = plugin_dir.name
# 1. Identify Source of Truth (English .py file)
py_file = plugin_dir / f"{plugin_name}.py"
if not py_file.exists():
# Try finding any .py file that matches the directory name pattern or is the main file
py_files = list(plugin_dir.glob("*.py"))
# Filter out _cn.py, templates, etc.
candidates = [
f
for f in py_files
if not f.name.endswith("_cn.py") and "TEMPLATE" not in f.name
]
if candidates:
py_file = candidates[0]
else:
return # Not a valid plugin dir
true_version = self.extract_version_from_py(py_file)
if not true_version:
log_warning(f"Skipping {plugin_name}: No version found in {py_file.name}")
return
log_info(f"Checking {plugin_name} (v{true_version})...")
# 2. Check Chinese .py file
cn_py_files = list(plugin_dir.glob("*_cn.py")) + list(
plugin_dir.glob("*中文*.py")
)
# Also check for files that are not the main file but might be the CN version
for f in plugin_dir.glob("*.py"):
if f != py_file and "TEMPLATE" not in f.name and f not in cn_py_files:
# Heuristic: if it has Chinese characters or ends in _cn
if re.search(r"[\u4e00-\u9fff]", f.name) or f.name.endswith("_cn.py"):
cn_py_files.append(f)
for cn_py in set(cn_py_files):
self.update_file_content(
cn_py, r"(version:\s*)([\d\.]+)", rf"\g<1>{true_version}", true_version
)
# 3. Check README.md (English)
readme = plugin_dir / "README.md"
if readme.exists():
# Pattern 1: **Version:** 1.0.0
self.update_file_content(
readme,
r"(\*\*Version:?\*\*\s*)([\d\.]+)",
rf"\g<1>{true_version}",
true_version,
)
# Pattern 2: | **Version:** 1.0.0 |
self.update_file_content(
readme,
r"(\|\s*\*\*Version:\*\*\s*)([\d\.]+)",
rf"\g<1>{true_version}",
true_version,
)
# 4. Check README_CN.md (Chinese)
readme_cn = plugin_dir / "README_CN.md"
if readme_cn.exists():
# Pattern: **版本:** 1.0.0
self.update_file_content(
readme_cn,
r"(\*\*版本:?\*\*\s*)([\d\.]+)",
rf"\g<1>{true_version}",
true_version,
)
# 5. Check Global Docs Index (docs/plugins/{type}/index.md)
index_md = self.docs_dir / plugin_type / "index.md"
if index_md.exists():
# Need to find the specific block for this plugin.
# This is harder with regex on the whole file.
# We assume the format: **Version:** X.Y.Z
# But we need to make sure we are updating the RIGHT plugin's version.
# Strategy: Look for the plugin title or link, then the version nearby.
# Extract title from py file to help search
title = self.extract_title(py_file)
if title:
self.update_version_in_index(index_md, title, true_version)
# 6. Check Global Docs Index CN (docs/plugins/{type}/index.zh.md)
index_zh = self.docs_dir / plugin_type / "index.zh.md"
if index_zh.exists():
# Try to find Chinese title? Or just use English title if listed?
# Often Chinese index uses English title or Chinese title.
# Let's try to extract Chinese title from cn_py if available
cn_title = None
if cn_py_files:
cn_title = self.extract_title(cn_py_files[0])
target_title = cn_title if cn_title else title
if target_title:
self.update_version_in_index(
index_zh, target_title, true_version, is_zh=True
)
# 7. Check Global Detail Page (docs/plugins/{type}/{name}.md)
# The doc filename usually matches the plugin directory name
detail_md = self.docs_dir / plugin_type / f"{plugin_name}.md"
if detail_md.exists():
self.update_file_content(
detail_md,
r'(<span class="version-badge">v)([\d\.]+)(</span>)',
rf"\g<1>{true_version}\g<3>",
true_version,
)
# 8. Check Global Detail Page CN (docs/plugins/{type}/{name}.zh.md)
detail_zh = self.docs_dir / plugin_type / f"{plugin_name}.zh.md"
if detail_zh.exists():
self.update_file_content(
detail_zh,
r'(<span class="version-badge">v)([\d\.]+)(</span>)',
rf"\g<1>{true_version}\g<3>",
true_version,
)
def extract_title(self, file_path: Path) -> Optional[str]:
try:
content = file_path.read_text(encoding="utf-8")
match = re.search(r"title:\s*(.+)", content)
if match:
return match.group(1).strip()
except:
pass
return None
def update_version_in_index(
self, file_path: Path, title: str, version: str, is_zh: bool = False
):
"""
Update version in index file.
Look for:
- ... **Title** ...
- ...
- **Version:** X.Y.Z
"""
try:
content = file_path.read_text(encoding="utf-8")
# Escape title for regex
safe_title = re.escape(title)
# Regex to find the plugin block and its version
# We look for the title, then non-greedy match until we find Version line
if is_zh:
ver_label = r"\*\*版本:\*\*"
else:
ver_label = r"\*\*Version:\*\*"
# Pattern: (Title ...)(Version: )(\d+\.\d+\.\d+)
# We allow some lines between title and version
pattern = rf"(\*\*{safe_title}\*\*[\s\S]*?{ver_label}\s*)([\d\.]+)"
match = re.search(pattern, content)
if match:
current_ver = match.group(2)
if current_ver != version:
if self.fix:
new_content = content.replace(
match.group(0), f"{match.group(1)}{version}"
)
file_path.write_text(new_content, encoding="utf-8")
log_success(
f"Fixed index for {title}: {current_ver} -> {version}"
)
self.fixed_count += 1
else:
log_error(
f"Mismatch in index for {title}: Found {current_ver}, Expected {version}"
)
self.issues_found += 1
else:
# log_warning(f"Could not find entry for '{title}' in {file_path.name}")
pass
except Exception as e:
log_error(f"Failed to check index {file_path}: {e}")
def run(self):
if not self.plugins_dir.exists():
log_error(f"Plugins directory not found: {self.plugins_dir}")
return
# Scan actions, filters, pipes
for type_dir in self.plugins_dir.iterdir():
if type_dir.is_dir() and type_dir.name in ["actions", "filters", "pipes"]:
for plugin_dir in type_dir.iterdir():
if plugin_dir.is_dir():
self.check_plugin(type_dir.name, plugin_dir)
print("-" * 40)
if self.issues_found > 0:
if self.fix:
print(f"Fixed {self.fixed_count} issues.")
else:
print(f"Found {self.issues_found} version inconsistencies.")
print(f"Run with --fix to automatically resolve them.")
sys.exit(1)
else:
print("All versions are consistent! ✨")
def main():
parser = argparse.ArgumentParser(description="Check version consistency.")
parser.add_argument("--fix", action="store_true", help="Fix inconsistencies")
args = parser.parse_args()
# Assume script is run from root or scripts dir
root = Path.cwd()
if (root / "scripts").exists():
pass
elif root.name == "scripts":
root = root.parent
checker = VersionChecker(str(root), fix=args.fix)
checker.run()
if __name__ == "__main__":
main()

View File

@@ -96,6 +96,15 @@ def scan_plugins_directory(plugins_dir: str) -> list[dict[str, Any]]:
for root, _dirs, files in os.walk(plugins_path):
for file in files:
if file.endswith(".py") and not file.startswith("__"):
# Skip specific files that should not trigger release
if file in [
"gemini_manifold.py",
"gemini_manifold_companion.py",
"ACTION_PLUGIN_TEMPLATE.py",
"ACTION_PLUGIN_TEMPLATE_CN.py",
]:
continue
file_path = os.path.join(root, file)
metadata = extract_plugin_metadata(file_path)
if metadata:
@@ -109,9 +118,7 @@ def scan_plugins_directory(plugins_dir: str) -> list[dict[str, Any]]:
return plugins
def compare_versions(
current: list[dict], previous_file: str
) -> dict[str, list[dict]]:
def compare_versions(current: list[dict], previous_file: str) -> dict[str, list[dict]]:
"""
Compare current plugin versions with a previous version file.
比较当前插件版本与之前的版本文件。
@@ -168,7 +175,9 @@ def format_markdown_table(plugins: list[dict]) -> str:
"|---------------|----------------|-------------|---------------------|",
]
for plugin in sorted(plugins, key=lambda x: (x.get("type", ""), x.get("title", ""))):
for plugin in sorted(
plugins, key=lambda x: (x.get("type", ""), x.get("title", ""))
):
title = plugin.get("title", "Unknown")
version = plugin.get("version", "Unknown")
plugin_type = plugin.get("type", "Unknown").capitalize()
@@ -181,7 +190,9 @@ def format_markdown_table(plugins: list[dict]) -> str:
return "\n".join(lines)
def format_release_notes(comparison: dict[str, list]) -> str:
def format_release_notes(
comparison: dict[str, list], ignore_removed: bool = False
) -> str:
"""
Format version comparison as release notes.
将版本比较格式化为发布说明。
@@ -206,7 +217,7 @@ def format_release_notes(comparison: dict[str, list]) -> str:
)
lines.append("")
if comparison["removed"]:
if comparison["removed"] and not ignore_removed:
lines.append("### 移除插件 / Removed Plugins")
for plugin in comparison["removed"]:
lines.append(f"- **{plugin['title']}** v{plugin['version']}")
@@ -239,6 +250,11 @@ def main():
metavar="FILE",
help="Compare with previous version JSON file",
)
parser.add_argument(
"--ignore-removed",
action="store_true",
help="Ignore removed plugins in output",
)
parser.add_argument(
"--output",
"-o",
@@ -257,7 +273,9 @@ def main():
if args.json:
output = json.dumps(comparison, indent=2, ensure_ascii=False)
else:
output = format_release_notes(comparison)
output = format_release_notes(
comparison, ignore_removed=args.ignore_removed
)
if not output.strip():
output = "No changes detected. / 未检测到更改。"
elif args.json:
@@ -268,13 +286,17 @@ def main():
# Default: simple list
lines = []
for plugin in sorted(plugins, key=lambda x: x.get("title", "")):
lines.append(f"{plugin.get('title', 'Unknown')}: v{plugin.get('version', '?')}")
lines.append(
f"{plugin.get('title', 'Unknown')}: v{plugin.get('version', '?')}"
)
output = "\n".join(lines)
# Write output
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)