Compare commits

...

56 Commits

Author SHA1 Message Date
fujie
3cc4478dd9 feat(deep-dive): add Deep Dive / 精读 action plugin
- New thinking chain structure: Context → Logic → Insight → Path
- Process-oriented timeline UI design
- OpenWebUI theme auto-adaptation (light/dark)
- Full markdown support (numbered lists, inline code, bold)
- Bilingual support (English: Deep Dive, Chinese: 精读)
- Add manual publish workflow for new plugins
2026-01-08 08:37:50 +08:00
github-actions[bot]
59f6f2ba97 chore: update community stats 2026-01-08 2026-01-08 00:35:51 +00:00
github-actions[bot]
172d9e0b41 chore: update community stats 2026-01-07 2026-01-07 23:08:41 +00:00
github-actions[bot]
de7086c9e1 chore: update community stats 2026-01-07 2026-01-07 22:08:12 +00:00
github-actions[bot]
5f63e8d1e2 chore: update community stats 2026-01-07 2026-01-07 21:08:36 +00:00
github-actions[bot]
3da0b894fd chore: update community stats 2026-01-07 2026-01-07 20:09:35 +00:00
github-actions[bot]
ad2d26aa16 chore: update community stats 2026-01-07 2026-01-07 19:08:58 +00:00
github-actions[bot]
a09f3e0bdb chore: update community stats 2026-01-07 2026-01-07 18:12:18 +00:00
github-actions[bot]
3a0faf27df chore: update community stats 2026-01-07 2026-01-07 17:11:23 +00:00
fujie
cd3e7309a8 refactor: create OpenWebUICommunityClient class to unify API operations 2026-01-08 00:44:25 +08:00
fujie
54cc10bb41 feat: optimize publish script to skip unchanged versions 2026-01-08 00:34:49 +08:00
fujie
24e7d34524 fix: robust version determination in release workflow 2026-01-08 00:25:37 +08:00
fujie
a58ce9e99e feat: 为所有插件配置添加 openwebui_id。 2026-01-08 00:16:56 +08:00
fujie
4a42dcf8de chore: update extract_plugin_versions.py script 2026-01-08 00:14:32 +08:00
fujie
5903ea0e40 docs: update plugin development workflow with market publishing steps 2026-01-08 00:13:23 +08:00
fujie
6d7a5b45cf feat: bump export_to_word to v0.4.3 and automate plugin publishing 2026-01-08 00:12:17 +08:00
github-actions[bot]
10433d38b3 chore: update community stats 2026-01-07 2026-01-07 16:11:43 +00:00
github-actions[bot]
bf2bc80b22 chore: update community stats 2026-01-07 2026-01-07 15:10:07 +00:00
fujie
1e0f5fb65a feat: improve release workflow and update community stats to Top 6 2026-01-07 22:35:24 +08:00
github-actions[bot]
7d5a696106 📊 更新社区统计数据 2026-01-07 2026-01-07 14:09:37 +00:00
fujie
cf86012d4d feat(infographic): upload PNG instead of SVG for better compatibility
- Convert SVG to PNG using canvas before uploading
- 2x scale for higher quality output
- Fix Word export compatibility issue (SVG not supported by python-docx)
- Update version to 1.4.1
- Update README.md and README_CN.md with new feature
2026-01-07 21:24:09 +08:00
github-actions[bot]
961c1cbca6 📊 更新社区统计数据 2026-01-07 2026-01-07 13:20:25 +00:00
fujie
7fb5c243fa feat(export-to-word): add S3 object storage support
- Add boto3 direct download for S3/MinIO stored images
- Implement 6-level file fallback: DB → S3 → Local → URL → API → Attributes
- Sync S3 support to Chinese version (export_to_word_cn.py)
- Update version to 0.4.2
- Rewrite README.md and README_CN.md following standard format
- Update docs version numbers
- Add file storage access guidelines to copilot-instructions.md
2026-01-07 20:59:33 +08:00
github-actions[bot]
f845281b72 📊 更新社区统计数据 2026-01-07 2026-01-07 12:14:53 +00:00
github-actions[bot]
0b2c6a2d36 📊 更新社区统计数据 2026-01-07 2026-01-07 11:08:40 +00:00
github-actions[bot]
245c37b2c3 📊 更新社区统计数据 2026-01-07 2026-01-07 10:09:52 +00:00
github-actions[bot]
d2a915a514 📊 更新社区统计数据 2026-01-07 2026-01-07 09:12:43 +00:00
github-actions[bot]
ae731f9bd6 📊 更新社区统计数据 2026-01-07 2026-01-07 08:11:59 +00:00
github-actions[bot]
2a8a8c5805 📊 更新社区统计数据 2026-01-07 2026-01-07 07:11:58 +00:00
github-actions[bot]
deb1272f62 📊 更新社区统计数据 2026-01-07 2026-01-07 06:13:00 +00:00
github-actions[bot]
51c41b8628 📊 更新社区统计数据 2026-01-07 2026-01-07 05:12:07 +00:00
github-actions[bot]
37893ded00 📊 更新社区统计数据 2026-01-07 2026-01-07 04:23:26 +00:00
github-actions[bot]
38fe50a898 📊 更新社区统计数据 2026-01-07 2026-01-07 03:37:14 +00:00
github-actions[bot]
1c731e70dc 📊 更新社区统计数据 2026-01-07 2026-01-07 02:46:45 +00:00
github-actions[bot]
a55aa4d8fd 📊 更新社区统计数据 2026-01-07 2026-01-07 01:37:09 +00:00
github-actions[bot]
6c79cb2f11 📊 更新社区统计数据 2026-01-07 2026-01-07 00:35:06 +00:00
fujie
ba7943bd6f fix: restore responsive sizing for infographic 2026-01-07 07:32:59 +08:00
fujie
6eb09c3eaa fix: use fixed dimensions to prevent title wrapping 2026-01-07 07:31:13 +08:00
fujie
63c5257162 fix: reduce infographic padding to prevent title wrapping 2026-01-07 07:12:46 +08:00
fujie
a2422262b5 fix: increase infographic width to prevent title wrapping 2026-01-07 07:09:26 +08:00
github-actions[bot]
4f49b111fd 📊 更新社区统计数据 2026-01-06 2026-01-06 23:08:20 +00:00
fujie
1d066fc1f0 fix: reduce infographic size and adjust layout margins 2026-01-07 07:05:20 +08:00
github-actions[bot]
e960c40351 📊 更新社区统计数据 2026-01-06 2026-01-06 22:08:33 +00:00
github-actions[bot]
96284a3652 📊 更新社区统计数据 2026-01-06 2026-01-06 21:08:09 +00:00
github-actions[bot]
ad2f38ec1f 📊 更新社区统计数据 2026-01-06 2026-01-06 20:09:22 +00:00
github-actions[bot]
87fc34d505 📊 更新社区统计数据 2026-01-06 2026-01-06 19:06:41 +00:00
github-actions[bot]
2aafd3cef7 📊 更新社区统计数据 2026-01-06 2026-01-06 18:12:20 +00:00
github-actions[bot]
afec54c4e0 📊 更新社区统计数据 2026-01-06 2026-01-06 17:10:30 +00:00
github-actions[bot]
905a9e67ca 📊 更新社区统计数据 2026-01-06 2026-01-06 16:10:24 +00:00
github-actions[bot]
ce56815e77 📊 更新社区统计数据 2026-01-06 2026-01-06 15:09:05 +00:00
fujie
2684098be1 docs: update doc standards & reformat infographic readme; feat: default image mode 2026-01-06 22:57:17 +08:00
fujie
57ebf24c75 feat: update Smart Infographic to v1.4.0 with static image output support 2026-01-06 22:35:46 +08:00
github-actions[bot]
9375df709f 📊 更新社区统计数据 2026-01-06 2026-01-06 14:09:06 +00:00
fujie
255e48bd33 docs(smart-mind-map): add comparison table for output modes 2026-01-06 21:50:22 +08:00
fujie
18993c7fbe docs(smart-mind-map): emphasize no HTML output in image mode 2026-01-06 21:46:22 +08:00
fujie
f3cf2b52fd docs(smart-mind-map): highlight v0.9.1 features in README header 2026-01-06 21:39:42 +08:00
49 changed files with 4906 additions and 530 deletions

View File

@@ -39,8 +39,8 @@ Every plugin **MUST** have bilingual versions for both code and documentation:
When adding or updating a plugin, you **MUST** update the following documentation files to maintain consistency: When adding or updating a plugin, you **MUST** update the following documentation files to maintain consistency:
### Plugin Directory ### Plugin Directory
- `README.md`: Update version, description, and usage. **Explicitly describe new features.** - `README.md`: Update version, description, and usage. **Explicitly describe new features in a prominent position at the beginning.**
- `README_CN.md`: Update version, description, and usage. **Explicitly describe new features.** - `README_CN.md`: Update version, description, and usage. **Explicitly describe new features in a prominent position at the beginning.**
### Global Documentation (`docs/`) ### Global Documentation (`docs/`)
- **Index Pages**: - **Index Pages**:
@@ -82,6 +82,11 @@ Reference: `.github/workflows/release.yml`
- Generates release notes based on changes. - Generates release notes based on changes.
- Creates a GitHub Release tag (e.g., `v2024.01.01-1`). - Creates a GitHub Release tag (e.g., `v2024.01.01-1`).
- Uploads individual `.py` files of **changed plugins only** as assets. - Uploads individual `.py` files of **changed plugins only** as assets.
4. **Market Publishing**:
- Workflow: `.github/workflows/publish_plugin.yml`
- Trigger: Release published.
- Action: Automatically updates the plugin code and metadata on OpenWebUI.com using `scripts/publish_plugin.py`.
- Requirement: `OPENWEBUI_API_KEY` secret must be set.
### Pull Request Check ### Pull Request Check
- Workflow: `.github/workflows/plugin-version-check.yml` - Workflow: `.github/workflows/plugin-version-check.yml`

View File

@@ -31,15 +31,75 @@ plugins/actions/export_to_docx/
- `README.md` - English documentation - `README.md` - English documentation
- `README_CN.md` - 中文文档 - `README_CN.md` - 中文文档
README 文件应包含以下内容: ### README 结构规范 (README Structure Standard)
- 功能描述 / Feature description
- 配置参数及默认值 / Configuration parameters with defaults 所有插件 README 必须遵循以下统一结构顺序:
- 安装和设置说明 / Installation and setup instructions
- 使用示例 / Usage examples 1. **标题 (Title)**: 插件名称,带 Emoji 图标
- 故障排除指南 / Troubleshooting guide 2. **元数据 (Metadata)**: 作者、版本、项目链接 (一行显示)
- 故障排除指南 / Troubleshooting guide - 格式: `**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** x.x.x | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)`
- 版本和作者信息 / Version and author information - **注意**: Author 和 Project 为固定值,仅需更新 Version 版本号
- **新增功能 / New Features**: 如果是更新现有插件,必须明确列出并描述新增功能(发布到官方市场的重要要求)。/ If updating an existing plugin, explicitly list and describe new features (Critical for official market release). 3. **描述 (Description)**: 一句话功能介绍
4. **最新更新 (What's New)**: **必须**放在描述之后,显著展示最新版本的变更点
5. **核心特性 (Key Features)**: 使用 Emoji + 粗体标题 + 描述格式
6. **使用方法 (How to Use)**: 按步骤说明
7. **配置参数 (Configuration/Valves)**: 使用表格格式,包含参数名、默认值、描述
8. **其他 (Others)**: 支持的模板类型、语法示例、故障排除等
完整示例 (Full Example):
```markdown
# 📊 Smart Plugin
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 1.0.0 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
A one-sentence description of this plugin.
## 🔥 What's New in v1.0.0
-**Feature Name**: Brief description of the feature.
- 🔧 **Configuration Change**: What changed in settings.
- 🐛 **Bug Fix**: What was fixed.
## ✨ Key Features
- 🚀 **Feature A**: Description of feature A.
- 🎨 **Feature B**: Description of feature B.
- 📥 **Feature C**: Description of feature C.
## 🚀 How to Use
1. **Install**: Search for "Plugin Name" in the Open WebUI Community and install.
2. **Trigger**: Enter your text in the chat, then click the **Action Button**.
3. **Result**: View the generated result.
## ⚙️ Configuration (Valves)
| Parameter | Default | Description |
| :--- | :--- | :--- |
| **Show Status (SHOW_STATUS)** | `True` | Whether to show status updates. |
| **Model ID (MODEL_ID)** | `Empty` | LLM model for processing. |
| **Output Mode (OUTPUT_MODE)** | `image` | `image` for static, `html` for interactive. |
## 🛠️ Supported Types (Optional)
| Category | Type Name | Use Case |
| :--- | :--- | :--- |
| **Category A** | `type-a`, `type-b` | Use case description |
## 📝 Advanced Example (Optional)
\`\`\`syntax
example code or syntax here
\`\`\`
```
### 文档内容要求 (Content Requirements)
- **新增功能**: 必须在 "What's New" 章节中明确列出,使用 Emoji + 粗体标题格式。
- **双语**: 必须提供 `README.md` (英文) 和 `README_CN.md` (中文)。
- **表格对齐**: 配置参数表格使用左对齐 `:---`
- **Emoji 规范**: 标题使用合适的 Emoji 增强可读性。
### 官方文档 (Official Documentation) ### 官方文档 (Official Documentation)
@@ -93,33 +153,7 @@ icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0i...(完整的 Base64 编
--- ---
## 👤 作者和许可证信息 (Author and License) (Author info is now part of the top metadata section, see "README Structure Standard" above)
所有 README 文件和主要文档必须包含以下统一信息:
```markdown
## Author
Fu-Jie
GitHub: [Fu-Jie/awesome-openwebui](https://github.com/Fu-Jie/awesome-openwebui)
## License
MIT License
```
中文版本:
```markdown
## 作者
Fu-Jie
GitHub: [Fu-Jie/awesome-openwebui](https://github.com/Fu-Jie/awesome-openwebui)
## 许可证
MIT License
```
--- ---
@@ -507,7 +541,164 @@ Base = declarative_base()
--- ---
## 🔧 代码规范 (Code Style) ## 📂 文件存储访问规范 (File Storage Access)
OpenWebUI 支持多种文件存储后端本地磁盘、S3/MinIO 对象存储等)。插件在访问用户上传的文件或生成的图片时,必须实现多级回退机制以兼容所有存储配置。
### 存储类型检测 (Storage Type Detection)
通过 `Files.get_file_by_id()` 获取的文件对象,其 `path` 属性决定了存储位置:
| Path 格式 | 存储类型 | 访问方式 |
|-----------|----------|----------|
| `s3://bucket/key` | S3/MinIO 对象存储 | boto3 直连或 API 回调 |
| `/app/backend/data/...` | Docker 卷存储 | 本地文件系统读取 |
| `./uploads/...` | 本地相对路径 | 本地文件系统读取 |
| `gs://bucket/key` | Google Cloud Storage | API 回调 |
### 多级回退机制 (Multi-level Fallback)
推荐实现以下优先级的文件获取策略:
```python
def _get_file_content(self, file_id: str, max_bytes: int) -> Optional[bytes]:
"""获取文件内容,支持多种存储后端"""
file_obj = Files.get_file_by_id(file_id)
if not file_obj:
return None
# 1⃣ 数据库直接存储 (小文件)
data_field = getattr(file_obj, "data", None)
if isinstance(data_field, dict):
if "bytes" in data_field:
return data_field["bytes"]
if "base64" in data_field:
return base64.b64decode(data_field["base64"])
# 2⃣ S3 直连 (对象存储 - 最快)
s3_path = getattr(file_obj, "path", None)
if isinstance(s3_path, str) and s3_path.startswith("s3://"):
data = self._read_from_s3(s3_path, max_bytes)
if data:
return data
# 3⃣ 本地文件系统 (磁盘存储)
for attr in ("path", "file_path"):
path = getattr(file_obj, attr, None)
if path and not path.startswith(("s3://", "gs://", "http")):
# 尝试多个常见路径
for base in ["", "./data", "/app/backend/data"]:
full_path = Path(base) / path if base else Path(path)
if full_path.exists():
return full_path.read_bytes()[:max_bytes]
# 4⃣ 公共 URL 下载
url = getattr(file_obj, "url", None)
if url and url.startswith("http"):
return self._download_from_url(url, max_bytes)
# 5⃣ 内部 API 回调 (通用兜底方案)
if self._api_base_url:
api_url = f"{self._api_base_url}/api/v1/files/{file_id}/content"
return self._download_from_api(api_url, self._api_token, max_bytes)
return None
```
### S3 直连实现 (S3 Direct Access)
当检测到 `s3://` 路径时,使用 `boto3` 直接访问对象存储,读取以下环境变量:
| 环境变量 | 说明 | 示例 |
|----------|------|------|
| `S3_ENDPOINT_URL` | S3 兼容服务端点 | `https://minio.example.com` |
| `S3_ACCESS_KEY_ID` | 访问密钥 ID | `minioadmin` |
| `S3_SECRET_ACCESS_KEY` | 访问密钥 | `minioadmin` |
| `S3_ADDRESSING_STYLE` | 寻址样式 | `auto`, `path`, `virtual` |
```python
# S3 直连示例
import boto3
from botocore.config import Config as BotoConfig
import os
def _read_from_s3(self, s3_path: str, max_bytes: int) -> Optional[bytes]:
"""从 S3 直接读取文件 (比 API 回调更快)"""
if not s3_path.startswith("s3://"):
return None
# 解析 s3://bucket/key
parts = s3_path[5:].split("/", 1)
bucket, key = parts[0], parts[1]
# 从环境变量读取配置
endpoint = os.environ.get("S3_ENDPOINT_URL")
access_key = os.environ.get("S3_ACCESS_KEY_ID")
secret_key = os.environ.get("S3_SECRET_ACCESS_KEY")
if not all([endpoint, access_key, secret_key]):
return None # 回退到 API 方式
s3_client = boto3.client(
"s3",
endpoint_url=endpoint,
aws_access_key_id=access_key,
aws_secret_access_key=secret_key,
config=BotoConfig(s3={"addressing_style": os.environ.get("S3_ADDRESSING_STYLE", "auto")})
)
response = s3_client.get_object(Bucket=bucket, Key=key)
return response["Body"].read(max_bytes)
```
### API 回调实现 (API Fallback)
当其他方式失败时,通过 OpenWebUI 内部 API 获取文件:
```python
def _download_from_api(self, api_url: str, token: str, max_bytes: int) -> Optional[bytes]:
"""通过 OpenWebUI API 获取文件内容"""
import urllib.request
headers = {"User-Agent": "OpenWebUI-Plugin"}
if token:
headers["Authorization"] = token
req = urllib.request.Request(api_url, headers=headers)
with urllib.request.urlopen(req, timeout=15) as response:
if 200 <= response.status < 300:
return response.read(max_bytes)
return None
```
### 获取 API 上下文 (API Context Extraction)
`action()` 方法中捕获请求上下文,用于 API 回调:
```python
async def action(self, body: dict, __request__=None, ...):
# 从请求对象获取 API 凭证
if __request__:
self._api_token = __request__.headers.get("Authorization")
self._api_base_url = str(__request__.base_url).rstrip("/")
else:
# 从环境变量获取端口作为备用
port = os.environ.get("PORT") or "8080"
self._api_base_url = f"http://localhost:{port}"
self._api_token = None
```
### 性能对比 (Performance Comparison)
| 方式 | 网络跳数 | 适用场景 |
|------|----------|----------|
| S3 直连 | 1 (插件 → S3) | 对象存储,最快 |
| 本地文件 | 0 | 磁盘存储,最快 |
| API 回调 | 2 (插件 → OpenWebUI → S3/磁盘) | 通用兜底 |
### 参考实现 (Reference Implementation)
- `plugins/actions/export_to_docx/export_to_word.py` - `_image_bytes_from_owui_file_id` 方法
### Python 规范 ### Python 规范

View File

@@ -42,13 +42,13 @@ jobs:
- name: Check for changes - name: Check for changes
id: check_changes id: check_changes
run: | run: |
git diff --quiet docs/community-stats.md docs/community-stats.en.md README.md README_CN.md || echo "changed=true" >> $GITHUB_OUTPUT git diff --quiet docs/community-stats.zh.md docs/community-stats.md README.md README_CN.md || echo "changed=true" >> $GITHUB_OUTPUT
- name: Commit and push changes - name: Commit and push changes
if: steps.check_changes.outputs.changed == 'true' if: steps.check_changes.outputs.changed == 'true'
run: | run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com" git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]" git config --local user.name "github-actions[bot]"
git add docs/community-stats.md docs/community-stats.en.md docs/community-stats.json README.md README_CN.md git add docs/community-stats.zh.md docs/community-stats.md docs/community-stats.json README.md README_CN.md
git commit -m "📊 更新社区统计数据 $(date +'%Y-%m-%d')" git commit -m "chore: update community stats $(date +'%Y-%m-%d')"
git push git push

View File

@@ -0,0 +1,68 @@
name: Publish New Plugin
on:
workflow_dispatch:
inputs:
plugin_dir:
description: 'Plugin directory (e.g., plugins/actions/deep-dive)'
required: true
type: string
dry_run:
description: 'Dry run mode (preview only)'
required: false
type: boolean
default: false
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
- name: Validate plugin directory
run: |
if [ ! -d "${{ github.event.inputs.plugin_dir }}" ]; then
echo "❌ Error: Directory '${{ github.event.inputs.plugin_dir }}' does not exist"
exit 1
fi
echo "✅ Found plugin directory: ${{ github.event.inputs.plugin_dir }}"
ls -la "${{ github.event.inputs.plugin_dir }}"
- name: Publish Plugin
env:
OPENWEBUI_API_KEY: ${{ secrets.OPENWEBUI_API_KEY }}
run: |
if [ "${{ github.event.inputs.dry_run }}" = "true" ]; then
echo "🔍 Dry run mode - previewing..."
python scripts/publish_plugin.py --new "${{ github.event.inputs.plugin_dir }}" --dry-run
else
echo "🚀 Publishing plugin..."
python scripts/publish_plugin.py --new "${{ github.event.inputs.plugin_dir }}"
fi
- name: Commit changes (if ID was added)
if: ${{ github.event.inputs.dry_run != 'true' }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Check if there are changes to commit
if git diff --quiet; then
echo "No changes to commit"
else
git add "${{ github.event.inputs.plugin_dir }}"
git commit -m "feat: add openwebui_id to ${{ github.event.inputs.plugin_dir }}"
git push
echo "✅ Committed and pushed openwebui_id changes"
fi

28
.github/workflows/publish_plugin.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: Publish Plugins to OpenWebUI Market
on:
release:
types: [published]
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
- name: Publish Plugins
env:
OPENWEBUI_API_KEY: ${{ secrets.OPENWEBUI_API_KEY }}
run: python scripts/publish_plugin.py

View File

@@ -193,10 +193,21 @@ jobs:
TODAY_PREFIX="v${TODAY}-" TODAY_PREFIX="v${TODAY}-"
# Count existing releases with today's date prefix # Count existing releases with today's date prefix
EXISTING_COUNT=$(gh release list --limit 100 | grep -c "^${TODAY_PREFIX}" || echo "0") # grep -c returns 1 if count is 0, so we use || true to avoid script failure
EXISTING_COUNT=$(gh release list --limit 100 2>/dev/null | grep -c "^${TODAY_PREFIX}" || true)
# Clean up output (handle potential newlines or fallback issues)
EXISTING_COUNT=$(echo "$EXISTING_COUNT" | tr -cd '0-9')
if [ -z "$EXISTING_COUNT" ]; then EXISTING_COUNT=0; fi
NEXT_NUM=$((EXISTING_COUNT + 1)) NEXT_NUM=$((EXISTING_COUNT + 1))
VERSION="${TODAY_PREFIX}${NEXT_NUM}" VERSION="${TODAY_PREFIX}${NEXT_NUM}"
# Final fallback to ensure VERSION is never empty
if [ -z "$VERSION" ]; then
VERSION="v$(date +'%Y.%m.%d-%H%M%S')"
fi
fi fi
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Release version: $VERSION" echo "Release version: $VERSION"
@@ -334,13 +345,34 @@ jobs:
echo "=== Release Notes ===" echo "=== Release Notes ==="
cat release_notes.md cat release_notes.md
- name: Create Git Tag
run: |
VERSION="${{ steps.version.outputs.version }}"
if [ -z "$VERSION" ]; then
echo "Error: Version is empty!"
exit 1
fi
if ! git rev-parse "$VERSION" >/dev/null 2>&1; then
echo "Creating tag $VERSION"
git tag "$VERSION"
git push origin "$VERSION"
else
echo "Tag $VERSION already exists"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create GitHub Release - name: Create GitHub Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: ${{ steps.version.outputs.version }} tag_name: ${{ steps.version.outputs.version }}
target_commitish: ${{ github.sha }}
name: ${{ github.event.inputs.release_title || steps.version.outputs.version }} name: ${{ github.event.inputs.release_title || steps.version.outputs.version }}
body_path: release_notes.md body_path: release_notes.md
prerelease: ${{ github.event.inputs.prerelease || false }} prerelease: ${{ github.event.inputs.prerelease || false }}
make_latest: true
files: | files: |
plugin_versions.json plugin_versions.json
env: env:

View File

@@ -7,25 +7,26 @@ A collection of enhancements, plugins, and prompts for [OpenWebUI](https://githu
<!-- STATS_START --> <!-- STATS_START -->
## 📊 Community Stats ## 📊 Community Stats
> 🕐 Auto-updated: 2026-01-06 21:19 > 🕐 Auto-updated: 2026-01-08 08:35
| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions | | 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |
|:---:|:---:|:---:|:---:| |:---:|:---:|:---:|:---:|
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **43** | **62** | **17** | | [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **50** | **64** | **18** |
| 📝 Posts | ⬇️ Downloads | 👁️ Views | 👍 Upvotes | 💾 Saves | | 📝 Posts | ⬇️ Downloads | 👁️ Views | 👍 Upvotes | 💾 Saves |
|:---:|:---:|:---:|:---:|:---:| |:---:|:---:|:---:|:---:|:---:|
| **11** | **794** | **8481** | **54** | **48** | | **11** | **916** | **9670** | **55** | **50** |
### 🔥 Top 5 Popular Plugins ### 🔥 Top 6 Popular Plugins
| Rank | Plugin | Downloads | Views | | Rank | Plugin | Downloads | Views |
|:---:|------|:---:|:---:| |:---:|------|:---:|:---:|
| 🥇 | [Turn Any Text into Beautiful Mind Maps](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 240 | 2133 | | 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 294 | 2550 |
| 🥈 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 171 | 459 | | 🥈 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 178 | 507 |
| 🥉 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 112 | 1236 | | 🥉 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 119 | 1308 |
| 4⃣ | [Flash Card ](https://openwebui.com/posts/flash_card_65a2ea8f) | 76 | 1421 | | 4⃣ | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 87 | 1123 |
| 5⃣ | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 65 | 909 | | 5⃣ | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | 84 | 1561 |
| 6⃣ | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | 69 | 644 |
*See full stats in [Community Stats Report](./docs/community-stats.md)* *See full stats in [Community Stats Report](./docs/community-stats.md)*
<!-- STATS_END --> <!-- STATS_END -->

View File

@@ -7,27 +7,28 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
<!-- STATS_START --> <!-- STATS_START -->
## 📊 社区统计 ## 📊 社区统计
> 🕐 自动更新于 2026-01-06 21:19 > 🕐 自动更新于 2026-01-08 08:35
| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 | | 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |
|:---:|:---:|:---:|:---:| |:---:|:---:|:---:|:---:|
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **43** | **62** | **17** | | [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **50** | **64** | **18** |
| 📝 发布 | ⬇️ 下载 | 👁️ 浏览 | 👍 点赞 | 💾 收藏 | | 📝 发布 | ⬇️ 下载 | 👁️ 浏览 | 👍 点赞 | 💾 收藏 |
|:---:|:---:|:---:|:---:|:---:| |:---:|:---:|:---:|:---:|:---:|
| **11** | **794** | **8481** | **54** | **48** | | **11** | **916** | **9670** | **55** | **50** |
### 🔥 热门插件 Top 5 ### 🔥 热门插件 Top 6
| 排名 | 插件 | 下载 | 浏览 | | 排名 | 插件 | 下载 | 浏览 |
|:---:|------|:---:|:---:| |:---:|------|:---:|:---:|
| 🥇 | [Turn Any Text into Beautiful Mind Maps](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 240 | 2133 | | 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 294 | 2550 |
| 🥈 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 171 | 459 | | 🥈 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 178 | 507 |
| 🥉 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 112 | 1236 | | 🥉 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 119 | 1308 |
| 4⃣ | [Flash Card ](https://openwebui.com/posts/flash_card_65a2ea8f) | 76 | 1421 | | 4⃣ | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 87 | 1123 |
| 5⃣ | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 65 | 909 | | 5⃣ | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | 84 | 1561 |
| 6⃣ | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | 69 | 644 |
*完整统计请查看 [社区统计报告](./docs/community-stats.md)* *完整统计请查看 [社区统计报告](./docs/community-stats.zh.md)*
<!-- STATS_END --> <!-- STATS_END -->
## 📦 项目内容 ## 📦 项目内容

View File

@@ -1,35 +0,0 @@
# 📊 OpenWebUI Community Stats Report
> 📅 Updated: 2026-01-06 21:19
## 📈 Overview
| Metric | Value |
|------|------|
| 📝 Total Posts | 11 |
| ⬇️ Total Downloads | 794 |
| 👁️ Total Views | 8481 |
| 👍 Total Upvotes | 54 |
| 💾 Total Saves | 48 |
| 💬 Total Comments | 13 |
## 📂 By Type
- **action**: 9
- **filter**: 2
## 📋 Posts List
| Rank | Title | Type | Version | Downloads | Views | Upvotes | Saves | Updated |
|:---:|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| 1 | [Turn Any Text into Beautiful Mind Maps](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action | 0.8.2 | 240 | 2133 | 10 | 15 | 2026-01-03 |
| 2 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.6 | 171 | 459 | 3 | 3 | 2026-01-03 |
| 3 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | filter | 1.1.0 | 112 | 1236 | 5 | 9 | 2025-12-31 |
| 4 | [Flash Card ](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 76 | 1421 | 8 | 5 | 2026-01-03 |
| 5 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.3.2 | 65 | 909 | 6 | 8 | 2026-01-03 |
| 6 | [Export to Word (Enhanced Formatting)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.0 | 51 | 504 | 5 | 4 | 2026-01-05 |
| 7 | [智能信息图](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.3.1 | 33 | 398 | 3 | 0 | 2025-12-29 |
| 8 | [导出为 Word-支持公式、流程图、表格和代码块](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.1 | 15 | 752 | 7 | 1 | 2026-01-05 |
| 9 | [智能生成交互式思维导图,帮助用户可视化知识](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.8.0 | 14 | 249 | 2 | 1 | 2025-12-31 |
| 10 | [闪记卡生成插件](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.2 | 12 | 309 | 3 | 1 | 2025-12-31 |
| 11 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | filter | 1.1.0 | 5 | 111 | 2 | 1 | 2025-12-31 |

View File

@@ -1,46 +1,46 @@
{ {
"total_posts": 11, "total_posts": 11,
"total_downloads": 794, "total_downloads": 916,
"total_views": 8481, "total_views": 9670,
"total_upvotes": 54, "total_upvotes": 55,
"total_downvotes": 1, "total_downvotes": 1,
"total_saves": 48, "total_saves": 50,
"total_comments": 13, "total_comments": 15,
"by_type": { "by_type": {
"action": 9, "action": 9,
"filter": 2 "filter": 2
}, },
"posts": [ "posts": [
{ {
"title": "Turn Any Text into Beautiful Mind Maps", "title": "Smart Mind Map",
"slug": "turn_any_text_into_beautiful_mind_maps_3094c59a", "slug": "turn_any_text_into_beautiful_mind_maps_3094c59a",
"type": "action", "type": "action",
"version": "0.8.2", "version": "0.9.1",
"author": "Fu-Jie", "author": "Fu-Jie",
"description": "Intelligently analyzes text content and generates interactive mind maps to help users structure and visualize knowledge.", "description": "Intelligently analyzes text content and generates interactive mind maps to help users structure and visualize knowledge.",
"downloads": 240, "downloads": 294,
"views": 2133, "views": 2550,
"upvotes": 10, "upvotes": 10,
"saves": 15, "saves": 16,
"comments": 8, "comments": 10,
"created_at": "2025-12-30", "created_at": "2025-12-30",
"updated_at": "2026-01-03", "updated_at": "2026-01-07",
"url": "https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a" "url": "https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a"
}, },
{ {
"title": "Export to Excel", "title": "Export to Excel",
"slug": "export_mulit_table_to_excel_244b8f9d", "slug": "export_mulit_table_to_excel_244b8f9d",
"type": "action", "type": "action",
"version": "0.3.6", "version": "0.3.7",
"author": "Fu-Jie", "author": "Fu-Jie",
"description": "Extracts tables from chat messages and exports them to Excel (.xlsx) files with smart formatting.", "description": "Extracts tables from chat messages and exports them to Excel (.xlsx) files with smart formatting.",
"downloads": 171, "downloads": 178,
"views": 459, "views": 507,
"upvotes": 3, "upvotes": 3,
"saves": 3, "saves": 3,
"comments": 0, "comments": 0,
"created_at": "2025-05-30", "created_at": "2025-05-30",
"updated_at": "2026-01-03", "updated_at": "2026-01-07",
"url": "https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d" "url": "https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d"
}, },
{ {
@@ -49,126 +49,126 @@
"type": "filter", "type": "filter",
"version": "1.1.0", "version": "1.1.0",
"author": "Fu-Jie", "author": "Fu-Jie",
"description": "This filter automatically compresses long conversation contexts by intelligently summarizing and removing intermediate messages while preserving critical information, thereby significantly reducing token consumption.", "description": "Reduces token consumption in long conversations while maintaining coherence through intelligent summarization and message compression.",
"downloads": 112, "downloads": 119,
"views": 1236, "views": 1308,
"upvotes": 5, "upvotes": 5,
"saves": 9, "saves": 9,
"comments": 0, "comments": 0,
"created_at": "2025-11-08", "created_at": "2025-11-08",
"updated_at": "2025-12-31", "updated_at": "2026-01-07",
"url": "https://openwebui.com/posts/async_context_compression_b1655bc8" "url": "https://openwebui.com/posts/async_context_compression_b1655bc8"
}, },
{ {
"title": "Flash Card ", "title": "📊 Smart Infographic (AntV)",
"slug": "smart_infographic_ad6f0c7f",
"type": "action",
"version": "1.4.1",
"author": "jeff",
"description": "AI-powered infographic generator based on AntV Infographic. Supports professional templates, auto-icon matching, and SVG/PNG downloads.",
"downloads": 87,
"views": 1123,
"upvotes": 7,
"saves": 8,
"comments": 2,
"created_at": "2025-12-28",
"updated_at": "2026-01-07",
"url": "https://openwebui.com/posts/smart_infographic_ad6f0c7f"
},
{
"title": "Flash Card",
"slug": "flash_card_65a2ea8f", "slug": "flash_card_65a2ea8f",
"type": "action", "type": "action",
"version": "0.2.4", "version": "0.2.4",
"author": "Fu-Jie", "author": "Fu-Jie",
"description": "Quickly generates beautiful flashcards from text, extracting key points and categories.", "description": "Quickly generates beautiful flashcards from text, extracting key points and categories.",
"downloads": 76, "downloads": 84,
"views": 1421, "views": 1561,
"upvotes": 8, "upvotes": 8,
"saves": 5, "saves": 5,
"comments": 2, "comments": 2,
"created_at": "2025-12-30", "created_at": "2025-12-30",
"updated_at": "2026-01-03", "updated_at": "2026-01-07",
"url": "https://openwebui.com/posts/flash_card_65a2ea8f" "url": "https://openwebui.com/posts/flash_card_65a2ea8f"
}, },
{ {
"title": "Smart Infographic", "title": "Export to Word (Enhanced)",
"slug": "smart_infographic_ad6f0c7f",
"type": "action",
"version": "1.3.2",
"author": "jeff",
"description": "AI-powered infographic generator based on AntV Infographic. Supports professional templates, auto-icon matching, and SVG/PNG downloads.",
"downloads": 65,
"views": 909,
"upvotes": 6,
"saves": 8,
"comments": 2,
"created_at": "2025-12-28",
"updated_at": "2026-01-03",
"url": "https://openwebui.com/posts/smart_infographic_ad6f0c7f"
},
{
"title": "Export to Word (Enhanced Formatting)",
"slug": "export_to_word_enhanced_formatting_fca6a315", "slug": "export_to_word_enhanced_formatting_fca6a315",
"type": "action", "type": "action",
"version": "0.4.0", "version": "0.4.3",
"author": "Fu-Jie", "author": "Fu-Jie",
"description": "Export the current conversation to a formatted Word doc with syntax highlighting, AI-generated titles, and perfect Markdown rendering (tables, quotes, lists).", "description": "Export current conversation from Markdown to Word (.docx) with Mermaid diagrams rendered client-side (Mermaid.js, SVG+PNG), LaTeX math, real hyperlinks, improved tables, syntax highlighting, and blockquote support.",
"downloads": 51, "downloads": 69,
"views": 504, "views": 644,
"upvotes": 5, "upvotes": 5,
"saves": 4, "saves": 5,
"comments": 0, "comments": 0,
"created_at": "2026-01-03", "created_at": "2026-01-03",
"updated_at": "2026-01-05", "updated_at": "2026-01-07",
"url": "https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315" "url": "https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315"
}, },
{ {
"title": "智能信息图", "title": "📊 智能信息图 (AntV Infographic)",
"slug": "智能信息图_e04a48ff", "slug": "智能信息图_e04a48ff",
"type": "action", "type": "action",
"version": "1.3.1", "version": "1.4.1",
"author": "jeff", "author": "jeff",
"description": "基于 AntV Infographic 的智能信息图生成插件。支持多种专业模板,自动图标匹配,并提供 SVG/PNG 下载功能。", "description": "基于 AntV Infographic 的智能信息图生成插件。支持多种专业模板,自动图标匹配,并提供 SVG/PNG 下载功能。",
"downloads": 33, "downloads": 33,
"views": 398, "views": 434,
"upvotes": 3, "upvotes": 3,
"saves": 0, "saves": 0,
"comments": 0, "comments": 0,
"created_at": "2025-12-28", "created_at": "2025-12-28",
"updated_at": "2025-12-29", "updated_at": "2026-01-07",
"url": "https://openwebui.com/posts/智能信息图_e04a48ff" "url": "https://openwebui.com/posts/智能信息图_e04a48ff"
}, },
{ {
"title": "导出为 Word-支持公式、流程图、表格和代码块", "title": "导出为 Word (增强版)",
"slug": "导出为_word_支持公式流程图表格和代码块_8a6306c0", "slug": "导出为_word_支持公式流程图表格和代码块_8a6306c0",
"type": "action", "type": "action",
"version": "0.4.1", "version": "0.4.3",
"author": "Fu-Jie", "author": "Fu-Jie",
"description": "将当前对话内容从 Markdown 转换并导出为 Word (.docx) 文件,支持中英文无乱码。", "description": "将对话导出为 Word (.docx),支持 Mermaid 图表 (客户端渲染 SVG+PNG)、LaTeX 数学公式、真实超链接、增强表格格式、代码高亮和引用块。",
"downloads": 15, "downloads": 20,
"views": 752, "views": 815,
"upvotes": 7, "upvotes": 7,
"saves": 1, "saves": 1,
"comments": 1, "comments": 1,
"created_at": "2026-01-04", "created_at": "2026-01-04",
"updated_at": "2026-01-05", "updated_at": "2026-01-07",
"url": "https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0" "url": "https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0"
}, },
{ {
"title": "智能生成交互式思维导图,帮助用户可视化知识", "title": "思维导图",
"slug": "智能生成交互式思维导图帮助用户可视化知识_8d4b097b", "slug": "智能生成交互式思维导图帮助用户可视化知识_8d4b097b",
"type": "action", "type": "action",
"version": "0.8.0", "version": "0.9.1",
"author": "", "author": "Fu-Jie",
"description": "智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。", "description": "智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。",
"downloads": 14, "downloads": 15,
"views": 249, "views": 273,
"upvotes": 2, "upvotes": 2,
"saves": 1, "saves": 1,
"comments": 0, "comments": 0,
"created_at": "2025-12-31", "created_at": "2025-12-31",
"updated_at": "2025-12-31", "updated_at": "2026-01-07",
"url": "https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b" "url": "https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b"
}, },
{ {
"title": "闪记卡生成插件", "title": "闪记卡 (Flash Card)",
"slug": "闪记卡生成插件_4a31eac3", "slug": "闪记卡生成插件_4a31eac3",
"type": "action", "type": "action",
"version": "0.2.2", "version": "0.2.4",
"author": "Fu-Jie", "author": "Fu-Jie",
"description": "快速将文本提炼为精美的学习记忆卡片,支持核心要点提取与分类。", "description": "快速将文本提炼为精美的学习记忆卡片,支持核心要点提取与分类。",
"downloads": 12, "downloads": 12,
"views": 309, "views": 329,
"upvotes": 3, "upvotes": 3,
"saves": 1, "saves": 1,
"comments": 0, "comments": 0,
"created_at": "2025-12-30", "created_at": "2025-12-30",
"updated_at": "2025-12-31", "updated_at": "2026-01-07",
"url": "https://openwebui.com/posts/闪记卡生成插件_4a31eac3" "url": "https://openwebui.com/posts/闪记卡生成插件_4a31eac3"
}, },
{ {
@@ -177,14 +177,14 @@
"type": "filter", "type": "filter",
"version": "1.1.0", "version": "1.1.0",
"author": "Fu-Jie", "author": "Fu-Jie",
"description": "在 LLM 响应完成后进行上下文摘要和压缩", "description": "通过智能摘要和消息压缩,降低长对话的 token 消耗,同时保持对话连贯性。",
"downloads": 5, "downloads": 5,
"views": 111, "views": 126,
"upvotes": 2, "upvotes": 2,
"saves": 1, "saves": 1,
"comments": 0, "comments": 0,
"created_at": "2025-11-08", "created_at": "2025-11-08",
"updated_at": "2025-12-31", "updated_at": "2026-01-07",
"url": "https://openwebui.com/posts/异步上下文压缩_5c0617cb" "url": "https://openwebui.com/posts/异步上下文压缩_5c0617cb"
} }
], ],
@@ -193,11 +193,11 @@
"name": "Fu-Jie", "name": "Fu-Jie",
"profile_url": "https://openwebui.com/u/Fu-Jie", "profile_url": "https://openwebui.com/u/Fu-Jie",
"profile_image": "https://community.s3.openwebui.com/uploads/users/b15d1348-4347-42b4-b815-e053342d6cb0/profile_d9510745-4bd4-4f8f-a997-4a21847d9300.webp", "profile_image": "https://community.s3.openwebui.com/uploads/users/b15d1348-4347-42b4-b815-e053342d6cb0/profile_d9510745-4bd4-4f8f-a997-4a21847d9300.webp",
"followers": 43, "followers": 50,
"following": 2, "following": 2,
"total_points": 62, "total_points": 64,
"post_points": 53, "post_points": 54,
"comment_points": 9, "comment_points": 10,
"contributions": 17 "contributions": 18
} }
} }

View File

@@ -1,35 +1,35 @@
# 📊 OpenWebUI 社区统计报告 # 📊 OpenWebUI Community Stats Report
> 📅 更新时间: 2026-01-06 21:19 > 📅 Updated: 2026-01-08 08:35
## 📈 总览 ## 📈 Overview
| 指标 | 数值 | | Metric | Value |
|------|------| |------|------|
| 📝 发布数量 | 11 | | 📝 Total Posts | 11 |
| ⬇️ 总下载量 | 794 | | ⬇️ Total Downloads | 916 |
| 👁️ 总浏览量 | 8481 | | 👁️ Total Views | 9670 |
| 👍 总点赞数 | 54 | | 👍 Total Upvotes | 55 |
| 💾 总收藏数 | 48 | | 💾 Total Saves | 50 |
| 💬 总评论数 | 13 | | 💬 Total Comments | 15 |
## 📂 按类型分类 ## 📂 By Type
- **action**: 9 - **action**: 9
- **filter**: 2 - **filter**: 2
## 📋 发布列表 ## 📋 Posts List
| 排名 | 标题 | 类型 | 版本 | 下载 | 浏览 | 点赞 | 收藏 | 更新日期 | | Rank | Title | Type | Version | Downloads | Views | Upvotes | Saves | Updated |
|:---:|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:| |:---:|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| 1 | [Turn Any Text into Beautiful Mind Maps](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action | 0.8.2 | 240 | 2133 | 10 | 15 | 2026-01-03 | | 1 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action | 0.9.1 | 294 | 2550 | 10 | 16 | 2026-01-07 |
| 2 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.6 | 171 | 459 | 3 | 3 | 2026-01-03 | | 2 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.7 | 178 | 507 | 3 | 3 | 2026-01-07 |
| 3 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | filter | 1.1.0 | 112 | 1236 | 5 | 9 | 2025-12-31 | | 3 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | filter | 1.1.0 | 119 | 1308 | 5 | 9 | 2026-01-07 |
| 4 | [Flash Card ](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 76 | 1421 | 8 | 5 | 2026-01-03 | | 4 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.4.1 | 87 | 1123 | 7 | 8 | 2026-01-07 |
| 5 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.3.2 | 65 | 909 | 6 | 8 | 2026-01-03 | | 5 | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 84 | 1561 | 8 | 5 | 2026-01-07 |
| 6 | [Export to Word (Enhanced Formatting)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.0 | 51 | 504 | 5 | 4 | 2026-01-05 | | 6 | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.3 | 69 | 644 | 5 | 5 | 2026-01-07 |
| 7 | [智能信息图](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.3.1 | 33 | 398 | 3 | 0 | 2025-12-29 | | 7 | [📊 智能信息图 (AntV Infographic)](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.4.1 | 33 | 434 | 3 | 0 | 2026-01-07 |
| 8 | [导出为 Word-支持公式、流程图、表格和代码块](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.1 | 15 | 752 | 7 | 1 | 2026-01-05 | | 8 | [导出为 Word (增强版)](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.3 | 20 | 815 | 7 | 1 | 2026-01-07 |
| 9 | [智能生成交互式思维导图,帮助用户可视化知识](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.8.0 | 14 | 249 | 2 | 1 | 2025-12-31 | | 9 | [思维导图](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.9.1 | 15 | 273 | 2 | 1 | 2026-01-07 |
| 10 | [闪记卡生成插件](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.2 | 12 | 309 | 3 | 1 | 2025-12-31 | | 10 | [闪记卡 (Flash Card)](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.4 | 12 | 329 | 3 | 1 | 2026-01-07 |
| 11 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | filter | 1.1.0 | 5 | 111 | 2 | 1 | 2025-12-31 | | 11 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | filter | 1.1.0 | 5 | 126 | 2 | 1 | 2026-01-07 |

View File

@@ -0,0 +1,35 @@
# 📊 OpenWebUI 社区统计报告
> 📅 更新时间: 2026-01-08 08:35
## 📈 总览
| 指标 | 数值 |
|------|------|
| 📝 发布数量 | 11 |
| ⬇️ 总下载量 | 916 |
| 👁️ 总浏览量 | 9670 |
| 👍 总点赞数 | 55 |
| 💾 总收藏数 | 50 |
| 💬 总评论数 | 15 |
## 📂 按类型分类
- **action**: 9
- **filter**: 2
## 📋 发布列表
| 排名 | 标题 | 类型 | 版本 | 下载 | 浏览 | 点赞 | 收藏 | 更新日期 |
|:---:|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| 1 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action | 0.9.1 | 294 | 2550 | 10 | 16 | 2026-01-07 |
| 2 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.7 | 178 | 507 | 3 | 3 | 2026-01-07 |
| 3 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | filter | 1.1.0 | 119 | 1308 | 5 | 9 | 2026-01-07 |
| 4 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.4.1 | 87 | 1123 | 7 | 8 | 2026-01-07 |
| 5 | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 84 | 1561 | 8 | 5 | 2026-01-07 |
| 6 | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.3 | 69 | 644 | 5 | 5 | 2026-01-07 |
| 7 | [📊 智能信息图 (AntV Infographic)](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.4.1 | 33 | 434 | 3 | 0 | 2026-01-07 |
| 8 | [导出为 Word (增强版)](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.3 | 20 | 815 | 7 | 1 | 2026-01-07 |
| 9 | [思维导图](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.9.1 | 15 | 273 | 2 | 1 | 2026-01-07 |
| 10 | [闪记卡 (Flash Card)](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.4 | 12 | 329 | 3 | 1 | 2026-01-07 |
| 11 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | filter | 1.1.0 | 5 | 126 | 2 | 1 | 2026-01-07 |

View File

@@ -0,0 +1,111 @@
# Deep Dive
<span class="category-badge action">Action</span>
<span class="version-badge">v1.0.0</span>
A comprehensive thinking lens that dives deep into any content - from context to logic, insights, and action paths.
---
## Overview
The Deep Dive plugin transforms how you understand complex content by guiding you through a structured thinking process. Rather than just summarizing, it deconstructs content across four phases:
- **🔍 The Context (What?)**: Panoramic view of the situation and background
- **🧠 The Logic (Why?)**: Deconstruction of reasoning and mental models
- **💎 The Insight (So What?)**: Non-obvious value and hidden implications
- **🚀 The Path (Now What?)**: Specific, prioritized strategic actions
## Features
- :material-brain: **Thinking Chain**: Complete structured analysis process
- :material-eye: **Deep Understanding**: Reveals hidden assumptions and blind spots
- :material-lightbulb-on: **Insight Extraction**: Finds the "Aha!" moments
- :material-rocket-launch: **Action Oriented**: Translates understanding into actionable steps
- :material-theme-light-dark: **Theme Adaptive**: Auto-adapts to OpenWebUI light/dark theme
- :material-translate: **Multi-language**: Outputs in user's preferred language
---
## Installation
1. Download the plugin file: [`deep_dive.py`](https://github.com/Fu-Jie/awesome-openwebui/tree/main/plugins/actions/deep-dive)
2. Upload to OpenWebUI: **Admin Panel****Settings****Functions**
3. Enable the plugin
---
## Usage
1. Provide any long text, article, or meeting notes in the chat
2. Click the **Deep Dive** button in the message action bar
3. Follow the visual timeline from Context → Logic → Insight → Path
---
## Configuration
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `SHOW_STATUS` | boolean | `true` | Show status updates during processing |
| `MODEL_ID` | string | `""` | LLM model for analysis (empty = current model) |
| `MIN_TEXT_LENGTH` | integer | `200` | Minimum text length for analysis |
| `CLEAR_PREVIOUS_HTML` | boolean | `true` | Clear previous plugin results |
| `MESSAGE_COUNT` | integer | `1` | Number of recent messages to analyze |
---
## Theme Support
Deep Dive automatically adapts to OpenWebUI's light/dark theme:
- Detects theme from parent document `<meta name="theme-color">` tag
- Falls back to `html/body` class or `data-theme` attribute
- Uses system preference `prefers-color-scheme: dark` as last resort
!!! tip "For Best Results"
Enable **iframe Sandbox Allow Same Origin** in OpenWebUI:
**Settings****Interface****Artifacts** → Check **iframe Sandbox Allow Same Origin**
---
## Example Output
The plugin generates a beautiful structured timeline:
```
┌─────────────────────────────────────┐
│ 🌊 Deep Dive Analysis │
│ 👤 User 📅 Date 📊 Word count │
├─────────────────────────────────────┤
│ 🔍 Phase 01: The Context │
│ [High-level panoramic view] │
│ │
│ 🧠 Phase 02: The Logic │
│ • Reasoning structure... │
│ • Hidden assumptions... │
│ │
│ 💎 Phase 03: The Insight │
│ • Non-obvious value... │
│ • Blind spots revealed... │
│ │
│ 🚀 Phase 04: The Path │
│ ▸ Priority Action 1... │
│ ▸ Priority Action 2... │
└─────────────────────────────────────┘
```
---
## Requirements
!!! note "Prerequisites"
- OpenWebUI v0.3.0 or later
- Uses the active LLM model for analysis
- Requires `markdown` Python package
---
## Source Code
[:fontawesome-brands-github: View on GitHub](https://github.com/Fu-Jie/awesome-openwebui/tree/main/plugins/actions/deep-dive){ .md-button }

View File

@@ -0,0 +1,111 @@
# 精读 (Deep Dive)
<span class="category-badge action">Action</span>
<span class="version-badge">v1.0.0</span>
全方位的思维透镜 —— 从背景全景到逻辑脉络,从深度洞察到行动路径。
---
## 概述
精读插件改变了您理解复杂内容的方式,通过结构化的思维过程引导您进行深度分析。它不仅仅是摘要,而是从四个阶段解构内容:
- **🔍 全景 (The Context)**: 情境与背景的高层级全景视图
- **🧠 脉络 (The Logic)**: 解构底层推理逻辑与思维模型
- **💎 洞察 (The Insight)**: 提取非显性价值与隐藏含义
- **🚀 路径 (The Path)**: 具体的、按优先级排列的战略行动
## 功能特性
- :material-brain: **思维链**: 完整的结构化分析过程
- :material-eye: **深度理解**: 揭示隐藏的假设和思维盲点
- :material-lightbulb-on: **洞察提取**: 发现"原来如此"的时刻
- :material-rocket-launch: **行动导向**: 将深度理解转化为可执行步骤
- :material-theme-light-dark: **主题自适应**: 自动适配 OpenWebUI 深色/浅色主题
- :material-translate: **多语言**: 以用户偏好语言输出
---
## 安装
1. 下载插件文件: [`deep_dive_cn.py`](https://github.com/Fu-Jie/awesome-openwebui/tree/main/plugins/actions/deep-dive)
2. 上传到 OpenWebUI: **管理面板****设置****Functions**
3. 启用插件
---
## 使用方法
1. 在聊天中提供任何长文本、文章或会议记录
2. 点击消息操作栏中的 **精读** 按钮
3. 沿着视觉时间轴从"全景"探索到"路径"
---
## 配置参数
| 选项 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| `SHOW_STATUS` | boolean | `true` | 处理过程中是否显示状态更新 |
| `MODEL_ID` | string | `""` | 用于分析的 LLM 模型(空 = 当前模型) |
| `MIN_TEXT_LENGTH` | integer | `200` | 分析所需的最小文本长度 |
| `CLEAR_PREVIOUS_HTML` | boolean | `true` | 是否清除之前的插件结果 |
| `MESSAGE_COUNT` | integer | `1` | 要分析的最近消息数量 |
---
## 主题支持
精读插件自动适配 OpenWebUI 的深色/浅色主题:
- 从父文档 `<meta name="theme-color">` 标签检测主题
- 回退到 `html/body` 的 class 或 `data-theme` 属性
- 最后使用系统偏好 `prefers-color-scheme: dark`
!!! tip "最佳效果"
请在 OpenWebUI 中启用 **iframe Sandbox Allow Same Origin**
**设置****界面****Artifacts** → 勾选 **iframe Sandbox Allow Same Origin**
---
## 输出示例
插件生成精美的结构化时间轴:
```
┌─────────────────────────────────────┐
│ 📖 精读分析报告 │
│ 👤 用户 📅 日期 📊 字数 │
├─────────────────────────────────────┤
│ 🔍 阶段 01: 全景 (The Context) │
│ [高层级全景视图内容] │
│ │
│ 🧠 阶段 02: 脉络 (The Logic) │
│ • 推理结构分析... │
│ • 隐藏假设识别... │
│ │
│ 💎 阶段 03: 洞察 (The Insight) │
│ • 非显性价值提取... │
│ • 思维盲点揭示... │
│ │
│ 🚀 阶段 04: 路径 (The Path) │
│ ▸ 优先级行动 1... │
│ ▸ 优先级行动 2... │
└─────────────────────────────────────┘
```
---
## 系统要求
!!! note "前提条件"
- OpenWebUI v0.3.0 或更高版本
- 使用当前活跃的 LLM 模型进行分析
- 需要 `markdown` Python 包
---
## 源代码
[:fontawesome-brands-github: 在 GitHub 上查看](https://github.com/Fu-Jie/awesome-openwebui/tree/main/plugins/actions/deep-dive){ .md-button }

View File

@@ -1,7 +1,7 @@
# Export to Word # Export to Word
<span class="category-badge action">Action</span> <span class="category-badge action">Action</span>
<span class="version-badge">v0.4.1</span> <span class="version-badge">v0.4.3</span>
Export conversation to Word (.docx) with **syntax highlighting**, **native math equations**, **Mermaid diagrams**, **citations**, and **enhanced table formatting**. Export conversation to Word (.docx) with **syntax highlighting**, **native math equations**, **Mermaid diagrams**, **citations**, and **enhanced table formatting**.
@@ -53,6 +53,8 @@ You can configure the following settings via the **Valves** button in the plugin
| `MATH_ENABLE` | Enable LaTeX math block conversion. | `True` | | `MATH_ENABLE` | Enable LaTeX math block conversion. | `True` |
| `MATH_INLINE_DOLLAR_ENABLE` | Enable inline `$ ... $` math conversion. | `True` | | `MATH_INLINE_DOLLAR_ENABLE` | Enable inline `$ ... $` math conversion. | `True` |
## 🔥 What's New in v0.4.3
### User-Level Configuration (UserValves) ### User-Level Configuration (UserValves)
Users can override the following settings in their personal settings: Users can override the following settings in their personal settings:
@@ -118,3 +120,4 @@ Users can override the following settings in their personal settings:
## Source Code ## Source Code
[:fontawesome-brands-github: View on GitHub](https://github.com/Fu-Jie/awesome-openwebui/tree/main/plugins/actions/export_to_docx){ .md-button } [:fontawesome-brands-github: View on GitHub](https://github.com/Fu-Jie/awesome-openwebui/tree/main/plugins/actions/export_to_docx){ .md-button }
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.4.3 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)

View File

@@ -1,7 +1,7 @@
# Export to Word导出为 Word # Export to Word导出为 Word
<span class="category-badge action">Action</span> <span class="category-badge action">Action</span>
<span class="version-badge">v0.4.1</span> <span class="version-badge">v0.4.3</span>
将当前对话导出为完美格式的 Word 文档,支持**代码语法高亮**、**原生数学公式**、**Mermaid 图表**、**引用资料**以及**增强表格**渲染。 将当前对话导出为完美格式的 Word 文档,支持**代码语法高亮**、**原生数学公式**、**Mermaid 图表**、**引用资料**以及**增强表格**渲染。
@@ -117,4 +117,4 @@ Export to Word 插件会把聊天消息从 Markdown 转成精致的 Word 文档
## 源码 ## 源码
[:fontawesome-brands-github: 在 GitHub 查看](https://github.com/Fu-Jie/awesome-openwebui/tree/main/plugins/actions/export_to_docx){ .md-button } [:fontawes**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.4.3 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)/tree/main/plugins/actions/export_to_docx){ .md-button }

View File

@@ -33,7 +33,7 @@ Actions are interactive plugins that:
Transform text into professional infographics using AntV visualization engine with various templates. Transform text into professional infographics using AntV visualization engine with various templates.
**Version:** 1.3.0 **Version:** 1.4.1
[:octicons-arrow-right-24: Documentation](smart-infographic.md) [:octicons-arrow-right-24: Documentation](smart-infographic.md)
@@ -63,19 +63,19 @@ Actions are interactive plugins that:
Export the current conversation to a formatted Word doc with **syntax highlighting**, **native math equations**, **Mermaid diagrams**, **citations**, and **enhanced table formatting**. Export the current conversation to a formatted Word doc with **syntax highlighting**, **native math equations**, **Mermaid diagrams**, **citations**, and **enhanced table formatting**.
**Version:** 0.4.1 **Version:** 0.4.2
[:octicons-arrow-right-24: Documentation](export-to-word.md) [:octicons-arrow-right-24: Documentation](export-to-word.md)
- :material-text-box-search:{ .lg .middle } **Summary** - :material-brain:{ .lg .middle } **Deep Dive**
--- ---
Generate concise summaries of long text content with key points extraction. A comprehensive thinking lens that dives deep into any content - Context → Logic → Insight → Path. Supports theme auto-adaptation.
**Version:** 0.1.0 **Version:** 1.0.0
[:octicons-arrow-right-24: Documentation](summary.md) [:octicons-arrow-right-24: Documentation](deep-dive.md)
- :material-image-text:{ .lg .middle } **Infographic to Markdown** - :material-image-text:{ .lg .middle } **Infographic to Markdown**

View File

@@ -33,7 +33,7 @@ Actions 是交互式插件,能够:
使用 AntV 可视化引擎,将文本转成专业的信息图。 使用 AntV 可视化引擎,将文本转成专业的信息图。
**版本:** 1.3.0 **版本:** 1.4.1
[:octicons-arrow-right-24: 查看文档](smart-infographic.md) [:octicons-arrow-right-24: 查看文档](smart-infographic.md)
@@ -63,19 +63,19 @@ Actions 是交互式插件,能够:
将当前对话导出为完美格式的 Word 文档,支持**代码语法高亮**、**原生数学公式**、**Mermaid 图表**、**引用资料**以及**增强表格**渲染。 将当前对话导出为完美格式的 Word 文档,支持**代码语法高亮**、**原生数学公式**、**Mermaid 图表**、**引用资料**以及**增强表格**渲染。
**版本:** 0.4.1 **版本:** 0.4.2
[:octicons-arrow-right-24: 查看文档](export-to-word.md) [:octicons-arrow-right-24: 查看文档](export-to-word.md)
- :material-text-box-search:{ .lg .middle } **Summary** - :material-brain:{ .lg .middle } **精读 (Deep Dive)**
--- ---
对长文本进行精简总结,提取要点 全方位的思维透镜 —— 全景 → 脉络 → 洞察 → 路径。支持主题自适应
**版本:** 0.1.0 **版本:** 1.0.0
[:octicons-arrow-right-24: 查看文档](summary.md) [:octicons-arrow-right-24: 查看文档](deep-dive.zh.md)
- :material-image-text:{ .lg .middle } **信息图转 Markdown** - :material-image-text:{ .lg .middle } **信息图转 Markdown**

View File

@@ -1,7 +1,7 @@
# Smart Infographic # Smart Infographic
<span class="category-badge action">Action</span> <span class="category-badge action">Action</span>
<span class="version-badge">v1.3.0</span> <span class="version-badge">v1.4.0</span>
An AntV Infographic engine powered plugin that transforms long text into professional, beautiful infographics with a single click. An AntV Infographic engine powered plugin that transforms long text into professional, beautiful infographics with a single click.
@@ -19,6 +19,8 @@ The Smart Infographic plugin uses AI to analyze text content and generate profes
- :material-download: **Multi-Format Export**: Download your infographics as **SVG**, **PNG**, or **Standalone HTML** file - :material-download: **Multi-Format Export**: Download your infographics as **SVG**, **PNG**, or **Standalone HTML** file
- :material-theme-light-dark: **Theme Support**: Supports Dark/Light modes, auto-adapts theme colors - :material-theme-light-dark: **Theme Support**: Supports Dark/Light modes, auto-adapts theme colors
- :material-cellphone-link: **Responsive Design**: Generated charts look great on both desktop and mobile devices - :material-cellphone-link: **Responsive Design**: Generated charts look great on both desktop and mobile devices
- :material-image: **Image Embedding**: Option to embed charts as static images for better compatibility
- :material-monitor-screenshot: **Adaptive Sizing**: Images automatically adapt to the chat container width
--- ---
@@ -60,6 +62,7 @@ The Smart Infographic plugin uses AI to analyze text content and generate profes
| `MIN_TEXT_LENGTH` | integer | `100` | Minimum characters required to trigger analysis | | `MIN_TEXT_LENGTH` | integer | `100` | Minimum characters required to trigger analysis |
| `CLEAR_PREVIOUS_HTML` | boolean | `false` | Whether to clear previous charts | | `CLEAR_PREVIOUS_HTML` | boolean | `false` | Whether to clear previous charts |
| `MESSAGE_COUNT` | integer | `1` | Number of recent messages to use for analysis | | `MESSAGE_COUNT` | integer | `1` | Number of recent messages to use for analysis |
| `OUTPUT_MODE` | string | `image` | `image` for static image embedding (default), `html` for interactive chart |
--- ---

View File

@@ -1,7 +1,7 @@
# Smart Infographic智能信息图 # Smart Infographic智能信息图
<span class="category-badge action">Action</span> <span class="category-badge action">Action</span>
<span class="version-badge">v1.0.0</span> <span class="version-badge">v1.4.0</span>
基于 AntV 信息图引擎,将长文本一键转成专业、美观的信息图。 基于 AntV 信息图引擎,将长文本一键转成专业、美观的信息图。
@@ -19,6 +19,8 @@ Smart Infographic 使用 AI 分析文本,并基于 AntV 可视化引擎生成
- :material-download: **多格式导出**:支持下载 **SVG**、**PNG**、**独立 HTML** - :material-download: **多格式导出**:支持下载 **SVG**、**PNG**、**独立 HTML**
- :material-theme-light-dark: **主题支持**:适配深色/浅色模式 - :material-theme-light-dark: **主题支持**:适配深色/浅色模式
- :material-cellphone-link: **响应式**:桌面与移动端都能良好展示 - :material-cellphone-link: **响应式**:桌面与移动端都能良好展示
- :material-image: **图片嵌入**:支持将图表作为静态图片嵌入,兼容性更好
- :material-monitor-screenshot: **自适应尺寸**:图片模式下自动适应聊天容器宽度
--- ---
@@ -60,6 +62,7 @@ Smart Infographic 使用 AI 分析文本,并基于 AntV 可视化引擎生成
| `MIN_TEXT_LENGTH` | integer | `100` | 触发分析的最小字符数 | | `MIN_TEXT_LENGTH` | integer | `100` | 触发分析的最小字符数 |
| `CLEAR_PREVIOUS_HTML` | boolean | `false` | 是否清空之前生成的图表 | | `CLEAR_PREVIOUS_HTML` | boolean | `false` | 是否清空之前生成的图表 |
| `MESSAGE_COUNT` | integer | `1` | 参与分析的最近消息条数 | | `MESSAGE_COUNT` | integer | `1` | 参与分析的最近消息条数 |
| `OUTPUT_MODE` | string | `image` | `image` 为静态图片嵌入(默认),`html` 为交互式图表 |
--- ---

View File

@@ -0,0 +1,83 @@
# 🌊 Deep Dive
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 1.0.0 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
A comprehensive thinking lens that dives deep into any content - from context to logic, insights, and action paths.
## 🔥 What's New in v1.0.0
-**Thinking Chain Structure**: Moves from surface understanding to deep strategic action.
- 🔍 **Phase 01: The Context**: Panoramic view of the situation and background.
- 🧠 **Phase 02: The Logic**: Deconstruction of the underlying reasoning and mental models.
- 💎 **Phase 03: The Insight**: Extraction of non-obvious value and hidden implications.
- 🚀 **Phase 04: The Path**: Definition of specific, prioritized strategic directions.
- 🎨 **Premium UI**: Modern, process-oriented design with a "Thinking Line" timeline.
- 🌗 **Theme Adaptive**: Automatically adapts to OpenWebUI's light/dark theme.
## ✨ Key Features
- 🌊 **Deep Thinking**: Not just a summary, but a full deconstruction of content.
- 🧠 **Logical Analysis**: Reveals how arguments are built and identifies hidden assumptions.
- 💎 **Value Extraction**: Finds the "Aha!" moments and blind spots.
- 🚀 **Action Oriented**: Translates deep understanding into immediate, actionable steps.
- 🌍 **Multi-language**: Automatically adapts to the user's preferred language.
- 🌗 **Theme Support**: Seamlessly switches between light and dark themes based on OpenWebUI settings.
## 🚀 How to Use
1. **Input Content**: Provide any text, article, or meeting notes in the chat.
2. **Trigger Deep Dive**: Click the **Deep Dive** action button.
3. **Explore the Chain**: Follow the visual timeline from Context to Path.
## ⚙️ Configuration (Valves)
| Parameter | Default | Description |
| :--- | :--- | :--- |
| **Show Status (SHOW_STATUS)** | `True` | Whether to show status updates during the thinking process. |
| **Model ID (MODEL_ID)** | `Empty` | LLM model for analysis. Empty = use current model. |
| **Min Text Length (MIN_TEXT_LENGTH)** | `200` | Minimum characters required for a meaningful deep dive. |
| **Clear Previous HTML (CLEAR_PREVIOUS_HTML)** | `True` | Whether to clear previous plugin results. |
| **Message Count (MESSAGE_COUNT)** | `1` | Number of recent messages to analyze. |
## 🌗 Theme Support
The plugin automatically detects and adapts to OpenWebUI's theme settings:
- **Detection Priority**:
1. Parent document `<meta name="theme-color">` tag
2. Parent document `html/body` class or `data-theme` attribute
3. System preference via `prefers-color-scheme: dark`
- **Requirements**: For best results, enable **iframe Sandbox Allow Same Origin** in OpenWebUI:
- Go to **Settings****Interface****Artifacts** → Check **iframe Sandbox Allow Same Origin**
## 🎨 Visual Preview
The plugin generates a structured thinking timeline:
```
┌─────────────────────────────────────┐
│ 🌊 Deep Dive Analysis │
│ 👤 User 📅 Date 📊 Word count │
├─────────────────────────────────────┤
│ 🔍 Phase 01: The Context │
│ [High-level panoramic view] │
│ │
│ 🧠 Phase 02: The Logic │
│ • Reasoning structure... │
│ • Hidden assumptions... │
│ │
│ 💎 Phase 03: The Insight │
│ • Non-obvious value... │
│ • Blind spots revealed... │
│ │
│ 🚀 Phase 04: The Path │
│ ▸ Priority Action 1... │
│ ▸ Priority Action 2... │
└─────────────────────────────────────┘
```
## 📂 Files
- `deep_dive.py` - English version
- `deep_dive_cn.py` - Chinese version (精读)

View File

@@ -0,0 +1,83 @@
# 📖 精读
**作者:** [Fu-Jie](https://github.com/Fu-Jie) | **版本:** 1.0.0 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
全方位的思维透镜 —— 从背景全景到逻辑脉络,从深度洞察到行动路径。
## 🔥 v1.0.0 更新内容
-**思维链结构**: 从表面理解一步步深入到战略行动。
- 🔍 **阶段 01: 全景 (The Context)**: 提供情境与背景的高层级全景视图。
- 🧠 **阶段 02: 脉络 (The Logic)**: 解构底层推理逻辑与思维模型。
- 💎 **阶段 03: 洞察 (The Insight)**: 提取非显性价值与隐藏的深层含义。
- 🚀 **阶段 04: 路径 (The Path)**: 定义具体的、按优先级排列的战略方向。
- 🎨 **高端 UI**: 现代化的过程导向设计,带有"思维导火索"时间轴。
- 🌗 **主题自适应**: 自动适配 OpenWebUI 的深色/浅色主题。
## ✨ 核心特性
- 📖 **深度思考**: 不仅仅是摘要,而是对内容的全面解构。
- 🧠 **逻辑分析**: 揭示论点是如何构建的,识别隐藏的假设。
- 💎 **价值提取**: 发现"原来如此"的时刻与思维盲点。
- 🚀 **行动导向**: 将深度理解转化为立即、可执行的步骤。
- 🌍 **多语言支持**: 自动适配用户的偏好语言。
- 🌗 **主题支持**: 根据 OpenWebUI 设置自动切换深色/浅色主题。
## 🚀 如何使用
1. **输入内容**: 在聊天中提供任何文本、文章或会议记录。
2. **触发精读**: 点击 **精读** 操作按钮。
3. **探索思维链**: 沿着视觉时间轴从"全景"探索到"路径"。
## ⚙️ 配置参数 (Valves)
| 参数 | 默认值 | 描述 |
| :--- | :--- | :--- |
| **显示状态 (SHOW_STATUS)** | `True` | 是否在思维过程中显示状态更新。 |
| **模型 ID (MODEL_ID)** | `空` | 用于分析的 LLM 模型。留空 = 使用当前模型。 |
| **最小文本长度 (MIN_TEXT_LENGTH)** | `200` | 进行有意义的精读所需的最小字符数。 |
| **清除旧 HTML (CLEAR_PREVIOUS_HTML)** | `True` | 是否清除之前的插件结果。 |
| **消息数量 (MESSAGE_COUNT)** | `1` | 要分析的最近消息数量。 |
## 🌗 主题支持
插件会自动检测并适配 OpenWebUI 的主题设置:
- **检测优先级**:
1. 父文档 `<meta name="theme-color">` 标签
2. 父文档 `html/body` 的 class 或 `data-theme` 属性
3. 系统偏好 `prefers-color-scheme: dark`
- **环境要求**: 为获得最佳效果,请在 OpenWebUI 中启用 **iframe Sandbox Allow Same Origin**
- 进入 **设置****界面****Artifacts** → 勾选 **iframe Sandbox Allow Same Origin**
## 🎨 视觉预览
插件生成结构化的思维时间轴:
```
┌─────────────────────────────────────┐
│ 📖 精读分析报告 │
│ 👤 用户 📅 日期 📊 字数 │
├─────────────────────────────────────┤
│ 🔍 阶段 01: 全景 (The Context) │
│ [高层级全景视图内容] │
│ │
│ 🧠 阶段 02: 脉络 (The Logic) │
│ • 推理结构分析... │
│ • 隐藏假设识别... │
│ │
│ 💎 阶段 03: 洞察 (The Insight) │
│ • 非显性价值提取... │
│ • 思维盲点揭示... │
│ │
│ 🚀 阶段 04: 路径 (The Path) │
│ ▸ 优先级行动 1... │
│ ▸ 优先级行动 2... │
└─────────────────────────────────────┘
```
## 📂 文件说明
- `deep_dive.py` - 英文版 (Deep Dive)
- `deep_dive_cn.py` - 中文版 (精读)

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 KiB

View File

@@ -0,0 +1,884 @@
"""
title: Deep Dive
author: Fu-Jie
author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui
version: 1.0.0
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xMiA3djE0Ii8+PHBhdGggZD0iTTMgMThhMSAxIDAgMCAxLTEtMVY0YTEgMSAwIDAgMSAxLTFoNWE0IDQgMCAwIDEgNCA0IDQgNCAwIDAgMSA0LTRoNWExIDEgMCAwIDEgMSAxdjEzYTEgMSAwIDAgMS0xIDFoLTZhMyAzIDAgMCAwLTMgMyAzIDMgMCAwIDAtMy0zeiIvPjxwYXRoIGQ9Ik02IDEyaDIiLz48cGF0aCBkPSJNMTYgMTJoMiIvPjwvc3ZnPg==
requirements: markdown
description: A comprehensive thinking lens that dives deep into any content - from context to logic, insights, and action paths.
"""
# Standard library imports
import re
import logging
from typing import Optional, Dict, Any, Callable, Awaitable
from datetime import datetime
# Third-party imports
from pydantic import BaseModel, Field
from fastapi import Request
import markdown
# OpenWebUI imports
from open_webui.utils.chat import generate_chat_completion
from open_webui.models.users import Users
# Logging setup
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
# =================================================================
# HTML Template - Process-Oriented Design with Theme Support
# =================================================================
HTML_WRAPPER_TEMPLATE = """
<!-- OPENWEBUI_PLUGIN_OUTPUT -->
<!DOCTYPE html>
<html lang="{user_language}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
:root {
--dd-bg-primary: #ffffff;
--dd-bg-secondary: #f8fafc;
--dd-bg-tertiary: #f1f5f9;
--dd-text-primary: #0f172a;
--dd-text-secondary: #334155;
--dd-text-dim: #64748b;
--dd-border: #e2e8f0;
--dd-accent: #3b82f6;
--dd-accent-soft: #eff6ff;
--dd-header-gradient: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
--dd-shadow: 0 10px 40px rgba(0,0,0,0.06);
--dd-code-bg: #f1f5f9;
}
.theme-dark {
--dd-bg-primary: #1e293b;
--dd-bg-secondary: #0f172a;
--dd-bg-tertiary: #334155;
--dd-text-primary: #f1f5f9;
--dd-text-secondary: #e2e8f0;
--dd-text-dim: #94a3b8;
--dd-border: #475569;
--dd-accent: #60a5fa;
--dd-accent-soft: rgba(59, 130, 246, 0.15);
--dd-header-gradient: linear-gradient(135deg, #0f172a 0%, #1e1e2e 100%);
--dd-shadow: 0 10px 40px rgba(0,0,0,0.3);
--dd-code-bg: #334155;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: 0;
padding: 10px;
background-color: transparent;
}
#main-container {
display: flex;
flex-direction: column;
gap: 24px;
width: 100%;
max-width: 900px;
margin: 0 auto;
}
.plugin-item {
background: var(--dd-bg-primary);
border-radius: 24px;
box-shadow: var(--dd-shadow);
overflow: hidden;
border: 1px solid var(--dd-border);
}
/* STYLES_INSERTION_POINT */
</style>
</head>
<body>
<div id="main-container">
<!-- CONTENT_INSERTION_POINT -->
</div>
<!-- SCRIPTS_INSERTION_POINT -->
<script>
(function() {
const parseColorLuma = (colorStr) => {
if (!colorStr) return null;
let m = colorStr.match(/^#?([0-9a-f]{6})$/i);
if (m) {
const hex = m[1];
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
}
m = colorStr.match(/rgba?\\s*\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)/i);
if (m) {
const r = parseInt(m[1], 10);
const g = parseInt(m[2], 10);
const b = parseInt(m[3], 10);
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
}
return null;
};
const getThemeFromMeta = (doc) => {
const metas = Array.from((doc || document).querySelectorAll('meta[name="theme-color"]'));
if (!metas.length) return null;
const color = metas[metas.length - 1].content.trim();
const luma = parseColorLuma(color);
if (luma === null) return null;
return luma < 0.5 ? 'dark' : 'light';
};
const getParentDocumentSafe = () => {
try {
if (!window.parent || window.parent === window) return null;
const pDoc = window.parent.document;
void pDoc.title;
return pDoc;
} catch (err) { return null; }
};
const getThemeFromParentClass = () => {
try {
if (!window.parent || window.parent === window) return null;
const pDoc = window.parent.document;
const html = pDoc.documentElement;
const body = pDoc.body;
const htmlClass = html ? html.className : '';
const bodyClass = body ? body.className : '';
const htmlDataTheme = html ? html.getAttribute('data-theme') : '';
if (htmlDataTheme === 'dark' || bodyClass.includes('dark') || htmlClass.includes('dark')) return 'dark';
if (htmlDataTheme === 'light' || bodyClass.includes('light') || htmlClass.includes('light')) return 'light';
return null;
} catch (err) { return null; }
};
const setTheme = () => {
const parentDoc = getParentDocumentSafe();
const metaTheme = parentDoc ? getThemeFromMeta(parentDoc) : null;
const parentClassTheme = getThemeFromParentClass();
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const chosen = metaTheme || parentClassTheme || (prefersDark ? 'dark' : 'light');
document.documentElement.classList.toggle('theme-dark', chosen === 'dark');
};
setTheme();
if (window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', setTheme);
}
})();
</script>
</body>
</html>
"""
# =================================================================
# LLM Prompts - Deep Dive Thinking Chain
# =================================================================
SYSTEM_PROMPT = """
You are a Deep Dive Analyst. Your goal is to guide the user through a comprehensive thinking process, moving from surface understanding to deep strategic action.
## Thinking Structure (STRICT)
You MUST analyze the input across these four specific dimensions:
### 1. 🔍 The Context (What?)
Provide a high-level panoramic view. What is this content about? What is the core situation, background, or problem being addressed? (2-3 paragraphs)
### 2. 🧠 The Logic (Why?)
Deconstruct the underlying structure. How is the argument built? What is the reasoning, the hidden assumptions, or the mental models at play? (Bullet points)
### 3. 💎 The Insight (So What?)
Extract the non-obvious value. What are the "Aha!" moments? What are the implications, the blind spots, or the unique perspectives revealed? (Bullet points)
### 4. 🚀 The Path (Now What?)
Define the strategic direction. What are the specific, prioritized next steps? How can this knowledge be applied immediately? (Actionable steps)
## Rules
- Output in the user's specified language.
- Maintain a professional, analytical, yet inspiring tone.
- Focus on the *process* of understanding, not just the result.
- No greetings or meta-commentary.
"""
USER_PROMPT = """
Initiate a Deep Dive into the following content:
**User Context:**
- User: {user_name}
- Time: {current_date_time_str}
- Language: {user_language}
**Content to Analyze:**
```
{long_text_content}
```
Please execute the full thinking chain: Context → Logic → Insight → Path.
"""
# =================================================================
# Premium CSS Design - Deep Dive Theme
# =================================================================
CSS_TEMPLATE = """
.deep-dive {
font-family: 'Inter', -apple-system, system-ui, sans-serif;
color: var(--dd-text-secondary);
}
.dd-header {
background: var(--dd-header-gradient);
padding: 40px 32px;
color: white;
position: relative;
}
.dd-header-badge {
display: inline-block;
padding: 4px 12px;
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 100px;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
margin-bottom: 16px;
}
.dd-title {
font-size: 2rem;
font-weight: 800;
margin: 0 0 12px 0;
letter-spacing: -0.02em;
}
.dd-meta {
display: flex;
gap: 20px;
font-size: 0.85rem;
opacity: 0.7;
}
.dd-body {
padding: 32px;
display: flex;
flex-direction: column;
gap: 40px;
position: relative;
background: var(--dd-bg-primary);
}
/* The Thinking Line */
.dd-body::before {
content: '';
position: absolute;
left: 52px;
top: 40px;
bottom: 40px;
width: 2px;
background: var(--dd-border);
z-index: 0;
}
.dd-step {
position: relative;
z-index: 1;
display: flex;
gap: 24px;
}
.dd-step-icon {
flex-shrink: 0;
width: 40px;
height: 40px;
background: var(--dd-bg-primary);
border: 2px solid var(--dd-border);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.03);
transition: all 0.3s ease;
}
.dd-step:hover .dd-step-icon {
border-color: var(--dd-accent);
transform: scale(1.1);
}
.dd-step-content {
flex: 1;
}
.dd-step-label {
font-size: 0.75rem;
font-weight: 700;
color: var(--dd-accent);
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 4px;
}
.dd-step-title {
font-size: 1.25rem;
font-weight: 700;
color: var(--dd-text-primary);
margin: 0 0 16px 0;
}
.dd-text {
line-height: 1.7;
font-size: 1rem;
}
.dd-text p { margin-bottom: 16px; }
.dd-text p:last-child { margin-bottom: 0; }
.dd-list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 12px;
}
.dd-list-item {
background: var(--dd-bg-secondary);
padding: 16px 20px;
border-radius: 12px;
border-left: 4px solid var(--dd-border);
transition: all 0.2s ease;
}
.dd-list-item:hover {
background: var(--dd-bg-tertiary);
border-left-color: var(--dd-accent);
transform: translateX(4px);
}
.dd-list-item strong {
color: var(--dd-text-primary);
display: block;
margin-bottom: 4px;
}
.dd-path-item {
background: var(--dd-accent-soft);
border-left-color: var(--dd-accent);
}
.dd-footer {
padding: 24px 32px;
background: var(--dd-bg-secondary);
border-top: 1px solid var(--dd-border);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.8rem;
color: var(--dd-text-dim);
}
.dd-tag {
padding: 2px 8px;
background: var(--dd-bg-tertiary);
border-radius: 4px;
font-weight: 600;
}
.dd-text code,
.dd-list-item code {
background: var(--dd-code-bg);
color: var(--dd-text-primary);
padding: 2px 6px;
border-radius: 4px;
font-family: 'SF Mono', 'Consolas', 'Monaco', monospace;
font-size: 0.85em;
}
.dd-list-item em {
font-style: italic;
color: var(--dd-text-dim);
}
"""
CONTENT_TEMPLATE = """
<div class="deep-dive">
<div class="dd-header">
<div class="dd-header-badge">Thinking Process</div>
<h1 class="dd-title">Deep Dive Analysis</h1>
<div class="dd-meta">
<span>👤 {user_name}</span>
<span>📅 {current_date_time_str}</span>
<span>📊 {word_count} words</span>
</div>
</div>
<div class="dd-body">
<!-- Step 1: Context -->
<div class="dd-step">
<div class="dd-step-icon">🔍</div>
<div class="dd-step-content">
<div class="dd-step-label">Phase 01</div>
<h2 class="dd-step-title">The Context</h2>
<div class="dd-text">{context_html}</div>
</div>
</div>
<!-- Step 2: Logic -->
<div class="dd-step">
<div class="dd-step-icon">🧠</div>
<div class="dd-step-content">
<div class="dd-step-label">Phase 02</div>
<h2 class="dd-step-title">The Logic</h2>
<div class="dd-text">{logic_html}</div>
</div>
</div>
<!-- Step 3: Insight -->
<div class="dd-step">
<div class="dd-step-icon">💎</div>
<div class="dd-step-content">
<div class="dd-step-label">Phase 03</div>
<h2 class="dd-step-title">The Insight</h2>
<div class="dd-text">{insight_html}</div>
</div>
</div>
<!-- Step 4: Path -->
<div class="dd-step">
<div class="dd-step-icon">🚀</div>
<div class="dd-step-content">
<div class="dd-step-label">Phase 04</div>
<h2 class="dd-step-title">The Path</h2>
<div class="dd-text">{path_html}</div>
</div>
</div>
</div>
<div class="dd-footer">
<span>Deep Dive Engine v1.0</span>
<span><span class="dd-tag">AI-Powered</span></span>
</div>
</div>
"""
class Action:
class Valves(BaseModel):
SHOW_STATUS: bool = Field(
default=True,
description="Whether to show operation status updates.",
)
MODEL_ID: str = Field(
default="",
description="LLM Model ID for analysis. Empty = use current model.",
)
MIN_TEXT_LENGTH: int = Field(
default=200,
description="Minimum text length for deep dive (chars).",
)
CLEAR_PREVIOUS_HTML: bool = Field(
default=True,
description="Whether to clear previous plugin results.",
)
MESSAGE_COUNT: int = Field(
default=1,
description="Number of recent messages to analyze.",
)
def __init__(self):
self.valves = self.Valves()
def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
"""Safely extracts user context information."""
if isinstance(__user__, (list, tuple)):
user_data = __user__[0] if __user__ else {}
elif isinstance(__user__, dict):
user_data = __user__
else:
user_data = {}
return {
"user_id": user_data.get("id", "unknown_user"),
"user_name": user_data.get("name", "User"),
"user_language": user_data.get("language", "en-US"),
}
def _process_llm_output(self, llm_output: str) -> Dict[str, str]:
"""Parse LLM output and convert to styled HTML."""
# Extract sections using flexible regex
context_match = re.search(
r"###\s*1\.\s*🔍?\s*The Context\s*\((.*?)\)\s*\n(.*?)(?=\n###|$)",
llm_output,
re.DOTALL | re.IGNORECASE,
)
logic_match = re.search(
r"###\s*2\.\s*🧠?\s*The Logic\s*\((.*?)\)\s*\n(.*?)(?=\n###|$)",
llm_output,
re.DOTALL | re.IGNORECASE,
)
insight_match = re.search(
r"###\s*3\.\s*💎?\s*The Insight\s*\((.*?)\)\s*\n(.*?)(?=\n###|$)",
llm_output,
re.DOTALL | re.IGNORECASE,
)
path_match = re.search(
r"###\s*4\.\s*🚀?\s*The Path\s*\((.*?)\)\s*\n(.*?)(?=\n###|$)",
llm_output,
re.DOTALL | re.IGNORECASE,
)
# Fallback if numbering is different
if not context_match:
context_match = re.search(
r"###\s*🔍?\s*The Context.*?\n(.*?)(?=\n###|$)",
llm_output,
re.DOTALL | re.IGNORECASE,
)
if not logic_match:
logic_match = re.search(
r"###\s*🧠?\s*The Logic.*?\n(.*?)(?=\n###|$)",
llm_output,
re.DOTALL | re.IGNORECASE,
)
if not insight_match:
insight_match = re.search(
r"###\s*💎?\s*The Insight.*?\n(.*?)(?=\n###|$)",
llm_output,
re.DOTALL | re.IGNORECASE,
)
if not path_match:
path_match = re.search(
r"###\s*🚀?\s*The Path.*?\n(.*?)(?=\n###|$)",
llm_output,
re.DOTALL | re.IGNORECASE,
)
context_md = (
context_match.group(1 if context_match.lastindex == 1 else 2).strip()
if context_match
else ""
)
logic_md = (
logic_match.group(1 if logic_match.lastindex == 1 else 2).strip()
if logic_match
else ""
)
insight_md = (
insight_match.group(1 if insight_match.lastindex == 1 else 2).strip()
if insight_match
else ""
)
path_md = (
path_match.group(1 if path_match.lastindex == 1 else 2).strip()
if path_match
else ""
)
if not any([context_md, logic_md, insight_md, path_md]):
context_md = llm_output.strip()
logger.warning("LLM output did not follow format. Using as context.")
md_extensions = ["nl2br"]
context_html = (
markdown.markdown(context_md, extensions=md_extensions)
if context_md
else '<p class="dd-no-content">No context extracted.</p>'
)
logic_html = (
self._process_list_items(logic_md, "logic")
if logic_md
else '<p class="dd-no-content">No logic deconstructed.</p>'
)
insight_html = (
self._process_list_items(insight_md, "insight")
if insight_md
else '<p class="dd-no-content">No insights found.</p>'
)
path_html = (
self._process_list_items(path_md, "path")
if path_md
else '<p class="dd-no-content">No path defined.</p>'
)
return {
"context_html": context_html,
"logic_html": logic_html,
"insight_html": insight_html,
"path_html": path_html,
}
def _process_list_items(self, md_content: str, section_type: str) -> str:
"""Convert markdown list to styled HTML cards with full markdown support."""
lines = md_content.strip().split("\n")
items = []
current_paragraph = []
for line in lines:
line = line.strip()
# Check for list item (bullet or numbered)
bullet_match = re.match(r"^[-*]\s+(.+)$", line)
numbered_match = re.match(r"^\d+\.\s+(.+)$", line)
if bullet_match or numbered_match:
# Flush any accumulated paragraph
if current_paragraph:
para_text = " ".join(current_paragraph)
para_html = self._convert_inline_markdown(para_text)
items.append(f"<p>{para_html}</p>")
current_paragraph = []
# Extract the list item content
text = (
bullet_match.group(1) if bullet_match else numbered_match.group(1)
)
# Handle bold title pattern: **Title:** Description or **Title**: Description
title_match = re.match(r"\*\*(.+?)\*\*[:\s]*(.*)$", text)
if title_match:
title = self._convert_inline_markdown(title_match.group(1))
desc = self._convert_inline_markdown(title_match.group(2).strip())
path_class = "dd-path-item" if section_type == "path" else ""
item_html = f'<div class="dd-list-item {path_class}"><strong>{title}</strong>{desc}</div>'
else:
text_html = self._convert_inline_markdown(text)
path_class = "dd-path-item" if section_type == "path" else ""
item_html = (
f'<div class="dd-list-item {path_class}">{text_html}</div>'
)
items.append(item_html)
elif line and not line.startswith("#"):
# Accumulate paragraph text
current_paragraph.append(line)
elif not line and current_paragraph:
# Empty line ends paragraph
para_text = " ".join(current_paragraph)
para_html = self._convert_inline_markdown(para_text)
items.append(f"<p>{para_html}</p>")
current_paragraph = []
# Flush remaining paragraph
if current_paragraph:
para_text = " ".join(current_paragraph)
para_html = self._convert_inline_markdown(para_text)
items.append(f"<p>{para_html}</p>")
if items:
return f'<div class="dd-list">{" ".join(items)}</div>'
return f'<p class="dd-no-content">No items found.</p>'
def _convert_inline_markdown(self, text: str) -> str:
"""Convert inline markdown (bold, italic, code) to HTML."""
# Convert inline code: `code` -> <code>code</code>
text = re.sub(r"`([^`]+)`", r"<code>\1</code>", text)
# Convert bold: **text** -> <strong>text</strong>
text = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", text)
# Convert italic: *text* -> <em>text</em> (but not inside **)
text = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"<em>\1</em>", text)
return text
async def _emit_status(
self,
emitter: Optional[Callable[[Any], Awaitable[None]]],
description: str,
done: bool = False,
):
"""Emits a status update event."""
if self.valves.SHOW_STATUS and emitter:
await emitter(
{"type": "status", "data": {"description": description, "done": done}}
)
async def _emit_notification(
self,
emitter: Optional[Callable[[Any], Awaitable[None]]],
content: str,
ntype: str = "info",
):
"""Emits a notification event."""
if emitter:
await emitter(
{"type": "notification", "data": {"type": ntype, "content": content}}
)
def _remove_existing_html(self, content: str) -> str:
"""Removes existing plugin-generated HTML."""
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
return re.sub(pattern, "", content).strip()
def _extract_text_content(self, content) -> str:
"""Extract text from message content."""
if isinstance(content, str):
return content
elif isinstance(content, list):
text_parts = []
for item in content:
if isinstance(item, dict) and item.get("type") == "text":
text_parts.append(item.get("text", ""))
elif isinstance(item, str):
text_parts.append(item)
return "\n".join(text_parts)
return str(content) if content else ""
def _merge_html(
self,
existing_html: str,
new_content: str,
new_styles: str = "",
user_language: str = "en-US",
) -> str:
"""Merges new content into HTML container."""
if "<!-- OPENWEBUI_PLUGIN_OUTPUT -->" in existing_html:
base_html = re.sub(r"^```html\s*", "", existing_html)
base_html = re.sub(r"\s*```$", "", base_html)
else:
base_html = HTML_WRAPPER_TEMPLATE.replace("{user_language}", user_language)
wrapped = f'<div class="plugin-item">\n{new_content}\n</div>'
if new_styles:
base_html = base_html.replace(
"/* STYLES_INSERTION_POINT */",
f"{new_styles}\n/* STYLES_INSERTION_POINT */",
)
base_html = base_html.replace(
"<!-- CONTENT_INSERTION_POINT -->",
f"{wrapped}\n<!-- CONTENT_INSERTION_POINT -->",
)
return base_html.strip()
def _build_content_html(self, context: dict) -> str:
"""Build content HTML."""
html = CONTENT_TEMPLATE
for key, value in context.items():
html = html.replace(f"{{{key}}}", str(value))
return html
async def action(
self,
body: dict,
__user__: Optional[Dict[str, Any]] = None,
__event_emitter__: Optional[Callable[[Any], Awaitable[None]]] = None,
__request__: Optional[Request] = None,
) -> Optional[dict]:
logger.info("Action: Deep Dive v1.0.0 started")
user_ctx = self._get_user_context(__user__)
user_id = user_ctx["user_id"]
user_name = user_ctx["user_name"]
user_language = user_ctx["user_language"]
now = datetime.now()
current_date_time_str = now.strftime("%b %d, %Y %H:%M")
original_content = ""
try:
messages = body.get("messages", [])
if not messages:
raise ValueError("No messages found.")
message_count = min(self.valves.MESSAGE_COUNT, len(messages))
recent_messages = messages[-message_count:]
aggregated_parts = []
for msg in recent_messages:
text = self._extract_text_content(msg.get("content"))
if text:
aggregated_parts.append(text)
if not aggregated_parts:
raise ValueError("No text content found.")
original_content = "\n\n---\n\n".join(aggregated_parts)
word_count = len(original_content.split())
if len(original_content) < self.valves.MIN_TEXT_LENGTH:
msg = f"Content too brief ({len(original_content)} chars). Deep Dive requires at least {self.valves.MIN_TEXT_LENGTH} chars for meaningful analysis."
await self._emit_notification(__event_emitter__, msg, "warning")
return {"messages": [{"role": "assistant", "content": f"⚠️ {msg}"}]}
await self._emit_notification(
__event_emitter__, "🌊 Initiating Deep Dive thinking process...", "info"
)
await self._emit_status(
__event_emitter__, "🌊 Deep Dive: Analyzing Context & Logic...", False
)
prompt = USER_PROMPT.format(
user_name=user_name,
current_date_time_str=current_date_time_str,
user_language=user_language,
long_text_content=original_content,
)
model = self.valves.MODEL_ID or body.get("model")
payload = {
"model": model,
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": prompt},
],
"stream": False,
}
user_obj = Users.get_user_by_id(user_id)
if not user_obj:
raise ValueError(f"User not found: {user_id}")
response = await generate_chat_completion(__request__, payload, user_obj)
llm_output = response["choices"][0]["message"]["content"]
processed = self._process_llm_output(llm_output)
context = {
"user_name": user_name,
"current_date_time_str": current_date_time_str,
"word_count": word_count,
**processed,
}
content_html = self._build_content_html(context)
# Handle existing HTML
existing = ""
match = re.search(
r"```html\s*(<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?)```",
original_content,
)
if match:
existing = match.group(1)
if self.valves.CLEAR_PREVIOUS_HTML or not existing:
original_content = self._remove_existing_html(original_content)
final_html = self._merge_html(
"", content_html, CSS_TEMPLATE, user_language
)
else:
original_content = self._remove_existing_html(original_content)
final_html = self._merge_html(
existing, content_html, CSS_TEMPLATE, user_language
)
body["messages"][-1][
"content"
] = f"{original_content}\n\n```html\n{final_html}\n```"
await self._emit_status(__event_emitter__, "🌊 Deep Dive complete!", True)
await self._emit_notification(
__event_emitter__,
f"🌊 Deep Dive complete, {user_name}! Thinking chain generated.",
"success",
)
except Exception as e:
logger.error(f"Deep Dive Error: {e}", exc_info=True)
body["messages"][-1][
"content"
] = f"{original_content}\n\n❌ **Error:** {str(e)}"
await self._emit_status(__event_emitter__, "Deep Dive failed.", True)
await self._emit_notification(
__event_emitter__, f"Error: {str(e)}", "error"
)
return body

Binary file not shown.

After

Width:  |  Height:  |  Size: 997 KiB

View File

@@ -0,0 +1,876 @@
"""
title: 精读
author: Fu-Jie
author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui
version: 1.0.0
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xMiA3djE0Ii8+PHBhdGggZD0iTTMgMThhMSAxIDAgMCAxLTEtMVY0YTEgMSAwIDAgMSAxLTFoNWE0IDQgMCAwIDEgNCA0IDQgNCAwIDAgMSA0LTRoNWExIDEgMCAwIDEgMSAxdjEzYTEgMSAwIDAgMS0xIDFoLTZhMyAzIDAgMCAwLTMgMyAzIDMgMCAwIDAtMy0zeiIvPjxwYXRoIGQ9Ik02IDEyaDIiLz48cGF0aCBkPSJNMTYgMTJoMiIvPjwvc3ZnPg==
requirements: markdown
description: 全方位的思维透镜 —— 从背景全景到逻辑脉络,从深度洞察到行动路径。
"""
# Standard library imports
import re
import logging
from typing import Optional, Dict, Any, Callable, Awaitable
from datetime import datetime
# Third-party imports
from pydantic import BaseModel, Field
from fastapi import Request
import markdown
# OpenWebUI imports
from open_webui.utils.chat import generate_chat_completion
from open_webui.models.users import Users
# Logging setup
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
# =================================================================
# HTML 模板 - 过程导向设计,支持主题自适应
# =================================================================
HTML_WRAPPER_TEMPLATE = """
<!-- OPENWEBUI_PLUGIN_OUTPUT -->
<!DOCTYPE html>
<html lang="{user_language}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
:root {
--dd-bg-primary: #ffffff;
--dd-bg-secondary: #f8fafc;
--dd-bg-tertiary: #f1f5f9;
--dd-text-primary: #0f172a;
--dd-text-secondary: #334155;
--dd-text-dim: #64748b;
--dd-border: #e2e8f0;
--dd-accent: #3b82f6;
--dd-accent-soft: #eff6ff;
--dd-header-gradient: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
--dd-shadow: 0 10px 40px rgba(0,0,0,0.06);
--dd-code-bg: #f1f5f9;
}
.theme-dark {
--dd-bg-primary: #1e293b;
--dd-bg-secondary: #0f172a;
--dd-bg-tertiary: #334155;
--dd-text-primary: #f1f5f9;
--dd-text-secondary: #e2e8f0;
--dd-text-dim: #94a3b8;
--dd-border: #475569;
--dd-accent: #60a5fa;
--dd-accent-soft: rgba(59, 130, 246, 0.15);
--dd-header-gradient: linear-gradient(135deg, #0f172a 0%, #1e1e2e 100%);
--dd-shadow: 0 10px 40px rgba(0,0,0,0.3);
--dd-code-bg: #334155;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: 0;
padding: 10px;
background-color: transparent;
}
#main-container {
display: flex;
flex-direction: column;
gap: 24px;
width: 100%;
max-width: 900px;
margin: 0 auto;
}
.plugin-item {
background: var(--dd-bg-primary);
border-radius: 24px;
box-shadow: var(--dd-shadow);
overflow: hidden;
border: 1px solid var(--dd-border);
}
/* STYLES_INSERTION_POINT */
</style>
</head>
<body>
<div id="main-container">
<!-- CONTENT_INSERTION_POINT -->
</div>
<!-- SCRIPTS_INSERTION_POINT -->
<script>
(function() {
const parseColorLuma = (colorStr) => {
if (!colorStr) return null;
let m = colorStr.match(/^#?([0-9a-f]{6})$/i);
if (m) {
const hex = m[1];
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
}
m = colorStr.match(/rgba?\\s*\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)/i);
if (m) {
const r = parseInt(m[1], 10);
const g = parseInt(m[2], 10);
const b = parseInt(m[3], 10);
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
}
return null;
};
const getThemeFromMeta = (doc) => {
const metas = Array.from((doc || document).querySelectorAll('meta[name="theme-color"]'));
if (!metas.length) return null;
const color = metas[metas.length - 1].content.trim();
const luma = parseColorLuma(color);
if (luma === null) return null;
return luma < 0.5 ? 'dark' : 'light';
};
const getParentDocumentSafe = () => {
try {
if (!window.parent || window.parent === window) return null;
const pDoc = window.parent.document;
void pDoc.title;
return pDoc;
} catch (err) { return null; }
};
const getThemeFromParentClass = () => {
try {
if (!window.parent || window.parent === window) return null;
const pDoc = window.parent.document;
const html = pDoc.documentElement;
const body = pDoc.body;
const htmlClass = html ? html.className : '';
const bodyClass = body ? body.className : '';
const htmlDataTheme = html ? html.getAttribute('data-theme') : '';
if (htmlDataTheme === 'dark' || bodyClass.includes('dark') || htmlClass.includes('dark')) return 'dark';
if (htmlDataTheme === 'light' || bodyClass.includes('light') || htmlClass.includes('light')) return 'light';
return null;
} catch (err) { return null; }
};
const setTheme = () => {
const parentDoc = getParentDocumentSafe();
const metaTheme = parentDoc ? getThemeFromMeta(parentDoc) : null;
const parentClassTheme = getThemeFromParentClass();
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const chosen = metaTheme || parentClassTheme || (prefersDark ? 'dark' : 'light');
document.documentElement.classList.toggle('theme-dark', chosen === 'dark');
};
setTheme();
if (window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', setTheme);
}
})();
</script>
</body>
</html>
"""
# =================================================================
# LLM 提示词 - 深度下潜思维链
# =================================================================
SYSTEM_PROMPT = """
你是一位“深度下潜 (Deep Dive)”分析专家。你的目标是引导用户完成一个全面的思维过程,从表面理解深入到战略行动。
## 思维结构 (严格遵守)
你必须从以下四个维度剖析输入内容:
### 1. 🔍 The Context (全景)
提供一个高层级的全景视图。内容是关于什么的核心情境、背景或正在解决的问题是什么2-3 段话)
### 2. 🧠 The Logic (脉络)
解构底层结构。论点是如何构建的?其中的推理逻辑、隐藏假设或起作用的思维模型是什么?(列表形式)
### 3. 💎 The Insight (洞察)
提取非显性的价值。有哪些“原来如此”的时刻?揭示了哪些深层含义、盲点或独特视角?(列表形式)
### 4. 🚀 The Path (路径)
定义战略方向。具体的、按优先级排列的下一步行动是什么?如何立即应用这些知识?(可执行步骤)
## 规则
- 使用用户指定的语言输出。
- 保持专业、分析性且富有启发性的语调。
- 聚焦于“理解的过程”,而不仅仅是结果。
- 不要包含寒暄或元对话。
"""
USER_PROMPT = """
对以下内容发起“深度下潜”:
**用户上下文:**
- 用户:{user_name}
- 时间:{current_date_time_str}
- 语言:{user_language}
**待分析内容:**
```
{long_text_content}
```
请执行完整的思维链:全景 (Context) → 脉络 (Logic) → 洞察 (Insight) → 路径 (Path)。
"""
# =================================================================
# 现代 CSS 设计 - 深度下潜主题
# =================================================================
CSS_TEMPLATE = """
.deep-dive {
font-family: 'Inter', -apple-system, system-ui, sans-serif;
color: var(--dd-text-secondary);
}
.dd-header {
background: var(--dd-header-gradient);
padding: 40px 32px;
color: white;
position: relative;
}
.dd-header-badge {
display: inline-block;
padding: 4px 12px;
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 100px;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
margin-bottom: 16px;
}
.dd-title {
font-size: 2rem;
font-weight: 800;
margin: 0 0 12px 0;
letter-spacing: -0.02em;
}
.dd-meta {
display: flex;
gap: 20px;
font-size: 0.85rem;
opacity: 0.7;
}
.dd-body {
padding: 32px;
display: flex;
flex-direction: column;
gap: 40px;
position: relative;
background: var(--dd-bg-primary);
}
/* 思维导火索 */
.dd-body::before {
content: '';
position: absolute;
left: 52px;
top: 40px;
bottom: 40px;
width: 2px;
background: var(--dd-border);
z-index: 0;
}
.dd-step {
position: relative;
z-index: 1;
display: flex;
gap: 24px;
}
.dd-step-icon {
flex-shrink: 0;
width: 40px;
height: 40px;
background: var(--dd-bg-primary);
border: 2px solid var(--dd-border);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.03);
transition: all 0.3s ease;
}
.dd-step:hover .dd-step-icon {
border-color: var(--dd-accent);
transform: scale(1.1);
}
.dd-step-content {
flex: 1;
}
.dd-step-label {
font-size: 0.75rem;
font-weight: 700;
color: var(--dd-accent);
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 4px;
}
.dd-step-title {
font-size: 1.25rem;
font-weight: 700;
color: var(--dd-text-primary);
margin: 0 0 16px 0;
}
.dd-text {
line-height: 1.7;
font-size: 1rem;
}
.dd-text p { margin-bottom: 16px; }
.dd-text p:last-child { margin-bottom: 0; }
.dd-list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 12px;
}
.dd-list-item {
background: var(--dd-bg-secondary);
padding: 16px 20px;
border-radius: 12px;
border-left: 4px solid var(--dd-border);
transition: all 0.2s ease;
}
.dd-list-item:hover {
background: var(--dd-bg-tertiary);
border-left-color: var(--dd-accent);
transform: translateX(4px);
}
.dd-list-item strong {
color: var(--dd-text-primary);
display: block;
margin-bottom: 4px;
}
.dd-path-item {
background: var(--dd-accent-soft);
border-left-color: var(--dd-accent);
}
.dd-footer {
padding: 24px 32px;
background: var(--dd-bg-secondary);
border-top: 1px solid var(--dd-border);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.8rem;
color: var(--dd-text-dim);
}
.dd-tag {
padding: 2px 8px;
background: var(--dd-bg-tertiary);
border-radius: 4px;
font-weight: 600;
}
.dd-text code,
.dd-list-item code {
background: var(--dd-code-bg);
color: var(--dd-text-primary);
padding: 2px 6px;
border-radius: 4px;
font-family: 'SF Mono', 'Consolas', 'Monaco', monospace;
font-size: 0.85em;
}
.dd-list-item em {
font-style: italic;
color: var(--dd-text-dim);
}
"""
CONTENT_TEMPLATE = """
<div class="deep-dive">
<div class="dd-header">
<div class="dd-header-badge">思维过程</div>
<h1 class="dd-title">精读分析报告</h1>
<div class="dd-meta">
<span>👤 {user_name}</span>
<span>📅 {current_date_time_str}</span>
<span>📊 {word_count} 字</span>
</div>
</div>
<div class="dd-body">
<!-- 第一步:全景 -->
<div class="dd-step">
<div class="dd-step-icon">🔍</div>
<div class="dd-step-content">
<div class="dd-step-label">Phase 01</div>
<h2 class="dd-step-title">全景 (The Context)</h2>
<div class="dd-text">{context_html}</div>
</div>
</div>
<!-- 第二步:脉络 -->
<div class="dd-step">
<div class="dd-step-icon">🧠</div>
<div class="dd-step-content">
<div class="dd-step-label">Phase 02</div>
<h2 class="dd-step-title">脉络 (The Logic)</h2>
<div class="dd-text">{logic_html}</div>
</div>
</div>
<!-- 第三步:洞察 -->
<div class="dd-step">
<div class="dd-step-icon">💎</div>
<div class="dd-step-content">
<div class="dd-step-label">Phase 03</div>
<h2 class="dd-step-title">洞察 (The Insight)</h2>
<div class="dd-text">{insight_html}</div>
</div>
</div>
<!-- 第四步:路径 -->
<div class="dd-step">
<div class="dd-step-icon">🚀</div>
<div class="dd-step-content">
<div class="dd-step-label">Phase 04</div>
<h2 class="dd-step-title">路径 (The Path)</h2>
<div class="dd-text">{path_html}</div>
</div>
</div>
</div>
<div class="dd-footer">
<span>Deep Dive Engine v1.0</span>
<span><span class="dd-tag">AI 驱动分析</span></span>
</div>
</div>
"""
class Action:
class Valves(BaseModel):
SHOW_STATUS: bool = Field(
default=True,
description="是否显示操作状态更新。",
)
MODEL_ID: str = Field(
default="",
description="用于分析的 LLM 模型 ID。留空则使用当前模型。",
)
MIN_TEXT_LENGTH: int = Field(
default=200,
description="深度下潜所需的最小文本长度(字符)。",
)
CLEAR_PREVIOUS_HTML: bool = Field(
default=True,
description="是否清除之前的插件结果。",
)
MESSAGE_COUNT: int = Field(
default=1,
description="要分析的最近消息数量。",
)
def __init__(self):
self.valves = self.Valves()
def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
"""安全提取用户上下文信息。"""
if isinstance(__user__, (list, tuple)):
user_data = __user__[0] if __user__ else {}
elif isinstance(__user__, dict):
user_data = __user__
else:
user_data = {}
return {
"user_id": user_data.get("id", "unknown_user"),
"user_name": user_data.get("name", "用户"),
"user_language": user_data.get("language", "zh-CN"),
}
def _process_llm_output(self, llm_output: str) -> Dict[str, str]:
"""解析 LLM 输出并转换为样式化 HTML。"""
# 使用灵活的正则提取各部分
context_match = re.search(
r"###\s*1\.\s*🔍?\s*(?:全景|The Context)\s*(?:\((.*?)\))?\s*\n(.*?)(?=\n###|$)",
llm_output,
re.DOTALL | re.IGNORECASE,
)
logic_match = re.search(
r"###\s*2\.\s*🧠?\s*(?:脉络|The Logic)\s*(?:\((.*?)\))?\s*\n(.*?)(?=\n###|$)",
llm_output,
re.DOTALL | re.IGNORECASE,
)
insight_match = re.search(
r"###\s*3\.\s*💎?\s*(?:洞察|The Insight)\s*(?:\((.*?)\))?\s*\n(.*?)(?=\n###|$)",
llm_output,
re.DOTALL | re.IGNORECASE,
)
path_match = re.search(
r"###\s*4\.\s*🚀?\s*(?:路径|The Path)\s*(?:\((.*?)\))?\s*\n(.*?)(?=\n###|$)",
llm_output,
re.DOTALL | re.IGNORECASE,
)
# 兜底正则
if not context_match:
context_match = re.search(
r"###\s*🔍?\s*(?:全景|The Context).*?\n(.*?)(?=\n###|$)",
llm_output,
re.DOTALL | re.IGNORECASE,
)
if not logic_match:
logic_match = re.search(
r"###\s*🧠?\s*(?:脉络|The Logic).*?\n(.*?)(?=\n###|$)",
llm_output,
re.DOTALL | re.IGNORECASE,
)
if not insight_match:
insight_match = re.search(
r"###\s*💎?\s*(?:洞察|The Insight).*?\n(.*?)(?=\n###|$)",
llm_output,
re.DOTALL | re.IGNORECASE,
)
if not path_match:
path_match = re.search(
r"###\s*🚀?\s*(?:路径|The Path).*?\n(.*?)(?=\n###|$)",
llm_output,
re.DOTALL | re.IGNORECASE,
)
context_md = (
context_match.group(context_match.lastindex).strip()
if context_match
else ""
)
logic_md = (
logic_match.group(logic_match.lastindex).strip() if logic_match else ""
)
insight_md = (
insight_match.group(insight_match.lastindex).strip()
if insight_match
else ""
)
path_md = path_match.group(path_match.lastindex).strip() if path_match else ""
if not any([context_md, logic_md, insight_md, path_md]):
context_md = llm_output.strip()
logger.warning("LLM 输出未遵循格式,将作为全景处理。")
md_extensions = ["nl2br"]
context_html = (
markdown.markdown(context_md, extensions=md_extensions)
if context_md
else '<p class="dd-no-content">未能提取全景信息。</p>'
)
logic_html = (
self._process_list_items(logic_md, "logic")
if logic_md
else '<p class="dd-no-content">未能解构脉络。</p>'
)
insight_html = (
self._process_list_items(insight_md, "insight")
if insight_md
else '<p class="dd-no-content">未能发现洞察。</p>'
)
path_html = (
self._process_list_items(path_md, "path")
if path_md
else '<p class="dd-no-content">未能定义路径。</p>'
)
return {
"context_html": context_html,
"logic_html": logic_html,
"insight_html": insight_html,
"path_html": path_html,
}
def _process_list_items(self, md_content: str, section_type: str) -> str:
"""将 markdown 列表转换为样式化卡片,支持完整的 markdown 格式。"""
lines = md_content.strip().split("\n")
items = []
current_paragraph = []
for line in lines:
line = line.strip()
# 检查列表项(无序或有序)
bullet_match = re.match(r"^[-*]\s+(.+)$", line)
numbered_match = re.match(r"^\d+\.\s+(.+)$", line)
if bullet_match or numbered_match:
# 清空累积的段落
if current_paragraph:
para_text = " ".join(current_paragraph)
para_html = self._convert_inline_markdown(para_text)
items.append(f"<p>{para_html}</p>")
current_paragraph = []
# 提取列表项内容
text = (
bullet_match.group(1) if bullet_match else numbered_match.group(1)
)
# 处理粗体标题模式:**标题:** 描述 或 **标题**: 描述
title_match = re.match(r"\*\*(.+?)\*\*[:\s]*(.*)$", text)
if title_match:
title = self._convert_inline_markdown(title_match.group(1))
desc = self._convert_inline_markdown(title_match.group(2).strip())
path_class = "dd-path-item" if section_type == "path" else ""
item_html = f'<div class="dd-list-item {path_class}"><strong>{title}</strong>{desc}</div>'
else:
text_html = self._convert_inline_markdown(text)
path_class = "dd-path-item" if section_type == "path" else ""
item_html = (
f'<div class="dd-list-item {path_class}">{text_html}</div>'
)
items.append(item_html)
elif line and not line.startswith("#"):
# 累积段落文本
current_paragraph.append(line)
elif not line and current_paragraph:
# 空行结束段落
para_text = " ".join(current_paragraph)
para_html = self._convert_inline_markdown(para_text)
items.append(f"<p>{para_html}</p>")
current_paragraph = []
# 清空剩余段落
if current_paragraph:
para_text = " ".join(current_paragraph)
para_html = self._convert_inline_markdown(para_text)
items.append(f"<p>{para_html}</p>")
if items:
return f'<div class="dd-list">{" ".join(items)}</div>'
return f'<p class="dd-no-content">未找到条目。</p>'
def _convert_inline_markdown(self, text: str) -> str:
"""将行内 markdown粗体、斜体、代码转换为 HTML。"""
# 转换行内代码:`code` -> <code>code</code>
text = re.sub(r"`([^`]+)`", r"<code>\1</code>", text)
# 转换粗体:**text** -> <strong>text</strong>
text = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", text)
# 转换斜体:*text* -> <em>text</em>(但不在 ** 内部)
text = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"<em>\1</em>", text)
return text
async def _emit_status(
self,
emitter: Optional[Callable[[Any], Awaitable[None]]],
description: str,
done: bool = False,
):
"""发送状态更新事件。"""
if self.valves.SHOW_STATUS and emitter:
await emitter(
{"type": "status", "data": {"description": description, "done": done}}
)
async def _emit_notification(
self,
emitter: Optional[Callable[[Any], Awaitable[None]]],
content: str,
ntype: str = "info",
):
"""发送通知事件。"""
if emitter:
await emitter(
{"type": "notification", "data": {"type": ntype, "content": content}}
)
def _remove_existing_html(self, content: str) -> str:
"""移除已有的插件生成的 HTML。"""
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
return re.sub(pattern, "", content).strip()
def _extract_text_content(self, content) -> str:
"""从消息内容中提取文本。"""
if isinstance(content, str):
return content
elif isinstance(content, list):
text_parts = []
for item in content:
if isinstance(item, dict) and item.get("type") == "text":
text_parts.append(item.get("text", ""))
elif isinstance(item, str):
text_parts.append(item)
return "\n".join(text_parts)
return str(content) if content else ""
def _merge_html(
self,
existing_html: str,
new_content: str,
new_styles: str = "",
user_language: str = "zh-CN",
) -> str:
"""合并新内容到 HTML 容器。"""
if "<!-- OPENWEBUI_PLUGIN_OUTPUT -->" in existing_html:
base_html = re.sub(r"^```html\s*", "", existing_html)
base_html = re.sub(r"\s*```$", "", base_html)
else:
base_html = HTML_WRAPPER_TEMPLATE.replace("{user_language}", user_language)
wrapped = f'<div class="plugin-item">\n{new_content}\n</div>'
if new_styles:
base_html = base_html.replace(
"/* STYLES_INSERTION_POINT */",
f"{new_styles}\n/* STYLES_INSERTION_POINT */",
)
base_html = base_html.replace(
"<!-- CONTENT_INSERTION_POINT -->",
f"{wrapped}\n<!-- CONTENT_INSERTION_POINT -->",
)
return base_html.strip()
def _build_content_html(self, context: dict) -> str:
"""构建内容 HTML。"""
html = CONTENT_TEMPLATE
for key, value in context.items():
html = html.replace(f"{{{key}}}", str(value))
return html
async def action(
self,
body: dict,
__user__: Optional[Dict[str, Any]] = None,
__event_emitter__: Optional[Callable[[Any], Awaitable[None]]] = None,
__request__: Optional[Request] = None,
) -> Optional[dict]:
logger.info("Action: 精读 v1.0.0 启动")
user_ctx = self._get_user_context(__user__)
user_id = user_ctx["user_id"]
user_name = user_ctx["user_name"]
user_language = user_ctx["user_language"]
now = datetime.now()
current_date_time_str = now.strftime("%Y年%m月%d%H:%M")
original_content = ""
try:
messages = body.get("messages", [])
if not messages:
raise ValueError("未找到消息内容。")
message_count = min(self.valves.MESSAGE_COUNT, len(messages))
recent_messages = messages[-message_count:]
aggregated_parts = []
for msg in recent_messages:
text = self._extract_text_content(msg.get("content"))
if text:
aggregated_parts.append(text)
if not aggregated_parts:
raise ValueError("未找到文本内容。")
original_content = "\n\n---\n\n".join(aggregated_parts)
word_count = len(original_content)
if len(original_content) < self.valves.MIN_TEXT_LENGTH:
msg = f"内容过短({len(original_content)} 字符)。精读至少需要 {self.valves.MIN_TEXT_LENGTH} 字符才能进行有意义的分析。"
await self._emit_notification(__event_emitter__, msg, "warning")
return {"messages": [{"role": "assistant", "content": f"⚠️ {msg}"}]}
await self._emit_notification(
__event_emitter__, "📖 正在发起精读分析...", "info"
)
await self._emit_status(
__event_emitter__, "📖 精读:正在分析全景与脉络...", False
)
prompt = USER_PROMPT.format(
user_name=user_name,
current_date_time_str=current_date_time_str,
user_language=user_language,
long_text_content=original_content,
)
model = self.valves.MODEL_ID or body.get("model")
payload = {
"model": model,
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": prompt},
],
"stream": False,
}
user_obj = Users.get_user_by_id(user_id)
if not user_obj:
raise ValueError(f"未找到用户:{user_id}")
response = await generate_chat_completion(__request__, payload, user_obj)
llm_output = response["choices"][0]["message"]["content"]
processed = self._process_llm_output(llm_output)
context = {
"user_name": user_name,
"current_date_time_str": current_date_time_str,
"word_count": word_count,
**processed,
}
content_html = self._build_content_html(context)
# 处理已有 HTML
existing = ""
match = re.search(
r"```html\s*(<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?)```",
original_content,
)
if match:
existing = match.group(1)
if self.valves.CLEAR_PREVIOUS_HTML or not existing:
original_content = self._remove_existing_html(original_content)
final_html = self._merge_html(
"", content_html, CSS_TEMPLATE, user_language
)
else:
original_content = self._remove_existing_html(original_content)
final_html = self._merge_html(
existing, content_html, CSS_TEMPLATE, user_language
)
body["messages"][-1][
"content"
] = f"{original_content}\n\n```html\n{final_html}\n```"
await self._emit_status(__event_emitter__, "📖 精读完成!", True)
await self._emit_notification(
__event_emitter__,
f"📖 精读完成,{user_name}!思维链已生成。",
"success",
)
except Exception as e:
logger.error(f"Deep Dive 错误:{e}", exc_info=True)
body["messages"][-1][
"content"
] = f"{original_content}\n\n❌ **错误:** {str(e)}"
await self._emit_status(__event_emitter__, "精读失败。", True)
await self._emit_notification(__event_emitter__, f"错误:{str(e)}", "error")
return body

View File

@@ -1,130 +1,88 @@
# Export to Word # 📝 Export to Word (Enhanced)
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.4.3 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
Export conversation to Word (.docx) with **syntax highlighting**, **native math equations**, **Mermaid diagrams**, **citations**, and **enhanced table formatting**. Export conversation to Word (.docx) with **syntax highlighting**, **native math equations**, **Mermaid diagrams**, **citations**, and **enhanced table formatting**.
## Features ## 🔥 What's New in v0.4.3
- **One-Click Export**: Adds an "Export to Word" action button to the chat. - **S3 Object Storage Support**: Direct access to images stored in S3/MinIO via boto3, bypassing API layer for faster exports.
- **Markdown Conversion**: Converts Markdown syntax to Word formatting (headings, bold, italic, code, tables, lists). - 🔧 **Multi-level File Fallback**: 6-level fallback mechanism for file retrieval (DB → S3 → Local → URL → API → Attributes).
- **Syntax Highlighting**: Code blocks are highlighted with Pygments (supports 500+ languages). - 🛡️ **Improved Error Handling**: Better logging and error messages for file retrieval failures.
- **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.
- **Smarter Filenames**: Configurable title source (Chat Title, AI Generated, or Markdown Title).
## Configuration ## ✨ Key Features
You can configure the following settings via the **Valves** button in the plugin settings: - 🚀 **One-Click Export**: Adds an "Export to Word" action button to the chat.
- 📄 **Markdown Conversion**: Full Markdown syntax support (headings, bold, italic, code, tables, lists).
- 🎨 **Syntax Highlighting**: Code blocks highlighted with Pygments (500+ languages).
- 🔢 **Native Math Equations**: LaTeX math (`$$...$$`, `\[...\]`, `$...$`) converted to editable Word equations.
- 📊 **Mermaid Diagrams**: Flowcharts and sequence diagrams rendered as images.
- 📚 **Citations & References**: Auto-generates References section with clickable citation links.
- 🧹 **Reasoning Stripping**: Automatically removes AI thinking blocks (`<think>`, `<analysis>`).
- 📋 **Enhanced Tables**: Smart column widths, alignment, header row repeat across pages.
- 💬 **Blockquote Support**: Markdown blockquotes with left border and gray styling.
- 🌐 **Multi-language Support**: Proper handling of Chinese and English text.
- **TITLE_SOURCE**: Choose how the document title/filename is generated. ## 🚀 How to Use
- `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.
- **MAX_EMBED_IMAGE_MB**: Maximum image size to embed into DOCX (MB). Default: `20`.
- **UI_LANGUAGE**: User interface language, supports `en` (English) and `zh` (Chinese). Default: `en`.
- **FONT_LATIN**: Font name for Latin characters. Default: `Times New Roman`.
- **FONT_ASIAN**: Font name for Asian characters. Default: `SimSun`.
- **FONT_CODE**: Font name for code blocks. Default: `Consolas`.
- **TABLE_HEADER_COLOR**: Table header background color (Hex without #). Default: `F2F2F2`.
- **TABLE_ZEBRA_COLOR**: Table alternating row background color (Hex without #). Default: `FBFBFB`.
- **MERMAID_JS_URL**: URL for the Mermaid.js library.
- **MERMAID_JSZIP_URL**: URL for the JSZip library (required for DOCX manipulation).
- **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.0`.
- **MERMAID_OPTIMIZE_LAYOUT**: Automatically convert LR (Left-Right) flowcharts to TD (Top-Down). Default: `False`.
- **MERMAID_BACKGROUND**: Background color for Mermaid diagrams (e.g., `white`, `transparent`). Default: `transparent`.
- **MERMAID_CAPTIONS_ENABLE**: Enable/disable figure captions for Mermaid diagrams. Default: `True`.
- **MERMAID_CAPTION_STYLE**: Paragraph style name for Mermaid captions. Default: `Caption`.
- **MERMAID_CAPTION_PREFIX**: Caption prefix label (e.g., 'Figure'). Empty = auto-detect based on language.
- **MATH_ENABLE**: Enable LaTeX math block conversion (`\[...\]` and `$$...$$`). Default: `True`.
- **MATH_INLINE_DOLLAR_ENABLE**: Enable inline `$ ... $` math conversion. Default: `True`.
## Supported Markdown Syntax 1. **Install**: Search for "Export to Word" in the Open WebUI Community and install.
2. **Trigger**: In any chat, click the "Export to Word" action button.
3. **Download**: The .docx file will be automatically downloaded.
| Syntax | Word Result | ## ⚙️ Configuration (Valves)
| :---------------------------------- | :------------------------------------ |
| `# 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 | Parameter | Default | Description |
| :--- | :--- | :--- |
| **Title Source (TITLE_SOURCE)** | `chat_title` | `chat_title`, `ai_generated`, or `markdown_title` |
| **Max Image Size (MAX_EMBED_IMAGE_MB)** | `20` | Maximum image size to embed (MB) |
| **UI Language (UI_LANGUAGE)** | `en` | `en` (English) or `zh` (Chinese) |
| **Latin Font (FONT_LATIN)** | `Times New Roman` | Font for Latin characters |
| **Asian Font (FONT_ASIAN)** | `SimSun` | Font for Asian characters |
| **Code Font (FONT_CODE)** | `Consolas` | Font for code blocks |
| **Table Header Color** | `F2F2F2` | Header background color (hex) |
| **Table Zebra Color** | `FBFBFB` | Alternating row color (hex) |
| **Mermaid PNG Scale** | `3.0` | Resolution multiplier for Mermaid images |
| **Math Enable** | `True` | Enable LaTeX math conversion |
1. Install the plugin. ## 🛠️ Supported Markdown Syntax
2. In any chat, click the "Export to Word" button.
3. The .docx file will be automatically downloaded to your device.
## Requirements | Syntax | Word Result |
| :--- | :--- |
| `# Heading 1` to `###### Heading 6` | Heading levels 1-6 |
| `**bold**` or `__bold__` | Bold text |
| `*italic*` or `_italic_` | Italic text |
| `` `inline code` `` | Monospace with gray background |
| ` ``` code block ``` ` | **Syntax highlighted** code block |
| `> blockquote` | Left-bordered gray italic text |
| `[link](url)` | Blue underlined link |
| `~~strikethrough~~` | Strikethrough text |
| `- item` or `* item` | Bullet list |
| `1. item` | Numbered list |
| Markdown tables | **Enhanced table** with smart widths |
| `$$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
- `python-docx==1.1.2` - Word document generation - `python-docx==1.1.2` - Word document generation
- `Pygments>=2.15.0` - Syntax highlighting - `Pygments>=2.15.0` - Syntax highlighting
- `latex2mathml` - LaTeX to MathML conversion - `latex2mathml` - LaTeX to MathML conversion
- `mathml2omml` - MathML to Office Math (OMML) conversion - `mathml2omml` - MathML to Office Math (OMML) conversion
All dependencies are declared in the plugin docstring. ## 📝 Changelog
## Font Configuration ### v0.4.3
- **S3 Object Storage**: Direct S3/MinIO access via boto3 for faster image retrieval.
- **6-Level Fallback**: Robust file retrieval: DB → S3 → Local → URL → API → Attributes.
- **Better Logging**: Improved error messages for debugging file access issues.
- **English Text**: Times New Roman ### v0.4.1
- **Chinese Text**: SimSun (宋体) for body, SimHei (黑体) for headings - **Chinese Parameter Names**: Localized configuration names for Chinese version.
- **Code**: Consolas
## Changelog
### v0.4.0 ### v0.4.0
- **Multi-language Support**: UI language switching (English/Chinese).
- **Multi-language Support**: Added UI language switching (English/Chinese) with localized messages. - **Font & Style Configuration**: Customizable fonts and table colors.
- **Font & Style Configuration**: Customizable fonts for Latin/Asian text and code, plus table colors. - **Mermaid Enhancements**: Hybrid SVG+PNG rendering, background color config.
- **Mermaid Enhancements**: - **Performance**: Real-time progress updates for large exports.
- Hybrid client-side rendering (SVG+PNG) for better clarity and compatibility.
- Configurable background color, fixing issues in dark mode.
- Added error boundaries to prevent export failures on render errors.
- **Performance**: Real-time progress updates for large document exports.
- **Bug Fixes**:
- Fixed parsing errors in Markdown tables containing code blocks or links.
- Fixed parsing issues with underscores (`_`), asterisks (`*`), and tildes (`~`) used as long separators.
- Enhanced error handling for image embedding.
### 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
GitHub: [Fu-Jie/awesome-openwebui](https://github.com/Fu-Jie/awesome-openwebui)
## License
MIT License

View File

@@ -1,134 +1,88 @@
# 导出为 Word # 📝 导出为 Word (增强版)
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.4.3 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
将对话导出为 Word (.docx),支持**代码语法高亮**、**原生数学公式**、**Mermaid 图表**、**引用参考**和**增强表格格式**。 将对话导出为 Word (.docx),支持**代码语法高亮**、**原生数学公式**、**Mermaid 图表**、**引用参考**和**增强表格格式**。
## 功能特点 ## 🔥 v0.4.3 更新内容
- **一键导出**:在聊天界面添加"导出为 Word"动作按钮 - **S3 对象存储支持**: 通过 boto3 直连 S3/MinIO绕过 API 层,导出速度更快
- **Markdown 转换**:将 Markdown 语法转换为 Word 格式(标题、粗体、斜体、代码、表格、列表)。 - 🔧 **多级文件回退**: 6 级文件获取机制(数据库 → S3 → 本地 → URL → API → 属性)。
- **代码语法高亮**:使用 Pygments 库为代码块添加语法高亮(支持 500+ 种语言) - 🛡️ **错误处理优化**: 更完善的日志记录和错误提示,便于调试文件访问问题
- **原生数学公式**LaTeX 公式(`$$...$$``\[...\]``$...$``\(...\)`)转换为可编辑的 Word 公式。
- **Mermaid 图表**Mermaid 流程图和时序图渲染为文档中的图片。
- **引用与参考**:自动从 OpenWebUI 来源生成参考资料章节,支持可点击的引用链接。
- **移除思考过程**:自动移除 AI 思考块(`<think>``<analysis>`)。
- **增强表格**:智能列宽、列对齐(`:---``---:``:---:`)、表头跨页重复。
- **引用块支持**Markdown 引用块渲染为带左侧边框的灰色斜体样式。
- **多语言支持**:正确处理中文和英文文本,无乱码问题。
- **智能文件名**可配置标题来源对话标题、AI 生成或 Markdown 标题)。
## 配置 ## ✨ 核心特性
您可以通过插件设置中的 **Valves** 按钮配置以下选项: - 🚀 **一键导出**: 在聊天界面添加"导出为 Word"动作按钮。
- 📄 **Markdown 转换**: 完整支持 Markdown 语法(标题、粗体、斜体、代码、表格、列表)。
- 🎨 **代码语法高亮**: 使用 Pygments 库高亮代码块(支持 500+ 种语言)。
- 🔢 **原生数学公式**: LaTeX 公式(`$$...$$``\[...\]``$...$`)转换为可编辑的 Word 公式。
- 📊 **Mermaid 图表**: 流程图和时序图渲染为文档中的图片。
- 📚 **引用与参考**: 自动生成参考资料章节,支持可点击的引用链接。
- 🧹 **移除思考过程**: 自动移除 AI 思考块(`<think>``<analysis>`)。
- 📋 **增强表格**: 智能列宽、对齐、表头跨页重复。
- 💬 **引用块支持**: Markdown 引用块渲染为带左侧边框的灰色斜体样式。
- 🌐 **多语言支持**: 正确处理中文和英文文本。
- **文档标题来源**:选择文档标题/文件名的生成方式。 ## 🚀 使用方法
- `chat_title`:使用对话标题(默认)。
- `ai_generated`:使用 AI 根据内容生成简短标题。
- `markdown_title`:从 Markdown 内容中提取第一个一级或二级标题。
- **最大嵌入图片大小MB**:嵌入图片的最大大小 (MB)。默认:`20`
- **界面语言**:界面语言,支持 `en` (英语) 和 `zh` (中文)。默认:`zh`
- **英文字体**:英文字体名称。默认:`Calibri`
- **中文字体**:中文字体名称。默认:`SimSun`
- **代码字体**:代码字体名称。默认:`Consolas`
- **表头背景色**:表头背景色(十六进制,不带#)。默认:`F2F2F2`
- **表格隔行背景色**:表格隔行背景色(十六进制,不带#)。默认:`FBFBFB`
- **Mermaid_JS地址**Mermaid.js 库的 URL。
- **JSZip库地址**JSZip 库的 URL用于 DOCX 操作)。
- **Mermaid_PNG缩放比例**Mermaid PNG 生成缩放比例(分辨率)。默认:`3.0`
- **Mermaid显示比例**Mermaid 在 Word 中的显示比例(视觉大小)。默认:`1.0`
- **Mermaid布局优化**:自动将 LR左右流程图转换为 TD上下。默认`False`
- **Mermaid背景色**Mermaid 图表背景色(如 `white`, `transparent`)。默认:`transparent`
- **启用Mermaid图注**:启用/禁用 Mermaid 图表的图注。默认:`True`
- **Mermaid图注样式**Mermaid 图注的段落样式名称。默认:`Caption`
- **Mermaid图注前缀**:图注前缀(如 '图')。留空则根据语言自动检测。
- **启用数学公式**:启用 LaTeX 数学公式块转换(`\[...\]``$$...$$`)。默认:`True`
- **启用行内公式**:启用行内 `$ ... $` 数学公式转换。默认:`True`
## 支持的 Markdown 语法 1. **安装**: 在 Open WebUI 社区搜索 "导出为 Word" 并安装。
2. **触发**: 在任意对话中,点击"导出为 Word"动作按钮。
3. **下载**: .docx 文件将自动下载到你的设备。
| 语法 | Word 效果 | ## ⚙️ 配置参数 (Valves)
| :---------------------------- | :-------------------------------- |
| `# 标题1``###### 标题6` | 标题级别 1-6 |
| `**粗体**``__粗体__` | 粗体文本 |
| `*斜体*``_斜体_` | 斜体文本 |
| `***粗斜体***` | 粗体 + 斜体 |
| `` `行内代码` `` | 等宽字体 + 灰色背景 |
| ` ``` 代码块 ``` ` | **语法高亮**的代码块 |
| `> 引用文本` | 带左侧边框的灰色斜体文本 |
| `[链接](url)` | 蓝色下划线链接文本 |
| `~~删除线~~` | 删除线文本 |
| `- 项目``* 项目` | 无序列表 |
| `1. 项目` | 有序列表 |
| Markdown 表格 | **增强表格**(智能列宽) |
| `---``***` | 水平分割线 |
| `$$LaTeX$$``\[LaTeX\]` | **原生 Word 公式**(块级) |
| `$LaTeX$``\(LaTeX\)` | **原生 Word 公式**(行内) |
| ` ```mermaid ... ``` ` | **Mermaid 图表**(图片形式) |
| `[1]` 引用标记 | **可点击链接**到参考资料 |
## 使用方法 | 参数 | 默认值 | 说明 |
| :--- | :--- | :--- |
| **文档标题来源** | `chat_title` | `chat_title`(对话标题)、`ai_generated`AI 生成)、`markdown_title`Markdown 标题)|
| **最大嵌入图片大小MB** | `20` | 嵌入图片的最大大小 (MB) |
| **界面语言** | `zh` | `en`(英语)或 `zh`(中文)|
| **英文字体** | `Calibri` | 英文字体名称 |
| **中文字体** | `SimSun` | 中文字体名称 |
| **代码字体** | `Consolas` | 代码块字体名称 |
| **表头背景色** | `F2F2F2` | 表头背景色(十六进制)|
| **表格隔行背景色** | `FBFBFB` | 表格隔行背景色(十六进制)|
| **Mermaid_PNG缩放比例** | `3.0` | Mermaid 图片分辨率倍数 |
| **启用数学公式** | `True` | 启用 LaTeX 公式转换 |
1. 安装插件。 ## 🛠️ 支持的 Markdown 语法
2. 在任意对话中,点击"导出为 Word"按钮。
3. .docx 文件将自动下载到你的设备。
## 依赖 | 语法 | Word 效果 |
| :--- | :--- |
| `# 标题1``###### 标题6` | 标题级别 1-6 |
| `**粗体**``__粗体__` | 粗体文本 |
| `*斜体*``_斜体_` | 斜体文本 |
| `` `行内代码` `` | 等宽字体 + 灰色背景 |
| ` ``` 代码块 ``` ` | **语法高亮**的代码块 |
| `> 引用文本` | 带左侧边框的灰色斜体文本 |
| `[链接](url)` | 蓝色下划线链接文本 |
| `~~删除线~~` | 删除线文本 |
| `- 项目``* 项目` | 无序列表 |
| `1. 项目` | 有序列表 |
| Markdown 表格 | **增强表格**(智能列宽)|
| `$$LaTeX$$``\[LaTeX\]` | **原生 Word 公式**(块级)|
| `$LaTeX$``\(LaTeX\)` | **原生 Word 公式**(行内)|
| ` ```mermaid ... ``` ` | **Mermaid 图表**(图片形式)|
| `[1]` 引用标记 | **可点击链接**到参考资料 |
## 📦 依赖
- `python-docx==1.1.2` - Word 文档生成 - `python-docx==1.1.2` - Word 文档生成
- `Pygments>=2.15.0` - 语法高亮 - `Pygments>=2.15.0` - 语法高亮
- `latex2mathml` - LaTeX 转 MathML - `latex2mathml` - LaTeX 转 MathML
- `mathml2omml` - MathML 转 Office Math (OMML) - `mathml2omml` - MathML 转 Office Math (OMML)
所有依赖已在插件文档字符串中声明。 ## 📝 更新日志
## 字体配置 ### v0.4.3
- **S3 对象存储**: 通过 boto3 直连 S3/MinIO图片获取速度更快。
- **英文文本**Times New Roman - **6 级回退机制**: 稳健的文件获取:数据库 → S3 → 本地 → URL → API → 属性。
- **中文文本**:宋体(正文)、黑体(标题) - **日志优化**: 改进错误提示,便于调试文件访问问题。
- **代码**Consolas
## 更新日志
### v0.4.1 ### v0.4.1
- **中文参数名**: 配置项名称和描述全部汉化。
- **中文参数名**: 将插件配置项名称和描述全部汉化,提升中文用户体验。
### v0.4.0 ### v0.4.0
- **多语言支持**: 界面语言切换(中文/英文)。
- **多语言支持**: 新增界面语言切换(中文/英文),提示信息更友好。
- **字体与样式配置**: 支持自定义中英文字体、代码字体以及表格颜色。 - **字体与样式配置**: 支持自定义中英文字体、代码字体以及表格颜色。
- **Mermaid 增强**: - **Mermaid 增强**: 混合 SVG+PNG 渲染,支持背景色配置。
- 客户端混合渲染SVG+PNG提高清晰度与兼容性。
- 支持背景色配置,修复深色模式下的显示问题。
- 增加错误边界,渲染失败时显示提示而非中断导出。
- **性能优化**: 导出大型文档时提供实时进度反馈。 - **性能优化**: 导出大型文档时提供实时进度反馈。
- **Bug 修复**:
- 修复 Markdown 表格中包含代码块或链接时的解析错误。
- 修复下划线(`_`)、星号(`*`)、波浪号(`~`)作为长分隔符时的解析问题。
- 增强图片嵌入的错误处理。
### 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
GitHub: [Fu-Jie/awesome-openwebui](https://github.com/Fu-Jie/awesome-openwebui)
## 许可证
MIT License

View File

@@ -3,7 +3,8 @@ title: Export to Word (Enhanced)
author: Fu-Jie author: Fu-Jie
author_url: https://github.com/Fu-Jie author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui funding_url: https://github.com/Fu-Jie/awesome-openwebui
version: 0.4.1 version: 0.4.3
openwebui_id: fca6a315-2a45-42cc-8c96-55cbc85f87f2
icon_url: data:image/svg+xml;base64,PHN2ZwogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICB3aWR0aD0iMjQiCiAgaGVpZ2h0PSIyNCIKICB2aWV3Qm94PSIwIDAgMjQgMjQiCiAgZmlsbD0ibm9uZSIKICBzdHJva2U9ImN1cnJlbnRDb2xvciIKICBzdHJva2Utd2lkdGg9IjIiCiAgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIgogIHN0cm9rZS1saW5lam9pbj0icm91bmQiCj4KICA8cGF0aCBkPSJNNiAyMmEyIDIgMCAwIDEtMi0yVjRhMiAyIDAgMCAxIDItMmg4YTIuNCAyLjQgMCAwIDEgMS43MDQuNzA2bDMuNTg4IDMuNTg4QTIuNCAyLjQgMCAwIDEgMjAgOHYxMmEyIDIgMCAwIDEtMiAyeiIgLz4KICA8cGF0aCBkPSJNMTQgMnY1YTEgMSAwIDAgMCAxIDFoNSIgLz4KICA8cGF0aCBkPSJNMTAgOUg4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxM0g4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxN0g4IiAvPgo8L3N2Zz4K icon_url: data:image/svg+xml;base64,PHN2ZwogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICB3aWR0aD0iMjQiCiAgaGVpZ2h0PSIyNCIKICB2aWV3Qm94PSIwIDAgMjQgMjQiCiAgZmlsbD0ibm9uZSIKICBzdHJva2U9ImN1cnJlbnRDb2xvciIKICBzdHJva2Utd2lkdGg9IjIiCiAgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIgogIHN0cm9rZS1saW5lam9pbj0icm91bmQiCj4KICA8cGF0aCBkPSJNNiAyMmEyIDIgMCAwIDEtMi0yVjRhMiAyIDAgMCAxIDItMmg4YTIuNCAyLjQgMCAwIDEgMS43MDQuNzA2bDMuNTg4IDMuNTg4QTIuNCAyLjQgMCAwIDEgMjAgOHYxMmEyIDIgMCAwIDEtMiAyeiIgLz4KICA8cGF0aCBkPSJNMTQgMnY1YTEgMSAwIDAgMCAxIDFoNSIgLz4KICA8cGF0aCBkPSJNMTAgOUg4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxM0g4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxN0g4IiAvPgo8L3N2Zz4K
requirements: python-docx, Pygments, latex2mathml, mathml2omml requirements: python-docx, Pygments, latex2mathml, mathml2omml
description: Export current conversation from Markdown to Word (.docx) with Mermaid diagrams rendered client-side (Mermaid.js, SVG+PNG), LaTeX math, real hyperlinks, improved tables, syntax highlighting, and blockquote support. description: Export current conversation from Markdown to Word (.docx) with Mermaid diagrams rendered client-side (Mermaid.js, SVG+PNG), LaTeX math, real hyperlinks, improved tables, syntax highlighting, and blockquote support.
@@ -65,6 +66,16 @@ try:
except Exception: except Exception:
LATEX_MATH_AVAILABLE = False LATEX_MATH_AVAILABLE = False
# boto3 for S3 direct access (faster than API fallback)
try:
import boto3
from botocore.config import Config as BotoConfig
import os
BOTO3_AVAILABLE = True
except ImportError:
BOTO3_AVAILABLE = False
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
@@ -290,6 +301,8 @@ class Action:
self._bookmark_id_counter: int = 1 self._bookmark_id_counter: int = 1
self._active_doc: Optional[Document] = None self._active_doc: Optional[Document] = None
self._user_lang: str = "en" # Will be set per-request self._user_lang: str = "en" # Will be set per-request
self._api_token: Optional[str] = None
self._api_base_url: Optional[str] = None
def _get_lang_key(self, user_language: str) -> str: def _get_lang_key(self, user_language: str) -> str:
"""Convert user language code to i18n key (e.g., 'zh-CN' -> 'zh', 'en-US' -> 'en').""" """Convert user language code to i18n key (e.g., 'zh-CN' -> 'zh', 'en-US' -> 'en')."""
@@ -349,6 +362,22 @@ class Action:
# Get user language from Valves configuration # Get user language from Valves configuration
self._user_lang = self._get_lang_key(self.valves.UI_LANGUAGE) self._user_lang = self._get_lang_key(self.valves.UI_LANGUAGE)
# Extract API connection info for file fetching (S3/Object Storage support)
def _get_default_base_url() -> str:
port = os.environ.get("PORT") or "8080"
return f"http://localhost:{port}"
if __request__:
try:
self._api_token = __request__.headers.get("Authorization")
self._api_base_url = str(__request__.base_url).rstrip("/")
except Exception:
self._api_token = None
self._api_base_url = _get_default_base_url()
else:
self._api_token = None
self._api_base_url = _get_default_base_url()
if __event_emitter__: if __event_emitter__:
last_assistant_message = body["messages"][-1] last_assistant_message = body["messages"][-1]
@@ -1075,19 +1104,85 @@ class Action:
b64 = m.group("b64") or "" b64 = m.group("b64") or ""
return self._decode_base64_limited(b64, max_bytes) return self._decode_base64_limited(b64, max_bytes)
def _read_from_s3(self, s3_path: str, max_bytes: int) -> Optional[bytes]:
"""Read file directly from S3 using environment variables for credentials."""
if not BOTO3_AVAILABLE:
return None
# Parse s3://bucket/key
if not s3_path.startswith("s3://"):
return None
path_without_prefix = s3_path[5:] # Remove 's3://'
parts = path_without_prefix.split("/", 1)
if len(parts) < 2:
return None
bucket = parts[0]
key = parts[1]
# Read S3 config from environment variables
endpoint_url = os.environ.get("S3_ENDPOINT_URL")
access_key = os.environ.get("S3_ACCESS_KEY_ID")
secret_key = os.environ.get("S3_SECRET_ACCESS_KEY")
addressing_style = os.environ.get("S3_ADDRESSING_STYLE", "auto")
if not all([endpoint_url, access_key, secret_key]):
logger.debug(
"S3 environment variables not fully configured, skipping S3 direct download."
)
return None
try:
s3_config = BotoConfig(
s3={"addressing_style": addressing_style},
connect_timeout=5,
read_timeout=15,
)
s3_client = boto3.client(
"s3",
endpoint_url=endpoint_url,
aws_access_key_id=access_key,
aws_secret_access_key=secret_key,
config=s3_config,
)
response = s3_client.get_object(Bucket=bucket, Key=key)
body = response["Body"]
data = body.read(max_bytes + 1)
body.close()
if len(data) > max_bytes:
return None
return data
except Exception as e:
logger.warning(f"S3 direct download failed for {s3_path}: {e}")
return None
def _image_bytes_from_owui_file_id( def _image_bytes_from_owui_file_id(
self, file_id: str, max_bytes: int self, file_id: str, max_bytes: int
) -> Optional[bytes]: ) -> Optional[bytes]:
if not file_id or Files is None: if not file_id:
return None
try:
file_obj = Files.get_file_by_id(file_id)
except Exception:
return None
if not file_obj:
return None return None
# Common patterns across Open WebUI versions / storage backends. if Files is None:
logger.error(
"Files model is not available (import failed). Cannot retrieve file content."
)
return None
try:
file_obj = Files.get_file_by_id(file_id)
except Exception as e:
logger.error(f"Files.get_file_by_id({file_id}) failed: {e}")
return None
if not file_obj:
logger.warning(f"File {file_id} not found in database.")
return None
# 1. Try data field (DB stored)
data_field = getattr(file_obj, "data", None) data_field = getattr(file_obj, "data", None)
if isinstance(data_field, dict): if isinstance(data_field, dict):
blob_value = data_field.get("bytes") blob_value = data_field.get("bytes")
@@ -1099,19 +1194,119 @@ class Action:
if isinstance(inline, str) and inline.strip(): if isinstance(inline, str) and inline.strip():
return self._decode_base64_limited(inline, max_bytes) return self._decode_base64_limited(inline, max_bytes)
# 2. Try S3 direct download (fastest for object storage)
s3_path = getattr(file_obj, "path", None)
if isinstance(s3_path, str) and s3_path.startswith("s3://"):
s3_data = self._read_from_s3(s3_path, max_bytes)
if s3_data is not None:
return s3_data
# 3. Try file paths (Disk stored)
# We try multiple path variations to be robust against CWD differences (e.g. Docker vs Local)
for attr in ("path", "file_path", "absolute_path"): for attr in ("path", "file_path", "absolute_path"):
candidate = getattr(file_obj, attr, None) candidate = getattr(file_obj, attr, None)
if isinstance(candidate, str) and candidate.strip(): if isinstance(candidate, str) and candidate.strip():
raw = self._read_file_bytes_limited(Path(candidate), max_bytes) # Skip obviously non-local paths (S3, GCS, HTTP)
if re.match(r"^(s3://|gs://|https?://)", candidate, re.IGNORECASE):
logger.debug(f"Skipping local read for non-local path: {candidate}")
continue
p = Path(candidate)
# Attempt 1: As-is (Absolute or relative to CWD)
raw = self._read_file_bytes_limited(p, max_bytes)
if raw is not None: if raw is not None:
return raw return raw
# Attempt 2: Relative to ./data (Common in OpenWebUI)
if not p.is_absolute():
try:
raw = self._read_file_bytes_limited(
Path("./data") / p, max_bytes
)
if raw is not None:
return raw
except Exception:
pass
# Attempt 3: Relative to /app/backend/data (Docker default)
try:
raw = self._read_file_bytes_limited(
Path("/app/backend/data") / p, max_bytes
)
if raw is not None:
return raw
except Exception:
pass
# 4. Try URL (Object Storage / S3 Public URL)
urls_to_try = []
url_attr = getattr(file_obj, "url", None)
if isinstance(url_attr, str) and url_attr:
urls_to_try.append(url_attr)
if isinstance(data_field, dict):
url_data = data_field.get("url")
if isinstance(url_data, str) and url_data:
urls_to_try.append(url_data)
if urls_to_try:
import urllib.request
for url in urls_to_try:
if not url.startswith(("http://", "https://")):
continue
try:
logger.info(
f"Attempting to download file {file_id} from URL: {url}"
)
# Use a timeout to avoid hanging
req = urllib.request.Request(
url, headers={"User-Agent": "OpenWebUI-Export-Plugin"}
)
with urllib.request.urlopen(req, timeout=15) as response:
if 200 <= response.status < 300:
data = response.read(max_bytes + 1)
if len(data) <= max_bytes:
return data
else:
logger.warning(
f"File {file_id} from URL is too large (> {max_bytes} bytes)"
)
except Exception as e:
logger.warning(f"Failed to download {file_id} from {url}: {e}")
# 5. Try fetching via Local API (Last resort for S3/Object Storage without direct URL)
# If we have the API token and base URL, we can try to fetch the content through the backend API.
if self._api_base_url:
api_url = f"{self._api_base_url}/api/v1/files/{file_id}/content"
try:
import urllib.request
headers = {"User-Agent": "OpenWebUI-Export-Plugin"}
if self._api_token:
headers["Authorization"] = self._api_token
req = urllib.request.Request(api_url, headers=headers)
with urllib.request.urlopen(req, timeout=15) as response:
if 200 <= response.status < 300:
data = response.read(max_bytes + 1)
if len(data) <= max_bytes:
return data
except Exception:
# API fetch failed, just fall through to the next method
pass
# 6. Try direct content attributes (last ditch)
for attr in ("content", "blob", "data"): for attr in ("content", "blob", "data"):
raw = getattr(file_obj, attr, None) raw = getattr(file_obj, attr, None)
if isinstance(raw, (bytes, bytearray)): if isinstance(raw, (bytes, bytearray)):
b = bytes(raw) b = bytes(raw)
return b if len(b) <= max_bytes else None return b if len(b) <= max_bytes else None
logger.warning(
f"File {file_id} found but no content accessible. Attributes: {dir(file_obj)}"
)
return None return None
def _add_image_placeholder(self, paragraph, alt: str, reason: str): def _add_image_placeholder(self, paragraph, alt: str, reason: str):

View File

@@ -3,7 +3,8 @@ title: 导出为 Word (增强版)
author: Fu-Jie author: Fu-Jie
author_url: https://github.com/Fu-Jie author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui funding_url: https://github.com/Fu-Jie/awesome-openwebui
version: 0.4.1 version: 0.4.3
openwebui_id: 8a6306c0-d005-4e46-aaae-8db3532c9ed5
icon_url: data:image/svg+xml;base64,PHN2ZwogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICB3aWR0aD0iMjQiCiAgaGVpZ2h0PSIyNCIKICB2aWV3Qm94PSIwIDAgMjQgMjQiCiAgZmlsbD0ibm9uZSIKICBzdHJva2U9ImN1cnJlbnRDb2xvciIKICBzdHJva2Utd2lkdGg9IjIiCiAgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIgogIHN0cm9rZS1saW5lam9pbj0icm91bmQiCj4KICA8cGF0aCBkPSJNNiAyMmEyIDIgMCAwIDEtMi0yVjRhMiAyIDAgMCAxIDItMmg4YTIuNCAyLjQgMCAwIDEgMS43MDQuNzA2bDMuNTg4IDMuNTg4QTIuNCAyLjQgMCAwIDEgMjAgOHYxMmEyIDIgMCAwIDEtMiAyeiIgLz4KICA8cGF0aCBkPSJNMTQgMnY1YTEgMSAwIDAgMCAxIDFoNSIgLz4KICA8cGF0aCBkPSJNMTAgOUg4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxM0g4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxN0g4IiAvPgo8L3N2Zz4K icon_url: data:image/svg+xml;base64,PHN2ZwogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICB3aWR0aD0iMjQiCiAgaGVpZ2h0PSIyNCIKICB2aWV3Qm94PSIwIDAgMjQgMjQiCiAgZmlsbD0ibm9uZSIKICBzdHJva2U9ImN1cnJlbnRDb2xvciIKICBzdHJva2Utd2lkdGg9IjIiCiAgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIgogIHN0cm9rZS1saW5lam9pbj0icm91bmQiCj4KICA8cGF0aCBkPSJNNiAyMmEyIDIgMCAwIDEtMi0yVjRhMiAyIDAgMCAxIDItMmg4YTIuNCAyLjQgMCAwIDEgMS43MDQuNzA2bDMuNTg4IDMuNTg4QTIuNCAyLjQgMCAwIDEgMjAgOHYxMmEyIDIgMCAwIDEtMiAyeiIgLz4KICA8cGF0aCBkPSJNMTQgMnY1YTEgMSAwIDAgMCAxIDFoNSIgLz4KICA8cGF0aCBkPSJNMTAgOUg4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxM0g4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxN0g4IiAvPgo8L3N2Zz4K
requirements: python-docx, Pygments, latex2mathml, mathml2omml requirements: python-docx, Pygments, latex2mathml, mathml2omml
description: 将对话导出为 Word (.docx),支持 Mermaid 图表 (客户端渲染 SVG+PNG)、LaTeX 数学公式、真实超链接、增强表格格式、代码高亮和引用块。 description: 将对话导出为 Word (.docx),支持 Mermaid 图表 (客户端渲染 SVG+PNG)、LaTeX 数学公式、真实超链接、增强表格格式、代码高亮和引用块。
@@ -65,6 +66,16 @@ try:
except Exception: except Exception:
LATEX_MATH_AVAILABLE = False LATEX_MATH_AVAILABLE = False
# boto3 for S3 direct access (faster than API fallback)
try:
import boto3
from botocore.config import Config as BotoConfig
import os
BOTO3_AVAILABLE = True
except ImportError:
BOTO3_AVAILABLE = False
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
@@ -290,6 +301,8 @@ class Action:
self._bookmark_id_counter: int = 1 self._bookmark_id_counter: int = 1
self._active_doc: Optional[Document] = None self._active_doc: Optional[Document] = None
self._user_lang: str = "en" # Will be set per-request self._user_lang: str = "en" # Will be set per-request
self._api_token: Optional[str] = None
self._api_base_url: Optional[str] = None
def _get_lang_key(self, user_language: str) -> str: def _get_lang_key(self, user_language: str) -> str:
"""Convert user language code to i18n key (e.g., 'zh-CN' -> 'zh', 'en-US' -> 'en').""" """Convert user language code to i18n key (e.g., 'zh-CN' -> 'zh', 'en-US' -> 'en')."""
@@ -347,6 +360,22 @@ class Action:
# Get user language from Valves configuration # Get user language from Valves configuration
self._user_lang = self._get_lang_key(self.valves.界面语言) self._user_lang = self._get_lang_key(self.valves.界面语言)
# Extract API connection info for file fetching (S3/Object Storage support)
def _get_default_base_url() -> str:
port = os.environ.get("PORT") or "8080"
return f"http://localhost:{port}"
if __request__:
try:
self._api_token = __request__.headers.get("Authorization")
self._api_base_url = str(__request__.base_url).rstrip("/")
except Exception:
self._api_token = None
self._api_base_url = _get_default_base_url()
else:
self._api_token = None
self._api_base_url = _get_default_base_url()
if __event_emitter__: if __event_emitter__:
last_assistant_message = body["messages"][-1] last_assistant_message = body["messages"][-1]
@@ -1073,19 +1102,85 @@ class Action:
b64 = m.group("b64") or "" b64 = m.group("b64") or ""
return self._decode_base64_limited(b64, max_bytes) return self._decode_base64_limited(b64, max_bytes)
def _read_from_s3(self, s3_path: str, max_bytes: int) -> Optional[bytes]:
"""Read file directly from S3 using environment variables for credentials."""
if not BOTO3_AVAILABLE:
return None
# Parse s3://bucket/key
if not s3_path.startswith("s3://"):
return None
path_without_prefix = s3_path[5:] # Remove 's3://'
parts = path_without_prefix.split("/", 1)
if len(parts) < 2:
return None
bucket = parts[0]
key = parts[1]
# Read S3 config from environment variables
endpoint_url = os.environ.get("S3_ENDPOINT_URL")
access_key = os.environ.get("S3_ACCESS_KEY_ID")
secret_key = os.environ.get("S3_SECRET_ACCESS_KEY")
addressing_style = os.environ.get("S3_ADDRESSING_STYLE", "auto")
if not all([endpoint_url, access_key, secret_key]):
logger.debug(
"S3 environment variables not fully configured, skipping S3 direct download."
)
return None
try:
s3_config = BotoConfig(
s3={"addressing_style": addressing_style},
connect_timeout=5,
read_timeout=15,
)
s3_client = boto3.client(
"s3",
endpoint_url=endpoint_url,
aws_access_key_id=access_key,
aws_secret_access_key=secret_key,
config=s3_config,
)
response = s3_client.get_object(Bucket=bucket, Key=key)
body = response["Body"]
data = body.read(max_bytes + 1)
body.close()
if len(data) > max_bytes:
return None
return data
except Exception as e:
logger.warning(f"S3 direct download failed for {s3_path}: {e}")
return None
def _image_bytes_from_owui_file_id( def _image_bytes_from_owui_file_id(
self, file_id: str, max_bytes: int self, file_id: str, max_bytes: int
) -> Optional[bytes]: ) -> Optional[bytes]:
if not file_id or Files is None: if not file_id:
return None
try:
file_obj = Files.get_file_by_id(file_id)
except Exception:
return None
if not file_obj:
return None return None
# Common patterns across Open WebUI versions / storage backends. if Files is None:
logger.error(
"Files model is not available (import failed). Cannot retrieve file content."
)
return None
try:
file_obj = Files.get_file_by_id(file_id)
except Exception as e:
logger.error(f"Files.get_file_by_id({file_id}) failed: {e}")
return None
if not file_obj:
logger.warning(f"File {file_id} not found in database.")
return None
# 1. Try data field (DB stored)
data_field = getattr(file_obj, "data", None) data_field = getattr(file_obj, "data", None)
if isinstance(data_field, dict): if isinstance(data_field, dict):
blob_value = data_field.get("bytes") blob_value = data_field.get("bytes")
@@ -1097,19 +1192,119 @@ class Action:
if isinstance(inline, str) and inline.strip(): if isinstance(inline, str) and inline.strip():
return self._decode_base64_limited(inline, max_bytes) return self._decode_base64_limited(inline, max_bytes)
# 2. Try S3 direct download (fastest for object storage)
s3_path = getattr(file_obj, "path", None)
if isinstance(s3_path, str) and s3_path.startswith("s3://"):
s3_data = self._read_from_s3(s3_path, max_bytes)
if s3_data is not None:
return s3_data
# 3. Try file paths (Disk stored)
# We try multiple path variations to be robust against CWD differences (e.g. Docker vs Local)
for attr in ("path", "file_path", "absolute_path"): for attr in ("path", "file_path", "absolute_path"):
candidate = getattr(file_obj, attr, None) candidate = getattr(file_obj, attr, None)
if isinstance(candidate, str) and candidate.strip(): if isinstance(candidate, str) and candidate.strip():
raw = self._read_file_bytes_limited(Path(candidate), max_bytes) # Skip obviously non-local paths (S3, GCS, HTTP)
if re.match(r"^(s3://|gs://|https?://)", candidate, re.IGNORECASE):
logger.debug(f"Skipping local read for non-local path: {candidate}")
continue
p = Path(candidate)
# Attempt 1: As-is (Absolute or relative to CWD)
raw = self._read_file_bytes_limited(p, max_bytes)
if raw is not None: if raw is not None:
return raw return raw
# Attempt 2: Relative to ./data (Common in OpenWebUI)
if not p.is_absolute():
try:
raw = self._read_file_bytes_limited(
Path("./data") / p, max_bytes
)
if raw is not None:
return raw
except Exception:
pass
# Attempt 3: Relative to /app/backend/data (Docker default)
try:
raw = self._read_file_bytes_limited(
Path("/app/backend/data") / p, max_bytes
)
if raw is not None:
return raw
except Exception:
pass
# 4. Try URL (Object Storage / S3 Public URL)
urls_to_try = []
url_attr = getattr(file_obj, "url", None)
if isinstance(url_attr, str) and url_attr:
urls_to_try.append(url_attr)
if isinstance(data_field, dict):
url_data = data_field.get("url")
if isinstance(url_data, str) and url_data:
urls_to_try.append(url_data)
if urls_to_try:
import urllib.request
for url in urls_to_try:
if not url.startswith(("http://", "https://")):
continue
try:
logger.info(
f"Attempting to download file {file_id} from URL: {url}"
)
# Use a timeout to avoid hanging
req = urllib.request.Request(
url, headers={"User-Agent": "OpenWebUI-Export-Plugin"}
)
with urllib.request.urlopen(req, timeout=15) as response:
if 200 <= response.status < 300:
data = response.read(max_bytes + 1)
if len(data) <= max_bytes:
return data
else:
logger.warning(
f"File {file_id} from URL is too large (> {max_bytes} bytes)"
)
except Exception as e:
logger.warning(f"Failed to download {file_id} from {url}: {e}")
# 5. Try fetching via Local API (Last resort for S3/Object Storage without direct URL)
# If we have the API token and base URL, we can try to fetch the content through the backend API.
if self._api_base_url:
api_url = f"{self._api_base_url}/api/v1/files/{file_id}/content"
try:
import urllib.request
headers = {"User-Agent": "OpenWebUI-Export-Plugin"}
if self._api_token:
headers["Authorization"] = self._api_token
req = urllib.request.Request(api_url, headers=headers)
with urllib.request.urlopen(req, timeout=15) as response:
if 200 <= response.status < 300:
data = response.read(max_bytes + 1)
if len(data) <= max_bytes:
return data
except Exception:
# API fetch failed, just fall through to the next method
pass
# 6. Try direct content attributes (last ditch)
for attr in ("content", "blob", "data"): for attr in ("content", "blob", "data"):
raw = getattr(file_obj, attr, None) raw = getattr(file_obj, attr, None)
if isinstance(raw, (bytes, bytearray)): if isinstance(raw, (bytes, bytearray)):
b = bytes(raw) b = bytes(raw)
return b if len(b) <= max_bytes else None return b if len(b) <= max_bytes else None
logger.warning(
f"File {file_id} found but no content accessible. Attributes: {dir(file_obj)}"
)
return None return None
def _add_image_placeholder(self, paragraph, alt: str, reason: str): def _add_image_placeholder(self, paragraph, alt: str, reason: str):

View File

@@ -4,6 +4,7 @@ author: Fu-Jie
author_url: https://github.com/Fu-Jie author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui funding_url: https://github.com/Fu-Jie/awesome-openwebui
version: 0.3.7 version: 0.3.7
openwebui_id: 244b8f9d-7459-47d6-84d3-c7ae8e3ec710
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNSAySDZhMiAyIDAgMCAwLTIgMnYxNmEyIDIgMCAwIDAgMiAyaDEyYTIgMiAwIDAgMCAyLTJWN1oiLz48cGF0aCBkPSJNMTQgMnY0YTIgMiAwIDAgMCAyIDJoNCIvPjxwYXRoIGQ9Ik04IDEzaDIiLz48cGF0aCBkPSJNMTQgMTNoMiIvPjxwYXRoIGQ9Ik04IDE3aDIiLz48cGF0aCBkPSJNMTQgMTdoMiIvPjwvc3ZnPg== icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNSAySDZhMiAyIDAgMCAwLTIgMnYxNmEyIDIgMCAwIDAgMiAyaDEyYTIgMiAwIDAgMCAyLTJWN1oiLz48cGF0aCBkPSJNMTQgMnY0YTIgMiAwIDAgMCAyIDJoNCIvPjxwYXRoIGQ9Ik04IDEzaDIiLz48cGF0aCBkPSJNMTQgMTNoMiIvPjxwYXRoIGQ9Ik04IDE3aDIiLz48cGF0aCBkPSJNMTQgMTdoMiIvPjwvc3ZnPg==
description: Extracts tables from chat messages and exports them to Excel (.xlsx) files with smart formatting. description: Extracts tables from chat messages and exports them to Excel (.xlsx) files with smart formatting.
""" """

View File

@@ -4,6 +4,7 @@ author: Fu-Jie
author_url: https://github.com/Fu-Jie author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui funding_url: https://github.com/Fu-Jie/awesome-openwebui
version: 0.2.4 version: 0.2.4
openwebui_id: 65a2ea8f-2a13-4587-9d76-55eea0035cc8
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwb2x5Z29uIHBvaW50cz0iMTIgMiAyIDcgMTIgMTIgMjIgNyAxMiAyIi8+PHBvbHlsaW5lIHBvaW50cz0iMiAxNyAxMiAyMiAyMiAxNyIvPjxwb2x5bGluZSBwb2ludHM9IjIgMTIgMTIgMTcgMjIgMTIiLz48L3N2Zz4= icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwb2x5Z29uIHBvaW50cz0iMTIgMiAyIDcgMTIgMTIgMjIgNyAxMiAyIi8+PHBvbHlsaW5lIHBvaW50cz0iMiAxNyAxMiAyMiAyMiAxNyIvPjxwb2x5bGluZSBwb2ludHM9IjIgMTIgMTIgMTcgMjIgMTIiLz48L3N2Zz4=
description: Quickly generates beautiful flashcards from text, extracting key points and categories. description: Quickly generates beautiful flashcards from text, extracting key points and categories.
""" """

View File

@@ -4,6 +4,7 @@ author: Fu-Jie
author_url: https://github.com/Fu-Jie author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui funding_url: https://github.com/Fu-Jie/awesome-openwebui
version: 0.2.4 version: 0.2.4
openwebui_id: 4a31eac3-a3c4-4c30-9ca5-dab36b5fac65
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwb2x5Z29uIHBvaW50cz0iMTIgMiAyIDcgMTIgMTIgMjIgNyAxMiAyIi8+PHBvbHlsaW5lIHBvaW50cz0iMiAxNyAxMiAyMiAyMiAxNyIvPjxwb2x5bGluZSBwb2ludHM9IjIgMTIgMTIgMTcgMjIgMTIiLz48L3N2Zz4= icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwb2x5Z29uIHBvaW50cz0iMTIgMiAyIDcgMTIgMTIgMjIgNyAxMiAyIi8+PHBvbHlsaW5lIHBvaW50cz0iMiAxNyAxMiAyMiAyMiAxNyIvPjxwb2x5bGluZSBwb2ludHM9IjIgMTIgMTIgMTcgMjIgMTIiLz48L3N2Zz4=
description: 快速将文本提炼为精美的学习记忆卡片,支持核心要点提取与分类。 description: 快速将文本提炼为精美的学习记忆卡片,支持核心要点提取与分类。
""" """

View File

@@ -1,7 +1,19 @@
# 📊 Smart Infographic (AntV) # 📊 Smart Infographic (AntV)
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 1.4.1 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
An Open WebUI plugin powered by the AntV Infographic engine. It transforms long text into professional, beautiful infographics with a single click. An Open WebUI plugin powered by the AntV Infographic engine. It transforms long text into professional, beautiful infographics with a single click.
## 🔥 What's New in v1.4.1
-**PNG Upload**: Infographics now upload as PNG format for better Word export compatibility.
- 🔧 **Canvas Conversion**: Uses browser canvas for high-quality SVG to PNG conversion (2x scale).
### Previous: v1.4.0
-**Default Mode Change**: Default output mode is now `image` (static image) for better compatibility.
- 📱 **Responsive Sizing**: Images now auto-adapt to the chat container width.
## ✨ Key Features ## ✨ Key Features
- 🚀 **AI-Powered Transformation**: Automatically analyzes text logic, extracts key points, and generates structured charts. - 🚀 **AI-Powered Transformation**: Automatically analyzes text logic, extracts key points, and generates structured charts.
@@ -11,15 +23,6 @@ An Open WebUI plugin powered by the AntV Infographic engine. It transforms long
- 🌈 **Highly Customizable**: Supports Dark/Light modes, auto-adapts theme colors, with bold titles and refined card layouts. - 🌈 **Highly Customizable**: Supports Dark/Light modes, auto-adapts theme colors, with bold titles and refined card layouts.
- 📱 **Responsive Design**: Generated charts look great on both desktop and mobile devices. - 📱 **Responsive Design**: Generated charts look great on both desktop and mobile devices.
## 🛠️ Supported Template Types
| Category | Template Name | Use Case |
| :--- | :--- | :--- |
| **Lists & Hierarchy** | `list-grid`, `tree-vertical`, `mindmap` | Features, Org Charts, Brainstorming |
| **Sequence & Relation** | `sequence-roadmap`, `relation-circle` | Roadmaps, Circular Flows, Steps |
| **Comparison & Analysis** | `compare-binary`, `compare-swot`, `quadrant-quarter` | Pros/Cons, SWOT, Quadrants |
| **Charts & Data** | `chart-bar`, `chart-line`, `chart-pie` | Trends, Distributions, Metrics |
## 🚀 How to Use ## 🚀 How to Use
1. **Install**: Search for "Smart Infographic" in the Open WebUI Community and install. 1. **Install**: Search for "Smart Infographic" in the Open WebUI Community and install.
@@ -38,6 +41,16 @@ You can adjust the following parameters in the plugin settings to optimize the g
| **Min Text Length (MIN_TEXT_LENGTH)** | `100` | Minimum characters required to trigger analysis, preventing accidental triggers on short text. | | **Min Text Length (MIN_TEXT_LENGTH)** | `100` | Minimum characters required to trigger analysis, preventing accidental triggers on short text. |
| **Clear Previous (CLEAR_PREVIOUS_HTML)** | `False` | Whether to clear previous charts. If `False`, new charts will be appended below. | | **Clear Previous (CLEAR_PREVIOUS_HTML)** | `False` | Whether to clear previous charts. If `False`, new charts will be appended below. |
| **Message Count (MESSAGE_COUNT)** | `1` | Number of recent messages to use for analysis. Increase this for more context. | | **Message Count (MESSAGE_COUNT)** | `1` | Number of recent messages to use for analysis. Increase this for more context. |
| **Output Mode (OUTPUT_MODE)** | `image` | `image` for static image embedding (default, better compatibility), `html` for interactive chart. |
## 🛠️ Supported Template Types
| Category | Template Name | Use Case |
| :--- | :--- | :--- |
| **Lists & Hierarchy** | `list-grid`, `tree-vertical`, `mindmap` | Features, Org Charts, Brainstorming |
| **Sequence & Relation** | `sequence-roadmap`, `relation-circle` | Roadmaps, Circular Flows, Steps |
| **Comparison & Analysis** | `compare-binary`, `compare-swot`, `quadrant-quarter` | Pros/Cons, SWOT, Quadrants |
| **Charts & Data** | `chart-bar`, `chart-line`, `chart-pie` | Trends, Distributions, Metrics |
## 📝 Syntax Example (For Advanced Users) ## 📝 Syntax Example (For Advanced Users)
@@ -54,18 +67,3 @@ data
- label Beautiful Design - label Beautiful Design
desc Uses AntV professional design standards desc Uses AntV professional design standards
``` ```
## 👨‍💻 Author
**jeff**
- GitHub: [Fu-Jie/awesome-openwebui](https://github.com/Fu-Jie/awesome-openwebui)
## 📄 License
MIT License
## Changelog
### v1.3.2
- Removed debug messages from output

View File

@@ -1,7 +1,19 @@
# 📊 智能信息图 (AntV Infographic) # 📊 智能信息图 (AntV Infographic)
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 1.4.1 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
基于 AntV Infographic 引擎的 Open WebUI 插件,能够将长文本内容一键转换为专业、美观的信息图表。 基于 AntV Infographic 引擎的 Open WebUI 插件,能够将长文本内容一键转换为专业、美观的信息图表。
## 🔥 v1.4.1 更新日志
-**PNG 上传**:信息图现在以 PNG 格式上传,与 Word 导出完美兼容。
- 🔧 **Canvas 转换**:使用浏览器 Canvas 高质量转换 SVG 为 PNG2倍缩放
### 此前: v1.4.0
-**默认模式变更**:默认输出模式调整为 `image`(静态图片)。
- 📱 **响应式尺寸**:图片模式下自动适应聊天容器宽度。
## ✨ 核心特性 ## ✨ 核心特性
- 🚀 **智能转换**:自动分析文本核心逻辑,提取关键点并生成结构化图表。 - 🚀 **智能转换**:自动分析文本核心逻辑,提取关键点并生成结构化图表。
@@ -11,15 +23,6 @@
- 🌈 **高度自定义**:支持深色/浅色模式,自动适配主题颜色,主标题加粗突出,卡片布局精美。 - 🌈 **高度自定义**:支持深色/浅色模式,自动适配主题颜色,主标题加粗突出,卡片布局精美。
- 📱 **响应式设计**:生成的图表在桌面端和移动端均有良好的展示效果。 - 📱 **响应式设计**:生成的图表在桌面端和移动端均有良好的展示效果。
## 🛠️ 支持的模板类型
| 分类 | 模板名称 | 适用场景 |
| :--- | :--- | :--- |
| **列表与层级** | `list-grid`, `tree-vertical`, `mindmap` | 功能亮点、组织架构、思维导图 |
| **顺序与关系** | `sequence-roadmap`, `relation-circle` | 发展历程、循环关系、步骤说明 |
| **对比与分析** | `compare-binary`, `compare-swot`, `quadrant-quarter` | 优劣势对比、SWOT 分析、象限图 |
| **图表与数据** | `chart-bar`, `chart-line`, `chart-pie` | 数据趋势、比例分布、数值对比 |
## 🚀 使用方法 ## 🚀 使用方法
1. **安装插件**:在 Open WebUI 插件市场搜索并安装。 1. **安装插件**:在 Open WebUI 插件市场搜索并安装。
@@ -38,6 +41,16 @@
| **最小文本长度 (MIN_TEXT_LENGTH)** | `100` | 触发分析所需的最小字符数,防止对过短的对话误操作。 | | **最小文本长度 (MIN_TEXT_LENGTH)** | `100` | 触发分析所需的最小字符数,防止对过短的对话误操作。 |
| **清除旧结果 (CLEAR_PREVIOUS_HTML)** | `False` | 每次生成是否清除之前的图表。若为 `False`,新图表将追加在下方。 | | **清除旧结果 (CLEAR_PREVIOUS_HTML)** | `False` | 每次生成是否清除之前的图表。若为 `False`,新图表将追加在下方。 |
| **上下文消息数 (MESSAGE_COUNT)** | `1` | 用于分析的最近消息条数。增加此值可让 AI 参考更多对话背景。 | | **上下文消息数 (MESSAGE_COUNT)** | `1` | 用于分析的最近消息条数。增加此值可让 AI 参考更多对话背景。 |
| **输出模式 (OUTPUT_MODE)** | `image` | `image` 为静态图片嵌入(默认,兼容性好),`html` 为交互式图表。 |
## 🛠️ 支持的模板类型
| 分类 | 模板名称 | 适用场景 |
| :--- | :--- | :--- |
| **列表与层级** | `list-grid`, `tree-vertical`, `mindmap` | 功能亮点、组织架构、思维导图 |
| **顺序与关系** | `sequence-roadmap`, `relation-circle` | 发展历程、循环关系、步骤说明 |
| **对比与分析** | `compare-binary`, `compare-swot`, `quadrant-quarter` | 优劣势对比、SWOT 分析、象限图 |
| **图表与数据** | `chart-bar`, `chart-line`, `chart-pie` | 数据趋势、比例分布、数值对比 |
## 📝 语法示例 (高级用户) ## 📝 语法示例 (高级用户)
@@ -54,18 +67,3 @@ data
- label 视觉精美 - label 视觉精美
desc 采用 AntV 专业设计规范 desc 采用 AntV 专业设计规范
``` ```
## 👨‍💻 作者
**jeff**
- GitHub: [Fu-Jie/awesome-openwebui](https://github.com/Fu-Jie/awesome-openwebui)
## 📄 许可证
MIT License
## 更新日志
### v1.3.2
- 移除输出中的调试信息

View File

@@ -3,12 +3,13 @@ title: 📊 Smart Infographic (AntV)
author: jeff author: jeff
author_url: https://github.com/Fu-Jie/awesome-openwebui author_url: https://github.com/Fu-Jie/awesome-openwebui
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogIDxsaW5lIHgxPSIxMiIgeTE9IjIwIiB4Mj0iMTIiIHkyPSIxMCIgLz4KICA8bGluZSB4MT0iMTgiIHkxPSIyMCIgeDI9IjE4IiB5Mj0iNCIgLz4KICA8bGluZSB4MT0iNiIgeTE9IjIwIiB4Mj0iNiIgeTI9IjE2IiAvPgo8L3N2Zz4= icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogIDxsaW5lIHgxPSIxMiIgeTE9IjIwIiB4Mj0iMTIiIHkyPSIxMCIgLz4KICA8bGluZSB4MT0iMTgiIHkxPSIyMCIgeDI9IjE4IiB5Mj0iNCIgLz4KICA8bGluZSB4MT0iNiIgeTE9IjIwIiB4Mj0iNiIgeTI9IjE2IiAvPgo8L3N2Zz4=
version: 1.3.2 version: 1.4.1
openwebui_id: ad6f0c7f-c571-4dea-821d-8e71697274cf
description: AI-powered infographic generator based on AntV Infographic. Supports professional templates, auto-icon matching, and SVG/PNG downloads. description: AI-powered infographic generator based on AntV Infographic. Supports professional templates, auto-icon matching, and SVG/PNG downloads.
""" """
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from typing import Optional, Dict, Any from typing import Optional, Dict, Any, Callable, Awaitable
import logging import logging
import time import time
import re import re
@@ -821,10 +822,54 @@ class Action:
default=1, default=1,
description="Number of recent messages to use for generation. Set to 1 for just the last message, or higher for more context.", description="Number of recent messages to use for generation. Set to 1 for just the last message, or higher for more context.",
) )
OUTPUT_MODE: str = Field(
default="image",
description="Output mode: 'html' for interactive HTML, or 'image' to embed as Markdown image (default).",
)
def __init__(self): def __init__(self):
self.valves = self.Valves() 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):
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):
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 ""
def _extract_infographic_syntax(self, llm_output: str) -> str: def _extract_infographic_syntax(self, llm_output: str) -> str:
"""Extract infographic syntax from LLM output""" """Extract infographic syntax from LLM output"""
match = re.search(r"```infographic\s*(.*?)\s*```", llm_output, re.DOTALL) match = re.search(r"```infographic\s*(.*?)\s*```", llm_output, re.DOTALL)
@@ -912,14 +957,359 @@ class Action:
return base_html.strip() return base_html.strip()
def _generate_image_js_code(
self,
unique_id: str,
chat_id: str,
message_id: str,
infographic_syntax: str,
) -> str:
"""Generate JavaScript code for frontend SVG rendering and image embedding"""
# Escape the syntax for JS embedding
syntax_escaped = (
infographic_syntax.replace("\\", "\\\\")
.replace("`", "\\`")
.replace("${", "\\${")
.replace("</script>", "<\\/script>")
)
return f"""
(async function() {{
const uniqueId = "{unique_id}";
const chatId = "{chat_id}";
const messageId = "{message_id}";
const defaultWidth = 1100;
const defaultHeight = 500;
// Auto-detect chat container width for responsive sizing
let svgWidth = defaultWidth;
let svgHeight = defaultHeight;
const chatContainer = document.getElementById('chat-container');
if (chatContainer) {{
const containerWidth = chatContainer.clientWidth;
if (containerWidth > 100) {{
// Use container width with padding (80% of container, leaving more space on the right)
svgWidth = Math.floor(containerWidth * 0.8);
// Maintain aspect ratio based on default dimensions
svgHeight = Math.floor(svgWidth * (defaultHeight / defaultWidth));
console.log("[Infographic Image] Auto-detected container width:", containerWidth, "-> SVG:", svgWidth, "x", svgHeight);
}}
}}
console.log("[Infographic Image] Starting render...");
console.log("[Infographic Image] chatId:", chatId, "messageId:", messageId);
try {{
// Load AntV Infographic if not loaded
if (typeof AntVInfographic === 'undefined') {{
console.log("[Infographic Image] Loading AntV Infographic...");
await new Promise((resolve, reject) => {{
const script = document.createElement('script');
script.src = 'https://unpkg.com/@antv/infographic@latest/dist/infographic.min.js';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
}});
}}
const {{ Infographic }} = AntVInfographic;
// Get syntax content
let syntaxContent = `{syntax_escaped}`;
console.log("[Infographic Image] Syntax length:", syntaxContent.length);
// Clean up syntax: remove code block markers
const backtick = String.fromCharCode(96);
const prefix = backtick + backtick + backtick + 'infographic';
const simplePrefix = backtick + backtick + backtick;
if (syntaxContent.toLowerCase().startsWith(prefix)) {{
syntaxContent = syntaxContent.substring(prefix.length).trim();
}} else if (syntaxContent.startsWith(simplePrefix)) {{
syntaxContent = syntaxContent.substring(simplePrefix.length).trim();
}}
if (syntaxContent.endsWith(simplePrefix)) {{
syntaxContent = syntaxContent.substring(0, syntaxContent.length - simplePrefix.length).trim();
}}
// Fix syntax: remove colons after keywords
syntaxContent = syntaxContent.replace(/^(data|items|children|theme|config):/gm, '$1');
syntaxContent = syntaxContent.replace(/(\\s)(children|items):/g, '$1$2');
// Ensure infographic prefix
if (!syntaxContent.trim().toLowerCase().startsWith('infographic')) {{
const firstWord = syntaxContent.trim().split(/\\s+/)[0].toLowerCase();
if (!['data', 'theme', 'design', 'items'].includes(firstWord)) {{
syntaxContent = 'infographic ' + syntaxContent;
}}
}}
// Template mapping
const TEMPLATE_MAPPING = {{
'list-grid': 'list-grid-compact-card',
'list-vertical': 'list-column-simple-vertical-arrow',
'tree-vertical': 'hierarchy-tree-tech-style-capsule-item',
'tree-horizontal': 'hierarchy-tree-lr-tech-style-capsule-item',
'mindmap': 'hierarchy-mindmap-branch-gradient-capsule-item',
'sequence-roadmap': 'sequence-roadmap-vertical-simple',
'sequence-zigzag': 'sequence-horizontal-zigzag-simple',
'sequence-horizontal': 'sequence-horizontal-zigzag-simple',
'relation-sankey': 'relation-sankey-simple',
'relation-circle': 'relation-circle-icon-badge',
'compare-binary': 'compare-binary-horizontal-simple-vs',
'compare-swot': 'compare-swot',
'quadrant-quarter': 'quadrant-quarter-simple-card',
'statistic-card': 'list-grid-compact-card',
'chart-bar': 'chart-bar-plain-text',
'chart-column': 'chart-column-simple',
'chart-line': 'chart-line-plain-text',
'chart-area': 'chart-area-simple',
'chart-pie': 'chart-pie-plain-text',
'chart-doughnut': 'chart-pie-donut-plain-text'
}};
for (const [key, value] of Object.entries(TEMPLATE_MAPPING)) {{
const regex = new RegExp(`infographic\\\\s+${{key}}(?=\\\\s|$)`, 'i');
if (regex.test(syntaxContent)) {{
syntaxContent = syntaxContent.replace(regex, `infographic ${{value}}`);
break;
}}
}}
// Create offscreen container
const container = document.createElement('div');
container.id = 'infographic-offscreen-' + uniqueId;
container.style.cssText = 'position:absolute;left:-9999px;top:-9999px;width:' + svgWidth + 'px;height:' + svgHeight + 'px;background:#ffffff;';
document.body.appendChild(container);
// Create infographic instance
const instance = new Infographic({{
container: '#' + container.id,
width: svgWidth,
height: svgHeight,
padding: 12,
}});
console.log("[Infographic Image] Rendering infographic...");
instance.render(syntaxContent);
// Wait for render to complete
await new Promise(resolve => setTimeout(resolve, 2000));
// Get SVG element
const svgEl = container.querySelector('svg');
if (!svgEl) {{
throw new Error('SVG element not found after rendering');
}}
// Get actual dimensions
const bbox = svgEl.getBoundingClientRect();
const width = bbox.width || svgWidth;
const height = bbox.height || svgHeight;
// Clone and prepare SVG for export
const clonedSvg = svgEl.cloneNode(true);
clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
clonedSvg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
clonedSvg.setAttribute('width', width);
clonedSvg.setAttribute('height', height);
// Add background rect
const bgRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
bgRect.setAttribute('width', '100%');
bgRect.setAttribute('height', '100%');
bgRect.setAttribute('fill', '#ffffff');
clonedSvg.insertBefore(bgRect, clonedSvg.firstChild);
// Serialize SVG to string
const svgData = new XMLSerializer().serializeToString(clonedSvg);
// Cleanup container
document.body.removeChild(container);
// Convert SVG to PNG using canvas for better compatibility
console.log("[Infographic Image] Converting SVG to PNG...");
const pngBlob = await new Promise((resolve, reject) => {{
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const scale = 2; // Higher resolution for clarity
canvas.width = Math.round(width * scale);
canvas.height = Math.round(height * scale);
// Fill white background
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.scale(scale, scale);
const img = new Image();
img.onload = () => {{
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob((blob) => {{
if (blob) {{
resolve(blob);
}} else {{
reject(new Error('Canvas toBlob failed'));
}}
}}, 'image/png');
}};
img.onerror = (e) => reject(new Error('Failed to load SVG as image: ' + e));
img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData)));
}});
const file = new File([pngBlob], `infographic-${{uniqueId}}.png`, {{ type: 'image/png' }});
// Upload file to OpenWebUI API
console.log("[Infographic Image] Uploading PNG file...");
const token = localStorage.getItem("token");
const formData = new FormData();
formData.append('file', file);
const uploadResponse = await fetch('/api/v1/files/', {{
method: 'POST',
headers: {{
'Authorization': `Bearer ${{token}}`
}},
body: formData
}});
if (!uploadResponse.ok) {{
throw new Error(`Upload failed: ${{uploadResponse.statusText}}`);
}}
const fileData = await uploadResponse.json();
const fileId = fileData.id;
const imageUrl = `/api/v1/files/${{fileId}}/content`;
console.log("[Infographic Image] PNG file uploaded, ID:", fileId);
// Generate markdown image with file URL
const markdownImage = `![📊 Infographic](${{imageUrl}})`;
// Update message via API
if (chatId && messageId) {{
// Helper function with retry logic
const fetchWithRetry = async (url, options, retries = 3) => {{
for (let i = 0; i < retries; i++) {{
try {{
const response = await fetch(url, options);
if (response.ok) return response;
if (i < retries - 1) {{
console.log(`[Infographic Image] Retry ${{i + 1}}/${{retries}} for ${{url}}`);
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
}}
}} catch (e) {{
if (i === retries - 1) throw e;
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
}}
}}
return null;
}};
// Get current chat data
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();
let updatedMessages = [];
let newContent = "";
if (chatData.chat && chatData.chat.messages) {{
updatedMessages = chatData.chat.messages.map(m => {{
if (m.id === messageId) {{
const originalContent = m.content || "";
// Remove existing infographic images
const infographicPattern = /\\n*!\\[📊[^\\]]*\\]\\((?:data:image\\/[^)]+|(?:\\/api\\/v1\\/files\\/[^)]+))\\)/g;
let cleanedContent = originalContent.replace(infographicPattern, "");
cleanedContent = cleanedContent.replace(/\\n{{3,}}/g, "\\n\\n").trim();
// Append new image
newContent = cleanedContent + "\\n\\n" + markdownImage;
// Update history object as well
if (chatData.chat.history && chatData.chat.history.messages) {{
if (chatData.chat.history.messages[messageId]) {{
chatData.chat.history.messages[messageId].content = newContent;
}}
}}
return {{ ...m, content: newContent }};
}}
return m;
}});
}}
if (!newContent) {{
console.warn("[Infographic Image] Could not find message to update");
return;
}}
// Try to update frontend display via event API
try {{
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 }}
}})
}});
}} catch (eventErr) {{
console.log("[Infographic Image] Event API not available, continuing...");
}}
// Persist to database
const updatePayload = {{
chat: {{
...chatData.chat,
messages: updatedMessages
}}
}};
const persistResponse = await fetchWithRetry(`/api/v1/chats/${{chatId}}`, {{
method: "POST",
headers: {{
"Content-Type": "application/json",
"Authorization": `Bearer ${{token}}`
}},
body: JSON.stringify(updatePayload)
}});
if (persistResponse && persistResponse.ok) {{
console.log("[Infographic Image] ✅ Message persisted successfully!");
}} else {{
console.error("[Infographic Image] ❌ Failed to persist message after retries");
}}
}} else {{
console.warn("[Infographic Image] ⚠️ Missing chatId or messageId, cannot persist");
}}
}} catch (error) {{
console.error("[Infographic Image] Error:", error);
}}
}})();
"""
async def action( async def action(
self, self,
body: dict, body: dict,
__user__: Optional[Dict[str, Any]] = None, __user__: Optional[Dict[str, Any]] = None,
__event_emitter__: Optional[Any] = None, __event_emitter__: Optional[Any] = None,
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
__metadata__: Optional[dict] = None,
__request__: Optional[Request] = None, __request__: Optional[Request] = None,
) -> Optional[dict]: ) -> Optional[dict]:
logger.info("Action: Infographic started (v1.0.0)") logger.info("Action: Infographic started (v1.4.0)")
# Get user information # Get user information
if isinstance(__user__, (list, tuple)): if isinstance(__user__, (list, tuple)):
@@ -1114,6 +1504,45 @@ class Action:
user_language, user_language,
) )
# Check output mode
if self.valves.OUTPUT_MODE == "image":
# Image mode: use JavaScript to render and embed as Markdown image
chat_id = self._extract_chat_id(body, body.get("metadata"))
message_id = self._extract_message_id(body, body.get("metadata"))
await self._emit_status(
__event_emitter__,
"📊 Infographic: Rendering image...",
False,
)
if __event_call__:
js_code = self._generate_image_js_code(
unique_id=unique_id,
chat_id=chat_id,
message_id=message_id,
infographic_syntax=infographic_syntax,
)
await __event_call__(
{
"type": "execute",
"data": {"code": js_code},
}
)
await self._emit_status(
__event_emitter__, "✅ Infographic: Image generated!", True
)
await self._emit_notification(
__event_emitter__,
f"📊 Infographic image generated, {user_name}!",
"success",
)
logger.info("Infographic generation completed in image mode")
return body
# HTML mode (default): embed as HTML block
html_embed_tag = f"```html\n{final_html}\n```" html_embed_tag = f"```html\n{final_html}\n```"
body["messages"][-1]["content"] = f"{original_content}\n\n{html_embed_tag}" body["messages"][-1]["content"] = f"{original_content}\n\n{html_embed_tag}"

View File

@@ -3,12 +3,13 @@ title: 📊 智能信息图 (AntV Infographic)
author: jeff author: jeff
author_url: https://github.com/Fu-Jie/awesome-openwebui author_url: https://github.com/Fu-Jie/awesome-openwebui
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogIDxsaW5lIHgxPSIxMiIgeTE9IjIwIiB4Mj0iMTIiIHkyPSIxMCIgLz4KICA8bGluZSB4MT0iMTgiIHkxPSIyMCIgeDI9IjE4IiB5Mj0iNCIgLz4KICA8bGluZSB4MT0iNiIgeTE9IjIwIiB4Mj0iNiIgeTI9IjE2IiAvPgo8L3N2Zz4= icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogIDxsaW5lIHgxPSIxMiIgeTE9IjIwIiB4Mj0iMTIiIHkyPSIxMCIgLz4KICA8bGluZSB4MT0iMTgiIHkxPSIyMCIgeDI9IjE4IiB5Mj0iNCIgLz4KICA8bGluZSB4MT0iNiIgeTE9IjIwIiB4Mj0iNiIgeTI9IjE2IiAvPgo8L3N2Zz4=
version: 1.3.2 version: 1.4.1
openwebui_id: e04a48ff-23ee-4a41-8ea7-66c19524e7c8
description: 基于 AntV Infographic 的智能信息图生成插件。支持多种专业模板,自动图标匹配,并提供 SVG/PNG 下载功能。 description: 基于 AntV Infographic 的智能信息图生成插件。支持多种专业模板,自动图标匹配,并提供 SVG/PNG 下载功能。
""" """
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from typing import Optional, Dict, Any from typing import Optional, Dict, Any, Callable, Awaitable
import logging import logging
import time import time
import re import re
@@ -849,6 +850,10 @@ class Action:
default=1, default=1,
description="用于生成的最近消息数量。设置为1仅使用最后一条消息更大值可包含更多上下文。", description="用于生成的最近消息数量。设置为1仅使用最后一条消息更大值可包含更多上下文。",
) )
OUTPUT_MODE: str = Field(
default="image",
description="输出模式:'html' 为交互式HTML'image' 将嵌入为Markdown图片默认",
)
def __init__(self): def __init__(self):
self.valves = self.Valves() self.valves = self.Valves()
@@ -862,6 +867,46 @@ class Action:
"Sunday": "星期日", "Sunday": "星期日",
} }
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")
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:
"""从 body 或 metadata 中提取 message_id"""
if isinstance(body, dict):
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 ""
def _extract_infographic_syntax(self, llm_output: str) -> str: def _extract_infographic_syntax(self, llm_output: str) -> str:
"""提取LLM输出中的infographic语法""" """提取LLM输出中的infographic语法"""
# 1. 优先匹配 ```infographic # 1. 优先匹配 ```infographic
@@ -973,14 +1018,359 @@ class Action:
return base_html.strip() return base_html.strip()
def _generate_image_js_code(
self,
unique_id: str,
chat_id: str,
message_id: str,
infographic_syntax: str,
) -> str:
"""生成前端 SVG 渲染和图片嵌入的 JavaScript 代码"""
# 转义语法以便在 JS 中嵌入
syntax_escaped = (
infographic_syntax.replace("\\", "\\\\")
.replace("`", "\\`")
.replace("${", "\\${")
.replace("</script>", "<\\/script>")
)
return f"""
(async function() {{
const uniqueId = "{unique_id}";
const chatId = "{chat_id}";
const messageId = "{message_id}";
const defaultWidth = 1100;
const defaultHeight = 500;
// 自动检测聊天容器宽度以实现响应式尺寸
let svgWidth = defaultWidth;
let svgHeight = defaultHeight;
const chatContainer = document.getElementById('chat-container');
if (chatContainer) {{
const containerWidth = chatContainer.clientWidth;
if (containerWidth > 100) {{
// 使用容器宽度的 80%(右边留更多空间)
svgWidth = Math.floor(containerWidth * 0.8);
// 根据默认尺寸保持宽高比
svgHeight = Math.floor(svgWidth * (defaultHeight / defaultWidth));
console.log("[Infographic Image] 自动检测容器宽度:", containerWidth, "-> SVG:", svgWidth, "x", svgHeight);
}}
}}
console.log("[Infographic Image] 开始渲染...");
console.log("[Infographic Image] chatId:", chatId, "messageId:", messageId);
try {{
// 加载 AntV Infographic如果未加载
if (typeof AntVInfographic === 'undefined') {{
console.log("[Infographic Image] 加载 AntV Infographic...");
await new Promise((resolve, reject) => {{
const script = document.createElement('script');
script.src = 'https://registry.npmmirror.com/@antv/infographic/0.2.1/files/dist/infographic.min.js';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
}});
}}
const {{ Infographic }} = AntVInfographic;
// 获取语法内容
let syntaxContent = `{syntax_escaped}`;
console.log("[Infographic Image] 语法长度:", syntaxContent.length);
// 清理语法:移除代码块标记
const backtick = String.fromCharCode(96);
const prefix = backtick + backtick + backtick + 'infographic';
const simplePrefix = backtick + backtick + backtick;
if (syntaxContent.toLowerCase().startsWith(prefix)) {{
syntaxContent = syntaxContent.substring(prefix.length).trim();
}} else if (syntaxContent.startsWith(simplePrefix)) {{
syntaxContent = syntaxContent.substring(simplePrefix.length).trim();
}}
if (syntaxContent.endsWith(simplePrefix)) {{
syntaxContent = syntaxContent.substring(0, syntaxContent.length - simplePrefix.length).trim();
}}
// 修复语法:移除关键字后的冒号
syntaxContent = syntaxContent.replace(/^(data|items|children|theme|config):/gm, '$1');
syntaxContent = syntaxContent.replace(/(\\s)(children|items):/g, '$1$2');
// 确保 infographic 前缀
if (!syntaxContent.trim().toLowerCase().startsWith('infographic')) {{
const firstWord = syntaxContent.trim().split(/\\s+/)[0].toLowerCase();
if (!['data', 'theme', 'design', 'items'].includes(firstWord)) {{
syntaxContent = 'infographic ' + syntaxContent;
}}
}}
// 模板映射
const TEMPLATE_MAPPING = {{
'list-grid': 'list-grid-compact-card',
'list-vertical': 'list-column-simple-vertical-arrow',
'tree-vertical': 'hierarchy-tree-tech-style-capsule-item',
'tree-horizontal': 'hierarchy-tree-lr-tech-style-capsule-item',
'mindmap': 'hierarchy-mindmap-branch-gradient-capsule-item',
'sequence-roadmap': 'sequence-roadmap-vertical-simple',
'sequence-zigzag': 'sequence-horizontal-zigzag-simple',
'sequence-horizontal': 'sequence-horizontal-zigzag-simple',
'relation-sankey': 'relation-sankey-simple',
'relation-circle': 'relation-circle-icon-badge',
'compare-binary': 'compare-binary-horizontal-simple-vs',
'compare-swot': 'compare-swot',
'quadrant-quarter': 'quadrant-quarter-simple-card',
'statistic-card': 'list-grid-compact-card',
'chart-bar': 'chart-bar-plain-text',
'chart-column': 'chart-column-simple',
'chart-line': 'chart-line-plain-text',
'chart-area': 'chart-area-simple',
'chart-pie': 'chart-pie-plain-text',
'chart-doughnut': 'chart-pie-donut-plain-text'
}};
for (const [key, value] of Object.entries(TEMPLATE_MAPPING)) {{
const regex = new RegExp(`infographic\\\\s+${{key}}(?=\\\\s|$)`, 'i');
if (regex.test(syntaxContent)) {{
syntaxContent = syntaxContent.replace(regex, `infographic ${{value}}`);
break;
}}
}}
// 创建离屏容器
const container = document.createElement('div');
container.id = 'infographic-offscreen-' + uniqueId;
container.style.cssText = 'position:absolute;left:-9999px;top:-9999px;width:' + svgWidth + 'px;height:' + svgHeight + 'px;background:#ffffff;';
document.body.appendChild(container);
// 创建信息图实例
const instance = new Infographic({{
container: '#' + container.id,
width: svgWidth,
height: svgHeight,
padding: 12,
}});
console.log("[Infographic Image] 渲染信息图...");
instance.render(syntaxContent);
// 等待渲染完成
await new Promise(resolve => setTimeout(resolve, 2000));
// 获取 SVG 元素
const svgEl = container.querySelector('svg');
if (!svgEl) {{
throw new Error('渲染后未找到 SVG 元素');
}}
// 获取实际尺寸
const bbox = svgEl.getBoundingClientRect();
const width = bbox.width || svgWidth;
const height = bbox.height || svgHeight;
// 克隆并准备导出的 SVG
const clonedSvg = svgEl.cloneNode(true);
clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
clonedSvg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
clonedSvg.setAttribute('width', width);
clonedSvg.setAttribute('height', height);
// 添加背景矩形
const bgRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
bgRect.setAttribute('width', '100%');
bgRect.setAttribute('height', '100%');
bgRect.setAttribute('fill', '#ffffff');
clonedSvg.insertBefore(bgRect, clonedSvg.firstChild);
// 序列化 SVG 为字符串
const svgData = new XMLSerializer().serializeToString(clonedSvg);
// 清理容器
document.body.removeChild(container);
// 使用 canvas 将 SVG 转换为 PNG 以提高兼容性
console.log("[Infographic Image] 正在将 SVG 转换为 PNG...");
const pngBlob = await new Promise((resolve, reject) => {{
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const scale = 2; // 更高分辨率以提高清晰度
canvas.width = Math.round(width * scale);
canvas.height = Math.round(height * scale);
// 填充白色背景
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.scale(scale, scale);
const img = new Image();
img.onload = () => {{
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob((blob) => {{
if (blob) {{
resolve(blob);
}} else {{
reject(new Error('Canvas toBlob 失败'));
}}
}}, 'image/png');
}};
img.onerror = (e) => reject(new Error('加载 SVG 图片失败: ' + e));
img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData)));
}});
const file = new File([pngBlob], `infographic-${{uniqueId}}.png`, {{ type: 'image/png' }});
// 上传文件到 OpenWebUI API
console.log("[Infographic Image] 上传 PNG 文件...");
const token = localStorage.getItem("token");
const formData = new FormData();
formData.append('file', file);
const uploadResponse = await fetch('/api/v1/files/', {{
method: 'POST',
headers: {{
'Authorization': `Bearer ${{token}}`
}},
body: formData
}});
if (!uploadResponse.ok) {{
throw new Error(`上传失败: ${{uploadResponse.statusText}}`);
}}
const fileData = await uploadResponse.json();
const fileId = fileData.id;
const imageUrl = `/api/v1/files/${{fileId}}/content`;
console.log("[Infographic Image] PNG 文件已上传, ID:", fileId);
// 生成带文件 URL 的 markdown 图片
const markdownImage = `![📊 信息图](${{imageUrl}})`;
// 通过 API 更新消息
if (chatId && messageId) {{
// 带重试逻辑的辅助函数
const fetchWithRetry = async (url, options, retries = 3) => {{
for (let i = 0; i < retries; i++) {{
try {{
const response = await fetch(url, options);
if (response.ok) return response;
if (i < retries - 1) {{
console.log(`[Infographic Image] 重试 ${{i + 1}}/${{retries}} for ${{url}}`);
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
}}
}} catch (e) {{
if (i === retries - 1) throw e;
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
}}
}}
return null;
}};
// 获取当前聊天数据
const getResponse = await fetch(`/api/v1/chats/${{chatId}}`, {{
method: "GET",
headers: {{ "Authorization": `Bearer ${{token}}` }}
}});
if (!getResponse.ok) {{
throw new Error("获取聊天数据失败: " + getResponse.status);
}}
const chatData = await getResponse.json();
let updatedMessages = [];
let newContent = "";
if (chatData.chat && chatData.chat.messages) {{
updatedMessages = chatData.chat.messages.map(m => {{
if (m.id === messageId) {{
const originalContent = m.content || "";
// 移除已有的信息图图片
const infographicPattern = /\\n*!\\[📊[^\\]]*\\]\\((?:data:image\\/[^)]+|(?:\\/api\\/v1\\/files\\/[^)]+))\\)/g;
let cleanedContent = originalContent.replace(infographicPattern, "");
cleanedContent = cleanedContent.replace(/\\n{{3,}}/g, "\\n\\n").trim();
// 追加新图片
newContent = cleanedContent + "\\n\\n" + markdownImage;
// 同时更新 history 对象
if (chatData.chat.history && chatData.chat.history.messages) {{
if (chatData.chat.history.messages[messageId]) {{
chatData.chat.history.messages[messageId].content = newContent;
}}
}}
return {{ ...m, content: newContent }};
}}
return m;
}});
}}
if (!newContent) {{
console.warn("[Infographic Image] 找不到要更新的消息");
return;
}}
// 尝试通过事件 API 更新前端显示
try {{
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 }}
}})
}});
}} catch (eventErr) {{
console.log("[Infographic Image] 事件 API 不可用,继续...");
}}
// 持久化到数据库
const updatePayload = {{
chat: {{
...chatData.chat,
messages: updatedMessages
}}
}};
const persistResponse = await fetchWithRetry(`/api/v1/chats/${{chatId}}`, {{
method: "POST",
headers: {{
"Content-Type": "application/json",
"Authorization": `Bearer ${{token}}`
}},
body: JSON.stringify(updatePayload)
}});
if (persistResponse && persistResponse.ok) {{
console.log("[Infographic Image] ✅ 消息持久化成功!");
}} else {{
console.error("[Infographic Image] ❌ 重试后消息持久化失败");
}}
}} else {{
console.warn("[Infographic Image] ⚠️ 缺少 chatId 或 messageId无法持久化");
}}
}} catch (error) {{
console.error("[Infographic Image] 错误:", error);
}}
}})();
"""
async def action( async def action(
self, self,
body: dict, body: dict,
__user__: Optional[Dict[str, Any]] = None, __user__: Optional[Dict[str, Any]] = None,
__event_emitter__: Optional[Any] = None, __event_emitter__: Optional[Any] = None,
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
__metadata__: Optional[dict] = None,
__request__: Optional[Request] = None, __request__: Optional[Request] = None,
) -> Optional[dict]: ) -> Optional[dict]:
logger.info("Action: 信息图启动 (v1.0.0)") logger.info("Action: 信息图启动 (v1.4.0)")
# 获取用户信息 # 获取用户信息
if isinstance(__user__, (list, tuple)): if isinstance(__user__, (list, tuple)):
@@ -1169,6 +1559,45 @@ class Action:
user_language, user_language,
) )
# 检查输出模式
if self.valves.OUTPUT_MODE == "image":
# 图片模式:使用 JavaScript 渲染并嵌入为 Markdown 图片
chat_id = self._extract_chat_id(body, body.get("metadata"))
message_id = self._extract_message_id(body, body.get("metadata"))
await self._emit_status(
__event_emitter__,
"📊 信息图: 正在渲染图片...",
False,
)
if __event_call__:
js_code = self._generate_image_js_code(
unique_id=unique_id,
chat_id=chat_id,
message_id=message_id,
infographic_syntax=infographic_syntax,
)
await __event_call__(
{
"type": "execute",
"data": {"code": js_code},
}
)
await self._emit_status(
__event_emitter__, "✅ 信息图: 图片生成完成!", True
)
await self._emit_notification(
__event_emitter__,
f"📊 信息图图片已生成,{user_name}",
"success",
)
logger.info("信息图生成完成(图片模式)")
return body
# HTML 模式(默认):嵌入为 HTML 块
html_embed_tag = f"```html\n{final_html}\n```" html_embed_tag = f"```html\n{final_html}\n```"
body["messages"][-1]["content"] = f"{original_content}\n\n{html_embed_tag}" body["messages"][-1]["content"] = f"{original_content}\n\n{html_embed_tag}"

View File

@@ -8,6 +8,25 @@ Smart Mind Map is a powerful OpenWebUI action plugin that intelligently analyzes
--- ---
## 🔥 What's New in v0.9.1
**New Feature: Image Output Mode**
- **Static Image Support**: Added `OUTPUT_MODE` configuration parameter.
- `html` (default): Interactive HTML mind map.
- `image`: Static SVG image embedded directly in Markdown (**No HTML code output**, cleaner chat history).
- **Efficient Storage**: Image mode uploads SVG to `/api/v1/files`, avoiding huge base64 strings in chat history.
- **Smart Features**: Auto-responsive width and automatic theme detection (light/dark) for generated images.
| Feature | HTML Mode (Default) | Image Mode |
| :--- | :--- | :--- |
| **Output Format** | Interactive HTML Block | Static Markdown Image |
| **Interactivity** | Zoom, Pan, Expand/Collapse | None (Static Image) |
| **Chat History** | Contains HTML Code | Clean (Image URL only) |
| **Storage** | Browser Rendering | `/api/v1/files` Upload |
---
## Core Features ## Core Features
-**Intelligent Text Analysis**: Automatically identifies core themes, key concepts, and hierarchical structures -**Intelligent Text Analysis**: Automatically identifies core themes, key concepts, and hierarchical structures
@@ -20,7 +39,7 @@ Smart Mind Map is a powerful OpenWebUI action plugin that intelligently analyzes
-**Real-time Rendering**: Renders mind maps directly in the chat interface without navigation -**Real-time Rendering**: Renders mind maps directly in the chat interface without navigation
-**Export Capabilities**: Supports PNG, SVG code, and Markdown source export -**Export Capabilities**: Supports PNG, SVG code, and Markdown source export
-**Customizable Configuration**: Configurable LLM model, minimum text length, and other parameters -**Customizable Configuration**: Configurable LLM model, minimum text length, and other parameters
-**Image Output Mode**: Generate static SVG images embedded directly in Markdown (no interactive HTML) -**Image Output Mode**: Generate static SVG images embedded directly in Markdown (**No HTML code output**, cleaner chat history)
--- ---

View File

@@ -8,6 +8,25 @@
--- ---
## 🔥 v0.9.1 更新亮点
**新功能:图片输出模式**
- **静态图片支持**:新增 `OUTPUT_MODE` 配置参数。
- `html`(默认):交互式 HTML 思维导图。
- `image`:静态 SVG 图片直接嵌入 Markdown**不输出 HTML 代码**,聊天记录更简洁)。
- **高效存储**:图片模式将 SVG 上传至 `/api/v1/files`,避免聊天记录中出现超长 Base64 字符串。
- **智能特性**:生成的图片支持自动响应式宽度和自动主题检测(亮色/暗色)。
| 特性 | HTML 模式 (默认) | 图片模式 |
| :--- | :--- | :--- |
| **输出格式** | 交互式 HTML 代码块 | 静态 Markdown 图片 |
| **交互性** | 缩放、拖拽、展开/折叠 | 无 (静态图片) |
| **聊天记录** | 包含 HTML 代码 | 简洁 (仅图片链接) |
| **存储方式** | 浏览器实时渲染 | `/api/v1/files` 上传 |
---
## 核心特性 ## 核心特性
-**智能文本分析**:自动识别文本的核心主题、关键概念和层次结构 -**智能文本分析**:自动识别文本的核心主题、关键概念和层次结构
@@ -20,7 +39,7 @@
-**实时渲染**:在聊天界面中直接渲染思维导图,无需跳转 -**实时渲染**:在聊天界面中直接渲染思维导图,无需跳转
-**导出功能**:支持 PNG、SVG 代码和 Markdown 源码导出 -**导出功能**:支持 PNG、SVG 代码和 Markdown 源码导出
-**自定义配置**:可配置 LLM 模型、最小文本长度等参数 -**自定义配置**:可配置 LLM 模型、最小文本长度等参数
-**图片输出模式**:生成静态 SVG 图片直接嵌入 Markdown无交互式 HTML -**图片输出模式**:生成静态 SVG 图片直接嵌入 Markdown**不输出 HTML 代码**,聊天记录更简洁
--- ---

View File

@@ -4,6 +4,7 @@ author: Fu-Jie
author_url: https://github.com/Fu-Jie author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui funding_url: https://github.com/Fu-Jie/awesome-openwebui
version: 0.9.1 version: 0.9.1
openwebui_id: 3094c59a-b4dd-4e0c-9449-15e2dd547dc4
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4= 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. description: Intelligently analyzes text content and generates interactive mind maps to help users structure and visualize knowledge.
""" """

View File

@@ -4,6 +4,7 @@ author: Fu-Jie
author_url: https://github.com/Fu-Jie author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui funding_url: https://github.com/Fu-Jie/awesome-openwebui
version: 0.9.1 version: 0.9.1
openwebui_id: 8d4b097b-219b-4dd2-b509-05fbe6388335
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4= icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4=
description: 智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。 description: 智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。
""" """

View File

@@ -6,6 +6,7 @@ author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui funding_url: https://github.com/Fu-Jie/awesome-openwebui
description: Reduces token consumption in long conversations while maintaining coherence through intelligent summarization and message compression. description: Reduces token consumption in long conversations while maintaining coherence through intelligent summarization and message compression.
version: 1.1.0 version: 1.1.0
openwebui_id: b1655bc8-6de9-4cad-8cb5-a6f7829a02ce
license: MIT license: MIT
═══════════════════════════════════════════════════════════════════════════════ ═══════════════════════════════════════════════════════════════════════════════

View File

@@ -6,6 +6,7 @@ author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui funding_url: https://github.com/Fu-Jie/awesome-openwebui
description: 通过智能摘要和消息压缩,降低长对话的 token 消耗,同时保持对话连贯性。 description: 通过智能摘要和消息压缩,降低长对话的 token 消耗,同时保持对话连贯性。
version: 1.1.0 version: 1.1.0
openwebui_id: 5c0617cb-a9e4-4bd6-a440-d276534ebd18
license: MIT license: MIT
═══════════════════════════════════════════════════════════════════════════════ ═══════════════════════════════════════════════════════════════════════════════

View File

@@ -140,22 +140,49 @@ def compare_versions(current: list[dict], previous_file: str) -> dict[str, list[
return {"added": current, "updated": [], "removed": []} return {"added": current, "updated": [], "removed": []}
# Create lookup dictionaries by title # Create lookup dictionaries by title
current_by_title = {p["title"]: p for p in current} # Helper to extract title/version from either simple dict or raw post object
previous_by_title = {p["title"]: p for p in previous} def get_info(p):
if "data" in p and "function" in p["data"]:
# It's a raw post object
manifest = p["data"]["function"].get("meta", {}).get("manifest", {})
title = manifest.get("title") or p.get("title")
version = manifest.get("version", "0.0.0")
return title, version, p
else:
# It's a simple dict
return p.get("title"), p.get("version"), p
current_by_title = {}
for p in current:
title, _, _ = get_info(p)
if title:
current_by_title[title] = p
previous_by_title = {}
for p in previous:
title, _, _ = get_info(p)
if title:
previous_by_title[title] = p
result = {"added": [], "updated": [], "removed": []} result = {"added": [], "updated": [], "removed": []}
# Find added and updated plugins # Find added and updated plugins
for title, plugin in current_by_title.items(): for title, plugin in current_by_title.items():
curr_title, curr_ver, _ = get_info(plugin)
if title not in previous_by_title: if title not in previous_by_title:
result["added"].append(plugin) result["added"].append(plugin)
elif plugin["version"] != previous_by_title[title]["version"]: else:
result["updated"].append( prev_plugin = previous_by_title[title]
{ _, prev_ver, _ = get_info(prev_plugin)
"current": plugin,
"previous": previous_by_title[title], if curr_ver != prev_ver:
} result["updated"].append(
) {
"current": plugin,
"previous": prev_plugin,
}
)
# Find removed plugins # Find removed plugins
for title, plugin in previous_by_title.items(): for title, plugin in previous_by_title.items():
@@ -212,9 +239,26 @@ def format_release_notes(
for update in comparison["updated"]: for update in comparison["updated"]:
curr = update["current"] curr = update["current"]
prev = update["previous"] prev = update["previous"]
lines.append(
f"- **{curr['title']}**: v{prev['version']} → v{curr['version']}" # Extract info safely
curr_manifest = (
curr.get("data", {})
.get("function", {})
.get("meta", {})
.get("manifest", {})
) )
curr_title = curr_manifest.get("title") or curr.get("title")
curr_ver = curr_manifest.get("version") or curr.get("version")
prev_manifest = (
prev.get("data", {})
.get("function", {})
.get("meta", {})
.get("manifest", {})
)
prev_ver = prev_manifest.get("version") or prev.get("version")
lines.append(f"- **{curr_title}**: v{prev_ver} → v{curr_ver}")
lines.append("") lines.append("")
if comparison["removed"] and not ignore_removed: if comparison["removed"] and not ignore_removed:

View File

@@ -0,0 +1,47 @@
"""
Fetch remote plugin versions from OpenWebUI Community
获取远程插件版本信息
"""
import json
import os
import sys
# Add current directory to path
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from openwebui_community_client import get_client
def main():
try:
client = get_client()
except ValueError as e:
print(f"Error: {e}")
sys.exit(1)
print("Fetching remote plugins from OpenWebUI Community...")
try:
posts = client.get_all_posts()
except Exception as e:
print(f"Error fetching posts: {e}")
sys.exit(1)
formatted_plugins = []
for post in posts:
post["type"] = "remote_plugin"
formatted_plugins.append(post)
output_file = "remote_versions.json"
with open(output_file, "w", encoding="utf-8") as f:
json.dump(formatted_plugins, f, indent=2, ensure_ascii=False)
print(
f"✅ Successfully saved {len(formatted_plugins)} remote plugins to {output_file}"
)
print(f" You can now compare local vs remote using:")
print(f" python scripts/extract_plugin_versions.py --compare {output_file}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,374 @@
"""
OpenWebUI Community Client
统一封装所有与 OpenWebUI 官方社区 (openwebui.com) 的 API 交互。
功能:
- 获取用户发布的插件/帖子
- 更新插件内容和元数据
- 版本比较
- 同步插件 ID
使用方法:
from openwebui_community_client import OpenWebUICommunityClient
client = OpenWebUICommunityClient(api_key="your_api_key")
posts = client.get_all_posts()
"""
import os
import re
import json
import base64
import requests
from datetime import datetime, timezone, timedelta
from typing import Optional, Dict, List, Any, Tuple
# 北京时区 (UTC+8)
BEIJING_TZ = timezone(timedelta(hours=8))
class OpenWebUICommunityClient:
"""OpenWebUI 官方社区 API 客户端"""
BASE_URL = "https://api.openwebui.com/api/v1"
def __init__(self, api_key: str, user_id: Optional[str] = None):
"""
初始化客户端
Args:
api_key: OpenWebUI API Key (JWT Token)
user_id: 用户 ID如果为 None 则从 token 中解析
"""
self.api_key = api_key
self.user_id = user_id or self._parse_user_id_from_token(api_key)
self.headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Accept": "application/json",
}
def _parse_user_id_from_token(self, token: str) -> Optional[str]:
"""从 JWT Token 中解析用户 ID"""
try:
parts = token.split(".")
if len(parts) >= 2:
payload = parts[1]
# 添加 padding
padding = 4 - len(payload) % 4
if padding != 4:
payload += "=" * padding
decoded = base64.urlsafe_b64decode(payload)
data = json.loads(decoded)
return data.get("id") or data.get("sub")
except Exception:
pass
return None
# ========== 帖子/插件获取 ==========
def get_user_posts(self, sort: str = "new", page: int = 1) -> List[Dict]:
"""
获取用户发布的帖子列表
Args:
sort: 排序方式 (new/top/hot)
page: 页码
Returns:
帖子列表
"""
url = f"{self.BASE_URL}/posts/user/{self.user_id}?sort={sort}&page={page}"
response = requests.get(url, headers=self.headers)
response.raise_for_status()
return response.json()
def get_all_posts(self, sort: str = "new") -> List[Dict]:
"""获取所有帖子(自动分页)"""
all_posts = []
page = 1
while True:
posts = self.get_user_posts(sort=sort, page=page)
if not posts:
break
all_posts.extend(posts)
page += 1
return all_posts
def get_post(self, post_id: str) -> Optional[Dict]:
"""
获取单个帖子详情
Args:
post_id: 帖子 ID
Returns:
帖子数据,如果不存在返回 None
"""
try:
url = f"{self.BASE_URL}/posts/{post_id}"
response = requests.get(url, headers=self.headers)
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
if e.response.status_code == 404:
return None
raise
# ========== 帖子/插件更新 ==========
def update_post(self, post_id: str, post_data: Dict) -> bool:
"""
更新帖子
Args:
post_id: 帖子 ID
post_data: 完整的帖子数据
Returns:
是否成功
"""
url = f"{self.BASE_URL}/posts/{post_id}/update"
response = requests.post(url, headers=self.headers, json=post_data)
response.raise_for_status()
return True
def update_plugin(
self,
post_id: str,
source_code: str,
readme_content: Optional[str] = None,
metadata: Optional[Dict] = None,
) -> bool:
"""
更新插件(代码 + README + 元数据)
Args:
post_id: 帖子 ID
source_code: 插件源代码
readme_content: README 内容(用于社区页面展示)
metadata: 插件元数据title, version, description 等)
Returns:
是否成功
"""
post_data = self.get_post(post_id)
if not post_data:
return False
# 确保结构存在
if "data" not in post_data:
post_data["data"] = {}
if "function" not in post_data["data"]:
post_data["data"]["function"] = {}
if "meta" not in post_data["data"]["function"]:
post_data["data"]["function"]["meta"] = {}
if "manifest" not in post_data["data"]["function"]["meta"]:
post_data["data"]["function"]["meta"]["manifest"] = {}
# 更新源代码
post_data["data"]["function"]["content"] = source_code
# 更新 README社区页面展示内容
if readme_content:
post_data["content"] = readme_content
# 更新元数据
if metadata:
post_data["data"]["function"]["meta"]["manifest"].update(metadata)
if "title" in metadata:
post_data["title"] = metadata["title"]
post_data["data"]["function"]["name"] = metadata["title"]
if "description" in metadata:
post_data["data"]["function"]["meta"]["description"] = metadata[
"description"
]
return self.update_post(post_id, post_data)
# ========== 版本比较 ==========
def get_remote_version(self, post_id: str) -> Optional[str]:
"""
获取远程插件版本
Args:
post_id: 帖子 ID
Returns:
版本号,如果不存在返回 None
"""
post_data = self.get_post(post_id)
if not post_data:
return None
return (
post_data.get("data", {})
.get("function", {})
.get("meta", {})
.get("manifest", {})
.get("version")
)
def version_needs_update(self, post_id: str, local_version: str) -> bool:
"""
检查是否需要更新
Args:
post_id: 帖子 ID
local_version: 本地版本号
Returns:
如果本地版本与远程不同,返回 True
"""
remote_version = self.get_remote_version(post_id)
if not remote_version:
return True # 远程不存在,需要更新
return local_version != remote_version
# ========== 插件发布 ==========
def publish_plugin_from_file(
self, file_path: str, force: bool = False
) -> Tuple[bool, str]:
"""
从文件发布插件
Args:
file_path: 插件文件路径
force: 是否强制更新(忽略版本检查)
Returns:
(是否成功, 消息)
"""
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
metadata = self._parse_frontmatter(content)
if not metadata:
return False, "No frontmatter found"
post_id = metadata.get("openwebui_id") or metadata.get("post_id")
if not post_id:
return False, "No openwebui_id found"
local_version = metadata.get("version")
# 版本检查
if not force and local_version:
if not self.version_needs_update(post_id, local_version):
return True, f"Skipped: version {local_version} matches remote"
# 查找 README
readme_content = self._find_readme(file_path)
# 更新
success = self.update_plugin(
post_id=post_id,
source_code=content,
readme_content=readme_content or metadata.get("description", ""),
metadata=metadata,
)
if success:
return True, f"Updated to version {local_version}"
return False, "Update failed"
def _parse_frontmatter(self, content: str) -> Dict[str, str]:
"""解析插件文件的 frontmatter"""
match = re.search(r'^\s*"""\n(.*?)\n"""', content, re.DOTALL)
if not match:
match = re.search(r'"""\n(.*?)\n"""', content, re.DOTALL)
if not match:
return {}
frontmatter = match.group(1)
meta = {}
for line in frontmatter.split("\n"):
if ":" in line:
key, value = line.split(":", 1)
meta[key.strip()] = value.strip()
return meta
def _find_readme(self, plugin_file_path: str) -> Optional[str]:
"""查找插件对应的 README 文件"""
plugin_dir = os.path.dirname(plugin_file_path)
base_name = os.path.basename(plugin_file_path).lower()
# 确定优先顺序
if base_name.endswith("_cn.py"):
readme_files = ["README_CN.md", "README.md"]
else:
readme_files = ["README.md", "README_CN.md"]
for readme_name in readme_files:
readme_path = os.path.join(plugin_dir, readme_name)
if os.path.exists(readme_path):
with open(readme_path, "r", encoding="utf-8") as f:
return f.read()
return None
# ========== 统计功能 ==========
def generate_stats(self, posts: List[Dict]) -> Dict:
"""
生成统计数据
Args:
posts: 帖子列表
Returns:
统计数据字典
"""
stats = {
"total_posts": len(posts),
"total_downloads": 0,
"total_likes": 0,
"posts_by_type": {},
"posts_detail": [],
"generated_at": datetime.now(BEIJING_TZ).isoformat(),
}
for post in posts:
downloads = post.get("downloadCount", 0)
likes = post.get("likeCount", 0)
post_type = post.get("type", "unknown")
stats["total_downloads"] += downloads
stats["total_likes"] += likes
stats["posts_by_type"][post_type] = (
stats["posts_by_type"].get(post_type, 0) + 1
)
stats["posts_detail"].append(
{
"id": post.get("id"),
"title": post.get("title"),
"type": post_type,
"downloads": downloads,
"likes": likes,
"created_at": post.get("createdAt"),
"updated_at": post.get("updatedAt"),
}
)
# 按下载量排序
stats["posts_detail"].sort(key=lambda x: x["downloads"], reverse=True)
return stats
# 便捷函数
def get_client(api_key: Optional[str] = None) -> OpenWebUICommunityClient:
"""
获取客户端实例
Args:
api_key: API Key如果为 None 则从环境变量获取
Returns:
OpenWebUICommunityClient 实例
"""
key = api_key or os.environ.get("OPENWEBUI_API_KEY")
if not key:
raise ValueError("OPENWEBUI_API_KEY not set")
return OpenWebUICommunityClient(key)

View File

@@ -339,8 +339,8 @@ class OpenWebUIStats:
stats: 统计数据 stats: 统计数据
lang: 语言 ("zh" 中文, "en" 英文) lang: 语言 ("zh" 中文, "en" 英文)
""" """
# 获取 Top 5 插件 # 获取 Top 6 插件
top_plugins = stats["posts"][:5] top_plugins = stats["posts"][:6]
# 中英文文本 # 中英文文本
texts = { texts = {
@@ -349,17 +349,17 @@ class OpenWebUIStats:
"updated": f"> 🕐 自动更新于 {get_beijing_time().strftime('%Y-%m-%d %H:%M')}", "updated": f"> 🕐 自动更新于 {get_beijing_time().strftime('%Y-%m-%d %H:%M')}",
"author_header": "| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |", "author_header": "| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |",
"header": "| 📝 发布 | ⬇️ 下载 | 👁️ 浏览 | 👍 点赞 | 💾 收藏 |", "header": "| 📝 发布 | ⬇️ 下载 | 👁️ 浏览 | 👍 点赞 | 💾 收藏 |",
"top5_title": "### 🔥 热门插件 Top 5", "top6_title": "### 🔥 热门插件 Top 6",
"top5_header": "| 排名 | 插件 | 下载 | 浏览 |", "top6_header": "| 排名 | 插件 | 下载 | 浏览 |",
"full_stats": "*完整统计请查看 [社区统计报告](./docs/community-stats.md)*", "full_stats": "*完整统计请查看 [社区统计报告](./docs/community-stats.zh.md)*",
}, },
"en": { "en": {
"title": "## 📊 Community Stats", "title": "## 📊 Community Stats",
"updated": f"> 🕐 Auto-updated: {get_beijing_time().strftime('%Y-%m-%d %H:%M')}", "updated": f"> 🕐 Auto-updated: {get_beijing_time().strftime('%Y-%m-%d %H:%M')}",
"author_header": "| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |", "author_header": "| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |",
"header": "| 📝 Posts | ⬇️ Downloads | 👁️ Views | 👍 Upvotes | 💾 Saves |", "header": "| 📝 Posts | ⬇️ Downloads | 👁️ Views | 👍 Upvotes | 💾 Saves |",
"top5_title": "### 🔥 Top 5 Popular Plugins", "top6_title": "### 🔥 Top 6 Popular Plugins",
"top5_header": "| Rank | Plugin | Downloads | Views |", "top6_header": "| Rank | Plugin | Downloads | Views |",
"full_stats": "*See full stats in [Community Stats Report](./docs/community-stats.md)*", "full_stats": "*See full stats in [Community Stats Report](./docs/community-stats.md)*",
}, },
} }
@@ -395,13 +395,13 @@ class OpenWebUIStats:
) )
lines.append("") lines.append("")
# Top 5 热门插件 # Top 6 热门插件
lines.append(t["top5_title"]) lines.append(t["top6_title"])
lines.append("") lines.append("")
lines.append(t["top5_header"]) lines.append(t["top6_header"])
lines.append("|:---:|------|:---:|:---:|") lines.append("|:---:|------|:---:|:---:|")
medals = ["🥇", "🥈", "🥉", "4", "5"] medals = ["🥇", "🥈", "🥉", "4", "5", "6"]
for i, post in enumerate(top_plugins): for i, post in enumerate(top_plugins):
medal = medals[i] if i < len(medals) else str(i + 1) medal = medals[i] if i < len(medals) else str(i + 1)
lines.append( lines.append(
@@ -520,14 +520,14 @@ def main():
script_dir = Path(__file__).parent.parent script_dir = Path(__file__).parent.parent
# 中文报告 # 中文报告
md_zh_path = script_dir / "docs" / "community-stats.md" md_zh_path = script_dir / "docs" / "community-stats.zh.md"
md_zh_content = stats_client.generate_markdown(stats, lang="zh") md_zh_content = stats_client.generate_markdown(stats, lang="zh")
with open(md_zh_path, "w", encoding="utf-8") as f: with open(md_zh_path, "w", encoding="utf-8") as f:
f.write(md_zh_content) f.write(md_zh_content)
print(f"\n✅ 中文报告已保存到: {md_zh_path}") print(f"\n✅ 中文报告已保存到: {md_zh_path}")
# 英文报告 # 英文报告
md_en_path = script_dir / "docs" / "community-stats.en.md" md_en_path = script_dir / "docs" / "community-stats.md"
md_en_content = stats_client.generate_markdown(stats, lang="en") md_en_content = stats_client.generate_markdown(stats, lang="en")
with open(md_en_path, "w", encoding="utf-8") as f: with open(md_en_path, "w", encoding="utf-8") as f:
f.write(md_en_content) f.write(md_en_content)

89
scripts/publish_plugin.py Normal file
View File

@@ -0,0 +1,89 @@
"""
Publish plugins to OpenWebUI Community
使用 OpenWebUICommunityClient 发布插件到官方社区
用法:
python scripts/publish_plugin.py # 只更新有版本变化的插件
python scripts/publish_plugin.py --force # 强制更新所有插件
"""
import os
import sys
import re
import argparse
# Add current directory to path
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from openwebui_community_client import OpenWebUICommunityClient, get_client
def find_plugins_with_id(plugins_dir: str) -> list:
"""查找所有带 openwebui_id 的插件文件"""
plugins = []
for root, _, files in os.walk(plugins_dir):
for file in files:
if file.endswith(".py"):
file_path = os.path.join(root, file)
with open(file_path, "r", encoding="utf-8") as f:
content = f.read(2000) # 只读前 2000 字符检查 ID
id_match = re.search(
r"(?:openwebui_id|post_id):\s*([a-z0-9-]+)", content
)
if id_match:
plugins.append(
{"file_path": file_path, "post_id": id_match.group(1).strip()}
)
return plugins
def main():
parser = argparse.ArgumentParser(description="Publish plugins to OpenWebUI Market")
parser.add_argument(
"--force", action="store_true", help="Force update even if version matches"
)
args = parser.parse_args()
try:
client = get_client()
except ValueError as e:
print(f"Error: {e}")
sys.exit(1)
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
plugins_dir = os.path.join(base_dir, "plugins")
plugins = find_plugins_with_id(plugins_dir)
print(f"Found {len(plugins)} plugins with OpenWebUI ID.\n")
updated = 0
skipped = 0
failed = 0
for plugin in plugins:
file_path = plugin["file_path"]
file_name = os.path.basename(file_path)
post_id = plugin["post_id"]
print(f"Processing {file_name} (ID: {post_id})...")
success, message = client.publish_plugin_from_file(file_path, force=args.force)
if success:
if "Skipped" in message:
print(f" ⏭️ {message}")
skipped += 1
else:
print(f"{message}")
updated += 1
else:
print(f"{message}")
failed += 1
print(f"\n{'='*50}")
print(f"Finished: {updated} updated, {skipped} skipped, {failed} failed")
if __name__ == "__main__":
main()

138
scripts/sync_plugin_ids.py Normal file
View File

@@ -0,0 +1,138 @@
"""
Sync OpenWebUI Post IDs to local plugin files
同步远程插件 ID 到本地文件
"""
import os
import sys
import re
import difflib
# Add current directory to path
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from openwebui_community_client import get_client
try:
from extract_plugin_versions import scan_plugins_directory
except ImportError:
print("Error: extract_plugin_versions.py not found.")
sys.exit(1)
def normalize(s):
if not s:
return ""
return re.sub(r"\s+", " ", s.lower().strip())
def insert_id_into_file(file_path, post_id):
with open(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
new_lines = []
inserted = False
in_frontmatter = False
for line in lines:
# Check for start/end of frontmatter
if line.strip() == '"""':
if not in_frontmatter:
in_frontmatter = True
else:
# End of frontmatter
in_frontmatter = False
# Check if ID already exists
if in_frontmatter and (
line.strip().startswith("openwebui_id:")
or line.strip().startswith("post_id:")
):
print(f" ID already exists in {os.path.basename(file_path)}")
return False
new_lines.append(line)
# Insert after version
if in_frontmatter and not inserted and line.strip().startswith("version:"):
new_lines.append(f"openwebui_id: {post_id}\n")
inserted = True
if inserted:
with open(file_path, "w", encoding="utf-8") as f:
f.writelines(new_lines)
return True
return False
def main():
try:
client = get_client()
except ValueError as e:
print(f"Error: {e}")
sys.exit(1)
print("Fetching remote posts from OpenWebUI Community...")
remote_posts = client.get_all_posts()
print(f"Fetched {len(remote_posts)} remote posts.")
plugins_dir = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "plugins"
)
local_plugins = scan_plugins_directory(plugins_dir)
print(f"Found {len(local_plugins)} local plugins.")
matched_count = 0
for plugin in local_plugins:
local_title = plugin.get("title", "")
if not local_title:
continue
file_path = plugin.get("file_path")
best_match = None
highest_ratio = 0.0
# 1. Try Exact Match on Manifest Title (High Confidence)
for post in remote_posts:
manifest_title = (
post.get("data", {})
.get("function", {})
.get("meta", {})
.get("manifest", {})
.get("title")
)
if manifest_title and normalize(manifest_title) == normalize(local_title):
best_match = post
highest_ratio = 1.0
break
# 2. Try Fuzzy Match on Post Title if no exact match
if not best_match:
for post in remote_posts:
post_title = post.get("title", "")
ratio = difflib.SequenceMatcher(
None, normalize(local_title), normalize(post_title)
).ratio()
if ratio > 0.8 and ratio > highest_ratio:
highest_ratio = ratio
best_match = post
if best_match:
post_id = best_match.get("id")
post_title = best_match.get("title")
print(
f"Match found: '{local_title}' <--> '{post_title}' (ID: {post_id}) [Score: {highest_ratio:.2f}]"
)
if insert_id_into_file(file_path, post_id):
print(f" -> Updated {os.path.basename(file_path)}")
matched_count += 1
else:
print(f"No match found for: '{local_title}'")
print(f"\nTotal updated: {matched_count}")
if __name__ == "__main__":
main()