Compare commits
80 Commits
v2026.01.1
...
v2026.01.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f650c64ffe | ||
|
|
6000c880de | ||
|
|
048fbb26d7 | ||
|
|
a88eda62cc | ||
|
|
957fb2dfb7 | ||
|
|
d2be5109ad | ||
|
|
80fdc52598 | ||
|
|
2b90ead3cf | ||
|
|
2aa5d77586 | ||
|
|
2b1b1ef939 | ||
|
|
4e21e06617 | ||
|
|
82ce1cef29 | ||
|
|
533eace74e | ||
|
|
83b3dcda65 | ||
|
|
e89373e0ed | ||
|
|
4b66a2bb1c | ||
|
|
59ba23da63 | ||
|
|
f8a89e222c | ||
|
|
096568f3e6 | ||
|
|
e10e12ebc9 | ||
|
|
c4df5eba47 | ||
|
|
0da3d3d881 | ||
|
|
6f4a62d1bc | ||
|
|
5d71c2a4d3 | ||
|
|
097707c168 | ||
|
|
8f4cfceb50 | ||
|
|
4ab5fab7d0 | ||
|
|
0e293be8bc | ||
|
|
182c12f81a | ||
|
|
1337a90911 | ||
|
|
2f0a347ab3 | ||
|
|
4eda286512 | ||
|
|
0fead8158d | ||
|
|
031bef563a | ||
|
|
04c3fd2bf9 | ||
|
|
cbbf6118b5 | ||
|
|
4c529369ce | ||
|
|
797dea0d77 | ||
|
|
a91aee31de | ||
|
|
8511b7df80 | ||
|
|
afd1e7a444 | ||
|
|
34b2c3d6cf | ||
|
|
d5c099dd15 | ||
|
|
8810223693 | ||
|
|
84974a2fb9 | ||
|
|
af847293af | ||
|
|
a44e80ce5b | ||
|
|
c2815e13e9 | ||
|
|
56bfa3a3ef | ||
|
|
a13c915f27 | ||
|
|
fb2d35237e | ||
|
|
3f19ecfd20 | ||
|
|
2fd96f07aa | ||
|
|
a1c1ed9840 | ||
|
|
c63701d05f | ||
|
|
863805dc68 | ||
|
|
98f7dff458 | ||
|
|
08c0dd984c | ||
|
|
e870ad8823 | ||
|
|
d687fffdb5 | ||
|
|
d534d8b319 | ||
|
|
d5c5158726 | ||
|
|
888026876f | ||
|
|
06e8d30900 | ||
|
|
cbf2ff7f93 | ||
|
|
abbe3fb248 | ||
|
|
7e44dde979 | ||
|
|
3649d75539 | ||
|
|
d3b4219a9a | ||
|
|
9e98d55e11 | ||
|
|
4b8515f682 | ||
|
|
d2f35ce396 | ||
|
|
f479f23b38 | ||
|
|
51048f9e5d | ||
|
|
1118ae34c4 | ||
|
|
7a5e1a4e12 | ||
|
|
8e377e1794 | ||
|
|
d66360b02d | ||
|
|
1ece648006 | ||
|
|
a262a716a3 |
47
.all-contributorsrc
Normal file
47
.all-contributorsrc
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"imageSize": 100,
|
||||||
|
"commit": false,
|
||||||
|
"commitType": "docs",
|
||||||
|
"commitConvention": "angular",
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"login": "rbb-dev",
|
||||||
|
"name": "rbb-dev",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/37469229?v=4",
|
||||||
|
"profile": "https://github.com/rbb-dev",
|
||||||
|
"contributions": [
|
||||||
|
"ideas",
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "dhaern",
|
||||||
|
"name": "Raxxoor",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/7317522?v=4",
|
||||||
|
"profile": "https://trade.xyz/?ref=BZ1RJRXWO",
|
||||||
|
"contributions": [
|
||||||
|
"bug",
|
||||||
|
"ideas"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "i-iooi-i",
|
||||||
|
"name": "ZOLO",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/1827701?v=4",
|
||||||
|
"profile": "https://github.com/i-iooi-i",
|
||||||
|
"contributions": [
|
||||||
|
"bug",
|
||||||
|
"ideas"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"contributorsPerLine": 7,
|
||||||
|
"skipCi": true,
|
||||||
|
"repoType": "github",
|
||||||
|
"repoHost": "https://github.com",
|
||||||
|
"projectName": "awesome-openwebui",
|
||||||
|
"projectOwner": "Fu-Jie"
|
||||||
|
}
|
||||||
83
.github/copilot-instructions.md
vendored
83
.github/copilot-instructions.md
vendored
@@ -40,7 +40,7 @@ plugins/actions/export_to_docx/
|
|||||||
- 格式: `**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** x.x.x | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)`
|
- 格式: `**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** x.x.x | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)`
|
||||||
- **注意**: Author 和 Project 为固定值,仅需更新 Version 版本号
|
- **注意**: Author 和 Project 为固定值,仅需更新 Version 版本号
|
||||||
3. **描述 (Description)**: 一句话功能介绍
|
3. **描述 (Description)**: 一句话功能介绍
|
||||||
4. **最新更新 (What's New)**: **必须**放在描述之后,显著展示最新版本的变更点
|
4. **最新更新 (What's New)**: **必须**放在描述之后,显著展示最新版本的变更点 (仅展示最近 3 次更新)
|
||||||
5. **核心特性 (Key Features)**: 使用 Emoji + 粗体标题 + 描述格式
|
5. **核心特性 (Key Features)**: 使用 Emoji + 粗体标题 + 描述格式
|
||||||
6. **使用方法 (How to Use)**: 按步骤说明
|
6. **使用方法 (How to Use)**: 按步骤说明
|
||||||
7. **配置参数 (Configuration/Valves)**: 使用表格格式,包含参数名、默认值、描述
|
7. **配置参数 (Configuration/Valves)**: 使用表格格式,包含参数名、默认值、描述
|
||||||
@@ -96,7 +96,7 @@ example code or syntax here
|
|||||||
|
|
||||||
### 文档内容要求 (Content Requirements)
|
### 文档内容要求 (Content Requirements)
|
||||||
|
|
||||||
- **新增功能**: 必须在 "What's New" 章节中明确列出,使用 Emoji + 粗体标题格式。
|
- **新增功能**: 必须在 "What's New" 章节中明确列出,使用 Emoji + 粗体标题格式 (仅保留最近 3 个版本的更新记录)。
|
||||||
- **双语**: 必须提供 `README.md` (英文) 和 `README_CN.md` (中文)。
|
- **双语**: 必须提供 `README.md` (英文) 和 `README_CN.md` (中文)。
|
||||||
- **表格对齐**: 配置参数表格使用左对齐 `:---`。
|
- **表格对齐**: 配置参数表格使用左对齐 `:---`。
|
||||||
- **Emoji 规范**: 标题使用合适的 Emoji 增强可读性。
|
- **Emoji 规范**: 标题使用合适的 Emoji 增强可读性。
|
||||||
@@ -320,6 +320,43 @@ logger.error(f"Processing failed: {e}", exc_info=True)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🛡️ Filter 插件开发规范 (Filter Plugin Standards)
|
||||||
|
|
||||||
|
### 1. 状态管理 (State Management) - **关键 (Critical)**
|
||||||
|
|
||||||
|
Filter 实例在 OpenWebUI 生命周期中是**单例 (Singleton)**。这意味着同一个 Filter 实例会处理所有并发请求。
|
||||||
|
|
||||||
|
- **❌ 禁止 (Prohibited)**: 使用 `self` 存储请求级别的临时状态(如 `self.temp_state`)。这会导致严重的**竞态条件 (Race Conditions)**,即一个请求的数据被另一个请求覆盖。
|
||||||
|
- **✅ 推荐 (Recommended)**:
|
||||||
|
- **无状态设计**: `inlet` 和 `outlet` 应该尽可能独立。
|
||||||
|
- **重新计算**: 在 `outlet` 中根据 `body['messages']` 重新计算所需的状态,而不是依赖 `inlet` 传递。
|
||||||
|
- **元数据传递**: 如果必须传递状态,尝试使用 `body` 中的临时字段(需谨慎处理清理)或 `__metadata__`(如果可写)。
|
||||||
|
|
||||||
|
### 2. 摘要注入角色 (Summary Injection Role)
|
||||||
|
|
||||||
|
当注入历史摘要或上下文时:
|
||||||
|
|
||||||
|
- **❌ 避免 (Avoid)**: 使用 `system` 角色(部分模型对 system prompt 位置敏感或不支持中间插入)。
|
||||||
|
- **❌ 避免 (Avoid)**: 使用 `user` 角色(容易混淆用户真实意图)。
|
||||||
|
- **✅ 推荐 (Recommended)**: 使用 **`assistant`** 角色。这通常被模型视为上下文历史的一部分,兼容性最好。
|
||||||
|
|
||||||
|
### 3. 模型默认值 (Model Defaults)
|
||||||
|
|
||||||
|
- **❌ 禁止 (Prohibited)**: 硬编码特定模型 ID(如 `gpt-3.5-turbo`)作为默认值。这会导致非 OpenAI 用户出错。
|
||||||
|
- **✅ 推荐 (Recommended)**:
|
||||||
|
- 默认值设为 `None` 或空字符串。
|
||||||
|
- 优先使用当前对话的模型 (`body.get("model")`)。
|
||||||
|
- 如果必须指定,通过 `Valves` 让用户配置。
|
||||||
|
|
||||||
|
### 4. 异步处理 (Async Processing)
|
||||||
|
|
||||||
|
对于耗时的后台任务(如摘要生成、日志记录):
|
||||||
|
|
||||||
|
- **✅ 推荐 (Recommended)**: 在 `outlet` 中使用 `asyncio.create_task()` 启动后台任务,确保不阻塞用户响应。
|
||||||
|
- **✅ 推荐 (Recommended)**: 在后台任务中捕获所有异常,防止崩溃影响主进程。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🎨 HTML 注入规范 (HTML Injection)
|
## 🎨 HTML 注入规范 (HTML Injection)
|
||||||
|
|
||||||
使用统一的标记和结构:
|
使用统一的标记和结构:
|
||||||
@@ -1550,6 +1587,48 @@ feat(flash-card): add _get_user_context for safer user info retrieval
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🤝 贡献者认可规范 (Contributor Recognition Standards)
|
||||||
|
|
||||||
|
本项目使用 [All Contributors](https://allcontributors.org/) 规范来认可所有形式的贡献。
|
||||||
|
|
||||||
|
### 1. 如何添加贡献者 (How to Add)
|
||||||
|
|
||||||
|
在 GitHub 的 **Issue** 或 **Pull Request** 评论区发送以下指令,Bot 会自动创建 PR 更新 README:
|
||||||
|
|
||||||
|
```text
|
||||||
|
@all-contributors please add @username for <contribution-type>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 常用贡献类型 (Common Contribution Types)
|
||||||
|
|
||||||
|
| 类型 (Type) | 含义 (Meaning) | 图标 (Icon) |
|
||||||
|
| :--- | :--- | :---: |
|
||||||
|
| **`ideas`** | 提供想法、功能建议或改进思路 | 🤔 |
|
||||||
|
| **`code`** | 编写并提交代码实现 | 💻 |
|
||||||
|
| **`bug`** | 报告 Bug 或发现逻辑缺陷 | 🐛 |
|
||||||
|
| **`doc`** | 改进文档、README 或注释 | 📖 |
|
||||||
|
| **`translation`** | 提供多语言翻译支持 | 🌍 |
|
||||||
|
| **`review`** | 进行代码审查 (Code Review) | 👀 |
|
||||||
|
| **`design`** | 提供 UI/UX 设计或图标 | 🎨 |
|
||||||
|
| **`question`** | 在讨论区回答用户问题 | 💬 |
|
||||||
|
| **`tutorial`** | 编写教程或使用指南 | ✅ |
|
||||||
|
|
||||||
|
### 3. 核心区别:`ideas` vs `code`
|
||||||
|
|
||||||
|
- **`ideas`**: 贡献者提供了核心思路、逻辑优化方案或功能需求,但未直接编写代码。
|
||||||
|
- **`code`**: 贡献者直接编写并提交了 Pull Request。
|
||||||
|
- **组合使用**: 如果贡献者既提出了方案又完成了实现,应同时添加:`for ideas, code`。
|
||||||
|
|
||||||
|
### 4. 多次贡献处理 (Multiple Contributions)
|
||||||
|
|
||||||
|
All Contributors 支持勋章累加,无需担心重复添加:
|
||||||
|
|
||||||
|
- **累加勋章**: 如果贡献者已在列表中,再次发送指令指定新类型(如 `@all-contributors please add @user for doc`),Bot 会自动将新勋章追加到该用户头像下方。
|
||||||
|
- **一次性添加**: 支持在单条指令中列出所有类型:`for code, doc, ideas`。
|
||||||
|
- **手动修正**: 若需删除或修正勋章,需手动编辑 `.all-contributorsrc` 文件中的 `contributions` 数组。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## <20>📚 参考资源 (Reference Resources)
|
## <20>📚 参考资源 (Reference Resources)
|
||||||
|
|
||||||
- [Action 插件模板 (英文)](plugins/actions/ACTION_PLUGIN_TEMPLATE.py)
|
- [Action 插件模板 (英文)](plugins/actions/ACTION_PLUGIN_TEMPLATE.py)
|
||||||
|
|||||||
30
.github/workflows/community-stats.yml
vendored
30
.github/workflows/community-stats.yml
vendored
@@ -32,16 +32,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
pip install requests python-dotenv
|
pip install requests python-dotenv
|
||||||
|
|
||||||
- name: Get previous stats
|
|
||||||
id: prev_stats
|
|
||||||
run: |
|
|
||||||
# 获取当前的 points 用于比较
|
|
||||||
if [ -f docs/community-stats.json ]; then
|
|
||||||
OLD_POINTS=$(jq -r '.user.total_points' docs/community-stats.json 2>/dev/null || echo "0")
|
|
||||||
echo "old_points=$OLD_POINTS" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "old_points=0" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Generate stats report
|
- name: Generate stats report
|
||||||
env:
|
env:
|
||||||
@@ -50,26 +41,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
python scripts/openwebui_stats.py
|
python scripts/openwebui_stats.py
|
||||||
|
|
||||||
- name: Check for significant changes
|
|
||||||
id: check_changes
|
|
||||||
run: |
|
|
||||||
# 获取新的 points
|
|
||||||
NEW_POINTS=$(jq -r '.user.total_points' docs/community-stats.json 2>/dev/null || echo "0")
|
|
||||||
|
|
||||||
echo "📊 Previous points: ${{ steps.prev_stats.outputs.old_points }}"
|
|
||||||
echo "📊 Current points: $NEW_POINTS"
|
|
||||||
|
|
||||||
# 只在 points 变化时才 commit
|
|
||||||
if [ "$NEW_POINTS" != "${{ steps.prev_stats.outputs.old_points }}" ]; then
|
|
||||||
echo "changed=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "✅ Points changed (${{ steps.prev_stats.outputs.old_points }} → $NEW_POINTS), will commit"
|
|
||||||
else
|
|
||||||
echo "changed=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "⏭️ Points unchanged, skipping commit"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Commit and push changes
|
- name: Commit and push changes
|
||||||
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]"
|
||||||
|
|||||||
5
.github/workflows/publish_plugin.yml
vendored
5
.github/workflows/publish_plugin.yml
vendored
@@ -1,6 +1,11 @@
|
|||||||
name: Publish Plugins to OpenWebUI Market
|
name: Publish Plugins to OpenWebUI Market
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'plugins/**/*.py'
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|||||||
@@ -1,87 +1,16 @@
|
|||||||
# 贡献指南 (Contributing Guide)
|
# Contributing Guide
|
||||||
|
|
||||||
感谢你对 **OpenWebUI Extras** 感兴趣!我们非常欢迎社区贡献更多的插件、提示词和创意。
|
Thank you for your interest in **OpenWebUI Extras**!
|
||||||
|
|
||||||
## 🤝 如何贡献
|
## 🚀 How to Contribute
|
||||||
|
|
||||||
### 1. 分享提示词 (Prompts)
|
1. **Fork** this repository.
|
||||||
|
2. **Add/Modify** the plugin file in the `plugins/` directory.
|
||||||
|
3. **Submit PR**: We will review and merge it.
|
||||||
|
|
||||||
如果你有一个好用的提示词:
|
## 💡 Important
|
||||||
1. 在 `prompts/` 目录下找到合适的分类(如 `coding/`, `writing/`)。如果没有合适的,可以新建一个文件夹。
|
|
||||||
2. 创建一个新的 `.md` 或 `.json` 文件。
|
|
||||||
3. 提交 Pull Request (PR)。
|
|
||||||
|
|
||||||
### 2. 开发插件 (Plugins)
|
- Ensure your plugin includes complete metadata (title, author, version, description).
|
||||||
|
- If updating an existing plugin, please **increment the version number** (e.g., `0.1.0` -> `0.1.1`) to trigger the auto-update.
|
||||||
|
|
||||||
如果你开发了一个新的 OpenWebUI 插件 (Function/Tool):
|
Thank you! 🚀
|
||||||
1. 确保你的插件代码包含完整的元数据(Frontmatter):
|
|
||||||
```python
|
|
||||||
"""
|
|
||||||
title: 插件名称
|
|
||||||
author: 你的名字
|
|
||||||
version: 0.1.0
|
|
||||||
description: 简短描述插件的功能
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
2. 将插件文件放入 `plugins/` 目录下的合适位置:
|
|
||||||
- `plugins/actions/`: 用于添加按钮或修改消息的 Action 插件。
|
|
||||||
- `plugins/filters/`: 用于拦截请求或响应的 Filter 插件。
|
|
||||||
- `plugins/pipes/`: 用于自定义模型或 API 的 Pipe 插件。
|
|
||||||
- `plugins/tools/`: 用于 LLM 调用的 Tool 插件。
|
|
||||||
3. 建议在 `docs/` 下添加一个简单的使用说明。
|
|
||||||
|
|
||||||
### 3. 改进文档
|
|
||||||
|
|
||||||
如果你发现文档有错误或可以改进的地方,直接提交 PR 即可。
|
|
||||||
|
|
||||||
## 🛠️ 开发规范
|
|
||||||
|
|
||||||
- **代码风格**:Python 代码请遵循 PEP 8 规范。
|
|
||||||
- **注释**:关键逻辑请添加注释,方便他人理解。
|
|
||||||
- **测试**:提交前请在本地 OpenWebUI 环境中测试通过。
|
|
||||||
|
|
||||||
## 📝 提交 PR
|
|
||||||
|
|
||||||
1. Fork 本仓库。
|
|
||||||
2. 创建一个新的分支 (`git checkout -b feature/AmazingFeature`)。
|
|
||||||
3. 提交你的修改 (`git commit -m 'Add some AmazingFeature'`)。
|
|
||||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)。
|
|
||||||
5. 开启一个 Pull Request。
|
|
||||||
|
|
||||||
## 📦 版本更新与发布
|
|
||||||
|
|
||||||
当你更新插件时,请遵循以下流程:
|
|
||||||
|
|
||||||
### 1. 更新版本号
|
|
||||||
|
|
||||||
在插件文件的 docstring 中更新版本号(遵循[语义化版本](https://semver.org/lang/zh-CN/)):
|
|
||||||
|
|
||||||
```python
|
|
||||||
"""
|
|
||||||
title: 我的插件
|
|
||||||
version: 0.2.0 # 更新此处
|
|
||||||
...
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 更新更新日志
|
|
||||||
|
|
||||||
在 `CHANGELOG.md` 的 `[Unreleased]` 部分添加你的更改:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
### Added / 新增
|
|
||||||
- 新功能描述
|
|
||||||
|
|
||||||
### Fixed / 修复
|
|
||||||
- Bug 修复描述
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 发布流程
|
|
||||||
|
|
||||||
维护者会通过以下方式发布新版本:
|
|
||||||
- 手动触发 GitHub Actions 中的 "Plugin Release" 工作流
|
|
||||||
- 或创建版本标签 (`v*`)
|
|
||||||
|
|
||||||
详细说明请参阅 [发布工作流文档](docs/release-workflow.zh.md)。
|
|
||||||
|
|
||||||
再次感谢你的贡献!🚀
|
|
||||||
|
|||||||
16
CONTRIBUTING_CN.md
Normal file
16
CONTRIBUTING_CN.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# 贡献指南
|
||||||
|
|
||||||
|
感谢你对 **OpenWebUI Extras** 感兴趣!
|
||||||
|
|
||||||
|
## 🚀 贡献流程
|
||||||
|
|
||||||
|
1. **Fork** 本仓库。
|
||||||
|
2. **修改/添加** `plugins/` 目录下的插件文件。
|
||||||
|
3. **提交 PR**: 我们会尽快审核并合并。
|
||||||
|
|
||||||
|
## 💡 注意事项
|
||||||
|
|
||||||
|
- 请确保插件包含完整的元数据(title, author, version, description)。
|
||||||
|
- 如果是更新已有插件,请记得**增加版本号**(如 `0.1.0` -> `0.1.1`),这样系统会自动同步更新。
|
||||||
|
|
||||||
|
再次感谢你的贡献!🚀
|
||||||
58
README.md
58
README.md
@@ -1,4 +1,7 @@
|
|||||||
# OpenWebUI Extras
|
# OpenWebUI Extras
|
||||||
|
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||||
|
[](#contributors-)
|
||||||
|
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||||
|
|
||||||
English | [中文](./README_CN.md)
|
English | [中文](./README_CN.md)
|
||||||
|
|
||||||
@@ -7,26 +10,28 @@ A collection of enhancements, plugins, and prompts for [OpenWebUI](https://githu
|
|||||||
<!-- STATS_START -->
|
<!-- STATS_START -->
|
||||||
## 📊 Community Stats
|
## 📊 Community Stats
|
||||||
|
|
||||||
> 🕐 Auto-updated: 2026-01-10 17:08
|
> 🕐 Auto-updated: 2026-01-12 23:10
|
||||||
|
|
||||||
| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |
|
| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |
|
||||||
|:---:|:---:|:---:|:---:|
|
|:---:|:---:|:---:|:---:|
|
||||||
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **71** | **72** | **21** |
|
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **88** | **91** | **22** |
|
||||||
|
|
||||||
| 📝 Posts | ⬇️ Downloads | 👁️ Views | 👍 Upvotes | 💾 Saves |
|
| 📝 Posts | ⬇️ Downloads | 👁️ Views | 👍 Upvotes | 💾 Saves |
|
||||||
|:---:|:---:|:---:|:---:|:---:|
|
|:---:|:---:|:---:|:---:|:---:|
|
||||||
| **14** | **1066** | **11486** | **64** | **66** |
|
| **14** | **1230** | **13650** | **80** | **79** |
|
||||||
|
|
||||||
### 🔥 Top 6 Popular Plugins
|
### 🔥 Top 6 Popular Plugins
|
||||||
|
|
||||||
| Rank | Plugin | Downloads | Views |
|
> 🕐 Auto-updated: 2026-01-12 23:10
|
||||||
|:---:|------|:---:|:---:|
|
|
||||||
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 341 | 3080 |
|
| Rank | Plugin | Version | Downloads | Views | Updated |
|
||||||
| 🥈 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 181 | 551 |
|
|:---:|------|:---:|:---:|:---:|:---:|
|
||||||
| 🥉 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 125 | 1390 |
|
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 0.9.1 | 392 | 3535 | 2026-01-07 |
|
||||||
| 4️⃣ | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 116 | 1355 |
|
| 🥈 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 0.3.7 | 187 | 604 | 2026-01-07 |
|
||||||
| 5️⃣ | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | 92 | 1735 |
|
| 🥉 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 1.4.9 | 145 | 1594 | 2026-01-11 |
|
||||||
| 6️⃣ | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | 87 | 800 |
|
| 4️⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 1.1.3 | 144 | 1596 | 2026-01-11 |
|
||||||
|
| 5️⃣ | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | 0.4.3 | 104 | 938 | 2026-01-07 |
|
||||||
|
| 6️⃣ | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | 0.2.4 | 101 | 1881 | 2026-01-07 |
|
||||||
|
|
||||||
*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 -->
|
||||||
@@ -40,15 +45,18 @@ Located in the `plugins/` directory, containing Python-based enhancements:
|
|||||||
#### Actions
|
#### Actions
|
||||||
- **Smart Mind Map** (`smart-mind-map`): Generates interactive mind maps from text.
|
- **Smart Mind Map** (`smart-mind-map`): Generates interactive mind maps from text.
|
||||||
- **Smart Infographic** (`infographic`): Transforms text into professional infographics using AntV.
|
- **Smart Infographic** (`infographic`): Transforms text into professional infographics using AntV.
|
||||||
- **Knowledge Card** (`knowledge-card`): Creates beautiful flashcards for learning.
|
- **Flash Card** (`flash-card`): Quickly generates beautiful flashcards for learning.
|
||||||
|
- **Deep Dive** (`deep-dive`): A comprehensive thinking lens that dives deep into any content.
|
||||||
- **Export to Excel** (`export_to_excel`): Exports chat history to Excel files.
|
- **Export to Excel** (`export_to_excel`): Exports chat history to Excel files.
|
||||||
- **Export to Word** (`export_to_docx`): Exports chat history to Word documents.
|
- **Export to Word** (`export_to_docx`): Exports chat history to Word documents.
|
||||||
- **Summary** (`summary`): Text summarization tool.
|
|
||||||
|
|
||||||
#### Filters
|
#### Filters
|
||||||
- **Async Context Compression** (`async-context-compression`): Optimizes token usage via context compression.
|
- **Async Context Compression** (`async-context-compression`): Optimizes token usage via context compression.
|
||||||
- **Context Enhancement** (`context_enhancement_filter`): Enhances chat context.
|
- **Context Enhancement** (`context_enhancement_filter`): Enhances chat context.
|
||||||
- **Gemini Manifold Companion** (`gemini_manifold_companion`): Companion filter for Gemini Manifold.
|
- **Gemini Manifold Companion** (`gemini_manifold_companion`): Companion filter for Gemini Manifold.
|
||||||
|
- **Gemini Multimodal Filter** (`web_gemini_multimodel_filter`): Provides multimodal capabilities (PDF, Office, Video) for any model via Gemini.
|
||||||
|
- **Markdown Normalizer** (`markdown_normalizer`): Fixes common Markdown formatting issues in LLM outputs.
|
||||||
|
- **Multi-Model Context Merger** (`multi_model_context_merger`): Automatically merges and injects context from multiple model responses.
|
||||||
|
|
||||||
|
|
||||||
#### Pipes
|
#### Pipes
|
||||||
@@ -104,3 +112,27 @@ If you have great prompts or plugins to share:
|
|||||||
3. Submit a Pull Request.
|
3. Submit a Pull Request.
|
||||||
|
|
||||||
[Contributing](./CONTRIBUTING.md)
|
[Contributing](./CONTRIBUTING.md)
|
||||||
|
|
||||||
|
## Contributors ✨
|
||||||
|
|
||||||
|
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||||
|
|
||||||
|
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
<!-- markdownlint-disable -->
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rbb-dev"><img src="https://avatars.githubusercontent.com/u/37469229?v=4?s=100" width="100px;" alt="rbb-dev"/><br /><sub><b>rbb-dev</b></sub></a><br /><a href="#ideas-rbb-dev" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/Fu-Jie/awesome-openwebui/commits?author=rbb-dev" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://trade.xyz/?ref=BZ1RJRXWO"><img src="https://avatars.githubusercontent.com/u/7317522?v=4?s=100" width="100px;" alt="Raxxoor"/><br /><sub><b>Raxxoor</b></sub></a><br /><a href="https://github.com/Fu-Jie/awesome-openwebui/issues?q=author%3Adhaern" title="Bug reports">🐛</a> <a href="#ideas-dhaern" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/i-iooi-i"><img src="https://avatars.githubusercontent.com/u/1827701?v=4?s=100" width="100px;" alt="ZOLO"/><br /><sub><b>ZOLO</b></sub></a><br /><a href="https://github.com/Fu-Jie/awesome-openwebui/issues?q=author%3Ai-iooi-i" title="Bug reports">🐛</a> <a href="#ideas-i-iooi-i" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- markdownlint-restore -->
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||||
|
|
||||||
|
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||||
|
|||||||
33
README_CN.md
33
README_CN.md
@@ -7,26 +7,28 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
|
|||||||
<!-- STATS_START -->
|
<!-- STATS_START -->
|
||||||
## 📊 社区统计
|
## 📊 社区统计
|
||||||
|
|
||||||
> 🕐 自动更新于 2026-01-10 17:08
|
> 🕐 自动更新于 2026-01-12 23:10
|
||||||
|
|
||||||
| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |
|
| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |
|
||||||
|:---:|:---:|:---:|:---:|
|
|:---:|:---:|:---:|:---:|
|
||||||
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **71** | **72** | **21** |
|
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **88** | **91** | **22** |
|
||||||
|
|
||||||
| 📝 发布 | ⬇️ 下载 | 👁️ 浏览 | 👍 点赞 | 💾 收藏 |
|
| 📝 发布 | ⬇️ 下载 | 👁️ 浏览 | 👍 点赞 | 💾 收藏 |
|
||||||
|:---:|:---:|:---:|:---:|:---:|
|
|:---:|:---:|:---:|:---:|:---:|
|
||||||
| **14** | **1066** | **11486** | **64** | **66** |
|
| **14** | **1230** | **13650** | **80** | **79** |
|
||||||
|
|
||||||
### 🔥 热门插件 Top 6
|
### 🔥 热门插件 Top 6
|
||||||
|
|
||||||
| 排名 | 插件 | 下载 | 浏览 |
|
> 🕐 自动更新于 2026-01-12 23:10
|
||||||
|:---:|------|:---:|:---:|
|
|
||||||
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 341 | 3080 |
|
| 排名 | 插件 | 版本 | 下载 | 浏览 | 更新日期 |
|
||||||
| 🥈 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 181 | 551 |
|
|:---:|------|:---:|:---:|:---:|:---:|
|
||||||
| 🥉 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 125 | 1390 |
|
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 0.9.1 | 392 | 3535 | 2026-01-07 |
|
||||||
| 4️⃣ | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 116 | 1355 |
|
| 🥈 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 0.3.7 | 187 | 604 | 2026-01-07 |
|
||||||
| 5️⃣ | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | 92 | 1735 |
|
| 🥉 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 1.4.9 | 145 | 1594 | 2026-01-11 |
|
||||||
| 6️⃣ | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | 87 | 800 |
|
| 4️⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 1.1.3 | 144 | 1596 | 2026-01-11 |
|
||||||
|
| 5️⃣ | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | 0.4.3 | 104 | 938 | 2026-01-07 |
|
||||||
|
| 6️⃣ | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | 0.2.4 | 101 | 1881 | 2026-01-07 |
|
||||||
|
|
||||||
*完整统计请查看 [社区统计报告](./docs/community-stats.zh.md)*
|
*完整统计请查看 [社区统计报告](./docs/community-stats.zh.md)*
|
||||||
<!-- STATS_END -->
|
<!-- STATS_END -->
|
||||||
@@ -40,15 +42,18 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
|
|||||||
#### Actions (交互增强)
|
#### Actions (交互增强)
|
||||||
- **Smart Mind Map** (`smart-mind-map`): 智能分析文本并生成交互式思维导图。
|
- **Smart Mind Map** (`smart-mind-map`): 智能分析文本并生成交互式思维导图。
|
||||||
- **Smart Infographic** (`infographic`): 基于 AntV 的智能信息图生成工具。
|
- **Smart Infographic** (`infographic`): 基于 AntV 的智能信息图生成工具。
|
||||||
- **Knowledge Card** (`knowledge-card`): 快速生成精美的学习记忆卡片。
|
- **Flash Card** (`flash-card`): 快速生成精美的学习记忆卡片。
|
||||||
|
- **Deep Dive** (`deep-dive`): 深度思考透镜,从背景、逻辑、洞察到行动路径的全方位分析。
|
||||||
- **Export to Excel** (`export_to_excel`): 将对话内容导出为 Excel 文件。
|
- **Export to Excel** (`export_to_excel`): 将对话内容导出为 Excel 文件。
|
||||||
- **Export to Word** (`export_to_docx`): 将对话内容导出为 Word 文档。
|
- **Export to Word** (`export_to_docx`): 将对话内容导出为 Word 文档。
|
||||||
- **Summary** (`summary`): 文本摘要生成工具。
|
|
||||||
|
|
||||||
#### Filters (消息处理)
|
#### Filters (消息处理)
|
||||||
- **Async Context Compression** (`async-context-compression`): 异步上下文压缩,优化 Token 使用。
|
- **Async Context Compression** (`async-context-compression`): 异步上下文压缩,优化 Token 使用。
|
||||||
- **Context Enhancement** (`context_enhancement_filter`): 上下文增强过滤器。
|
- **Context Enhancement** (`context_enhancement_filter`): 上下文增强过滤器。
|
||||||
- **Gemini Manifold Companion** (`gemini_manifold_companion`): Gemini Manifold 配套增强。
|
- **Gemini Manifold Companion** (`gemini_manifold_companion`): Gemini Manifold 配套增强。
|
||||||
|
- **Gemini Multimodal Filter** (`web_gemini_multimodel_filter`): 为任意模型提供多模态能力(PDF、Office、视频等),支持智能路由和字幕精修。
|
||||||
|
- **Markdown Normalizer** (`markdown_normalizer`): 修复 LLM 输出中常见的 Markdown 格式问题。
|
||||||
|
- **Multi-Model Context Merger** (`multi_model_context_merger`): 自动合并并注入多模型回答的上下文。
|
||||||
|
|
||||||
#### Pipes (模型管道)
|
#### Pipes (模型管道)
|
||||||
- **Gemini Manifold** (`gemini_mainfold`): 集成 Gemini 模型的管道。
|
- **Gemini Manifold** (`gemini_mainfold`): 集成 Gemini 模型的管道。
|
||||||
@@ -105,4 +110,4 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
|
|||||||
2. 将你的文件添加到对应的 `prompts/` 或 `plugins/` 目录。
|
2. 将你的文件添加到对应的 `prompts/` 或 `plugins/` 目录。
|
||||||
3. 提交 Pull Request。
|
3. 提交 Pull Request。
|
||||||
|
|
||||||
[贡献指南](./CONTRIBUTING.md) | [更新日志](./CHANGELOG.md)
|
[贡献指南](./CONTRIBUTING_CN.md) | [更新日志](./CHANGELOG.md)
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
{
|
{
|
||||||
"total_posts": 14,
|
"total_posts": 14,
|
||||||
"total_downloads": 1066,
|
"total_downloads": 1230,
|
||||||
"total_views": 11486,
|
"total_views": 13650,
|
||||||
"total_upvotes": 64,
|
"total_upvotes": 80,
|
||||||
"total_downvotes": 2,
|
"total_downvotes": 2,
|
||||||
"total_saves": 66,
|
"total_saves": 79,
|
||||||
"total_comments": 15,
|
"total_comments": 18,
|
||||||
"by_type": {
|
"by_type": {
|
||||||
"unknown": 1,
|
"action": 13,
|
||||||
"action": 11,
|
"unknown": 1
|
||||||
"filter": 2
|
|
||||||
},
|
},
|
||||||
"posts": [
|
"posts": [
|
||||||
{
|
{
|
||||||
@@ -19,11 +18,11 @@
|
|||||||
"version": "0.9.1",
|
"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": 341,
|
"downloads": 392,
|
||||||
"views": 3080,
|
"views": 3535,
|
||||||
"upvotes": 10,
|
"upvotes": 11,
|
||||||
"saves": 21,
|
"saves": 22,
|
||||||
"comments": 10,
|
"comments": 11,
|
||||||
"created_at": "2025-12-30",
|
"created_at": "2025-12-30",
|
||||||
"updated_at": "2026-01-07",
|
"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"
|
||||||
@@ -35,8 +34,8 @@
|
|||||||
"version": "0.3.7",
|
"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": 181,
|
"downloads": 187,
|
||||||
"views": 551,
|
"views": 604,
|
||||||
"upvotes": 3,
|
"upvotes": 3,
|
||||||
"saves": 4,
|
"saves": 4,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
@@ -44,53 +43,37 @@
|
|||||||
"updated_at": "2026-01-07",
|
"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"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"title": "Async Context Compression",
|
|
||||||
"slug": "async_context_compression_b1655bc8",
|
|
||||||
"type": "filter",
|
|
||||||
"version": "1.1.0",
|
|
||||||
"author": "Fu-Jie",
|
|
||||||
"description": "Reduces token consumption in long conversations while maintaining coherence through intelligent summarization and message compression.",
|
|
||||||
"downloads": 125,
|
|
||||||
"views": 1390,
|
|
||||||
"upvotes": 5,
|
|
||||||
"saves": 9,
|
|
||||||
"comments": 0,
|
|
||||||
"created_at": "2025-11-08",
|
|
||||||
"updated_at": "2026-01-07",
|
|
||||||
"url": "https://openwebui.com/posts/async_context_compression_b1655bc8"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"title": "📊 Smart Infographic (AntV)",
|
"title": "📊 Smart Infographic (AntV)",
|
||||||
"slug": "smart_infographic_ad6f0c7f",
|
"slug": "smart_infographic_ad6f0c7f",
|
||||||
"type": "action",
|
"type": "action",
|
||||||
"version": "1.4.1",
|
"version": "1.4.9",
|
||||||
"author": "jeff",
|
"author": "Fu-Jie",
|
||||||
"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.",
|
||||||
"downloads": 116,
|
"downloads": 145,
|
||||||
"views": 1355,
|
"views": 1594,
|
||||||
"upvotes": 7,
|
"upvotes": 8,
|
||||||
"saves": 9,
|
"saves": 10,
|
||||||
"comments": 2,
|
"comments": 2,
|
||||||
"created_at": "2025-12-28",
|
"created_at": "2025-12-28",
|
||||||
"updated_at": "2026-01-07",
|
"updated_at": "2026-01-11",
|
||||||
"url": "https://openwebui.com/posts/smart_infographic_ad6f0c7f"
|
"url": "https://openwebui.com/posts/smart_infographic_ad6f0c7f"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Flash Card",
|
"title": "Async Context Compression",
|
||||||
"slug": "flash_card_65a2ea8f",
|
"slug": "async_context_compression_b1655bc8",
|
||||||
"type": "action",
|
"type": "action",
|
||||||
"version": "0.2.4",
|
"version": "1.1.3",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "Quickly generates beautiful flashcards from text, extracting key points and categories.",
|
"description": "Reduces token consumption in long conversations while maintaining coherence through intelligent summarization and message compression.",
|
||||||
"downloads": 92,
|
"downloads": 144,
|
||||||
"views": 1735,
|
"views": 1596,
|
||||||
"upvotes": 8,
|
"upvotes": 7,
|
||||||
"saves": 6,
|
"saves": 11,
|
||||||
"comments": 2,
|
"comments": 0,
|
||||||
"created_at": "2025-12-30",
|
"created_at": "2025-11-08",
|
||||||
"updated_at": "2026-01-07",
|
"updated_at": "2026-01-11",
|
||||||
"url": "https://openwebui.com/posts/flash_card_65a2ea8f"
|
"url": "https://openwebui.com/posts/async_context_compression_b1655bc8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Export to Word (Enhanced)",
|
"title": "Export to Word (Enhanced)",
|
||||||
@@ -99,30 +82,30 @@
|
|||||||
"version": "0.4.3",
|
"version": "0.4.3",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"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.",
|
||||||
"downloads": 87,
|
"downloads": 104,
|
||||||
"views": 800,
|
"views": 938,
|
||||||
"upvotes": 5,
|
"upvotes": 6,
|
||||||
"saves": 8,
|
"saves": 10,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
"created_at": "2026-01-03",
|
"created_at": "2026-01-03",
|
||||||
"updated_at": "2026-01-07",
|
"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": "📊 智能信息图 (AntV Infographic)",
|
"title": "Flash Card",
|
||||||
"slug": "智能信息图_e04a48ff",
|
"slug": "flash_card_65a2ea8f",
|
||||||
"type": "action",
|
"type": "action",
|
||||||
"version": "1.4.1",
|
"version": "0.2.4",
|
||||||
"author": "jeff",
|
"author": "Fu-Jie",
|
||||||
"description": "基于 AntV Infographic 的智能信息图生成插件。支持多种专业模板,自动图标匹配,并提供 SVG/PNG 下载功能。",
|
"description": "Quickly generates beautiful flashcards from text, extracting key points and categories.",
|
||||||
"downloads": 35,
|
"downloads": 101,
|
||||||
"views": 480,
|
"views": 1881,
|
||||||
"upvotes": 3,
|
"upvotes": 8,
|
||||||
"saves": 0,
|
"saves": 6,
|
||||||
"comments": 0,
|
"comments": 2,
|
||||||
"created_at": "2025-12-28",
|
"created_at": "2025-12-30",
|
||||||
"updated_at": "2026-01-07",
|
"updated_at": "2026-01-07",
|
||||||
"url": "https://openwebui.com/posts/智能信息图_e04a48ff"
|
"url": "https://openwebui.com/posts/flash_card_65a2ea8f"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "导出为 Word (增强版)",
|
"title": "导出为 Word (增强版)",
|
||||||
@@ -131,15 +114,31 @@
|
|||||||
"version": "0.4.3",
|
"version": "0.4.3",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "将对话导出为 Word (.docx),支持 Mermaid 图表 (客户端渲染 SVG+PNG)、LaTeX 数学公式、真实超链接、增强表格格式、代码高亮和引用块。",
|
"description": "将对话导出为 Word (.docx),支持 Mermaid 图表 (客户端渲染 SVG+PNG)、LaTeX 数学公式、真实超链接、增强表格格式、代码高亮和引用块。",
|
||||||
"downloads": 31,
|
"downloads": 40,
|
||||||
"views": 929,
|
"views": 1038,
|
||||||
"upvotes": 8,
|
"upvotes": 9,
|
||||||
"saves": 2,
|
"saves": 2,
|
||||||
"comments": 1,
|
"comments": 1,
|
||||||
"created_at": "2026-01-04",
|
"created_at": "2026-01-04",
|
||||||
"updated_at": "2026-01-07",
|
"updated_at": "2026-01-07",
|
||||||
"url": "https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0"
|
"url": "https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "📊 智能信息图 (AntV Infographic)",
|
||||||
|
"slug": "智能信息图_e04a48ff",
|
||||||
|
"type": "action",
|
||||||
|
"version": "1.4.9",
|
||||||
|
"author": "Fu-Jie",
|
||||||
|
"description": "基于 AntV Infographic 的智能信息图生成插件。支持多种专业模板,自动图标匹配,并提供 SVG/PNG 下载功能。",
|
||||||
|
"downloads": 35,
|
||||||
|
"views": 525,
|
||||||
|
"upvotes": 3,
|
||||||
|
"saves": 0,
|
||||||
|
"comments": 0,
|
||||||
|
"created_at": "2025-12-28",
|
||||||
|
"updated_at": "2026-01-11",
|
||||||
|
"url": "https://openwebui.com/posts/智能信息图_e04a48ff"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Deep Dive",
|
"title": "Deep Dive",
|
||||||
"slug": "deep_dive_c0b846e4",
|
"slug": "deep_dive_c0b846e4",
|
||||||
@@ -147,10 +146,10 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "A comprehensive thinking lens that dives deep into any content - from context to logic, insights, and action paths.",
|
"description": "A comprehensive thinking lens that dives deep into any content - from context to logic, insights, and action paths.",
|
||||||
"downloads": 22,
|
"downloads": 35,
|
||||||
"views": 259,
|
"views": 351,
|
||||||
"upvotes": 3,
|
"upvotes": 3,
|
||||||
"saves": 3,
|
"saves": 4,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
"created_at": "2026-01-08",
|
"created_at": "2026-01-08",
|
||||||
"updated_at": "2026-01-08",
|
"updated_at": "2026-01-08",
|
||||||
@@ -163,8 +162,8 @@
|
|||||||
"version": "0.9.1",
|
"version": "0.9.1",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。",
|
"description": "智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。",
|
||||||
"downloads": 17,
|
"downloads": 19,
|
||||||
"views": 304,
|
"views": 339,
|
||||||
"upvotes": 2,
|
"upvotes": 2,
|
||||||
"saves": 1,
|
"saves": 1,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
@@ -172,6 +171,22 @@
|
|||||||
"updated_at": "2026-01-07",
|
"updated_at": "2026-01-07",
|
||||||
"url": "https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b"
|
"url": "https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "异步上下文压缩",
|
||||||
|
"slug": "异步上下文压缩_5c0617cb",
|
||||||
|
"type": "action",
|
||||||
|
"version": "1.1.3",
|
||||||
|
"author": "Fu-Jie",
|
||||||
|
"description": "通过智能摘要和消息压缩,降低长对话的 token 消耗,同时保持对话连贯性。",
|
||||||
|
"downloads": 12,
|
||||||
|
"views": 254,
|
||||||
|
"upvotes": 4,
|
||||||
|
"saves": 1,
|
||||||
|
"comments": 0,
|
||||||
|
"created_at": "2025-11-08",
|
||||||
|
"updated_at": "2026-01-11",
|
||||||
|
"url": "https://openwebui.com/posts/异步上下文压缩_5c0617cb"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "闪记卡 (Flash Card)",
|
"title": "闪记卡 (Flash Card)",
|
||||||
"slug": "闪记卡生成插件_4a31eac3",
|
"slug": "闪记卡生成插件_4a31eac3",
|
||||||
@@ -180,7 +195,7 @@
|
|||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "快速将文本提炼为精美的学习记忆卡片,支持核心要点提取与分类。",
|
"description": "快速将文本提炼为精美的学习记忆卡片,支持核心要点提取与分类。",
|
||||||
"downloads": 12,
|
"downloads": 12,
|
||||||
"views": 345,
|
"views": 376,
|
||||||
"upvotes": 4,
|
"upvotes": 4,
|
||||||
"saves": 1,
|
"saves": 1,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
@@ -188,22 +203,6 @@
|
|||||||
"updated_at": "2026-01-07",
|
"updated_at": "2026-01-07",
|
||||||
"url": "https://openwebui.com/posts/闪记卡生成插件_4a31eac3"
|
"url": "https://openwebui.com/posts/闪记卡生成插件_4a31eac3"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"title": "异步上下文压缩",
|
|
||||||
"slug": "异步上下文压缩_5c0617cb",
|
|
||||||
"type": "filter",
|
|
||||||
"version": "1.1.0",
|
|
||||||
"author": "Fu-Jie",
|
|
||||||
"description": "通过智能摘要和消息压缩,降低长对话的 token 消耗,同时保持对话连贯性。",
|
|
||||||
"downloads": 6,
|
|
||||||
"views": 153,
|
|
||||||
"upvotes": 2,
|
|
||||||
"saves": 1,
|
|
||||||
"comments": 0,
|
|
||||||
"created_at": "2025-11-08",
|
|
||||||
"updated_at": "2026-01-07",
|
|
||||||
"url": "https://openwebui.com/posts/异步上下文压缩_5c0617cb"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"title": "精读",
|
"title": "精读",
|
||||||
"slug": "精读_99830b0f",
|
"slug": "精读_99830b0f",
|
||||||
@@ -211,8 +210,8 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "全方位的思维透镜 —— 从背景全景到逻辑脉络,从深度洞察到行动路径。",
|
"description": "全方位的思维透镜 —— 从背景全景到逻辑脉络,从深度洞察到行动路径。",
|
||||||
"downloads": 1,
|
"downloads": 4,
|
||||||
"views": 86,
|
"views": 131,
|
||||||
"upvotes": 2,
|
"upvotes": 2,
|
||||||
"saves": 1,
|
"saves": 1,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
@@ -228,10 +227,10 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"description": "",
|
"description": "",
|
||||||
"downloads": 0,
|
"downloads": 0,
|
||||||
"views": 19,
|
"views": 488,
|
||||||
"upvotes": 2,
|
"upvotes": 10,
|
||||||
"saves": 0,
|
"saves": 6,
|
||||||
"comments": 0,
|
"comments": 2,
|
||||||
"created_at": "2026-01-10",
|
"created_at": "2026-01-10",
|
||||||
"updated_at": "2026-01-10",
|
"updated_at": "2026-01-10",
|
||||||
"url": "https://openwebui.com/posts/debug_open_webui_plugins_in_your_browser_81bf7960"
|
"url": "https://openwebui.com/posts/debug_open_webui_plugins_in_your_browser_81bf7960"
|
||||||
@@ -242,11 +241,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": 71,
|
"followers": 88,
|
||||||
"following": 2,
|
"following": 2,
|
||||||
"total_points": 72,
|
"total_points": 91,
|
||||||
"post_points": 62,
|
"post_points": 78,
|
||||||
"comment_points": 10,
|
"comment_points": 13,
|
||||||
"contributions": 21
|
"contributions": 22
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,39 +1,38 @@
|
|||||||
# 📊 OpenWebUI Community Stats Report
|
# 📊 OpenWebUI Community Stats Report
|
||||||
|
|
||||||
> 📅 Updated: 2026-01-10 17:08
|
> 📅 Updated: 2026-01-12 23:10
|
||||||
|
|
||||||
## 📈 Overview
|
## 📈 Overview
|
||||||
|
|
||||||
| Metric | Value |
|
| Metric | Value |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 📝 Total Posts | 14 |
|
| 📝 Total Posts | 14 |
|
||||||
| ⬇️ Total Downloads | 1066 |
|
| ⬇️ Total Downloads | 1230 |
|
||||||
| 👁️ Total Views | 11486 |
|
| 👁️ Total Views | 13650 |
|
||||||
| 👍 Total Upvotes | 64 |
|
| 👍 Total Upvotes | 80 |
|
||||||
| 💾 Total Saves | 66 |
|
| 💾 Total Saves | 79 |
|
||||||
| 💬 Total Comments | 15 |
|
| 💬 Total Comments | 18 |
|
||||||
|
|
||||||
## 📂 By Type
|
## 📂 By Type
|
||||||
|
|
||||||
|
- **action**: 13
|
||||||
- **unknown**: 1
|
- **unknown**: 1
|
||||||
- **action**: 11
|
|
||||||
- **filter**: 2
|
|
||||||
|
|
||||||
## 📋 Posts List
|
## 📋 Posts List
|
||||||
|
|
||||||
| Rank | Title | Type | Version | Downloads | Views | Upvotes | Saves | Updated |
|
| Rank | Title | Type | Version | Downloads | Views | Upvotes | Saves | Updated |
|
||||||
|:---:|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
|:---:|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||||
| 1 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action | 0.9.1 | 341 | 3080 | 10 | 21 | 2026-01-07 |
|
| 1 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action | 0.9.1 | 392 | 3535 | 11 | 22 | 2026-01-07 |
|
||||||
| 2 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.7 | 181 | 551 | 3 | 4 | 2026-01-07 |
|
| 2 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.7 | 187 | 604 | 3 | 4 | 2026-01-07 |
|
||||||
| 3 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | filter | 1.1.0 | 125 | 1390 | 5 | 9 | 2026-01-07 |
|
| 3 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.4.9 | 145 | 1594 | 8 | 10 | 2026-01-11 |
|
||||||
| 4 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.4.1 | 116 | 1355 | 7 | 9 | 2026-01-07 |
|
| 4 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | action | 1.1.3 | 144 | 1596 | 7 | 11 | 2026-01-11 |
|
||||||
| 5 | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 92 | 1735 | 8 | 6 | 2026-01-07 |
|
| 5 | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.3 | 104 | 938 | 6 | 10 | 2026-01-07 |
|
||||||
| 6 | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.3 | 87 | 800 | 5 | 8 | 2026-01-07 |
|
| 6 | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 101 | 1881 | 8 | 6 | 2026-01-07 |
|
||||||
| 7 | [📊 智能信息图 (AntV Infographic)](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.4.1 | 35 | 480 | 3 | 0 | 2026-01-07 |
|
| 7 | [导出为 Word (增强版)](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.3 | 40 | 1038 | 9 | 2 | 2026-01-07 |
|
||||||
| 8 | [导出为 Word (增强版)](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.3 | 31 | 929 | 8 | 2 | 2026-01-07 |
|
| 8 | [📊 智能信息图 (AntV Infographic)](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.4.9 | 35 | 525 | 3 | 0 | 2026-01-11 |
|
||||||
| 9 | [Deep Dive](https://openwebui.com/posts/deep_dive_c0b846e4) | action | 1.0.0 | 22 | 259 | 3 | 3 | 2026-01-08 |
|
| 9 | [Deep Dive](https://openwebui.com/posts/deep_dive_c0b846e4) | action | 1.0.0 | 35 | 351 | 3 | 4 | 2026-01-08 |
|
||||||
| 10 | [思维导图](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.9.1 | 17 | 304 | 2 | 1 | 2026-01-07 |
|
| 10 | [思维导图](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.9.1 | 19 | 339 | 2 | 1 | 2026-01-07 |
|
||||||
| 11 | [闪记卡 (Flash Card)](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.4 | 12 | 345 | 4 | 1 | 2026-01-07 |
|
| 11 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | action | 1.1.3 | 12 | 254 | 4 | 1 | 2026-01-11 |
|
||||||
| 12 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | filter | 1.1.0 | 6 | 153 | 2 | 1 | 2026-01-07 |
|
| 12 | [闪记卡 (Flash Card)](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.4 | 12 | 376 | 4 | 1 | 2026-01-07 |
|
||||||
| 13 | [精读](https://openwebui.com/posts/精读_99830b0f) | action | 1.0.0 | 1 | 86 | 2 | 1 | 2026-01-08 |
|
| 13 | [精读](https://openwebui.com/posts/精读_99830b0f) | action | 1.0.0 | 4 | 131 | 2 | 1 | 2026-01-08 |
|
||||||
| 14 | [ 🛠️ Debug Open WebUI Plugins in Your Browser](https://openwebui.com/posts/debug_open_webui_plugins_in_your_browser_81bf7960) | unknown | | 0 | 19 | 2 | 0 | 2026-01-10 |
|
| 14 | [ 🛠️ Debug Open WebUI Plugins in Your Browser](https://openwebui.com/posts/debug_open_webui_plugins_in_your_browser_81bf7960) | unknown | | 0 | 488 | 10 | 6 | 2026-01-10 |
|
||||||
|
|||||||
@@ -1,39 +1,38 @@
|
|||||||
# 📊 OpenWebUI 社区统计报告
|
# 📊 OpenWebUI 社区统计报告
|
||||||
|
|
||||||
> 📅 更新时间: 2026-01-10 17:08
|
> 📅 更新时间: 2026-01-12 23:10
|
||||||
|
|
||||||
## 📈 总览
|
## 📈 总览
|
||||||
|
|
||||||
| 指标 | 数值 |
|
| 指标 | 数值 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 📝 发布数量 | 14 |
|
| 📝 发布数量 | 14 |
|
||||||
| ⬇️ 总下载量 | 1066 |
|
| ⬇️ 总下载量 | 1230 |
|
||||||
| 👁️ 总浏览量 | 11486 |
|
| 👁️ 总浏览量 | 13650 |
|
||||||
| 👍 总点赞数 | 64 |
|
| 👍 总点赞数 | 80 |
|
||||||
| 💾 总收藏数 | 66 |
|
| 💾 总收藏数 | 79 |
|
||||||
| 💬 总评论数 | 15 |
|
| 💬 总评论数 | 18 |
|
||||||
|
|
||||||
## 📂 按类型分类
|
## 📂 按类型分类
|
||||||
|
|
||||||
|
- **action**: 13
|
||||||
- **unknown**: 1
|
- **unknown**: 1
|
||||||
- **action**: 11
|
|
||||||
- **filter**: 2
|
|
||||||
|
|
||||||
## 📋 发布列表
|
## 📋 发布列表
|
||||||
|
|
||||||
| 排名 | 标题 | 类型 | 版本 | 下载 | 浏览 | 点赞 | 收藏 | 更新日期 |
|
| 排名 | 标题 | 类型 | 版本 | 下载 | 浏览 | 点赞 | 收藏 | 更新日期 |
|
||||||
|:---:|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
|:---:|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||||
| 1 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action | 0.9.1 | 341 | 3080 | 10 | 21 | 2026-01-07 |
|
| 1 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action | 0.9.1 | 392 | 3535 | 11 | 22 | 2026-01-07 |
|
||||||
| 2 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.7 | 181 | 551 | 3 | 4 | 2026-01-07 |
|
| 2 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.7 | 187 | 604 | 3 | 4 | 2026-01-07 |
|
||||||
| 3 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | filter | 1.1.0 | 125 | 1390 | 5 | 9 | 2026-01-07 |
|
| 3 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.4.9 | 145 | 1594 | 8 | 10 | 2026-01-11 |
|
||||||
| 4 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.4.1 | 116 | 1355 | 7 | 9 | 2026-01-07 |
|
| 4 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | action | 1.1.3 | 144 | 1596 | 7 | 11 | 2026-01-11 |
|
||||||
| 5 | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 92 | 1735 | 8 | 6 | 2026-01-07 |
|
| 5 | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.3 | 104 | 938 | 6 | 10 | 2026-01-07 |
|
||||||
| 6 | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.3 | 87 | 800 | 5 | 8 | 2026-01-07 |
|
| 6 | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 101 | 1881 | 8 | 6 | 2026-01-07 |
|
||||||
| 7 | [📊 智能信息图 (AntV Infographic)](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.4.1 | 35 | 480 | 3 | 0 | 2026-01-07 |
|
| 7 | [导出为 Word (增强版)](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.3 | 40 | 1038 | 9 | 2 | 2026-01-07 |
|
||||||
| 8 | [导出为 Word (增强版)](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.3 | 31 | 929 | 8 | 2 | 2026-01-07 |
|
| 8 | [📊 智能信息图 (AntV Infographic)](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.4.9 | 35 | 525 | 3 | 0 | 2026-01-11 |
|
||||||
| 9 | [Deep Dive](https://openwebui.com/posts/deep_dive_c0b846e4) | action | 1.0.0 | 22 | 259 | 3 | 3 | 2026-01-08 |
|
| 9 | [Deep Dive](https://openwebui.com/posts/deep_dive_c0b846e4) | action | 1.0.0 | 35 | 351 | 3 | 4 | 2026-01-08 |
|
||||||
| 10 | [思维导图](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.9.1 | 17 | 304 | 2 | 1 | 2026-01-07 |
|
| 10 | [思维导图](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.9.1 | 19 | 339 | 2 | 1 | 2026-01-07 |
|
||||||
| 11 | [闪记卡 (Flash Card)](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.4 | 12 | 345 | 4 | 1 | 2026-01-07 |
|
| 11 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | action | 1.1.3 | 12 | 254 | 4 | 1 | 2026-01-11 |
|
||||||
| 12 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | filter | 1.1.0 | 6 | 153 | 2 | 1 | 2026-01-07 |
|
| 12 | [闪记卡 (Flash Card)](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.4 | 12 | 376 | 4 | 1 | 2026-01-07 |
|
||||||
| 13 | [精读](https://openwebui.com/posts/精读_99830b0f) | action | 1.0.0 | 1 | 86 | 2 | 1 | 2026-01-08 |
|
| 13 | [精读](https://openwebui.com/posts/精读_99830b0f) | action | 1.0.0 | 4 | 131 | 2 | 1 | 2026-01-08 |
|
||||||
| 14 | [ 🛠️ Debug Open WebUI Plugins in Your Browser](https://openwebui.com/posts/debug_open_webui_plugins_in_your_browser_81bf7960) | unknown | | 0 | 19 | 2 | 0 | 2026-01-10 |
|
| 14 | [ 🛠️ Debug Open WebUI Plugins in Your Browser](https://openwebui.com/posts/debug_open_webui_plugins_in_your_browser_81bf7960) | unknown | | 0 | 488 | 10 | 6 | 2026-01-10 |
|
||||||
|
|||||||
@@ -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.4.1
|
**Version:** 1.4.9
|
||||||
|
|
||||||
[:octicons-arrow-right-24: Documentation](smart-infographic.md)
|
[:octicons-arrow-right-24: Documentation](smart-infographic.md)
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ Actions 是交互式插件,能够:
|
|||||||
|
|
||||||
使用 AntV 可视化引擎,将文本转成专业的信息图。
|
使用 AntV 可视化引擎,将文本转成专业的信息图。
|
||||||
|
|
||||||
**版本:** 1.4.1
|
**版本:** 1.4.9
|
||||||
|
|
||||||
[:octicons-arrow-right-24: 查看文档](smart-infographic.md)
|
[:octicons-arrow-right-24: 查看文档](smart-infographic.md)
|
||||||
|
|
||||||
|
|||||||
@@ -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.4.0</span>
|
<span class="version-badge">v1.4.9</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.
|
||||||
|
|
||||||
@@ -14,8 +14,8 @@ The Smart Infographic plugin uses AI to analyze text content and generate profes
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- :material-robot: **AI-Powered Transformation**: Automatically analyzes text logic, extracts key points, and generates structured charts
|
- :material-robot: **AI-Powered Transformation**: Automatically analyzes text logic, extracts key points, and generates structured charts
|
||||||
- :material-palette: **Professional Templates**: Includes various AntV official templates: Lists, Trees, Mindmaps, Comparison Tables, Flowcharts, and Statistical Charts
|
- :material-palette: **70+ Professional Templates**: Includes various AntV official templates: Lists, Trees, Roadmaps, Timelines, Comparison Tables, SWOT, Quadrants, and Statistical Charts
|
||||||
- :material-magnify: **Auto-Icon Matching**: Built-in logic to search and match the most relevant Material Design Icons based on content
|
- :material-magnify: **Auto-Icon Matching**: Built-in logic to search and match the most relevant icons (Iconify) and illustrations (unDraw)
|
||||||
- :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
|
||||||
@@ -37,10 +37,11 @@ The Smart Infographic plugin uses AI to analyze text content and generate profes
|
|||||||
|
|
||||||
| Category | Template Name | Use Case |
|
| Category | Template Name | Use Case |
|
||||||
|:---------|:--------------|:---------|
|
|:---------|:--------------|:---------|
|
||||||
| **Lists & Hierarchy** | `list-grid`, `tree-vertical`, `mindmap` | Features, Org Charts, Brainstorming |
|
| **Sequence** | `sequence-timeline-simple`, `sequence-roadmap-vertical-simple`, `sequence-snake-steps-compact-card` | Timelines, Roadmaps, Processes |
|
||||||
| **Sequence & Relation** | `sequence-roadmap`, `relation-circle` | Roadmaps, Circular Flows, Steps |
|
| **Lists** | `list-grid-candy-card-lite`, `list-row-horizontal-icon-arrow`, `list-column-simple-vertical-arrow` | Features, Bullet Points, Lists |
|
||||||
| **Comparison & Analysis** | `compare-binary`, `compare-swot`, `quadrant-quarter` | Pros/Cons, SWOT, Quadrants |
|
| **Comparison** | `compare-binary-horizontal-underline-text-vs`, `compare-swot`, `quadrant-quarter-simple-card` | Pros/Cons, SWOT, Quadrants |
|
||||||
| **Charts & Data** | `chart-bar`, `chart-line`, `chart-pie` | Trends, Distributions, Metrics |
|
| **Hierarchy** | `hierarchy-tree-tech-style-capsule-item`, `hierarchy-structure` | Org Charts, Structures |
|
||||||
|
| **Charts** | `chart-column-simple`, `chart-bar-plain-text`, `chart-line-plain-text`, `chart-wordcloud` | Trends, Distributions, Metrics |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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.4.0</span>
|
<span class="version-badge">v1.4.9</span>
|
||||||
|
|
||||||
基于 AntV 信息图引擎,将长文本一键转成专业、美观的信息图。
|
基于 AntV 信息图引擎,将长文本一键转成专业、美观的信息图。
|
||||||
|
|
||||||
@@ -14,8 +14,8 @@ Smart Infographic 使用 AI 分析文本,并基于 AntV 可视化引擎生成
|
|||||||
## 功能特性
|
## 功能特性
|
||||||
|
|
||||||
- :material-robot: **AI 转换**:自动分析文本逻辑,提取要点并生成结构化图表
|
- :material-robot: **AI 转换**:自动分析文本逻辑,提取要点并生成结构化图表
|
||||||
- :material-palette: **专业模板**:内置 AntV 官方模板:列表、树、思维导图、对比表、流程图、统计图等
|
- :material-palette: **70+ 专业模板**:内置多种 AntV 官方模板,包括列表、树图、路线图、时间线、对比图、SWOT、象限图及统计图表等
|
||||||
- :material-magnify: **自动匹配图标**:根据内容自动选择最合适的 Material Design Icons
|
- :material-magnify: **自动匹配图标**:内置图标搜索逻辑,支持 Iconify 图标和 unDraw 插图自动匹配
|
||||||
- :material-download: **多格式导出**:支持下载 **SVG**、**PNG**、**独立 HTML**
|
- :material-download: **多格式导出**:支持下载 **SVG**、**PNG**、**独立 HTML**
|
||||||
- :material-theme-light-dark: **主题支持**:适配深色/浅色模式
|
- :material-theme-light-dark: **主题支持**:适配深色/浅色模式
|
||||||
- :material-cellphone-link: **响应式**:桌面与移动端都能良好展示
|
- :material-cellphone-link: **响应式**:桌面与移动端都能良好展示
|
||||||
@@ -37,10 +37,11 @@ Smart Infographic 使用 AI 分析文本,并基于 AntV 可视化引擎生成
|
|||||||
|
|
||||||
| 分类 | 模板名称 | 典型场景 |
|
| 分类 | 模板名称 | 典型场景 |
|
||||||
|:---------|:--------------|:---------|
|
|:---------|:--------------|:---------|
|
||||||
| **列表与层级** | `list-grid`, `tree-vertical`, `mindmap` | 特性列表、组织结构、头脑风暴 |
|
| **时序与流程** | `sequence-timeline-simple`, `sequence-roadmap-vertical-simple`, `sequence-snake-steps-compact-card` | 时间线、路线图、步骤说明 |
|
||||||
| **序列与关系** | `sequence-roadmap`, `relation-circle` | 路线图、循环流程、步骤拆解 |
|
| **列表与网格** | `list-grid-candy-card-lite`, `list-row-horizontal-icon-arrow`, `list-column-simple-vertical-arrow` | 功能亮点、要点列举、清单 |
|
||||||
| **对比与分析** | `compare-binary`, `compare-swot`, `quadrant-quarter` | 优劣势、SWOT、象限分析 |
|
| **对比与分析** | `compare-binary-horizontal-underline-text-vs`, `compare-swot`, `quadrant-quarter-simple-card` | 优劣势对比、SWOT 分析、象限图 |
|
||||||
| **图表与数据** | `chart-bar`, `chart-line`, `chart-pie` | 趋势、分布、指标对比 |
|
| **层级与结构** | `hierarchy-tree-tech-style-capsule-item`, `hierarchy-structure` | 组织架构、层级关系 |
|
||||||
|
| **图表与数据** | `chart-column-simple`, `chart-bar-plain-text`, `chart-line-plain-text`, `chart-wordcloud` | 数据趋势、比例分布、数值对比 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Async Context Compression
|
# Async Context Compression
|
||||||
|
|
||||||
<span class="category-badge filter">Filter</span>
|
<span class="category-badge filter">Filter</span>
|
||||||
<span class="version-badge">v1.1.0</span>
|
<span class="version-badge">v1.1.3</span>
|
||||||
|
|
||||||
Reduces token consumption in long conversations through intelligent summarization while maintaining conversational coherence.
|
Reduces token consumption in long conversations through intelligent summarization while maintaining conversational coherence.
|
||||||
|
|
||||||
@@ -29,6 +29,11 @@ This is especially useful for:
|
|||||||
- :material-clock-fast: **Async Processing**: Non-blocking background compression
|
- :material-clock-fast: **Async Processing**: Non-blocking background compression
|
||||||
- :material-memory: **Context Preservation**: Keeps important information
|
- :material-memory: **Context Preservation**: Keeps important information
|
||||||
- :material-currency-usd-off: **Cost Reduction**: Minimize token usage
|
- :material-currency-usd-off: **Cost Reduction**: Minimize token usage
|
||||||
|
- :material-console: **Frontend Debugging**: Debug logs in browser console
|
||||||
|
- :material-alert-circle-check: **Enhanced Error Reporting**: Clear error status notifications
|
||||||
|
- :material-check-all: **Open WebUI v0.7.x Compatibility**: Dynamic DB session handling
|
||||||
|
- :material-account-convert: **Improved Compatibility**: Summary role changed to `assistant`
|
||||||
|
- :material-shield-check: **Enhanced Stability**: Resolved race conditions in state management
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Async Context Compression(异步上下文压缩)
|
# Async Context Compression(异步上下文压缩)
|
||||||
|
|
||||||
<span class="category-badge filter">Filter</span>
|
<span class="category-badge filter">Filter</span>
|
||||||
<span class="version-badge">v1.1.0</span>
|
<span class="version-badge">v1.1.3</span>
|
||||||
|
|
||||||
通过智能摘要减少长对话的 token 消耗,同时保持对话连贯。
|
通过智能摘要减少长对话的 token 消耗,同时保持对话连贯。
|
||||||
|
|
||||||
@@ -29,6 +29,11 @@ Async Context Compression 过滤器通过以下方式帮助管理长对话的 to
|
|||||||
- :material-clock-fast: **异步处理**:后台非阻塞压缩
|
- :material-clock-fast: **异步处理**:后台非阻塞压缩
|
||||||
- :material-memory: **保留上下文**:尽量保留重要信息
|
- :material-memory: **保留上下文**:尽量保留重要信息
|
||||||
- :material-currency-usd-off: **降低成本**:减少 token 使用
|
- :material-currency-usd-off: **降低成本**:减少 token 使用
|
||||||
|
- :material-console: **前端调试**:支持浏览器控制台日志
|
||||||
|
- :material-alert-circle-check: **增强错误报告**:清晰的错误状态通知
|
||||||
|
- :material-check-all: **Open WebUI v0.7.x 兼容性**:动态数据库会话处理
|
||||||
|
- :material-account-convert: **兼容性提升**:摘要角色改为 `assistant`
|
||||||
|
- :material-shield-check: **稳定性增强**:解决状态管理竞态条件
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Filters act as middleware in the message pipeline:
|
|||||||
|
|
||||||
Reduces token consumption in long conversations through intelligent summarization while maintaining coherence.
|
Reduces token consumption in long conversations through intelligent summarization while maintaining coherence.
|
||||||
|
|
||||||
**Version:** 1.1.0
|
**Version:** 1.1.3
|
||||||
|
|
||||||
[:octicons-arrow-right-24: Documentation](async-context-compression.md)
|
[:octicons-arrow-right-24: Documentation](async-context-compression.md)
|
||||||
|
|
||||||
@@ -46,6 +46,16 @@ Filters act as middleware in the message pipeline:
|
|||||||
|
|
||||||
[:octicons-arrow-right-24: Documentation](gemini-manifold-companion.md)
|
[:octicons-arrow-right-24: Documentation](gemini-manifold-companion.md)
|
||||||
|
|
||||||
|
- :material-format-paint:{ .lg .middle } **Markdown Normalizer**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixes common Markdown formatting issues in LLM outputs, including Mermaid syntax, code blocks, and LaTeX formulas.
|
||||||
|
|
||||||
|
**Version:** 1.0.1
|
||||||
|
|
||||||
|
[:octicons-arrow-right-24: Documentation](markdown_normalizer.md)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Filter 充当消息管线中的中间件:
|
|||||||
|
|
||||||
通过智能总结减少长对话的 token 消耗,同时保持连贯性。
|
通过智能总结减少长对话的 token 消耗,同时保持连贯性。
|
||||||
|
|
||||||
**版本:** 1.1.0
|
**版本:** 1.1.3
|
||||||
|
|
||||||
[:octicons-arrow-right-24: 查看文档](async-context-compression.md)
|
[:octicons-arrow-right-24: 查看文档](async-context-compression.md)
|
||||||
|
|
||||||
@@ -46,6 +46,16 @@ Filter 充当消息管线中的中间件:
|
|||||||
|
|
||||||
[:octicons-arrow-right-24: 查看文档](gemini-manifold-companion.md)
|
[:octicons-arrow-right-24: 查看文档](gemini-manifold-companion.md)
|
||||||
|
|
||||||
|
- :material-format-paint:{ .lg .middle } **Markdown Normalizer**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
修复 LLM 输出中常见的 Markdown 格式问题,包括 Mermaid 语法、代码块和 LaTeX 公式。
|
||||||
|
|
||||||
|
**版本:** 1.0.1
|
||||||
|
|
||||||
|
[:octicons-arrow-right-24: 查看文档](markdown_normalizer.zh.md)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
46
docs/plugins/filters/markdown_normalizer.md
Normal file
46
docs/plugins/filters/markdown_normalizer.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Markdown Normalizer Filter
|
||||||
|
|
||||||
|
A production-grade content normalizer filter for Open WebUI that fixes common Markdown formatting issues in LLM outputs. It ensures that code blocks, LaTeX formulas, Mermaid diagrams, and other Markdown elements are rendered correctly.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* **Mermaid Syntax Fix**: Automatically fixes common Mermaid syntax errors, such as unquoted node labels (including multi-line labels and citations) and unclosed subgraphs, ensuring diagrams render correctly.
|
||||||
|
* **Frontend Console Debugging**: Supports printing structured debug logs directly to the browser console (F12) for easier troubleshooting.
|
||||||
|
* **Code Block Formatting**: Fixes broken code block prefixes, suffixes, and indentation.
|
||||||
|
* **LaTeX Normalization**: Standardizes LaTeX formula delimiters (`\[` -> `$$`, `\(` -> `$`).
|
||||||
|
* **Thought Tag Normalization**: Unifies thought tags (`<think>`, `<thinking>` -> `<thought>`).
|
||||||
|
* **Escape Character Fix**: Cleans up excessive escape characters (`\\n`, `\\t`).
|
||||||
|
* **List Formatting**: Ensures proper newlines in list items.
|
||||||
|
* **Heading Fix**: Adds missing spaces in headings (`#Heading` -> `# Heading`).
|
||||||
|
* **Table Fix**: Adds missing closing pipes in tables.
|
||||||
|
* **XML Cleanup**: Removes leftover XML artifacts.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Install the plugin in Open WebUI.
|
||||||
|
2. Enable the filter globally or for specific models.
|
||||||
|
3. Configure the enabled fixes in the **Valves** settings.
|
||||||
|
4. (Optional) **Show Debug Log** is enabled by default in Valves. This prints structured logs to the browser console (F12).
|
||||||
|
> [!WARNING]
|
||||||
|
> As this is an initial version, some "negative fixes" might occur (e.g., breaking valid Markdown). If you encounter issues, please check the console logs, copy the "Original" vs "Normalized" content, and submit an issue.
|
||||||
|
|
||||||
|
## Configuration (Valves)
|
||||||
|
|
||||||
|
* `priority`: Filter priority (default: 50).
|
||||||
|
* `enable_escape_fix`: Fix excessive escape characters.
|
||||||
|
* `enable_thought_tag_fix`: Normalize thought tags.
|
||||||
|
* `enable_code_block_fix`: Fix code block formatting.
|
||||||
|
* `enable_latex_fix`: Normalize LaTeX formulas.
|
||||||
|
* `enable_list_fix`: Fix list item newlines (Experimental).
|
||||||
|
* `enable_unclosed_block_fix`: Auto-close unclosed code blocks.
|
||||||
|
* `enable_fullwidth_symbol_fix`: Fix full-width symbols in code blocks.
|
||||||
|
* `enable_mermaid_fix`: Fix Mermaid syntax errors.
|
||||||
|
* `enable_heading_fix`: Fix missing space in headings.
|
||||||
|
* `enable_table_fix`: Fix missing closing pipe in tables.
|
||||||
|
* `enable_xml_tag_cleanup`: Cleanup leftover XML tags.
|
||||||
|
* `show_status`: Show status notification when fixes are applied.
|
||||||
|
* `show_debug_log`: Print debug logs to browser console.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
46
docs/plugins/filters/markdown_normalizer.zh.md
Normal file
46
docs/plugins/filters/markdown_normalizer.zh.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Markdown 格式化过滤器 (Markdown Normalizer)
|
||||||
|
|
||||||
|
这是一个用于 Open WebUI 的生产级内容格式化过滤器,旨在修复 LLM 输出中常见的 Markdown 格式问题。它能确保代码块、LaTeX 公式、Mermaid 图表和其他 Markdown 元素被正确渲染。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
* **Mermaid 语法修复**: 自动修复常见的 Mermaid 语法错误,如未加引号的节点标签(支持多行标签和引用标记)和未闭合的子图 (Subgraph),确保图表能正确渲染。
|
||||||
|
* **前端控制台调试**: 支持将结构化的调试日志直接打印到浏览器控制台 (F12),方便排查问题。
|
||||||
|
* **代码块格式化**: 修复破损的代码块前缀、后缀和缩进问题。
|
||||||
|
* **LaTeX 规范化**: 标准化 LaTeX 公式定界符 (`\[` -> `$$`, `\(` -> `$`)。
|
||||||
|
* **思维标签规范化**: 统一思维链标签 (`<think>`, `<thinking>` -> `<thought>`)。
|
||||||
|
* **转义字符修复**: 清理过度的转义字符 (`\\n`, `\\t`)。
|
||||||
|
* **列表格式化**: 确保列表项有正确的换行。
|
||||||
|
* **标题修复**: 修复标题中缺失的空格 (`#标题` -> `# 标题`)。
|
||||||
|
* **表格修复**: 修复表格中缺失的闭合管道符。
|
||||||
|
* **XML 清理**: 移除残留的 XML 标签。
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
1. 在 Open WebUI 中安装此插件。
|
||||||
|
2. 全局启用或为特定模型启用此过滤器。
|
||||||
|
3. 在 **Valves** 设置中配置需要启用的修复项。
|
||||||
|
4. (可选) **显示调试日志 (Show Debug Log)** 在 Valves 中默认开启。这会将结构化的日志打印到浏览器控制台 (F12)。
|
||||||
|
> [!WARNING]
|
||||||
|
> 由于这是初版,可能会出现“负向修复”的情况(例如破坏了原本正确的格式)。如果您遇到问题,请务必查看控制台日志,复制“原始 (Original)”与“规范化 (Normalized)”的内容对比,并提交 Issue 反馈。
|
||||||
|
|
||||||
|
## 配置项 (Valves)
|
||||||
|
|
||||||
|
* `priority`: 过滤器优先级 (默认: 50)。
|
||||||
|
* `enable_escape_fix`: 修复过度的转义字符。
|
||||||
|
* `enable_thought_tag_fix`: 规范化思维标签。
|
||||||
|
* `enable_code_block_fix`: 修复代码块格式。
|
||||||
|
* `enable_latex_fix`: 规范化 LaTeX 公式。
|
||||||
|
* `enable_list_fix`: 修复列表项换行 (实验性)。
|
||||||
|
* `enable_unclosed_block_fix`: 自动闭合未闭合的代码块。
|
||||||
|
* `enable_fullwidth_symbol_fix`: 修复代码块中的全角符号。
|
||||||
|
* `enable_mermaid_fix`: 修复 Mermaid 语法错误。
|
||||||
|
* `enable_heading_fix`: 修复标题中缺失的空格。
|
||||||
|
* `enable_table_fix`: 修复表格中缺失的闭合管道符。
|
||||||
|
* `enable_xml_tag_cleanup`: 清理残留的 XML 标签。
|
||||||
|
* `show_status`: 应用修复时显示状态通知。
|
||||||
|
* `show_debug_log`: 在浏览器控制台打印调试日志。
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
# 📊 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)
|
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 1.4.9 | **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
|
## 🔥 What's New in v1.4.9
|
||||||
|
|
||||||
|
- 🎨 **70+ Official Templates**: Integrated comprehensive AntV infographic template library.
|
||||||
|
- 🖼️ **Iconify & unDraw Support**: Richer visuals with official icons and illustrations.
|
||||||
|
- 📏 **Visual Optimization**: Improved text wrapping, adaptive sizing, and layout refinement.
|
||||||
- ✨ **PNG Upload**: Infographics now upload as PNG format for better Word export compatibility.
|
- ✨ **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).
|
- 🔧 **Canvas Conversion**: Uses browser canvas for high-quality SVG to PNG conversion (2x scale).
|
||||||
|
|
||||||
@@ -17,8 +20,8 @@ An Open WebUI plugin powered by the AntV Infographic engine. It transforms long
|
|||||||
## ✨ 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.
|
||||||
- 🎨 **Professional Templates**: Includes various AntV official templates: Lists, Trees, Mindmaps, Comparison Tables, Flowcharts, and Statistical Charts.
|
- 🎨 **70+ Professional Templates**: Includes various AntV official templates: Lists, Trees, Roadmaps, Timelines, Comparison Tables, SWOT, Quadrants, and Statistical Charts.
|
||||||
- 🔍 **Auto-Icon Matching**: Built-in logic to search and match the most relevant Material Design Icons based on content.
|
- 🔍 **Auto-Icon Matching**: Built-in logic to search and match the most relevant icons (Iconify) and illustrations (unDraw).
|
||||||
- 📥 **Multi-Format Export**: Download your infographics as **SVG**, **PNG**, or a **Standalone HTML** file.
|
- 📥 **Multi-Format Export**: Download your infographics as **SVG**, **PNG**, or a **Standalone HTML** file.
|
||||||
- 🌈 **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.
|
||||||
@@ -47,10 +50,11 @@ You can adjust the following parameters in the plugin settings to optimize the g
|
|||||||
|
|
||||||
| Category | Template Name | Use Case |
|
| Category | Template Name | Use Case |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| **Lists & Hierarchy** | `list-grid`, `tree-vertical`, `mindmap` | Features, Org Charts, Brainstorming |
|
| **Sequence** | `sequence-timeline-simple`, `sequence-roadmap-vertical-simple`, `sequence-snake-steps-compact-card` | Timelines, Roadmaps, Processes |
|
||||||
| **Sequence & Relation** | `sequence-roadmap`, `relation-circle` | Roadmaps, Circular Flows, Steps |
|
| **Lists** | `list-grid-candy-card-lite`, `list-row-horizontal-icon-arrow`, `list-column-simple-vertical-arrow` | Features, Bullet Points, Lists |
|
||||||
| **Comparison & Analysis** | `compare-binary`, `compare-swot`, `quadrant-quarter` | Pros/Cons, SWOT, Quadrants |
|
| **Comparison** | `compare-binary-horizontal-underline-text-vs`, `compare-swot`, `quadrant-quarter-simple-card` | Pros/Cons, SWOT, Quadrants |
|
||||||
| **Charts & Data** | `chart-bar`, `chart-line`, `chart-pie` | Trends, Distributions, Metrics |
|
| **Hierarchy** | `hierarchy-tree-tech-style-capsule-item`, `hierarchy-structure` | Org Charts, Structures |
|
||||||
|
| **Charts** | `chart-column-simple`, `chart-bar-plain-text`, `chart-line-plain-text`, `chart-wordcloud` | Trends, Distributions, Metrics |
|
||||||
|
|
||||||
## 📝 Syntax Example (For Advanced Users)
|
## 📝 Syntax Example (For Advanced Users)
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
# 📊 智能信息图 (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)
|
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 1.4.9 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
|
|
||||||
基于 AntV Infographic 引擎的 Open WebUI 插件,能够将长文本内容一键转换为专业、美观的信息图表。
|
基于 AntV Infographic 引擎的 Open WebUI 插件,能够将长文本内容一键转换为专业、美观的信息图表。
|
||||||
|
|
||||||
## 🔥 v1.4.1 更新日志
|
## 🔥 v1.4.9 更新日志
|
||||||
|
|
||||||
|
- 🎨 **70+ 官方模板**:全面集成 AntV 官方信息图模板库。
|
||||||
|
- 🖼️ **图标与插图支持**:支持 Iconify 图标库与 unDraw 插图库,视觉效果更丰富。
|
||||||
|
- 📏 **视觉优化**:改进文本换行逻辑,优化自适应尺寸,提升卡片布局精细度。
|
||||||
- ✨ **PNG 上传**:信息图现在以 PNG 格式上传,与 Word 导出完美兼容。
|
- ✨ **PNG 上传**:信息图现在以 PNG 格式上传,与 Word 导出完美兼容。
|
||||||
- 🔧 **Canvas 转换**:使用浏览器 Canvas 高质量转换 SVG 为 PNG(2倍缩放)。
|
- 🔧 **Canvas 转换**:使用浏览器 Canvas 高质量转换 SVG 为 PNG(2倍缩放)。
|
||||||
|
|
||||||
@@ -17,8 +20,8 @@
|
|||||||
## ✨ 核心特性
|
## ✨ 核心特性
|
||||||
|
|
||||||
- 🚀 **智能转换**:自动分析文本核心逻辑,提取关键点并生成结构化图表。
|
- 🚀 **智能转换**:自动分析文本核心逻辑,提取关键点并生成结构化图表。
|
||||||
- 🎨 **专业模板**:内置多种 AntV 官方模板,包括列表、树图、思维导图、对比图、流程图及统计图表等。
|
- 🎨 **70+ 专业模板**:内置多种 AntV 官方模板,包括列表、树图、路线图、时间线、对比图、SWOT、象限图及统计图表等。
|
||||||
- 🔍 **自动图标匹配**:内置图标搜索逻辑,根据内容自动匹配最相关的 Material Design Icons。
|
- 🔍 **自动图标匹配**:内置图标搜索逻辑,支持 Iconify 图标和 unDraw 插图自动匹配。
|
||||||
- 📥 **多格式导出**:支持一键下载为 **SVG**、**PNG** 或 **独立 HTML** 文件。
|
- 📥 **多格式导出**:支持一键下载为 **SVG**、**PNG** 或 **独立 HTML** 文件。
|
||||||
- 🌈 **高度自定义**:支持深色/浅色模式,自动适配主题颜色,主标题加粗突出,卡片布局精美。
|
- 🌈 **高度自定义**:支持深色/浅色模式,自动适配主题颜色,主标题加粗突出,卡片布局精美。
|
||||||
- 📱 **响应式设计**:生成的图表在桌面端和移动端均有良好的展示效果。
|
- 📱 **响应式设计**:生成的图表在桌面端和移动端均有良好的展示效果。
|
||||||
@@ -47,10 +50,11 @@
|
|||||||
|
|
||||||
| 分类 | 模板名称 | 适用场景 |
|
| 分类 | 模板名称 | 适用场景 |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| **列表与层级** | `list-grid`, `tree-vertical`, `mindmap` | 功能亮点、组织架构、思维导图 |
|
| **时序与流程** | `sequence-timeline-simple`, `sequence-roadmap-vertical-simple`, `sequence-snake-steps-compact-card` | 时间线、路线图、步骤说明 |
|
||||||
| **顺序与关系** | `sequence-roadmap`, `relation-circle` | 发展历程、循环关系、步骤说明 |
|
| **列表与网格** | `list-grid-candy-card-lite`, `list-row-horizontal-icon-arrow`, `list-column-simple-vertical-arrow` | 功能亮点、要点列举、清单 |
|
||||||
| **对比与分析** | `compare-binary`, `compare-swot`, `quadrant-quarter` | 优劣势对比、SWOT 分析、象限图 |
|
| **对比与分析** | `compare-binary-horizontal-underline-text-vs`, `compare-swot`, `quadrant-quarter-simple-card` | 优劣势对比、SWOT 分析、象限图 |
|
||||||
| **图表与数据** | `chart-bar`, `chart-line`, `chart-pie` | 数据趋势、比例分布、数值对比 |
|
| **层级与结构** | `hierarchy-tree-tech-style-capsule-item`, `hierarchy-structure` | 组织架构、层级关系 |
|
||||||
|
| **图表与数据** | `chart-column-simple`, `chart-bar-plain-text`, `chart-line-plain-text`, `chart-wordcloud` | 数据趋势、比例分布、数值对比 |
|
||||||
|
|
||||||
## 📝 语法示例 (高级用户)
|
## 📝 语法示例 (高级用户)
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
title: 📊 Smart Infographic (AntV)
|
title: 📊 Smart Infographic (AntV)
|
||||||
author: jeff
|
author: Fu-Jie
|
||||||
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.4.1
|
version: 1.4.9
|
||||||
openwebui_id: ad6f0c7f-c571-4dea-821d-8e71697274cf
|
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.
|
||||||
"""
|
"""
|
||||||
@@ -47,24 +47,63 @@ Infographic syntax is a Mermaid-like declarative syntax for describing infograph
|
|||||||
|
|
||||||
### Template Library & Selection Guide
|
### Template Library & Selection Guide
|
||||||
|
|
||||||
Choose the most appropriate template based on the content structure:
|
Choose the most appropriate template based on content structure.
|
||||||
|
|
||||||
#### 1. List & Hierarchy
|
**Template Selection Guidelines (Official):**
|
||||||
- **List**: `list-grid` (Grid Cards), `list-vertical` (Vertical List)
|
- Strict sequential order (processes/steps/trends) → `sequence-*` series
|
||||||
- **Tree**: `tree-vertical` (Vertical Tree), `tree-horizontal` (Horizontal Tree)
|
- Timeline → `sequence-timeline-simple`
|
||||||
- **Mindmap**: `mindmap` (Mind Map)
|
- Roadmap → `sequence-roadmap-vertical-simple`
|
||||||
|
- Zigzag steps → `sequence-horizontal-zigzag-underline-text`
|
||||||
|
- Snake steps → `sequence-snake-steps-compact-card`
|
||||||
|
- Listing viewpoints → `list-row-horizontal-icon-arrow` or `list-column-simple-vertical-arrow`
|
||||||
|
- Comparative analysis (A vs B) → `compare-binary-horizontal-underline-text-vs`
|
||||||
|
- SWOT analysis → `compare-swot`
|
||||||
|
- Hierarchical structure (tree) → `hierarchy-tree-tech-style-capsule-item`
|
||||||
|
- Data charts → `chart-*` series
|
||||||
|
- Quadrant analysis → `quadrant-quarter-simple-card`
|
||||||
|
- Grid lists (bullet points) → `list-grid-candy-card-lite`
|
||||||
|
- Relationship display → `relation-circle-icon-badge`
|
||||||
|
|
||||||
#### 2. Sequence & Relationship
|
**Available Templates:**
|
||||||
- **Process**: `sequence-roadmap` (Roadmap), `sequence-zigzag` (Zigzag Process), `sequence-horizontal` (Horizontal Process)
|
|
||||||
- **Relationship**: `relation-sankey` (Sankey Diagram), `relation-circle` (Circular Relationship)
|
|
||||||
|
|
||||||
#### 3. Comparison & Analysis
|
*Sequence (时序/流程):*
|
||||||
- **Comparison**: `compare-binary` (Binary Comparison), `list-grid` (Multi-item Grid Comparison)
|
`sequence-timeline-simple`, `sequence-roadmap-vertical-simple`, `sequence-horizontal-zigzag-underline-text`,
|
||||||
- **Analysis**: `compare-swot` (SWOT Analysis), `quadrant-quarter` (Quadrant Chart)
|
`sequence-snake-steps-compact-card`, `sequence-zigzag-steps-underline-text`, `sequence-circular-simple`,
|
||||||
|
`sequence-pyramid-simple`, `sequence-ascending-steps`
|
||||||
|
|
||||||
#### 4. Charts & Data
|
*List (列表):*
|
||||||
- **Statistics**: `statistic-card` (Statistic Cards)
|
`list-grid-candy-card-lite`, `list-grid-badge-card`, `list-row-horizontal-icon-arrow`,
|
||||||
- **Charts**: `chart-bar` (Bar Chart), `chart-column` (Column Chart), `chart-line` (Line Chart), `chart-pie` (Pie Chart), `chart-doughnut` (Doughnut Chart), `chart-area` (Area Chart)
|
`list-column-simple-vertical-arrow`, `list-column-done-list`
|
||||||
|
|
||||||
|
*Compare (对比):*
|
||||||
|
`compare-binary-horizontal-underline-text-vs`, `compare-binary-horizontal-simple-fold`,
|
||||||
|
`compare-hierarchy-left-right-circle-node-pill-badge`, `compare-swot`
|
||||||
|
|
||||||
|
*Hierarchy (层级):*
|
||||||
|
`hierarchy-tree-tech-style-capsule-item`, `hierarchy-tree-curved-line-rounded-rect-node`, `hierarchy-structure`
|
||||||
|
|
||||||
|
*Chart (图表):*
|
||||||
|
`chart-column-simple`, `chart-bar-plain-text`, `chart-line-plain-text`,
|
||||||
|
`chart-pie-plain-text`, `chart-pie-donut-plain-text`, `chart-wordcloud`
|
||||||
|
|
||||||
|
*Other:*
|
||||||
|
`quadrant-quarter-simple-card`, `relation-circle-icon-badge`
|
||||||
|
|
||||||
|
**Text Capacity by Template Type:**
|
||||||
|
- HIGH capacity (long descriptions OK): `list-column-*`, `compare-binary-*`, `sequence-timeline-*`
|
||||||
|
- MEDIUM capacity: `list-row-*`, `sequence-roadmap-*`
|
||||||
|
- LOW capacity (short text only): `list-grid-*`, `hierarchy-*`, `sequence-steps`
|
||||||
|
|
||||||
|
### Icon and Illustration Resources
|
||||||
|
|
||||||
|
**Icons (Iconify):**
|
||||||
|
- Format: `<collection>/<icon-name>`, e.g., `mdi/rocket-launch`
|
||||||
|
- Popular: `mdi/*` (Material Design), `fa/*` (Font Awesome), `bi/*` (Bootstrap)
|
||||||
|
- Examples: `mdi/code-tags`, `mdi/chart-line`, `mdi/account-group`, `mdi/cloud`
|
||||||
|
|
||||||
|
**Illustrations (unDraw):**
|
||||||
|
- Format: filename without .svg, e.g., `coding`, `team-work`
|
||||||
|
- Use `illus` field instead of `icon`
|
||||||
|
|
||||||
### Data Structure Examples
|
### Data Structure Examples
|
||||||
|
|
||||||
@@ -211,6 +250,12 @@ data
|
|||||||
- `children`: Nested items (for trees, SWOT, etc.)
|
- `children`: Nested items (for trees, SWOT, etc.)
|
||||||
- `illus`: Illustration icon (specific to some templates like Quadrant)
|
- `illus`: Illustration icon (specific to some templates like Quadrant)
|
||||||
|
|
||||||
|
### Content Refinement Principles
|
||||||
|
1. **Brevity is King**: Infographics are visual. Keep text to a minimum.
|
||||||
|
2. **Title Limit**: Keep `label` (item titles) under 15 characters (approx. 10 Chinese characters).
|
||||||
|
3. **Description Limit**: Keep `desc` (item descriptions) under 40 characters (approx. 20 Chinese characters / 2 lines).
|
||||||
|
4. **Impact**: Use strong verbs and nouns. Avoid filler words.
|
||||||
|
|
||||||
## Output Requirements
|
## Output Requirements
|
||||||
1. **Language**: Output content in the user's language.
|
1. **Language**: Output content in the user's language.
|
||||||
2. **Format**: Wrap output in ```infographic ... ```.
|
2. **Format**: Wrap output in ```infographic ... ```.
|
||||||
@@ -233,9 +278,18 @@ User Language: {user_language}
|
|||||||
|
|
||||||
Please select the most appropriate infographic template based on text characteristics and output standard infographic syntax. Pay attention to correct indentation format (two spaces).
|
Please select the most appropriate infographic template based on text characteristics and output standard infographic syntax. Pay attention to correct indentation format (two spaces).
|
||||||
|
|
||||||
**Important Note:**
|
**Visual Optimization Guide (MUST FOLLOW):**
|
||||||
- If using `list-grid` format, ensure each card's `desc` description is limited to **maximum 30 Chinese characters** (or **approximately 60 English characters**) to maintain visual consistency with all descriptions fitting in 2 lines.
|
- **Point-based Generation:** Infographics are not articles. Extract KEYWORDS ONLY, avoid complete sentences.
|
||||||
- Descriptions should be concise and highlight key points.
|
- **Main Title (`data.title`):** **MUST** be ≤ **15 Chinese characters** (or ≤30 English characters). Trim version numbers or details if needed.
|
||||||
|
- **Subtitle (`data.desc`):** **MUST** be ≤ **20 Chinese characters** (or ≤40 English characters).
|
||||||
|
- **Card Title (`label`):** **MUST** be ≤ **6 Chinese characters** (or ≤12 English characters). Use 2-4 keywords only.
|
||||||
|
- **Card Description (`desc`):** **MUST** be ≤ **12 Chinese characters** (or ≤24 English characters). Use short phrases.
|
||||||
|
|
||||||
|
⚠️ **CRITICAL**: If the original text is too long, you MUST rephrase and shorten it. Do NOT simply truncate with "...".
|
||||||
|
Examples:
|
||||||
|
- ❌ "多步任务与工具协作能力" → ✅ "多步任务协作"
|
||||||
|
- ❌ "Open WebUI v0.7.x 重大版本更新" → ✅ "v0.7 核心更新"
|
||||||
|
- ❌ "自动查找历史聊天记录" → ✅ "历史检索"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# =================================================================
|
# =================================================================
|
||||||
@@ -340,8 +394,9 @@ CSS_TEMPLATE_INFOGRAPHIC = """
|
|||||||
.infographic-container-wrapper .infographic-render-container {
|
.infographic-container-wrapper .infographic-render-container {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
min-height: 600px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
overflow: visible; /* Ensure content is visible */
|
overflow: visible;
|
||||||
transition: height 0.3s ease;
|
transition: height 0.3s ease;
|
||||||
}
|
}
|
||||||
.infographic-render-container svg text {
|
.infographic-render-container svg text {
|
||||||
@@ -349,35 +404,59 @@ CSS_TEMPLATE_INFOGRAPHIC = """
|
|||||||
}
|
}
|
||||||
.infographic-render-container svg foreignObject {
|
.infographic-render-container svg foreignObject {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif !important;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif !important;
|
||||||
line-height: 1.4 !important;
|
line-height: 1.3 !important;
|
||||||
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
/* Main title styles */
|
/* Main title styles */
|
||||||
.infographic-render-container svg foreignObject[data-element-type="title"] > * {
|
.infographic-render-container svg foreignObject[data-element-type="title"] > * {
|
||||||
font-size: 1.5em !important;
|
font-size: 1.3em !important;
|
||||||
font-weight: bold !important;
|
font-weight: 800 !important;
|
||||||
line-height: 1.4 !important;
|
line-height: 1.3 !important;
|
||||||
white-space: nowrap !important;
|
white-space: normal !important;
|
||||||
|
word-break: break-word !important;
|
||||||
|
display: -webkit-box !important;
|
||||||
|
-webkit-line-clamp: 2 !important;
|
||||||
|
-webkit-box-orient: vertical !important;
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
text-overflow: ellipsis !important;
|
text-overflow: ellipsis !important;
|
||||||
|
text-align: center !important;
|
||||||
}
|
}
|
||||||
/* Page subtitle and card title styles */
|
/* Page subtitle styles */
|
||||||
.infographic-render-container svg foreignObject[data-element-type="desc"] > *,
|
.infographic-render-container svg foreignObject[data-element-type="desc"] > * {
|
||||||
.infographic-render-container svg foreignObject[data-element-type="item-label"] > * {
|
font-size: 0.85em !important;
|
||||||
font-size: 0.6em !important;
|
line-height: 1.3 !important;
|
||||||
line-height: 1.4 !important;
|
white-space: normal !important;
|
||||||
white-space: nowrap !important;
|
word-break: break-word !important;
|
||||||
overflow: hidden !important;
|
overflow: visible !important;
|
||||||
text-overflow: ellipsis !important;
|
text-align: center !important;
|
||||||
}
|
|
||||||
/* Card title with extra bottom spacing */
|
|
||||||
.infographic-render-container svg foreignObject[data-element-type="item-label"] > * {
|
|
||||||
padding-bottom: 8px !important;
|
|
||||||
display: block !important;
|
display: block !important;
|
||||||
|
color: var(--ig-muted-text-color) !important;
|
||||||
}
|
}
|
||||||
/* Card description text keeps normal wrapping */
|
/* Card title styles */
|
||||||
|
.infographic-render-container svg foreignObject[data-element-type="item-label"] > * {
|
||||||
|
font-size: 0.9em !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
line-height: 1.3 !important;
|
||||||
|
white-space: normal !important;
|
||||||
|
word-break: break-word !important;
|
||||||
|
display: -webkit-box !important;
|
||||||
|
-webkit-line-clamp: 2 !important;
|
||||||
|
-webkit-box-orient: vertical !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
text-overflow: ellipsis !important;
|
||||||
|
padding-bottom: 2px !important;
|
||||||
|
}
|
||||||
|
/* Card description text */
|
||||||
.infographic-render-container svg foreignObject[data-element-type="item-desc"] > * {
|
.infographic-render-container svg foreignObject[data-element-type="item-desc"] > * {
|
||||||
|
font-size: 0.8em !important;
|
||||||
line-height: 1.4 !important;
|
line-height: 1.4 !important;
|
||||||
white-space: normal !important;
|
white-space: normal !important;
|
||||||
|
word-break: break-word !important;
|
||||||
|
display: -webkit-box !important;
|
||||||
|
-webkit-line-clamp: 2 !important;
|
||||||
|
-webkit-box-orient: vertical !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
text-overflow: ellipsis !important;
|
||||||
}
|
}
|
||||||
.infographic-container-wrapper .download-area {
|
.infographic-container-wrapper .download-area {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -533,37 +612,41 @@ SCRIPT_TEMPLATE_INFOGRAPHIC = """
|
|||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// 2. Template Mapping Configuration
|
// 2. Template Mapping Configuration (Official AntV Structure IDs)
|
||||||
const TEMPLATE_MAPPING = {{
|
const TEMPLATE_MAPPING = {{
|
||||||
// List & Hierarchy
|
// List & Hierarchy - map short names to full template names
|
||||||
'list-grid': 'list-grid-compact-card',
|
'list-grid': 'list-grid-compact-card',
|
||||||
|
'list-column': 'list-column-simple-vertical-arrow',
|
||||||
|
'list-row': 'list-row-simple-horizontal-arrow',
|
||||||
|
'hierarchy-tree': 'hierarchy-tree-tech-style-capsule-item',
|
||||||
|
|
||||||
|
// Sequence & Timeline
|
||||||
|
'sequence-roadmap-vertical': 'sequence-roadmap-vertical-simple',
|
||||||
|
'sequence-timeline': 'sequence-timeline-simple',
|
||||||
|
'sequence-steps': 'sequence-steps-simple',
|
||||||
|
'sequence-horizontal-zigzag': 'sequence-horizontal-zigzag-simple',
|
||||||
|
|
||||||
|
// Comparison
|
||||||
|
'compare-binary-horizontal': 'compare-binary-horizontal-simple-vs',
|
||||||
|
'compare-hierarchy-row': 'compare-hierarchy-row-simple',
|
||||||
|
|
||||||
|
// Charts
|
||||||
|
'chart-column': 'chart-column-simple',
|
||||||
|
'quadrant': 'quadrant-quarter-simple-card',
|
||||||
|
|
||||||
|
// Legacy mappings for backward compatibility
|
||||||
'list-vertical': 'list-column-simple-vertical-arrow',
|
'list-vertical': 'list-column-simple-vertical-arrow',
|
||||||
'tree-vertical': 'hierarchy-tree-tech-style-capsule-item',
|
'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 & Relationship
|
|
||||||
'sequence-roadmap': 'sequence-roadmap-vertical-simple',
|
'sequence-roadmap': 'sequence-roadmap-vertical-simple',
|
||||||
'sequence-zigzag': 'sequence-horizontal-zigzag-simple',
|
'sequence-zigzag': 'sequence-horizontal-zigzag-simple',
|
||||||
'sequence-horizontal': 'sequence-horizontal-zigzag-simple',
|
|
||||||
'relation-sankey': 'relation-sankey-simple',
|
|
||||||
'relation-circle': 'relation-circle-icon-badge',
|
|
||||||
|
|
||||||
// Comparison & Analysis
|
|
||||||
'compare-binary': 'compare-binary-horizontal-simple-vs',
|
'compare-binary': 'compare-binary-horizontal-simple-vs',
|
||||||
'compare-swot': 'compare-swot',
|
|
||||||
'quadrant-quarter': 'quadrant-quarter-simple-card',
|
|
||||||
|
|
||||||
// Charts & Data
|
|
||||||
'statistic-card': 'list-grid-compact-card',
|
|
||||||
'chart-bar': 'chart-bar-plain-text',
|
'chart-bar': 'chart-bar-plain-text',
|
||||||
'chart-column': 'chart-column-simple',
|
|
||||||
'chart-line': 'chart-line-plain-text',
|
'chart-line': 'chart-line-plain-text',
|
||||||
'chart-area': 'chart-area-simple',
|
|
||||||
'chart-pie': 'chart-pie-plain-text',
|
'chart-pie': 'chart-pie-plain-text',
|
||||||
'chart-doughnut': 'chart-pie-donut-plain-text'
|
'chart-doughnut': 'chart-pie-donut-plain-text'
|
||||||
}};
|
}};
|
||||||
|
|
||||||
|
|
||||||
// 3. Apply Mapping Strategy
|
// 3. Apply Mapping Strategy
|
||||||
for (const [key, value] of Object.entries(TEMPLATE_MAPPING)) {{
|
for (const [key, value] of Object.entries(TEMPLATE_MAPPING)) {{
|
||||||
const regex = new RegExp(`infographic\\\\s+${{key}}(?=\\\\s|$)`, 'i');
|
const regex = new RegExp(`infographic\\\\s+${{key}}(?=\\\\s|$)`, 'i');
|
||||||
@@ -629,10 +712,48 @@ SCRIPT_TEMPLATE_INFOGRAPHIC = """
|
|||||||
containerEl.dataset.infographicRendered = 'true';
|
containerEl.dataset.infographicRendered = 'true';
|
||||||
console.log('[Infographic] Rendering complete');
|
console.log('[Infographic] Rendering complete');
|
||||||
|
|
||||||
// Auto-adjust height
|
// Auto-adjust height and tag elements
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const svg = containerEl.querySelector('svg');
|
const svg = containerEl.querySelector('svg');
|
||||||
if (svg) {
|
if (svg) {
|
||||||
|
// 1. Tag elements for CSS styling
|
||||||
|
const fos = Array.from(svg.querySelectorAll('foreignObject'));
|
||||||
|
let titleFound = false;
|
||||||
|
let descFound = false;
|
||||||
|
|
||||||
|
fos.forEach((fo) => {
|
||||||
|
const text = fo.textContent.trim();
|
||||||
|
if (!text || fo.querySelector('i') || (fo.querySelector('svg') && fo.querySelectorAll('*').length < 5)) {
|
||||||
|
fo.setAttribute('data-element-type', 'icon');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamically increase height and width to accommodate wrapped text
|
||||||
|
const currentHeight = parseInt(fo.getAttribute('height') || '0');
|
||||||
|
if (currentHeight > 0 && currentHeight < 200) {
|
||||||
|
fo.setAttribute('height', Math.round(currentHeight * 1.8).toString());
|
||||||
|
}
|
||||||
|
const currentWidth = parseInt(fo.getAttribute('width') || '0');
|
||||||
|
if (currentWidth > 0 && currentWidth < 300) {
|
||||||
|
fo.setAttribute('width', Math.max(Math.round(currentWidth * 1.2), 180).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!titleFound) {
|
||||||
|
fo.setAttribute('data-element-type', 'title');
|
||||||
|
titleFound = true;
|
||||||
|
} else if (!descFound) {
|
||||||
|
fo.setAttribute('data-element-type', 'desc');
|
||||||
|
descFound = true;
|
||||||
|
} else {
|
||||||
|
if (fo.querySelector('strong') || fo.style.fontWeight === 'bold' || text.length < 15) {
|
||||||
|
fo.setAttribute('data-element-type', 'item-label');
|
||||||
|
} else {
|
||||||
|
fo.setAttribute('data-element-type', 'item-desc');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Adjust height
|
||||||
const bbox = svg.getBoundingClientRect();
|
const bbox = svg.getBoundingClientRect();
|
||||||
let contentHeight = bbox.height;
|
let contentHeight = bbox.height;
|
||||||
if (svg.viewBox && svg.viewBox.baseVal && svg.viewBox.baseVal.height) {
|
if (svg.viewBox && svg.viewBox.baseVal && svg.viewBox.baseVal.height) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
title: 📊 智能信息图 (AntV Infographic)
|
title: 📊 智能信息图 (AntV Infographic)
|
||||||
author: jeff
|
author: Fu-Jie
|
||||||
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.4.1
|
version: 1.4.9
|
||||||
openwebui_id: e04a48ff-23ee-4a41-8ea7-66c19524e7c8
|
openwebui_id: e04a48ff-23ee-4a41-8ea7-66c19524e7c8
|
||||||
description: 基于 AntV Infographic 的智能信息图生成插件。支持多种专业模板,自动图标匹配,并提供 SVG/PNG 下载功能。
|
description: 基于 AntV Infographic 的智能信息图生成插件。支持多种专业模板,自动图标匹配,并提供 SVG/PNG 下载功能。
|
||||||
"""
|
"""
|
||||||
@@ -45,32 +45,61 @@ Infographic syntax is a Mermaid-like declarative syntax for describing infograph
|
|||||||
- ❌ Wrong: `children:` `items:` `data:` (with colons)
|
- ❌ Wrong: `children:` `items:` `data:` (with colons)
|
||||||
- ✅ Correct: `children` `items` `data` (without colons)
|
- ✅ Correct: `children` `items` `data` (without colons)
|
||||||
|
|
||||||
### Template Library & Selection Guide
|
### 模板库与选择指南
|
||||||
|
|
||||||
#### 1. List & Hierarchy (Text-heavy)
|
根据内容结构选择最合适的模板。
|
||||||
- **Linear & Short (Steps/Phases)** -> `list-row-horizontal-icon-arrow`
|
|
||||||
- **Linear & Long (Rankings/Details)** -> `list-vertical`
|
|
||||||
- **Grouped / Parallel (Features/Catalog)** -> `list-grid`
|
|
||||||
- **Hierarchical (Org Chart/Taxonomy)** -> `tree-vertical` or `tree-horizontal`
|
|
||||||
- **Central Idea (Brainstorming)** -> `mindmap`
|
|
||||||
|
|
||||||
#### 2. Sequence & Relationship (Flow-based)
|
**模板选择指南 (官方):**
|
||||||
- **Time-based (History/Plan)** -> `sequence-roadmap-vertical-simple`
|
- 严格时序 (流程/步骤/趋势) → `sequence-*` 系列
|
||||||
- **Process Flow (Complex)** -> `sequence-zigzag` or `sequence-horizontal`
|
- 时间线 → `sequence-timeline-simple`
|
||||||
- **Resource Flow / Distribution** -> `relation-sankey`
|
- 路线图 → `sequence-roadmap-vertical-simple`
|
||||||
- **Circular Relationship** -> `relation-circle`
|
- 折线步骤 → `sequence-horizontal-zigzag-underline-text`
|
||||||
|
- 蛇形步骤 → `sequence-snake-steps-compact-card`
|
||||||
|
- 列举要点 → `list-row-horizontal-icon-arrow` 或 `list-column-simple-vertical-arrow`
|
||||||
|
- 对比分析 (A vs B) → `compare-binary-horizontal-underline-text-vs`
|
||||||
|
- SWOT 分析 → `compare-swot`
|
||||||
|
- 层级结构 (树状图) → `hierarchy-tree-tech-style-capsule-item`
|
||||||
|
- 数据图表 → `chart-*` 系列
|
||||||
|
- 象限分析 → `quadrant-quarter-simple-card`
|
||||||
|
- 网格列表 → `list-grid-candy-card-lite`
|
||||||
|
- 关系展示 → `relation-circle-icon-badge`
|
||||||
|
|
||||||
#### 3. Comparison & Analysis
|
**可用模板:**
|
||||||
- **Binary Comparison (A vs B)** -> `compare-binary`
|
|
||||||
- **SWOT Analysis** -> `compare-swot`
|
|
||||||
- **Quadrant Analysis (Importance vs Urgency)** -> `quadrant-quarter`
|
|
||||||
- **Multi-item Grid Comparison** -> `list-grid` (use for comparing multiple items)
|
|
||||||
|
|
||||||
#### 4. Charts & Data (Metric-heavy)
|
*Sequence (时序/流程):*
|
||||||
- **Key Metrics / Data Cards** -> `statistic-card`
|
`sequence-timeline-simple`, `sequence-roadmap-vertical-simple`, `sequence-horizontal-zigzag-underline-text`,
|
||||||
- **Distribution / Comparison** -> `chart-bar` or `chart-column`
|
`sequence-snake-steps-compact-card`, `sequence-zigzag-steps-underline-text`, `sequence-circular-simple`
|
||||||
- **Trend over Time** -> `chart-line` or `chart-area`
|
|
||||||
- **Proportion / Part-to-Whole** -> `chart-pie` or `chart-doughnut`
|
*List (列表):*
|
||||||
|
`list-grid-candy-card-lite`, `list-grid-badge-card`, `list-row-horizontal-icon-arrow`,
|
||||||
|
`list-column-simple-vertical-arrow`, `list-column-done-list`
|
||||||
|
|
||||||
|
*Compare (对比):*
|
||||||
|
`compare-binary-horizontal-underline-text-vs`, `compare-swot`
|
||||||
|
|
||||||
|
*Hierarchy (层级):*
|
||||||
|
`hierarchy-tree-tech-style-capsule-item`, `hierarchy-structure`
|
||||||
|
|
||||||
|
*Chart (图表):*
|
||||||
|
`chart-column-simple`, `chart-bar-plain-text`, `chart-pie-plain-text`, `chart-wordcloud`
|
||||||
|
|
||||||
|
*Other:*
|
||||||
|
`quadrant-quarter-simple-card`, `relation-circle-icon-badge`
|
||||||
|
|
||||||
|
**按容量分类:**
|
||||||
|
- 高容量 (长描述): `list-column-*`, `compare-binary-*`, `sequence-timeline-*`
|
||||||
|
- 中容量: `list-row-*`, `sequence-roadmap-*`
|
||||||
|
- 低容量 (短文本): `list-grid-*`, `hierarchy-*`
|
||||||
|
|
||||||
|
### 图标和插图资源
|
||||||
|
|
||||||
|
**图标 (Iconify):**
|
||||||
|
- 格式: `<集合>/<图标名>`, 如 `mdi/rocket-launch`
|
||||||
|
- 常用: `mdi/*`, `fa/*`, `bi/*`
|
||||||
|
|
||||||
|
**插图 (unDraw):**
|
||||||
|
- 格式: 文件名 (不含 .svg), 如 `coding`, `team-work`
|
||||||
|
- 使用 `illus` 字段
|
||||||
|
|
||||||
### Infographic Syntax Guide
|
### Infographic Syntax Guide
|
||||||
|
|
||||||
@@ -203,6 +232,12 @@ data
|
|||||||
desc Plan for next sprint
|
desc Plan for next sprint
|
||||||
illus mdi/star
|
illus mdi/star
|
||||||
|
|
||||||
|
### Content Refinement Principles
|
||||||
|
1. **Brevity is King**: Infographics are visual. Keep text to a minimum.
|
||||||
|
2. **Title Limit**: Keep `label` (item titles) under 15 characters.
|
||||||
|
3. **Description Limit**: Keep `desc` (item descriptions) under 25 characters (approx. 2 lines).
|
||||||
|
4. **Impact**: Use strong verbs and nouns. Avoid filler words.
|
||||||
|
|
||||||
### Output Rules
|
### Output Rules
|
||||||
1. **Strict Syntax**: Follow the indentation and formatting rules exactly.
|
1. **Strict Syntax**: Follow the indentation and formatting rules exactly.
|
||||||
2. **No Explanations**: Output ONLY the syntax code block.
|
2. **No Explanations**: Output ONLY the syntax code block.
|
||||||
@@ -224,9 +259,11 @@ USER_PROMPT_GENERATE_INFOGRAPHIC = """
|
|||||||
|
|
||||||
请根据文本特点选择最合适的信息图模板,并输出规范的 infographic 语法。注意保持正确的缩进格式(两个空格)。
|
请根据文本特点选择最合适的信息图模板,并输出规范的 infographic 语法。注意保持正确的缩进格式(两个空格)。
|
||||||
|
|
||||||
**重要提示:**
|
**视觉优化指南:**
|
||||||
- 如果使用 `list-grid` 格式,请确保每个卡片的 `desc` 描述文字控制在 **30个汉字**(或约60个英文字符)**以内**,以保证所有卡片描述都只占用2行,维持视觉一致性。
|
- **要点化生成:** 信息图不是文章。请将内容转化为“关键词+短语”的形式,严禁生成长难句。
|
||||||
- 描述应简洁精炼,突出核心要点。
|
- **标题限制:** 每个卡片的 `label`(标题)请控制在 **8个汉字**以内。
|
||||||
|
- **描述限制:** 每个卡片的 `desc`(描述)请控制在 **15个汉字**以内,确保即使在小屏幕上也能完整显示。
|
||||||
|
- **结构化思维:** 优先使用并列、递进或对比结构,使信息一目了然。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# =================================================================
|
# =================================================================
|
||||||
@@ -333,7 +370,7 @@ CSS_TEMPLATE_INFOGRAPHIC = """
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
min-height: 600px;
|
min-height: 600px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
overflow: visible; /* Ensure content is visible */
|
overflow: visible;
|
||||||
transition: height 0.3s ease;
|
transition: height 0.3s ease;
|
||||||
}
|
}
|
||||||
.infographic-render-container svg text {
|
.infographic-render-container svg text {
|
||||||
@@ -341,35 +378,58 @@ CSS_TEMPLATE_INFOGRAPHIC = """
|
|||||||
}
|
}
|
||||||
.infographic-render-container svg foreignObject {
|
.infographic-render-container svg foreignObject {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif !important;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif !important;
|
||||||
line-height: 1.4 !important;
|
line-height: 1.3 !important;
|
||||||
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
/* 主标题样式 */
|
/* 主标题样式 */
|
||||||
.infographic-render-container svg foreignObject[data-element-type="title"] > * {
|
.infographic-render-container svg foreignObject[data-element-type="title"] > * {
|
||||||
font-size: 1.5em !important;
|
font-size: 1.3em !important;
|
||||||
font-weight: bold !important;
|
font-weight: 800 !important;
|
||||||
line-height: 1.4 !important;
|
line-height: 1.3 !important;
|
||||||
white-space: nowrap !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
text-overflow: ellipsis !important;
|
|
||||||
}
|
|
||||||
/* 页面副标题和卡片标题样式 */
|
|
||||||
.infographic-render-container svg foreignObject[data-element-type="desc"] > *,
|
|
||||||
.infographic-render-container svg foreignObject[data-element-type="item-label"] > * {
|
|
||||||
font-size: 0.6em !important;
|
|
||||||
line-height: 1.4 !important;
|
|
||||||
white-space: nowrap !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
text-overflow: ellipsis !important;
|
|
||||||
}
|
|
||||||
/* 卡片标题额外增加底部间距 */
|
|
||||||
.infographic-render-container svg foreignObject[data-element-type="item-label"] > * {
|
|
||||||
padding-bottom: 8px !important;
|
|
||||||
display: block !important;
|
|
||||||
}
|
|
||||||
/* 卡片描述文字保持正常换行 */
|
|
||||||
.infographic-render-container svg foreignObject[data-element-type="item-desc"] > * {
|
|
||||||
line-height: 1.4 !important;
|
|
||||||
white-space: normal !important;
|
white-space: normal !important;
|
||||||
|
word-break: break-word !important;
|
||||||
|
display: -webkit-box !important;
|
||||||
|
-webkit-line-clamp: 2 !important;
|
||||||
|
-webkit-box-orient: vertical !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
text-overflow: ellipsis !important;
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
/* 页面副标题样式 */
|
||||||
|
.infographic-render-container svg foreignObject[data-element-type="desc"] > * {
|
||||||
|
font-size: 0.85em !important;
|
||||||
|
line-height: 1.3 !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
text-overflow: ellipsis !important;
|
||||||
|
text-align: center !important;
|
||||||
|
display: block !important;
|
||||||
|
color: var(--ig-muted-text-color) !important;
|
||||||
|
}
|
||||||
|
/* 卡片标题样式 */
|
||||||
|
.infographic-render-container svg foreignObject[data-element-type="item-label"] > * {
|
||||||
|
font-size: 0.9em !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
line-height: 1.3 !important;
|
||||||
|
white-space: normal !important;
|
||||||
|
word-break: break-word !important;
|
||||||
|
display: -webkit-box !important;
|
||||||
|
-webkit-line-clamp: 2 !important;
|
||||||
|
-webkit-box-orient: vertical !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
text-overflow: ellipsis !important;
|
||||||
|
padding-bottom: 2px !important;
|
||||||
|
}
|
||||||
|
/* 卡片描述文字 */
|
||||||
|
.infographic-render-container svg foreignObject[data-element-type="item-desc"] > * {
|
||||||
|
font-size: 0.82em !important;
|
||||||
|
line-height: 1.3 !important;
|
||||||
|
white-space: normal !important;
|
||||||
|
display: -webkit-box !important;
|
||||||
|
-webkit-line-clamp: 2 !important;
|
||||||
|
-webkit-box-orient: vertical !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
text-overflow: ellipsis !important;
|
||||||
}
|
}
|
||||||
.infographic-container-wrapper .download-area {
|
.infographic-container-wrapper .download-area {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -537,34 +597,36 @@ SCRIPT_TEMPLATE_INFOGRAPHIC = """
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 模板映射配置
|
// 2. 模板映射配置 (官方 AntV 结构 ID)
|
||||||
// 2. 模板映射配置
|
|
||||||
const TEMPLATE_MAPPING = {
|
const TEMPLATE_MAPPING = {
|
||||||
// 列表与层级
|
// 列表与层级 - 短名称映射到完整模板名
|
||||||
'list-grid': 'list-grid-compact-card',
|
'list-grid': 'list-grid-compact-card',
|
||||||
|
'list-column': 'list-column-simple-vertical-arrow',
|
||||||
|
'list-row': 'list-row-simple-horizontal-arrow',
|
||||||
|
'hierarchy-tree': 'hierarchy-tree-tech-style-capsule-item',
|
||||||
|
|
||||||
|
// 时序与时间线
|
||||||
|
'sequence-roadmap-vertical': 'sequence-roadmap-vertical-simple',
|
||||||
|
'sequence-timeline': 'sequence-timeline-simple',
|
||||||
|
'sequence-steps': 'sequence-steps-simple',
|
||||||
|
'sequence-horizontal-zigzag': 'sequence-horizontal-zigzag-simple',
|
||||||
|
|
||||||
|
// 对比
|
||||||
|
'compare-binary-horizontal': 'compare-binary-horizontal-simple-vs',
|
||||||
|
'compare-hierarchy-row': 'compare-hierarchy-row-simple',
|
||||||
|
|
||||||
|
// 图表
|
||||||
|
'chart-column': 'chart-column-simple',
|
||||||
|
'quadrant': 'quadrant-quarter-simple-card',
|
||||||
|
|
||||||
|
// 向后兼容的旧映射
|
||||||
'list-vertical': 'list-column-simple-vertical-arrow',
|
'list-vertical': 'list-column-simple-vertical-arrow',
|
||||||
'tree-vertical': 'hierarchy-tree-tech-style-capsule-item',
|
'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-roadmap': 'sequence-roadmap-vertical-simple',
|
||||||
'sequence-zigzag': 'sequence-horizontal-zigzag-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-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-bar': 'chart-bar-plain-text',
|
||||||
'chart-column': 'chart-column-simple',
|
|
||||||
'chart-line': 'chart-line-plain-text',
|
'chart-line': 'chart-line-plain-text',
|
||||||
'chart-area': 'chart-area-simple', // 暂无直接对应
|
|
||||||
'chart-pie': 'chart-pie-plain-text',
|
'chart-pie': 'chart-pie-plain-text',
|
||||||
'chart-doughnut': 'chart-pie-donut-plain-text'
|
'chart-doughnut': 'chart-pie-donut-plain-text'
|
||||||
};
|
};
|
||||||
@@ -657,10 +719,48 @@ SCRIPT_TEMPLATE_INFOGRAPHIC = """
|
|||||||
containerEl.dataset.infographicRendered = 'true';
|
containerEl.dataset.infographicRendered = 'true';
|
||||||
console.log('[Infographic] 渲染完成');
|
console.log('[Infographic] 渲染完成');
|
||||||
|
|
||||||
// 自动调整高度
|
// 自动调整高度与元素标记
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const svg = containerEl.querySelector('svg');
|
const svg = containerEl.querySelector('svg');
|
||||||
if (svg) {
|
if (svg) {
|
||||||
|
// 1. 标记元素以便 CSS 应用样式
|
||||||
|
const fos = Array.from(svg.querySelectorAll('foreignObject'));
|
||||||
|
let titleFound = false;
|
||||||
|
let descFound = false;
|
||||||
|
|
||||||
|
fos.forEach((fo) => {
|
||||||
|
const text = fo.textContent.trim();
|
||||||
|
if (!text || fo.querySelector('i') || (fo.querySelector('svg') && fo.querySelectorAll('*').length < 5)) {
|
||||||
|
fo.setAttribute('data-element-type', 'icon');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态增加高度和宽度,容纳换行后的文字
|
||||||
|
const currentHeight = parseInt(fo.getAttribute('height') || '0');
|
||||||
|
if (currentHeight > 0 && currentHeight < 200) {
|
||||||
|
fo.setAttribute('height', Math.round(currentHeight * 1.8).toString());
|
||||||
|
}
|
||||||
|
const currentWidth = parseInt(fo.getAttribute('width') || '0');
|
||||||
|
if (currentWidth > 0 && currentWidth < 300) {
|
||||||
|
fo.setAttribute('width', Math.max(Math.round(currentWidth * 1.2), 180).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!titleFound) {
|
||||||
|
fo.setAttribute('data-element-type', 'title');
|
||||||
|
titleFound = true;
|
||||||
|
} else if (!descFound) {
|
||||||
|
fo.setAttribute('data-element-type', 'desc');
|
||||||
|
descFound = true;
|
||||||
|
} else {
|
||||||
|
if (fo.querySelector('strong') || fo.style.fontWeight === 'bold' || text.length < 15) {
|
||||||
|
fo.setAttribute('data-element-type', 'item-label');
|
||||||
|
} else {
|
||||||
|
fo.setAttribute('data-element-type', 'item-desc');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 调整高度
|
||||||
const bbox = svg.getBoundingClientRect();
|
const bbox = svg.getBoundingClientRect();
|
||||||
let contentHeight = bbox.height;
|
let contentHeight = bbox.height;
|
||||||
if (svg.viewBox && svg.viewBox.baseVal && svg.viewBox.baseVal.height) {
|
if (svg.viewBox && svg.viewBox.baseVal && svg.viewBox.baseVal.height) {
|
||||||
|
|||||||
@@ -1,15 +1,26 @@
|
|||||||
# Async Context Compression Filter
|
# Async Context Compression Filter
|
||||||
|
|
||||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 1.1.0 | **License:** MIT
|
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 1.1.3 | **License:** MIT
|
||||||
|
|
||||||
This filter reduces token consumption in long conversations through intelligent summarization and message compression while keeping conversations coherent.
|
This filter reduces token consumption in long conversations through intelligent summarization and message compression while keeping conversations coherent.
|
||||||
|
|
||||||
## What's new in 1.1.0
|
## What's new in 1.1.3
|
||||||
|
- **Improved Compatibility**: Changed summary injection role from `user` to `assistant` for better compatibility across different LLMs.
|
||||||
|
- **Enhanced Stability**: Fixed a race condition in state management that could cause "inlet state not found" warnings in high-concurrency scenarios.
|
||||||
|
- **Bug Fixes**: Corrected default model handling to prevent misleading logs when no model is specified.
|
||||||
|
|
||||||
|
## What's new in 1.1.2
|
||||||
|
|
||||||
|
- **Open WebUI v0.7.x Compatibility**: Resolved a critical database session binding error affecting Open WebUI v0.7.x users. The plugin now dynamically discovers the database engine and session context, ensuring compatibility across versions.
|
||||||
|
- **Enhanced Error Reporting**: Errors during background summary generation are now reported via both the status bar and browser console.
|
||||||
|
- **Robust Model Handling**: Improved handling of missing or invalid model IDs to prevent crashes.
|
||||||
|
|
||||||
|
## What's new in 1.1.1
|
||||||
|
|
||||||
|
- **Frontend Debugging**: Added `show_debug_log` option to print debug info to the browser console (F12).
|
||||||
|
- **Optimized Compression**: Improved token calculation logic to prevent aggressive truncation of history, ensuring more context is retained.
|
||||||
|
|
||||||
|
|
||||||
- Reuses Open WebUI's shared database connection by default (no custom engine or env vars required).
|
|
||||||
- Token-based thresholds (`compression_threshold_tokens`, `max_context_tokens`) for safer long-context handling.
|
|
||||||
- Per-model overrides via `model_thresholds` for mixed-model workflows.
|
|
||||||
- Documentation now mirrors the latest async workflow and retention-first injection.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -54,6 +65,7 @@ It is recommended to keep this filter early in the chain so it runs before filte
|
|||||||
| `summary_temperature` | `0.3` | Randomness for summary generation. Lower is more deterministic. |
|
| `summary_temperature` | `0.3` | Randomness for summary generation. Lower is more deterministic. |
|
||||||
| `model_thresholds` | `{}` | Per-model overrides for `compression_threshold_tokens` and `max_context_tokens` (useful for mixed models). |
|
| `model_thresholds` | `{}` | Per-model overrides for `compression_threshold_tokens` and `max_context_tokens` (useful for mixed models). |
|
||||||
| `debug_mode` | `true` | Log verbose debug info. Set to `false` in production. |
|
| `debug_mode` | `true` | Log verbose debug info. Set to `false` in production. |
|
||||||
|
| `show_debug_log` | `false` | Print debug logs to browser console (F12). Useful for frontend debugging. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
# 异步上下文压缩过滤器
|
# 异步上下文压缩过滤器
|
||||||
|
|
||||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie) | **版本:** 1.2.0 | **许可证:** MIT
|
**作者:** [Fu-Jie](https://github.com/Fu-Jie) | **版本:** 1.1.3 | **许可证:** MIT
|
||||||
|
|
||||||
> **重要提示**:为了确保所有过滤器的可维护性和易用性,每个过滤器都应附带清晰、完整的文档,以确保其功能、配置和使用方法得到充分说明。
|
> **重要提示**:为了确保所有过滤器的可维护性和易用性,每个过滤器都应附带清晰、完整的文档,以确保其功能、配置和使用方法得到充分说明。
|
||||||
|
|
||||||
本过滤器通过智能摘要和消息压缩技术,在保持对话连贯性的同时,显著降低长对话的 Token 消耗。
|
本过滤器通过智能摘要和消息压缩技术,在保持对话连贯性的同时,显著降低长对话的 Token 消耗。
|
||||||
|
|
||||||
## 1.1.0 版本更新
|
## 1.1.3 版本更新
|
||||||
|
- **兼容性提升**: 将摘要注入角色从 `user` 改为 `assistant`,以提高在不同 LLM 之间的兼容性。
|
||||||
|
- **稳定性增强**: 修复了状态管理中的竞态条件,解决了高并发场景下可能出现的“无法获取 inlet 状态”警告。
|
||||||
|
- **Bug 修复**: 修正了默认模型处理逻辑,防止在未指定模型时产生误导性日志。
|
||||||
|
|
||||||
|
## 1.1.2 版本更新
|
||||||
|
|
||||||
|
- **Open WebUI v0.7.x 兼容性**: 修复了影响 Open WebUI v0.7.x 用户的严重数据库会话绑定错误。插件现在动态发现数据库引擎和会话上下文,确保跨版本兼容性。
|
||||||
|
- **增强错误报告**: 后台摘要生成过程中的错误现在会通过状态栏和浏览器控制台同时报告。
|
||||||
|
- **健壮的模型处理**: 改进了对缺失或无效模型 ID 的处理,防止程序崩溃。
|
||||||
|
|
||||||
|
## 1.1.1 版本更新
|
||||||
|
|
||||||
|
- **前端调试**: 新增 `show_debug_log` 选项,支持在浏览器控制台 (F12) 打印调试信息。
|
||||||
|
- **压缩优化**: 优化 Token 计算逻辑,防止历史记录被过度截断,保留更多上下文。
|
||||||
|
|
||||||
|
|
||||||
- 默认复用 OpenWebUI 内置数据库连接,无需自建引擎、无需配置 `DATABASE_URL`。
|
|
||||||
- 基于 Token 的阈值控制(`compression_threshold_tokens`、`max_context_tokens`),长上下文更安全。
|
|
||||||
- 支持 `model_thresholds` 为不同模型设置专属阈值,适合混用多模型场景。
|
|
||||||
- 文档同步最新异步工作流与“先保留再注入”策略。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -94,6 +105,11 @@
|
|||||||
- **默认值**: `true`
|
- **默认值**: `true`
|
||||||
- **描述**: 是否在 Open WebUI 的控制台日志中打印详细的调试信息(如 Token 计数、压缩进度、数据库操作等)。生产环境建议设为 `false`。
|
- **描述**: 是否在 Open WebUI 的控制台日志中打印详细的调试信息(如 Token 计数、压缩进度、数据库操作等)。生产环境建议设为 `false`。
|
||||||
|
|
||||||
|
#### `show_debug_log`
|
||||||
|
|
||||||
|
- **默认值**: `false`
|
||||||
|
- **描述**: 是否在浏览器控制台 (F12) 打印调试日志。便于前端调试。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 故障排除
|
## 故障排除
|
||||||
|
|||||||
@@ -5,7 +5,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
|
||||||
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.1
|
version: 1.1.3
|
||||||
openwebui_id: b1655bc8-6de9-4cad-8cb5-a6f7829a02ce
|
openwebui_id: b1655bc8-6de9-4cad-8cb5-a6f7829a02ce
|
||||||
license: MIT
|
license: MIT
|
||||||
|
|
||||||
@@ -249,6 +249,7 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
import hashlib
|
import hashlib
|
||||||
import time
|
import time
|
||||||
|
import contextlib
|
||||||
|
|
||||||
# Open WebUI built-in imports
|
# Open WebUI built-in imports
|
||||||
from open_webui.utils.chat import generate_chat_completion
|
from open_webui.utils.chat import generate_chat_completion
|
||||||
@@ -257,9 +258,10 @@ from fastapi.requests import Request
|
|||||||
from open_webui.main import app as webui_app
|
from open_webui.main import app as webui_app
|
||||||
|
|
||||||
# Open WebUI internal database (re-use shared connection)
|
# Open WebUI internal database (re-use shared connection)
|
||||||
from open_webui.internal.db import engine as owui_engine
|
try:
|
||||||
from open_webui.internal.db import Session as owui_Session
|
from open_webui.internal import db as owui_db
|
||||||
from open_webui.internal.db import Base as owui_Base
|
except ModuleNotFoundError: # pragma: no cover - filter runs inside Open WebUI
|
||||||
|
owui_db = None
|
||||||
|
|
||||||
# Try to import tiktoken
|
# Try to import tiktoken
|
||||||
try:
|
try:
|
||||||
@@ -269,14 +271,91 @@ except ImportError:
|
|||||||
|
|
||||||
# Database imports
|
# Database imports
|
||||||
from sqlalchemy import Column, String, Text, DateTime, Integer, inspect
|
from sqlalchemy import Column, String, Text, DateTime, Integer, inspect
|
||||||
|
from sqlalchemy.orm import declarative_base, sessionmaker
|
||||||
|
from sqlalchemy.engine import Engine
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
def _discover_owui_engine(db_module: Any) -> Optional[Engine]:
|
||||||
|
"""Discover the Open WebUI SQLAlchemy engine via provided db module helpers."""
|
||||||
|
if db_module is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
db_context = getattr(db_module, "get_db_context", None) or getattr(
|
||||||
|
db_module, "get_db", None
|
||||||
|
)
|
||||||
|
if callable(db_context):
|
||||||
|
try:
|
||||||
|
with db_context() as session:
|
||||||
|
try:
|
||||||
|
return session.get_bind()
|
||||||
|
except AttributeError:
|
||||||
|
return getattr(session, "bind", None) or getattr(
|
||||||
|
session, "engine", None
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[DB Discover] get_db_context failed: {exc}")
|
||||||
|
|
||||||
|
for attr in ("engine", "ENGINE", "bind", "BIND"):
|
||||||
|
candidate = getattr(db_module, attr, None)
|
||||||
|
if candidate is not None:
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _discover_owui_schema(db_module: Any) -> Optional[str]:
|
||||||
|
"""Discover the Open WebUI database schema name if configured."""
|
||||||
|
if db_module is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
base = getattr(db_module, "Base", None)
|
||||||
|
metadata = getattr(base, "metadata", None) if base is not None else None
|
||||||
|
candidate = getattr(metadata, "schema", None) if metadata is not None else None
|
||||||
|
if isinstance(candidate, str) and candidate.strip():
|
||||||
|
return candidate.strip()
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[DB Discover] Base metadata schema lookup failed: {exc}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
metadata_obj = getattr(db_module, "metadata_obj", None)
|
||||||
|
candidate = (
|
||||||
|
getattr(metadata_obj, "schema", None) if metadata_obj is not None else None
|
||||||
|
)
|
||||||
|
if isinstance(candidate, str) and candidate.strip():
|
||||||
|
return candidate.strip()
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[DB Discover] metadata_obj schema lookup failed: {exc}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from open_webui import env as owui_env
|
||||||
|
|
||||||
|
candidate = getattr(owui_env, "DATABASE_SCHEMA", None)
|
||||||
|
if isinstance(candidate, str) and candidate.strip():
|
||||||
|
return candidate.strip()
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[DB Discover] env schema lookup failed: {exc}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
owui_engine = _discover_owui_engine(owui_db)
|
||||||
|
owui_schema = _discover_owui_schema(owui_db)
|
||||||
|
owui_Base = getattr(owui_db, "Base", None) if owui_db is not None else None
|
||||||
|
if owui_Base is None:
|
||||||
|
owui_Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
class ChatSummary(owui_Base):
|
class ChatSummary(owui_Base):
|
||||||
"""Chat Summary Storage Table"""
|
"""Chat Summary Storage Table"""
|
||||||
|
|
||||||
__tablename__ = "chat_summary"
|
__tablename__ = "chat_summary"
|
||||||
__table_args__ = {"extend_existing": True}
|
__table_args__ = (
|
||||||
|
{"extend_existing": True, "schema": owui_schema}
|
||||||
|
if owui_schema
|
||||||
|
else {"extend_existing": True}
|
||||||
|
)
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
chat_id = Column(String(255), unique=True, nullable=False, index=True)
|
chat_id = Column(String(255), unique=True, nullable=False, index=True)
|
||||||
@@ -289,14 +368,69 @@ class ChatSummary(owui_Base):
|
|||||||
class Filter:
|
class Filter:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.valves = self.Valves()
|
self.valves = self.Valves()
|
||||||
|
self._owui_db = owui_db
|
||||||
self._db_engine = owui_engine
|
self._db_engine = owui_engine
|
||||||
self._SessionLocal = owui_Session
|
self._db_engine = owui_engine
|
||||||
self.temp_state = {} # Used to pass temporary data between inlet and outlet
|
self._fallback_session_factory = (
|
||||||
|
sessionmaker(bind=self._db_engine) if self._db_engine else None
|
||||||
|
)
|
||||||
|
self._fallback_session_factory = (
|
||||||
|
sessionmaker(bind=self._db_engine) if self._db_engine else None
|
||||||
|
)
|
||||||
self._init_database()
|
self._init_database()
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _db_session(self):
|
||||||
|
"""Yield a database session using Open WebUI helpers with graceful fallbacks."""
|
||||||
|
db_module = self._owui_db
|
||||||
|
db_context = None
|
||||||
|
if db_module is not None:
|
||||||
|
db_context = getattr(db_module, "get_db_context", None) or getattr(
|
||||||
|
db_module, "get_db", None
|
||||||
|
)
|
||||||
|
|
||||||
|
if callable(db_context):
|
||||||
|
with db_context() as session:
|
||||||
|
yield session
|
||||||
|
return
|
||||||
|
|
||||||
|
factory = None
|
||||||
|
if db_module is not None:
|
||||||
|
factory = getattr(db_module, "SessionLocal", None) or getattr(
|
||||||
|
db_module, "ScopedSession", None
|
||||||
|
)
|
||||||
|
if callable(factory):
|
||||||
|
session = factory()
|
||||||
|
try:
|
||||||
|
yield session
|
||||||
|
finally:
|
||||||
|
close = getattr(session, "close", None)
|
||||||
|
if callable(close):
|
||||||
|
close()
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._fallback_session_factory is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Open WebUI database session is unavailable. Ensure Open WebUI's database layer is initialized."
|
||||||
|
)
|
||||||
|
|
||||||
|
session = self._fallback_session_factory()
|
||||||
|
try:
|
||||||
|
yield session
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
session.close()
|
||||||
|
except Exception as exc: # pragma: no cover - best-effort cleanup
|
||||||
|
print(f"[Database] ⚠️ Failed to close fallback session: {exc}")
|
||||||
|
|
||||||
def _init_database(self):
|
def _init_database(self):
|
||||||
"""Initializes the database table using Open WebUI's shared connection."""
|
"""Initializes the database table using Open WebUI's shared connection."""
|
||||||
try:
|
try:
|
||||||
|
if self._db_engine is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Open WebUI database engine is unavailable. Ensure Open WebUI is configured with a valid DATABASE_URL."
|
||||||
|
)
|
||||||
|
|
||||||
# Check if table exists using SQLAlchemy inspect
|
# Check if table exists using SQLAlchemy inspect
|
||||||
inspector = inspect(self._db_engine)
|
inspector = inspect(self._db_engine)
|
||||||
if not inspector.has_table("chat_summary"):
|
if not inspector.has_table("chat_summary"):
|
||||||
@@ -366,7 +500,7 @@ class Filter:
|
|||||||
def _save_summary(self, chat_id: str, summary: str, compressed_count: int):
|
def _save_summary(self, chat_id: str, summary: str, compressed_count: int):
|
||||||
"""Saves the summary to the database."""
|
"""Saves the summary to the database."""
|
||||||
try:
|
try:
|
||||||
with self._SessionLocal() as session:
|
with self._db_session() as session:
|
||||||
# Find existing record
|
# Find existing record
|
||||||
existing = session.query(ChatSummary).filter_by(chat_id=chat_id).first()
|
existing = session.query(ChatSummary).filter_by(chat_id=chat_id).first()
|
||||||
|
|
||||||
@@ -406,7 +540,7 @@ class Filter:
|
|||||||
def _load_summary_record(self, chat_id: str) -> Optional[ChatSummary]:
|
def _load_summary_record(self, chat_id: str) -> Optional[ChatSummary]:
|
||||||
"""Loads the summary record object from the database."""
|
"""Loads the summary record object from the database."""
|
||||||
try:
|
try:
|
||||||
with self._SessionLocal() as session:
|
with self._db_session() as session:
|
||||||
record = session.query(ChatSummary).filter_by(chat_id=chat_id).first()
|
record = session.query(ChatSummary).filter_by(chat_id=chat_id).first()
|
||||||
if record:
|
if record:
|
||||||
# Detach the object from the session so it can be used after session close
|
# Detach the object from the session so it can be used after session close
|
||||||
@@ -487,41 +621,25 @@ class Filter:
|
|||||||
"max_context_tokens": self.valves.max_context_tokens,
|
"max_context_tokens": self.valves.max_context_tokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _inject_summary_to_first_message(self, message: dict, summary: str) -> dict:
|
def _extract_chat_id(self, body: dict, metadata: Optional[dict]) -> str:
|
||||||
"""Injects the summary into the first message (prepended to content)."""
|
"""Extract chat_id from body or metadata."""
|
||||||
content = message.get("content", "")
|
if isinstance(body, dict):
|
||||||
summary_block = f"【Historical Conversation Summary】\n{summary}\n\n---\nBelow is the recent conversation:\n\n"
|
chat_id = body.get("chat_id")
|
||||||
|
if isinstance(chat_id, str) and chat_id.strip():
|
||||||
|
return chat_id.strip()
|
||||||
|
|
||||||
# Handle different content types
|
body_metadata = body.get("metadata", {})
|
||||||
if isinstance(content, list): # Multimodal content
|
if isinstance(body_metadata, dict):
|
||||||
# Find the first text part and insert the summary before it
|
chat_id = body_metadata.get("chat_id")
|
||||||
new_content = []
|
if isinstance(chat_id, str) and chat_id.strip():
|
||||||
summary_inserted = False
|
return chat_id.strip()
|
||||||
|
|
||||||
for part in content:
|
if isinstance(metadata, dict):
|
||||||
if (
|
chat_id = metadata.get("chat_id")
|
||||||
isinstance(part, dict)
|
if isinstance(chat_id, str) and chat_id.strip():
|
||||||
and part.get("type") == "text"
|
return chat_id.strip()
|
||||||
and not summary_inserted
|
|
||||||
):
|
|
||||||
# Prepend summary to the first text part
|
|
||||||
new_content.append(
|
|
||||||
{"type": "text", "text": summary_block + part.get("text", "")}
|
|
||||||
)
|
|
||||||
summary_inserted = True
|
|
||||||
else:
|
|
||||||
new_content.append(part)
|
|
||||||
|
|
||||||
# If no text part, insert at the beginning
|
return ""
|
||||||
if not summary_inserted:
|
|
||||||
new_content.insert(0, {"type": "text", "text": summary_block})
|
|
||||||
|
|
||||||
message["content"] = new_content
|
|
||||||
|
|
||||||
elif isinstance(content, str): # Plain text
|
|
||||||
message["content"] = summary_block + content
|
|
||||||
|
|
||||||
return message
|
|
||||||
|
|
||||||
async def _emit_debug_log(
|
async def _emit_debug_log(
|
||||||
self,
|
self,
|
||||||
@@ -632,7 +750,15 @@ class Filter:
|
|||||||
Compression Strategy: Only responsible for injecting existing summaries, no Token calculation.
|
Compression Strategy: Only responsible for injecting existing summaries, no Token calculation.
|
||||||
"""
|
"""
|
||||||
messages = body.get("messages", [])
|
messages = body.get("messages", [])
|
||||||
chat_id = __metadata__["chat_id"]
|
chat_id = self._extract_chat_id(body, __metadata__)
|
||||||
|
|
||||||
|
if not chat_id:
|
||||||
|
await self._log(
|
||||||
|
"[Inlet] ❌ Missing chat_id in metadata, skipping compression",
|
||||||
|
type="error",
|
||||||
|
event_call=__event_call__,
|
||||||
|
)
|
||||||
|
return body
|
||||||
|
|
||||||
if self.valves.debug_mode or self.valves.show_debug_log:
|
if self.valves.debug_mode or self.valves.show_debug_log:
|
||||||
await self._log(
|
await self._log(
|
||||||
@@ -644,15 +770,9 @@ class Filter:
|
|||||||
# Target is to compress up to the (total - keep_last) message
|
# Target is to compress up to the (total - keep_last) message
|
||||||
target_compressed_count = max(0, len(messages) - self.valves.keep_last)
|
target_compressed_count = max(0, len(messages) - self.valves.keep_last)
|
||||||
|
|
||||||
# [Optimization] Simple state cleanup check
|
# Record the target compression progress for the original messages, for use in outlet
|
||||||
if chat_id in self.temp_state:
|
# Target is to compress up to the (total - keep_last) message
|
||||||
await self._log(
|
target_compressed_count = max(0, len(messages) - self.valves.keep_last)
|
||||||
f"[Inlet] ⚠️ Overwriting unconsumed old state (Chat ID: {chat_id})",
|
|
||||||
type="warning",
|
|
||||||
event_call=__event_call__,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.temp_state[chat_id] = target_compressed_count
|
|
||||||
|
|
||||||
await self._log(
|
await self._log(
|
||||||
f"[Inlet] Recorded target compression progress: {target_compressed_count}",
|
f"[Inlet] Recorded target compression progress: {target_compressed_count}",
|
||||||
@@ -685,7 +805,7 @@ class Filter:
|
|||||||
f"---\n"
|
f"---\n"
|
||||||
f"Below is the recent conversation:"
|
f"Below is the recent conversation:"
|
||||||
)
|
)
|
||||||
summary_msg = {"role": "user", "content": summary_content}
|
summary_msg = {"role": "assistant", "content": summary_content}
|
||||||
|
|
||||||
# 3. Tail messages (Tail) - All messages starting from the last compression point
|
# 3. Tail messages (Tail) - All messages starting from the last compression point
|
||||||
# Note: Must ensure head messages are not duplicated
|
# Note: Must ensure head messages are not duplicated
|
||||||
@@ -747,19 +867,37 @@ class Filter:
|
|||||||
Executed after the LLM response is complete.
|
Executed after the LLM response is complete.
|
||||||
Calculates Token count in the background and triggers summary generation (does not block current response, does not affect content output).
|
Calculates Token count in the background and triggers summary generation (does not block current response, does not affect content output).
|
||||||
"""
|
"""
|
||||||
chat_id = __metadata__["chat_id"]
|
chat_id = self._extract_chat_id(body, __metadata__)
|
||||||
model = body.get("model", "gpt-3.5-turbo")
|
if not chat_id:
|
||||||
|
await self._log(
|
||||||
|
"[Outlet] ❌ Missing chat_id in metadata, skipping compression",
|
||||||
|
type="error",
|
||||||
|
event_call=__event_call__,
|
||||||
|
)
|
||||||
|
return body
|
||||||
|
model = body.get("model") or ""
|
||||||
|
|
||||||
|
# Calculate target compression progress directly
|
||||||
|
# Assuming body['messages'] in outlet contains the full history (including new response)
|
||||||
|
messages = body.get("messages", [])
|
||||||
|
target_compressed_count = max(0, len(messages) - self.valves.keep_last)
|
||||||
|
|
||||||
if self.valves.debug_mode or self.valves.show_debug_log:
|
if self.valves.debug_mode or self.valves.show_debug_log:
|
||||||
await self._log(
|
await self._log(
|
||||||
f"\n{'='*60}\n[Outlet] Chat ID: {chat_id}\n[Outlet] Response complete",
|
f"\n{'='*60}\n[Outlet] Chat ID: {chat_id}\n[Outlet] Response complete\n[Outlet] Calculated target compression progress: {target_compressed_count} (Messages: {len(messages)})",
|
||||||
event_call=__event_call__,
|
event_call=__event_call__,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Process Token calculation and summary generation asynchronously in the background (do not wait for completion, do not affect output)
|
# Process Token calculation and summary generation asynchronously in the background (do not wait for completion, do not affect output)
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
self._check_and_generate_summary_async(
|
self._check_and_generate_summary_async(
|
||||||
chat_id, model, body, __user__, __event_emitter__, __event_call__
|
chat_id,
|
||||||
|
model,
|
||||||
|
body,
|
||||||
|
__user__,
|
||||||
|
target_compressed_count,
|
||||||
|
__event_emitter__,
|
||||||
|
__event_call__,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -776,6 +914,7 @@ class Filter:
|
|||||||
model: str,
|
model: str,
|
||||||
body: dict,
|
body: dict,
|
||||||
user_data: Optional[dict],
|
user_data: Optional[dict],
|
||||||
|
target_compressed_count: Optional[int],
|
||||||
__event_emitter__: Callable[[Any], Awaitable[None]] = None,
|
__event_emitter__: Callable[[Any], Awaitable[None]] = None,
|
||||||
__event_call__: Callable[[Any], Awaitable[None]] = None,
|
__event_call__: Callable[[Any], Awaitable[None]] = None,
|
||||||
):
|
):
|
||||||
@@ -820,6 +959,7 @@ class Filter:
|
|||||||
chat_id,
|
chat_id,
|
||||||
body,
|
body,
|
||||||
user_data,
|
user_data,
|
||||||
|
target_compressed_count,
|
||||||
__event_emitter__,
|
__event_emitter__,
|
||||||
__event_call__,
|
__event_call__,
|
||||||
)
|
)
|
||||||
@@ -836,12 +976,20 @@ class Filter:
|
|||||||
event_call=__event_call__,
|
event_call=__event_call__,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _clean_model_id(self, model_id: Optional[str]) -> Optional[str]:
|
||||||
|
"""Cleans the model ID by removing whitespace and quotes."""
|
||||||
|
if not model_id:
|
||||||
|
return None
|
||||||
|
cleaned = model_id.strip().strip('"').strip("'")
|
||||||
|
return cleaned if cleaned else None
|
||||||
|
|
||||||
async def _generate_summary_async(
|
async def _generate_summary_async(
|
||||||
self,
|
self,
|
||||||
messages: list,
|
messages: list,
|
||||||
chat_id: str,
|
chat_id: str,
|
||||||
body: dict,
|
body: dict,
|
||||||
user_data: Optional[dict],
|
user_data: Optional[dict],
|
||||||
|
target_compressed_count: Optional[int],
|
||||||
__event_emitter__: Callable[[Any], Awaitable[None]] = None,
|
__event_emitter__: Callable[[Any], Awaitable[None]] = None,
|
||||||
__event_call__: Callable[[Any], Awaitable[None]] = None,
|
__event_call__: Callable[[Any], Awaitable[None]] = None,
|
||||||
):
|
):
|
||||||
@@ -858,12 +1006,11 @@ class Filter:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 1. Get target compression progress
|
# 1. Get target compression progress
|
||||||
# Prioritize getting from temp_state (calculated by inlet). If unavailable (e.g., after restart), assume current is full history.
|
# If target_compressed_count is not passed (should not happen with new logic), estimate it
|
||||||
target_compressed_count = self.temp_state.pop(chat_id, None)
|
|
||||||
if target_compressed_count is None:
|
if target_compressed_count is None:
|
||||||
target_compressed_count = max(0, len(messages) - self.valves.keep_last)
|
target_compressed_count = max(0, len(messages) - self.valves.keep_last)
|
||||||
await self._log(
|
await self._log(
|
||||||
f"[🤖 Async Summary Task] ⚠️ Could not get inlet state, estimating progress using current message count: {target_compressed_count}",
|
f"[🤖 Async Summary Task] ⚠️ target_compressed_count is None, estimating: {target_compressed_count}",
|
||||||
type="warning",
|
type="warning",
|
||||||
event_call=__event_call__,
|
event_call=__event_call__,
|
||||||
)
|
)
|
||||||
@@ -892,9 +1039,17 @@ class Filter:
|
|||||||
# 3. Check Token limit and truncate (Max Context Truncation)
|
# 3. Check Token limit and truncate (Max Context Truncation)
|
||||||
# [Optimization] Use the summary model's (if any) threshold to decide how many middle messages can be processed
|
# [Optimization] Use the summary model's (if any) threshold to decide how many middle messages can be processed
|
||||||
# This allows using a long-window model (like gemini-flash) to compress history exceeding the current model's window
|
# This allows using a long-window model (like gemini-flash) to compress history exceeding the current model's window
|
||||||
summary_model_id = self.valves.summary_model or body.get(
|
summary_model_id = self._clean_model_id(
|
||||||
"model", "gpt-3.5-turbo"
|
self.valves.summary_model
|
||||||
)
|
) or self._clean_model_id(body.get("model"))
|
||||||
|
|
||||||
|
if not summary_model_id:
|
||||||
|
await self._log(
|
||||||
|
"[🤖 Async Summary Task] ⚠️ Summary model does not exist, skipping compression",
|
||||||
|
type="warning",
|
||||||
|
event_call=__event_call__,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
thresholds = self._get_model_thresholds(summary_model_id)
|
thresholds = self._get_model_thresholds(summary_model_id)
|
||||||
# Note: Using the summary model's max context limit here
|
# Note: Using the summary model's max context limit here
|
||||||
@@ -966,9 +1121,21 @@ class Filter:
|
|||||||
)
|
)
|
||||||
|
|
||||||
new_summary = await self._call_summary_llm(
|
new_summary = await self._call_summary_llm(
|
||||||
None, conversation_text, body, user_data, __event_call__
|
None,
|
||||||
|
conversation_text,
|
||||||
|
{**body, "model": summary_model_id},
|
||||||
|
user_data,
|
||||||
|
__event_call__,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not new_summary:
|
||||||
|
await self._log(
|
||||||
|
"[🤖 Async Summary Task] ⚠️ Summary generation returned empty result, skipping save",
|
||||||
|
type="warning",
|
||||||
|
event_call=__event_call__,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# 6. Save new summary
|
# 6. Save new summary
|
||||||
await self._log(
|
await self._log(
|
||||||
"[Optimization] Saving summary in a background thread to avoid blocking the event loop.",
|
"[Optimization] Saving summary in a background thread to avoid blocking the event loop.",
|
||||||
@@ -1007,6 +1174,18 @@ class Filter:
|
|||||||
type="error",
|
type="error",
|
||||||
event_call=__event_call__,
|
event_call=__event_call__,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if __event_emitter__:
|
||||||
|
await __event_emitter__(
|
||||||
|
{
|
||||||
|
"type": "status",
|
||||||
|
"data": {
|
||||||
|
"description": f"Summary Error: {str(e)[:100]}...",
|
||||||
|
"done": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@@ -1088,7 +1267,17 @@ This conversation may contain previous summaries (as system messages or text) an
|
|||||||
Based on the content above, generate the summary:
|
Based on the content above, generate the summary:
|
||||||
"""
|
"""
|
||||||
# Determine the model to use
|
# Determine the model to use
|
||||||
model = self.valves.summary_model or body.get("model", "")
|
model = self._clean_model_id(self.valves.summary_model) or self._clean_model_id(
|
||||||
|
body.get("model")
|
||||||
|
)
|
||||||
|
|
||||||
|
if not model:
|
||||||
|
await self._log(
|
||||||
|
"[🤖 LLM Call] ⚠️ Summary model does not exist, skipping summary generation",
|
||||||
|
type="warning",
|
||||||
|
event_call=__event_call__,
|
||||||
|
)
|
||||||
|
return ""
|
||||||
|
|
||||||
await self._log(f"[🤖 LLM Call] Model: {model}", event_call=__event_call__)
|
await self._log(f"[🤖 LLM Call] Model: {model}", event_call=__event_call__)
|
||||||
|
|
||||||
@@ -1142,7 +1331,12 @@ Based on the content above, generate the summary:
|
|||||||
return summary
|
return summary
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_message = f"Error occurred while calling LLM ({model}) to generate summary: {str(e)}"
|
error_msg = str(e)
|
||||||
|
# Handle specific error messages
|
||||||
|
if "Model not found" in error_msg:
|
||||||
|
error_message = f"Summary model '{model}' not found."
|
||||||
|
else:
|
||||||
|
error_message = f"Summary LLM Error ({model}): {error_msg}"
|
||||||
if not self.valves.summary_model:
|
if not self.valves.summary_model:
|
||||||
error_message += (
|
error_message += (
|
||||||
"\n[Hint] You did not specify a summary_model, so the filter attempted to use the current conversation's model. "
|
"\n[Hint] You did not specify a summary_model, so the filter attempted to use the current conversation's model. "
|
||||||
|
|||||||
@@ -5,7 +5,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
|
||||||
description: 通过智能摘要和消息压缩,降低长对话的 token 消耗,同时保持对话连贯性。
|
description: 通过智能摘要和消息压缩,降低长对话的 token 消耗,同时保持对话连贯性。
|
||||||
version: 1.1.1
|
version: 1.1.3
|
||||||
openwebui_id: 5c0617cb-a9e4-4bd6-a440-d276534ebd18
|
openwebui_id: 5c0617cb-a9e4-4bd6-a440-d276534ebd18
|
||||||
license: MIT
|
license: MIT
|
||||||
|
|
||||||
@@ -290,7 +290,8 @@ class Filter:
|
|||||||
self.valves = self.Valves()
|
self.valves = self.Valves()
|
||||||
self._db_engine = owui_engine
|
self._db_engine = owui_engine
|
||||||
self._SessionLocal = owui_Session
|
self._SessionLocal = owui_Session
|
||||||
self.temp_state = {} # 用于在 inlet 和 outlet 之间传递临时数据
|
self._SessionLocal = owui_Session
|
||||||
|
self._init_database()
|
||||||
self._init_database()
|
self._init_database()
|
||||||
|
|
||||||
def _init_database(self):
|
def _init_database(self):
|
||||||
@@ -471,42 +472,6 @@ class Filter:
|
|||||||
"max_context_tokens": self.valves.max_context_tokens,
|
"max_context_tokens": self.valves.max_context_tokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _inject_summary_to_first_message(self, message: dict, summary: str) -> dict:
|
|
||||||
"""将摘要注入到第一条消息中(追加到内容前面)"""
|
|
||||||
content = message.get("content", "")
|
|
||||||
summary_block = f"【历史对话摘要】\n{summary}\n\n---\n以下是最近的对话:\n\n"
|
|
||||||
|
|
||||||
# 处理不同内容类型
|
|
||||||
if isinstance(content, list): # 多模态内容
|
|
||||||
# 查找第一个文本部分并在其前面插入摘要
|
|
||||||
new_content = []
|
|
||||||
summary_inserted = False
|
|
||||||
|
|
||||||
for part in content:
|
|
||||||
if (
|
|
||||||
isinstance(part, dict)
|
|
||||||
and part.get("type") == "text"
|
|
||||||
and not summary_inserted
|
|
||||||
):
|
|
||||||
# 在第一个文本部分前插入摘要
|
|
||||||
new_content.append(
|
|
||||||
{"type": "text", "text": summary_block + part.get("text", "")}
|
|
||||||
)
|
|
||||||
summary_inserted = True
|
|
||||||
else:
|
|
||||||
new_content.append(part)
|
|
||||||
|
|
||||||
# 如果没有文本部分,在开头插入
|
|
||||||
if not summary_inserted:
|
|
||||||
new_content.insert(0, {"type": "text", "text": summary_block})
|
|
||||||
|
|
||||||
message["content"] = new_content
|
|
||||||
|
|
||||||
elif isinstance(content, str): # 纯文本
|
|
||||||
message["content"] = summary_block + content
|
|
||||||
|
|
||||||
return message
|
|
||||||
|
|
||||||
async def _emit_debug_log(
|
async def _emit_debug_log(
|
||||||
self,
|
self,
|
||||||
__event_call__,
|
__event_call__,
|
||||||
@@ -628,15 +593,9 @@ class Filter:
|
|||||||
# 目标是压缩到倒数第 keep_last 条之前
|
# 目标是压缩到倒数第 keep_last 条之前
|
||||||
target_compressed_count = max(0, len(messages) - self.valves.keep_last)
|
target_compressed_count = max(0, len(messages) - self.valves.keep_last)
|
||||||
|
|
||||||
# [优化] 简单的状态清理检查
|
# 记录原始消息的目标压缩进度,供 outlet 使用
|
||||||
if chat_id in self.temp_state:
|
# 目标是压缩到倒数第 keep_last 条之前
|
||||||
await self._log(
|
target_compressed_count = max(0, len(messages) - self.valves.keep_last)
|
||||||
f"[Inlet] ⚠️ 覆盖未消费的旧状态 (Chat ID: {chat_id})",
|
|
||||||
type="warning",
|
|
||||||
event_call=__event_call__,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.temp_state[chat_id] = target_compressed_count
|
|
||||||
|
|
||||||
await self._log(
|
await self._log(
|
||||||
f"[Inlet] 记录目标压缩进度: {target_compressed_count}",
|
f"[Inlet] 记录目标压缩进度: {target_compressed_count}",
|
||||||
@@ -669,7 +628,7 @@ class Filter:
|
|||||||
f"---\n"
|
f"---\n"
|
||||||
f"以下是最近的对话:"
|
f"以下是最近的对话:"
|
||||||
)
|
)
|
||||||
summary_msg = {"role": "user", "content": summary_content}
|
summary_msg = {"role": "assistant", "content": summary_content}
|
||||||
|
|
||||||
# 3. 尾部消息 (Tail) - 从上次压缩点开始的所有消息
|
# 3. 尾部消息 (Tail) - 从上次压缩点开始的所有消息
|
||||||
# 注意:这里必须确保不重复包含头部消息
|
# 注意:这里必须确保不重复包含头部消息
|
||||||
@@ -732,18 +691,29 @@ class Filter:
|
|||||||
在后台计算 Token 数并触发摘要生成(不阻塞当前响应,不影响内容输出)
|
在后台计算 Token 数并触发摘要生成(不阻塞当前响应,不影响内容输出)
|
||||||
"""
|
"""
|
||||||
chat_id = __metadata__["chat_id"]
|
chat_id = __metadata__["chat_id"]
|
||||||
model = body.get("model", "gpt-3.5-turbo")
|
model = body.get("model") or ""
|
||||||
|
|
||||||
|
# 直接计算目标压缩进度
|
||||||
|
# 假设 outlet 中的 body['messages'] 包含完整历史(包括新响应)
|
||||||
|
messages = body.get("messages", [])
|
||||||
|
target_compressed_count = max(0, len(messages) - self.valves.keep_last)
|
||||||
|
|
||||||
if self.valves.debug_mode or self.valves.show_debug_log:
|
if self.valves.debug_mode or self.valves.show_debug_log:
|
||||||
await self._log(
|
await self._log(
|
||||||
f"\n{'='*60}\n[Outlet] Chat ID: {chat_id}\n[Outlet] 响应完成",
|
f"\n{'='*60}\n[Outlet] Chat ID: {chat_id}\n[Outlet] 响应完成\n[Outlet] 计算目标压缩进度: {target_compressed_count} (消息数: {len(messages)})",
|
||||||
event_call=__event_call__,
|
event_call=__event_call__,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 在后台异步处理 Token 计算和摘要生成(不等待完成,不影响输出)
|
# 在后台异步处理 Token 计算和摘要生成(不等待完成,不影响输出)
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
self._check_and_generate_summary_async(
|
self._check_and_generate_summary_async(
|
||||||
chat_id, model, body, __user__, __event_emitter__, __event_call__
|
chat_id,
|
||||||
|
model,
|
||||||
|
body,
|
||||||
|
__user__,
|
||||||
|
target_compressed_count,
|
||||||
|
__event_emitter__,
|
||||||
|
__event_call__,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -760,6 +730,7 @@ class Filter:
|
|||||||
model: str,
|
model: str,
|
||||||
body: dict,
|
body: dict,
|
||||||
user_data: Optional[dict],
|
user_data: Optional[dict],
|
||||||
|
target_compressed_count: Optional[int],
|
||||||
__event_emitter__: Callable[[Any], Awaitable[None]] = None,
|
__event_emitter__: Callable[[Any], Awaitable[None]] = None,
|
||||||
__event_call__: Callable[[Any], Awaitable[None]] = None,
|
__event_call__: Callable[[Any], Awaitable[None]] = None,
|
||||||
):
|
):
|
||||||
@@ -804,6 +775,7 @@ class Filter:
|
|||||||
chat_id,
|
chat_id,
|
||||||
body,
|
body,
|
||||||
user_data,
|
user_data,
|
||||||
|
target_compressed_count,
|
||||||
__event_emitter__,
|
__event_emitter__,
|
||||||
__event_call__,
|
__event_call__,
|
||||||
)
|
)
|
||||||
@@ -820,12 +792,20 @@ class Filter:
|
|||||||
event_call=__event_call__,
|
event_call=__event_call__,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _clean_model_id(self, model_id: Optional[str]) -> Optional[str]:
|
||||||
|
"""Cleans the model ID by removing whitespace and quotes."""
|
||||||
|
if not model_id:
|
||||||
|
return None
|
||||||
|
cleaned = model_id.strip().strip('"').strip("'")
|
||||||
|
return cleaned if cleaned else None
|
||||||
|
|
||||||
async def _generate_summary_async(
|
async def _generate_summary_async(
|
||||||
self,
|
self,
|
||||||
messages: list,
|
messages: list,
|
||||||
chat_id: str,
|
chat_id: str,
|
||||||
body: dict,
|
body: dict,
|
||||||
user_data: Optional[dict],
|
user_data: Optional[dict],
|
||||||
|
target_compressed_count: Optional[int],
|
||||||
__event_emitter__: Callable[[Any], Awaitable[None]] = None,
|
__event_emitter__: Callable[[Any], Awaitable[None]] = None,
|
||||||
__event_call__: Callable[[Any], Awaitable[None]] = None,
|
__event_call__: Callable[[Any], Awaitable[None]] = None,
|
||||||
):
|
):
|
||||||
@@ -840,12 +820,11 @@ class Filter:
|
|||||||
await self._log(f"\n[🤖 异步摘要任务] 开始...", event_call=__event_call__)
|
await self._log(f"\n[🤖 异步摘要任务] 开始...", event_call=__event_call__)
|
||||||
|
|
||||||
# 1. 获取目标压缩进度
|
# 1. 获取目标压缩进度
|
||||||
# 优先从 temp_state 获取(由 inlet 计算),如果获取不到(例如重启后),则假设当前是完整历史
|
# 如果未传递 target_compressed_count(新逻辑下不应发生),则进行估算
|
||||||
target_compressed_count = self.temp_state.pop(chat_id, None)
|
|
||||||
if target_compressed_count is None:
|
if target_compressed_count is None:
|
||||||
target_compressed_count = max(0, len(messages) - self.valves.keep_last)
|
target_compressed_count = max(0, len(messages) - self.valves.keep_last)
|
||||||
await self._log(
|
await self._log(
|
||||||
f"[🤖 异步摘要任务] ⚠️ 无法获取 inlet 状态,使用当前消息数估算进度: {target_compressed_count}",
|
f"[🤖 异步摘要任务] ⚠️ target_compressed_count 为 None,进行估算: {target_compressed_count}",
|
||||||
type="warning",
|
type="warning",
|
||||||
event_call=__event_call__,
|
event_call=__event_call__,
|
||||||
)
|
)
|
||||||
@@ -874,7 +853,17 @@ class Filter:
|
|||||||
# 3. 检查 Token 上限并截断 (Max Context Truncation)
|
# 3. 检查 Token 上限并截断 (Max Context Truncation)
|
||||||
# [优化] 使用摘要模型(如果有)的阈值来决定能处理多少中间消息
|
# [优化] 使用摘要模型(如果有)的阈值来决定能处理多少中间消息
|
||||||
# 这样可以用长窗口模型(如 gemini-flash)来压缩超过当前模型窗口的历史记录
|
# 这样可以用长窗口模型(如 gemini-flash)来压缩超过当前模型窗口的历史记录
|
||||||
summary_model_id = self.valves.summary_model or body.get("model")
|
summary_model_id = self._clean_model_id(
|
||||||
|
self.valves.summary_model
|
||||||
|
) or self._clean_model_id(body.get("model"))
|
||||||
|
|
||||||
|
if not summary_model_id:
|
||||||
|
await self._log(
|
||||||
|
"[🤖 异步摘要任务] ⚠️ 摘要模型不存在,跳过压缩",
|
||||||
|
type="warning",
|
||||||
|
event_call=__event_call__,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
thresholds = self._get_model_thresholds(summary_model_id)
|
thresholds = self._get_model_thresholds(summary_model_id)
|
||||||
# 注意:这里使用的是摘要模型的最大上下文限制
|
# 注意:这里使用的是摘要模型的最大上下文限制
|
||||||
@@ -946,9 +935,21 @@ class Filter:
|
|||||||
)
|
)
|
||||||
|
|
||||||
new_summary = await self._call_summary_llm(
|
new_summary = await self._call_summary_llm(
|
||||||
None, conversation_text, body, user_data, __event_call__
|
None,
|
||||||
|
conversation_text,
|
||||||
|
{**body, "model": summary_model_id},
|
||||||
|
user_data,
|
||||||
|
__event_call__,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not new_summary:
|
||||||
|
await self._log(
|
||||||
|
"[🤖 异步摘要任务] ⚠️ 摘要生成返回空结果,跳过保存",
|
||||||
|
type="warning",
|
||||||
|
event_call=__event_call__,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# 6. 保存新摘要
|
# 6. 保存新摘要
|
||||||
await self._log(
|
await self._log(
|
||||||
"[优化] 在后台线程中保存摘要以避免阻塞事件循环。",
|
"[优化] 在后台线程中保存摘要以避免阻塞事件循环。",
|
||||||
@@ -987,6 +988,18 @@ class Filter:
|
|||||||
type="error",
|
type="error",
|
||||||
event_call=__event_call__,
|
event_call=__event_call__,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if __event_emitter__:
|
||||||
|
await __event_emitter__(
|
||||||
|
{
|
||||||
|
"type": "status",
|
||||||
|
"data": {
|
||||||
|
"description": f"摘要生成错误: {str(e)[:100]}...",
|
||||||
|
"done": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@@ -1068,7 +1081,17 @@ class Filter:
|
|||||||
请根据上述内容,生成摘要:
|
请根据上述内容,生成摘要:
|
||||||
"""
|
"""
|
||||||
# 确定使用的模型
|
# 确定使用的模型
|
||||||
model = self.valves.summary_model or body.get("model", "")
|
model = self._clean_model_id(self.valves.summary_model) or self._clean_model_id(
|
||||||
|
body.get("model")
|
||||||
|
)
|
||||||
|
|
||||||
|
if not model:
|
||||||
|
await self._log(
|
||||||
|
"[🤖 LLM 调用] ⚠️ 摘要模型不存在,跳过摘要生成",
|
||||||
|
type="warning",
|
||||||
|
event_call=__event_call__,
|
||||||
|
)
|
||||||
|
return ""
|
||||||
|
|
||||||
await self._log(f"[🤖 LLM 调用] 模型: {model}", event_call=__event_call__)
|
await self._log(f"[🤖 LLM 调用] 模型: {model}", event_call=__event_call__)
|
||||||
|
|
||||||
@@ -1122,7 +1145,12 @@ class Filter:
|
|||||||
return summary
|
return summary
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_message = f"调用 LLM ({model}) 生成摘要时发生错误: {str(e)}"
|
error_msg = str(e)
|
||||||
|
# Handle specific error messages
|
||||||
|
if "Model not found" in error_msg:
|
||||||
|
error_message = f"摘要模型 '{model}' 不存在。"
|
||||||
|
else:
|
||||||
|
error_message = f"摘要 LLM 错误 ({model}): {error_msg}"
|
||||||
if not self.valves.summary_model:
|
if not self.valves.summary_model:
|
||||||
error_message += (
|
error_message += (
|
||||||
"\n[提示] 您未指定 summary_model,因此过滤器尝试使用当前对话的模型。"
|
"\n[提示] 您未指定 summary_model,因此过滤器尝试使用当前对话的模型。"
|
||||||
|
|||||||
@@ -125,19 +125,45 @@ if x == 1:
|
|||||||
```
|
```
|
||||||
|
|
||||||
## 7. Mermaid 语法修复 (Mermaid Syntax Fix)
|
## 7. Mermaid 语法修复 (Mermaid Syntax Fix)
|
||||||
**功能**: 修复 Mermaid 图表中常见的语法错误,特别是未加引号的标签包含特殊字符的情况。
|
**功能**: 修复 Mermaid 图表中常见的语法错误,特别是未加引号的标签包含特殊字符、嵌套括号或 HTML 标签的情况。
|
||||||
**默认**: 开启 (`enable_mermaid_fix = True`)
|
**默认**: 开启 (`enable_mermaid_fix = True`)
|
||||||
**示例**:
|
|
||||||
* **Before**:
|
### 7.1 基础特殊字符
|
||||||
```mermaid
|
**Before**:
|
||||||
graph TD
|
```mermaid
|
||||||
A[Label with (parens)] --> B(Label with [brackets])
|
graph TD
|
||||||
```
|
A[Label with (parens)] --> B(Label with [brackets])
|
||||||
* **After**:
|
```
|
||||||
```mermaid
|
**After**:
|
||||||
graph TD
|
```mermaid
|
||||||
A["Label with (parens)"] --> B("Label with [brackets]")
|
graph TD
|
||||||
```
|
A["Label with (parens)"] --> B("Label with [brackets]")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 嵌套括号修复 (v1.1.0+)
|
||||||
|
**Before**:
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A((开始: 发现可疑快照)) --> B[物理损坏(Allocation Errors)]
|
||||||
|
```
|
||||||
|
**After**:
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A(("开始: 发现可疑快照")) --> B["物理损坏(Allocation Errors)"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 包含 HTML 标签 (v1.1.0+)
|
||||||
|
**Before**:
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[第一步<br/>环境隔离] --> B{状态?}
|
||||||
|
```
|
||||||
|
**After**:
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A["第一步<br/>环境隔离"] --> B{"状态?"}
|
||||||
|
```
|
||||||
|
*注:插件已优化 HTML 保护机制,允许包含 `<br/>` 等标签的 Mermaid 图表正常触发修复。*
|
||||||
|
|
||||||
## 8. XML 标签清理 (XML Cleanup)
|
## 8. XML 标签清理 (XML Cleanup)
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
# Markdown Normalizer Filter
|
# Markdown Normalizer Filter
|
||||||
|
|
||||||
A production-grade content normalizer filter for Open WebUI that fixes common Markdown formatting issues in LLM outputs. It ensures that code blocks, LaTeX formulas, Mermaid diagrams, and other Markdown elements are rendered correctly.
|
**Author:** [Fu-Jie](https://github.com/Fu-Jie)
|
||||||
|
**Version:** 1.1.0
|
||||||
|
|
||||||
|
A content normalizer filter for Open WebUI that fixes common Markdown formatting issues in LLM outputs. It ensures that code blocks, LaTeX formulas, Mermaid diagrams, and other Markdown elements are rendered correctly.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* **Mermaid Syntax Fix**: Automatically fixes common Mermaid syntax errors, such as unquoted node labels and unclosed subgraphs, ensuring diagrams render correctly.
|
* **Mermaid Syntax Fix**: Automatically fixes common Mermaid syntax errors, such as unquoted node labels (including multi-line labels and citations) and unclosed subgraphs, ensuring diagrams render correctly.
|
||||||
* **Frontend Console Debugging**: Supports printing structured debug logs directly to the browser console (F12) for easier troubleshooting.
|
* **Frontend Console Debugging**: Supports printing structured debug logs directly to the browser console (F12) for easier troubleshooting.
|
||||||
* **Code Block Formatting**: Fixes broken code block prefixes, suffixes, and indentation.
|
* **Code Block Formatting**: Fixes broken code block prefixes, suffixes, and indentation.
|
||||||
* **LaTeX Normalization**: Standardizes LaTeX formula delimiters (`\[` -> `$$`, `\(` -> `$`).
|
* **LaTeX Normalization**: Standardizes LaTeX formula delimiters (`\[` -> `$$`, `\(` -> `$`).
|
||||||
@@ -20,7 +23,9 @@ A production-grade content normalizer filter for Open WebUI that fixes common Ma
|
|||||||
1. Install the plugin in Open WebUI.
|
1. Install the plugin in Open WebUI.
|
||||||
2. Enable the filter globally or for specific models.
|
2. Enable the filter globally or for specific models.
|
||||||
3. Configure the enabled fixes in the **Valves** settings.
|
3. Configure the enabled fixes in the **Valves** settings.
|
||||||
4. (Optional) Enable **Show Debug Log** in Valves to view detailed logs in the browser console.
|
4. (Optional) **Show Debug Log** is enabled by default in Valves. This prints structured logs to the browser console (F12).
|
||||||
|
> [!WARNING]
|
||||||
|
> As this is an initial version, some "negative fixes" might occur (e.g., breaking valid Markdown). If you encounter issues, please check the console logs, copy the "Original" vs "Normalized" content, and submit an issue.
|
||||||
|
|
||||||
## Configuration (Valves)
|
## Configuration (Valves)
|
||||||
|
|
||||||
@@ -39,6 +44,14 @@ A production-grade content normalizer filter for Open WebUI that fixes common Ma
|
|||||||
* `show_status`: Show status notification when fixes are applied.
|
* `show_status`: Show status notification when fixes are applied.
|
||||||
* `show_debug_log`: Print debug logs to browser console.
|
* `show_debug_log`: Print debug logs to browser console.
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
### v1.1.0
|
||||||
|
* **Mermaid Fix Refinement**: Improved regex to handle nested parentheses in node labels (e.g., `ID("Label (text)")`) and avoided matching connection labels.
|
||||||
|
* **HTML Safeguard Optimization**: Refined `_contains_html` to allow common tags like `<br/>`, `<b>`, `<i>`, etc., ensuring Mermaid diagrams with these tags are still normalized.
|
||||||
|
* **Full-width Symbol Cleanup**: Fixed duplicate keys and incorrect quote mapping in `FULLWIDTH_MAP`.
|
||||||
|
* **Bug Fixes**: Fixed missing `Dict` import in Python files.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
# Markdown 格式化过滤器 (Markdown Normalizer)
|
# Markdown 格式化过滤器 (Markdown Normalizer)
|
||||||
|
|
||||||
这是一个用于 Open WebUI 的生产级内容格式化过滤器,旨在修复 LLM 输出中常见的 Markdown 格式问题。它能确保代码块、LaTeX 公式、Mermaid 图表和其他 Markdown 元素被正确渲染。
|
**作者:** [Fu-Jie](https://github.com/Fu-Jie)
|
||||||
|
**版本:** 1.1.0
|
||||||
|
|
||||||
|
这是一个用于 Open WebUI 的内容格式化过滤器,旨在修复 LLM 输出中常见的 Markdown 格式问题。它能确保代码块、LaTeX 公式、Mermaid 图表和其他 Markdown 元素被正确渲染。
|
||||||
|
|
||||||
## 功能特性
|
## 功能特性
|
||||||
|
|
||||||
* **Mermaid 语法修复**: 自动修复常见的 Mermaid 语法错误,如未加引号的节点标签和未闭合的子图 (Subgraph),确保图表能正确渲染。
|
* **Mermaid 语法修复**: 自动修复常见的 Mermaid 语法错误,如未加引号的节点标签(支持多行标签和引用标记)和未闭合的子图 (Subgraph),确保图表能正确渲染。
|
||||||
* **前端控制台调试**: 支持将结构化的调试日志直接打印到浏览器控制台 (F12),方便排查问题。
|
* **前端控制台调试**: 支持将结构化的调试日志直接打印到浏览器控制台 (F12),方便排查问题。
|
||||||
* **代码块格式化**: 修复破损的代码块前缀、后缀和缩进问题。
|
* **代码块格式化**: 修复破损的代码块前缀、后缀和缩进问题。
|
||||||
* **LaTeX 规范化**: 标准化 LaTeX 公式定界符 (`\[` -> `$$`, `\(` -> `$`)。
|
* **LaTeX 规范化**: 标准化 LaTeX 公式定界符 (`\[` -> `$$`, `\(` -> `$`)。
|
||||||
@@ -20,7 +23,9 @@
|
|||||||
1. 在 Open WebUI 中安装此插件。
|
1. 在 Open WebUI 中安装此插件。
|
||||||
2. 全局启用或为特定模型启用此过滤器。
|
2. 全局启用或为特定模型启用此过滤器。
|
||||||
3. 在 **Valves** 设置中配置需要启用的修复项。
|
3. 在 **Valves** 设置中配置需要启用的修复项。
|
||||||
4. (可选) 在 Valves 中开启 **显示调试日志 (Show Debug Log)** 以在浏览器控制台中查看详细日志。
|
4. (可选) **显示调试日志 (Show Debug Log)** 在 Valves 中默认开启。这会将结构化的日志打印到浏览器控制台 (F12)。
|
||||||
|
> [!WARNING]
|
||||||
|
> 由于这是初版,可能会出现“负向修复”的情况(例如破坏了原本正确的格式)。如果您遇到问题,请务必查看控制台日志,复制“原始 (Original)”与“规范化 (Normalized)”的内容对比,并提交 Issue 反馈。
|
||||||
|
|
||||||
## 配置项 (Valves)
|
## 配置项 (Valves)
|
||||||
|
|
||||||
@@ -39,6 +44,14 @@
|
|||||||
* `show_status`: 应用修复时显示状态通知。
|
* `show_status`: 应用修复时显示状态通知。
|
||||||
* `show_debug_log`: 在浏览器控制台打印调试日志。
|
* `show_debug_log`: 在浏览器控制台打印调试日志。
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
### v1.1.0
|
||||||
|
* **Mermaid 修复优化**: 改进了正则表达式以处理节点标签中的嵌套括号(如 `ID("标签 (文本)")`),并避免误匹配连接线上的文字。
|
||||||
|
* **HTML 保护机制优化**: 优化了 `_contains_html` 检测,允许 `<br/>`, `<b>`, `<i>` 等常见标签,确保包含这些标签的 Mermaid 图表能被正常规范化。
|
||||||
|
* **全角符号清理**: 修复了 `FULLWIDTH_MAP` 中的重复键名和错误的引号映射。
|
||||||
|
* **Bug 修复**: 修复了 Python 文件中缺失的 `Dict` 类型导入。
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
BIN
plugins/filters/markdown_normalizer/markdown_normalizer.png
Normal file
BIN
plugins/filters/markdown_normalizer/markdown_normalizer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 472 KiB |
@@ -1,14 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
title: Markdown Normalizer
|
title: Markdown Normalizer
|
||||||
author: Fu-Jie
|
author: Fu-Jie
|
||||||
author_url: https://github.com/Fu-Jie
|
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||||
funding_url: https://github.com/Fu-Jie/awesome-openwebui
|
funding_url: https://github.com/open-webui
|
||||||
version: 1.0.0
|
version: 1.1.0
|
||||||
description: A production-grade content normalizer filter that fixes common Markdown formatting issues in LLM outputs, such as broken code blocks, LaTeX formulas, and list formatting.
|
description: A content normalizer filter that fixes common Markdown formatting issues in LLM outputs, such as broken code blocks, LaTeX formulas, and list formatting.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from typing import Optional, List, Callable
|
from typing import Optional, List, Callable, Dict
|
||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
import logging
|
import logging
|
||||||
@@ -75,7 +75,7 @@ class ContentNormalizer:
|
|||||||
# Priority: Longer delimiters match first
|
# Priority: Longer delimiters match first
|
||||||
"mermaid_node": re.compile(
|
"mermaid_node": re.compile(
|
||||||
r'("[^"\\]*(?:\\.[^"\\]*)*")|' # Match quoted strings first (Group 1)
|
r'("[^"\\]*(?:\\.[^"\\]*)*")|' # Match quoted strings first (Group 1)
|
||||||
r"(\w+)\s*(?:"
|
r"(\w+)(?:"
|
||||||
r"(\(\(\()(?![\"])(.*?)(?<![\"])(\)\)\))|" # (((...))) Double Circle
|
r"(\(\(\()(?![\"])(.*?)(?<![\"])(\)\)\))|" # (((...))) Double Circle
|
||||||
r"(\(\()(?![\"])(.*?)(?<![\"])(\)\))|" # ((...)) Circle
|
r"(\(\()(?![\"])(.*?)(?<![\"])(\)\))|" # ((...)) Circle
|
||||||
r"(\(\[)(?![\"])(.*?)(?<![\"])(\]\))|" # ([...]) Stadium
|
r"(\(\[)(?![\"])(.*?)(?<![\"])(\]\))|" # ([...]) Stadium
|
||||||
@@ -86,11 +86,13 @@ class ContentNormalizer:
|
|||||||
r"(\[\\)(?![\"])(.*?)(?<![\"])(\\\])|" # [\...\] Parallelogram Alt
|
r"(\[\\)(?![\"])(.*?)(?<![\"])(\\\])|" # [\...\] Parallelogram Alt
|
||||||
r"(\[/)(?![\"])(.*?)(?<![\"])(\\\])|" # [/...\] Trapezoid
|
r"(\[/)(?![\"])(.*?)(?<![\"])(\\\])|" # [/...\] Trapezoid
|
||||||
r"(\[\\)(?![\"])(.*?)(?<![\"])(/\])|" # [\.../] Trapezoid Alt
|
r"(\[\\)(?![\"])(.*?)(?<![\"])(/\])|" # [\.../] Trapezoid Alt
|
||||||
r"(\()(?![\"])(.*?)(?<![\"])(\))|" # (...) Round
|
r"(\()(?![\"])([^)]*?)(?<![\"])(\))|" # (...) Round - Modified to be safer
|
||||||
r"(\[)(?![\"])(.*?)(?<![\"])(\])|" # [...] Square
|
r"(\[)(?![\"])(.*?)(?<![\"])(\])|" # [...] Square
|
||||||
r"(\{)(?![\"])(.*?)(?<![\"])(\})|" # {...} Rhombus
|
r"(\{)(?![\"])(.*?)(?<![\"])(\})|" # {...} Rhombus
|
||||||
r"(>)(?![\"])(.*?)(?<![\"])(\])" # >...] Asymmetric
|
r"(>)(?![\"])(.*?)(?<![\"])(\])" # >...] Asymmetric
|
||||||
r")"
|
r")"
|
||||||
|
r"(\s*\[\d+\])?", # Capture optional citation [1]
|
||||||
|
re.DOTALL,
|
||||||
),
|
),
|
||||||
# Heading: #Heading -> # Heading
|
# Heading: #Heading -> # Heading
|
||||||
"heading_space": re.compile(r"^(#+)([^ \n#])", re.MULTILINE),
|
"heading_space": re.compile(r"^(#+)([^ \n#])", re.MULTILINE),
|
||||||
@@ -265,9 +267,10 @@ class ContentNormalizer:
|
|||||||
":": ":",
|
":": ":",
|
||||||
"?": "?",
|
"?": "?",
|
||||||
"!": "!",
|
"!": "!",
|
||||||
'"': '"',
|
"“": '"',
|
||||||
'"': '"',
|
"”": '"',
|
||||||
""": "'", """: "'",
|
"‘": "'",
|
||||||
|
"’": "'",
|
||||||
}
|
}
|
||||||
|
|
||||||
parts = content.split("```")
|
parts = content.split("```")
|
||||||
@@ -290,15 +293,20 @@ class ContentNormalizer:
|
|||||||
id_str = match.group(2)
|
id_str = match.group(2)
|
||||||
|
|
||||||
# Find matching shape group
|
# Find matching shape group
|
||||||
# Groups start at index 3 (in match.group terms) or index 2 (in match.groups() tuple)
|
|
||||||
# Tuple: (String, ID, Open1, Content1, Close1, ...)
|
|
||||||
groups = match.groups()
|
groups = match.groups()
|
||||||
for i in range(2, len(groups), 3):
|
citation = groups[-1] or "" # Last group is citation
|
||||||
|
|
||||||
|
# Iterate over shape groups (excluding the last citation group)
|
||||||
|
for i in range(2, len(groups) - 1, 3):
|
||||||
if groups[i] is not None:
|
if groups[i] is not None:
|
||||||
open_char = groups[i]
|
open_char = groups[i]
|
||||||
content = groups[i + 1]
|
content = groups[i + 1]
|
||||||
close_char = groups[i + 2]
|
close_char = groups[i + 2]
|
||||||
|
|
||||||
|
# Append citation to content if present
|
||||||
|
if citation:
|
||||||
|
content += citation
|
||||||
|
|
||||||
# Escape quotes in content
|
# Escape quotes in content
|
||||||
content = content.replace('"', '\\"')
|
content = content.replace('"', '\\"')
|
||||||
|
|
||||||
@@ -397,15 +405,52 @@ class Filter:
|
|||||||
default=True, description="Show status notification when fixes are applied"
|
default=True, description="Show status notification when fixes are applied"
|
||||||
)
|
)
|
||||||
show_debug_log: bool = Field(
|
show_debug_log: bool = Field(
|
||||||
default=False, description="Print debug logs to browser console (F12)"
|
default=True, description="Print debug logs to browser console (F12)"
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.valves = self.Valves()
|
self.valves = self.Valves()
|
||||||
|
|
||||||
|
def _get_chat_context(
|
||||||
|
self, body: dict, __metadata__: Optional[dict] = None
|
||||||
|
) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Unified extraction of chat context information (chat_id, message_id).
|
||||||
|
Prioritizes extraction from body, then metadata.
|
||||||
|
"""
|
||||||
|
chat_id = ""
|
||||||
|
message_id = ""
|
||||||
|
|
||||||
|
# 1. Try to get from body
|
||||||
|
if isinstance(body, dict):
|
||||||
|
chat_id = body.get("chat_id", "")
|
||||||
|
message_id = body.get("id", "") # message_id is usually 'id' in body
|
||||||
|
|
||||||
|
# Check body.metadata as fallback
|
||||||
|
if not chat_id or not message_id:
|
||||||
|
body_metadata = body.get("metadata", {})
|
||||||
|
if isinstance(body_metadata, dict):
|
||||||
|
if not chat_id:
|
||||||
|
chat_id = body_metadata.get("chat_id", "")
|
||||||
|
if not message_id:
|
||||||
|
message_id = body_metadata.get("message_id", "")
|
||||||
|
|
||||||
|
# 2. Try to get from __metadata__ (as supplement)
|
||||||
|
if __metadata__ and isinstance(__metadata__, dict):
|
||||||
|
if not chat_id:
|
||||||
|
chat_id = __metadata__.get("chat_id", "")
|
||||||
|
if not message_id:
|
||||||
|
message_id = __metadata__.get("message_id", "")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"chat_id": str(chat_id).strip(),
|
||||||
|
"message_id": str(message_id).strip(),
|
||||||
|
}
|
||||||
|
|
||||||
def _contains_html(self, content: str) -> bool:
|
def _contains_html(self, content: str) -> bool:
|
||||||
"""Check if content contains HTML tags (to avoid breaking HTML output)"""
|
"""Check if content contains HTML tags (to avoid breaking HTML output)"""
|
||||||
pattern = r"<\s*/?\s*(?:html|head|body|div|span|p|br|hr|ul|ol|li|table|thead|tbody|tfoot|tr|td|th|img|a|b|i|strong|em|code|pre|blockquote|h[1-6]|script|style|form|input|button|label|select|option|iframe|link|meta|title)\b"
|
# Removed common Mermaid-compatible tags like br, b, i, strong, em, span
|
||||||
|
pattern = r"<\s*/?\s*(?:html|head|body|div|p|hr|ul|ol|li|table|thead|tbody|tfoot|tr|td|th|img|a|code|pre|blockquote|h[1-6]|script|style|form|input|button|label|select|option|iframe|link|meta|title)\b"
|
||||||
return bool(re.search(pattern, content, re.IGNORECASE))
|
return bool(re.search(pattern, content, re.IGNORECASE))
|
||||||
|
|
||||||
async def _emit_status(self, __event_emitter__, applied_fixes: List[str]):
|
async def _emit_status(self, __event_emitter__, applied_fixes: List[str]):
|
||||||
@@ -431,24 +476,23 @@ class Filter:
|
|||||||
print(f"Error emitting status: {e}")
|
print(f"Error emitting status: {e}")
|
||||||
|
|
||||||
async def _emit_debug_log(
|
async def _emit_debug_log(
|
||||||
self, __event_call__, applied_fixes: List[str], original: str, normalized: str
|
self,
|
||||||
|
__event_call__,
|
||||||
|
applied_fixes: List[str],
|
||||||
|
original: str,
|
||||||
|
normalized: str,
|
||||||
|
chat_id: str = "",
|
||||||
):
|
):
|
||||||
"""Emit debug log to browser console via JS execution"""
|
"""Emit debug log to browser console via JS execution"""
|
||||||
if not self.valves.show_debug_log or not __event_call__:
|
if not self.valves.show_debug_log or not __event_call__:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Prepare data for JS
|
|
||||||
log_data = {
|
|
||||||
"fixes": applied_fixes,
|
|
||||||
"original": original,
|
|
||||||
"normalized": normalized,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Construct JS code
|
# Construct JS code
|
||||||
js_code = f"""
|
js_code = f"""
|
||||||
(async function() {{
|
(async function() {{
|
||||||
console.group("🛠️ Markdown Normalizer Debug");
|
console.group("🛠️ Markdown Normalizer Debug");
|
||||||
|
console.log("Chat ID:", {json.dumps(chat_id)});
|
||||||
console.log("Applied Fixes:", {json.dumps(applied_fixes, ensure_ascii=False)});
|
console.log("Applied Fixes:", {json.dumps(applied_fixes, ensure_ascii=False)});
|
||||||
console.log("Original Content:", {json.dumps(original, ensure_ascii=False)});
|
console.log("Original Content:", {json.dumps(original, ensure_ascii=False)});
|
||||||
console.log("Normalized Content:", {json.dumps(normalized, ensure_ascii=False)});
|
console.log("Normalized Content:", {json.dumps(normalized, ensure_ascii=False)});
|
||||||
@@ -514,11 +558,13 @@ class Filter:
|
|||||||
await self._emit_status(
|
await self._emit_status(
|
||||||
__event_emitter__, normalizer.applied_fixes
|
__event_emitter__, normalizer.applied_fixes
|
||||||
)
|
)
|
||||||
|
chat_ctx = self._get_chat_context(body, __metadata__)
|
||||||
await self._emit_debug_log(
|
await self._emit_debug_log(
|
||||||
__event_call__,
|
__event_call__,
|
||||||
normalizer.applied_fixes,
|
normalizer.applied_fixes,
|
||||||
content,
|
content,
|
||||||
new_content,
|
new_content,
|
||||||
|
chat_id=chat_ctx["chat_id"],
|
||||||
)
|
)
|
||||||
|
|
||||||
return body
|
return body
|
||||||
|
|||||||
BIN
plugins/filters/markdown_normalizer/markdown_normalizer_cn.png
Normal file
BIN
plugins/filters/markdown_normalizer/markdown_normalizer_cn.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 472 KiB |
@@ -1,14 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
title: Markdown 格式修复器 (Markdown Normalizer)
|
title: Markdown 格式修复器 (Markdown Normalizer)
|
||||||
author: Fu-Jie
|
author: Fu-Jie
|
||||||
author_url: https://github.com/Fu-Jie
|
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||||
funding_url: https://github.com/Fu-Jie/awesome-openwebui
|
funding_url: https://github.com/open-webui
|
||||||
version: 1.0.0
|
version: 1.1.0
|
||||||
description: 生产级内容规范化过滤器,修复 LLM 输出中常见的 Markdown 格式问题,如损坏的代码块、LaTeX 公式、Mermaid 图表和列表格式。
|
description: 内容规范化过滤器,修复 LLM 输出中常见的 Markdown 格式问题,如损坏的代码块、LaTeX 公式、Mermaid 图表和列表格式。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from typing import Optional, List, Callable
|
from typing import Optional, List, Callable, Dict
|
||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -70,7 +70,7 @@ class ContentNormalizer:
|
|||||||
# 优先级:长定界符优先匹配
|
# 优先级:长定界符优先匹配
|
||||||
"mermaid_node": re.compile(
|
"mermaid_node": re.compile(
|
||||||
r'("[^"\\]*(?:\\.[^"\\]*)*")|' # Match quoted strings first (Group 1)
|
r'("[^"\\]*(?:\\.[^"\\]*)*")|' # Match quoted strings first (Group 1)
|
||||||
r"(\w+)\s*(?:"
|
r"(\w+)(?:"
|
||||||
r"(\(\(\()(?![\"])(.*?)(?<![\"])(\)\)\))|" # (((...))) Double Circle
|
r"(\(\(\()(?![\"])(.*?)(?<![\"])(\)\)\))|" # (((...))) Double Circle
|
||||||
r"(\(\()(?![\"])(.*?)(?<![\"])(\)\))|" # ((...)) Circle
|
r"(\(\()(?![\"])(.*?)(?<![\"])(\)\))|" # ((...)) Circle
|
||||||
r"(\(\[)(?![\"])(.*?)(?<![\"])(\]\))|" # ([...]) Stadium
|
r"(\(\[)(?![\"])(.*?)(?<![\"])(\]\))|" # ([...]) Stadium
|
||||||
@@ -81,11 +81,13 @@ class ContentNormalizer:
|
|||||||
r"(\[\\)(?![\"])(.*?)(?<![\"])(\\\])|" # [\...\] Parallelogram Alt
|
r"(\[\\)(?![\"])(.*?)(?<![\"])(\\\])|" # [\...\] Parallelogram Alt
|
||||||
r"(\[/)(?![\"])(.*?)(?<![\"])(\\\])|" # [/...\] Trapezoid
|
r"(\[/)(?![\"])(.*?)(?<![\"])(\\\])|" # [/...\] Trapezoid
|
||||||
r"(\[\\)(?![\"])(.*?)(?<![\"])(/\])|" # [\.../] Trapezoid Alt
|
r"(\[\\)(?![\"])(.*?)(?<![\"])(/\])|" # [\.../] Trapezoid Alt
|
||||||
r"(\()(?![\"])(.*?)(?<![\"])(\))|" # (...) Round
|
r"(\()(?![\"])([^)]*?)(?<![\"])(\))|" # (...) Round - Modified to be safer
|
||||||
r"(\[)(?![\"])(.*?)(?<![\"])(\])|" # [...] Square
|
r"(\[)(?![\"])(.*?)(?<![\"])(\])|" # [...] Square
|
||||||
r"(\{)(?![\"])(.*?)(?<![\"])(\})|" # {...} Rhombus
|
r"(\{)(?![\"])(.*?)(?<![\"])(\})|" # {...} Rhombus
|
||||||
r"(>)(?![\"])(.*?)(?<![\"])(\])" # >...] Asymmetric
|
r"(>)(?![\"])(.*?)(?<![\"])(\])" # >...] Asymmetric
|
||||||
r")"
|
r")"
|
||||||
|
r"(\s*\[\d+\])?", # Capture optional citation [1]
|
||||||
|
re.DOTALL,
|
||||||
),
|
),
|
||||||
# Heading: #Heading -> # Heading
|
# Heading: #Heading -> # Heading
|
||||||
"heading_space": re.compile(r"^(#+)([^ \n#])", re.MULTILINE),
|
"heading_space": re.compile(r"^(#+)([^ \n#])", re.MULTILINE),
|
||||||
@@ -260,9 +262,10 @@ class ContentNormalizer:
|
|||||||
":": ":",
|
":": ":",
|
||||||
"?": "?",
|
"?": "?",
|
||||||
"!": "!",
|
"!": "!",
|
||||||
'"': '"',
|
"“": '"',
|
||||||
'"': '"',
|
"”": '"',
|
||||||
""": "'", """: "'",
|
"‘": "'",
|
||||||
|
"’": "'",
|
||||||
}
|
}
|
||||||
|
|
||||||
parts = content.split("```")
|
parts = content.split("```")
|
||||||
@@ -285,15 +288,20 @@ class ContentNormalizer:
|
|||||||
id_str = match.group(2)
|
id_str = match.group(2)
|
||||||
|
|
||||||
# Find matching shape group
|
# Find matching shape group
|
||||||
# Groups start at index 3 (in match.group terms) or index 2 (in match.groups() tuple)
|
|
||||||
# Tuple: (String, ID, Open1, Content1, Close1, ...)
|
|
||||||
groups = match.groups()
|
groups = match.groups()
|
||||||
for i in range(2, len(groups), 3):
|
citation = groups[-1] or "" # Last group is citation
|
||||||
|
|
||||||
|
# Iterate over shape groups (excluding the last citation group)
|
||||||
|
for i in range(2, len(groups) - 1, 3):
|
||||||
if groups[i] is not None:
|
if groups[i] is not None:
|
||||||
open_char = groups[i]
|
open_char = groups[i]
|
||||||
content = groups[i + 1]
|
content = groups[i + 1]
|
||||||
close_char = groups[i + 2]
|
close_char = groups[i + 2]
|
||||||
|
|
||||||
|
# Append citation to content if present
|
||||||
|
if citation:
|
||||||
|
content += citation
|
||||||
|
|
||||||
# 如果内容包含引号,进行转义
|
# 如果内容包含引号,进行转义
|
||||||
content = content.replace('"', '\\"')
|
content = content.replace('"', '\\"')
|
||||||
|
|
||||||
@@ -397,15 +405,52 @@ class Filter:
|
|||||||
)
|
)
|
||||||
show_status: bool = Field(default=True, description="应用修复时显示状态通知")
|
show_status: bool = Field(default=True, description="应用修复时显示状态通知")
|
||||||
show_debug_log: bool = Field(
|
show_debug_log: bool = Field(
|
||||||
default=False, description="在浏览器控制台打印调试日志 (F12)"
|
default=True, description="在浏览器控制台打印调试日志 (F12)"
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.valves = self.Valves()
|
self.valves = self.Valves()
|
||||||
|
|
||||||
|
def _get_chat_context(
|
||||||
|
self, body: dict, __metadata__: Optional[dict] = None
|
||||||
|
) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
统一提取聊天上下文信息 (chat_id, message_id)。
|
||||||
|
优先从 body 中提取,其次从 metadata 中提取。
|
||||||
|
"""
|
||||||
|
chat_id = ""
|
||||||
|
message_id = ""
|
||||||
|
|
||||||
|
# 1. 尝试从 body 获取
|
||||||
|
if isinstance(body, dict):
|
||||||
|
chat_id = body.get("chat_id", "")
|
||||||
|
message_id = body.get("id", "") # message_id 在 body 中通常是 id
|
||||||
|
|
||||||
|
# 再次检查 body.metadata
|
||||||
|
if not chat_id or not message_id:
|
||||||
|
body_metadata = body.get("metadata", {})
|
||||||
|
if isinstance(body_metadata, dict):
|
||||||
|
if not chat_id:
|
||||||
|
chat_id = body_metadata.get("chat_id", "")
|
||||||
|
if not message_id:
|
||||||
|
message_id = body_metadata.get("message_id", "")
|
||||||
|
|
||||||
|
# 2. 尝试从 __metadata__ 获取 (作为补充)
|
||||||
|
if __metadata__ and isinstance(__metadata__, dict):
|
||||||
|
if not chat_id:
|
||||||
|
chat_id = __metadata__.get("chat_id", "")
|
||||||
|
if not message_id:
|
||||||
|
message_id = __metadata__.get("message_id", "")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"chat_id": str(chat_id).strip(),
|
||||||
|
"message_id": str(message_id).strip(),
|
||||||
|
}
|
||||||
|
|
||||||
def _contains_html(self, content: str) -> bool:
|
def _contains_html(self, content: str) -> bool:
|
||||||
"""Check if content contains HTML tags (to avoid breaking HTML output)"""
|
"""Check if content contains HTML tags (to avoid breaking HTML output)"""
|
||||||
pattern = r"<\s*/?\s*(?:html|head|body|div|span|p|br|hr|ul|ol|li|table|thead|tbody|tfoot|tr|td|th|img|a|b|i|strong|em|code|pre|blockquote|h[1-6]|script|style|form|input|button|label|select|option|iframe|link|meta|title)\b"
|
# Removed common Mermaid-compatible tags like br, b, i, strong, em, span
|
||||||
|
pattern = r"<\s*/?\s*(?:html|head|body|div|p|hr|ul|ol|li|table|thead|tbody|tfoot|tr|td|th|img|a|code|pre|blockquote|h[1-6]|script|style|form|input|button|label|select|option|iframe|link|meta|title)\b"
|
||||||
return bool(re.search(pattern, content, re.IGNORECASE))
|
return bool(re.search(pattern, content, re.IGNORECASE))
|
||||||
|
|
||||||
async def _emit_status(self, __event_emitter__, applied_fixes: List[str]):
|
async def _emit_status(self, __event_emitter__, applied_fixes: List[str]):
|
||||||
@@ -456,24 +501,23 @@ class Filter:
|
|||||||
"""Emit debug log to browser console via JS execution"""
|
"""Emit debug log to browser console via JS execution"""
|
||||||
|
|
||||||
async def _emit_debug_log(
|
async def _emit_debug_log(
|
||||||
self, __event_call__, applied_fixes: List[str], original: str, normalized: str
|
self,
|
||||||
|
__event_call__,
|
||||||
|
applied_fixes: List[str],
|
||||||
|
original: str,
|
||||||
|
normalized: str,
|
||||||
|
chat_id: str = "",
|
||||||
):
|
):
|
||||||
"""Emit debug log to browser console via JS execution"""
|
"""Emit debug log to browser console via JS execution"""
|
||||||
if not self.valves.show_debug_log or not __event_call__:
|
if not self.valves.show_debug_log or not __event_call__:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Prepare data for JS
|
|
||||||
log_data = {
|
|
||||||
"fixes": applied_fixes,
|
|
||||||
"original": original,
|
|
||||||
"normalized": normalized,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Construct JS code
|
# Construct JS code
|
||||||
js_code = f"""
|
js_code = f"""
|
||||||
(async function() {{
|
(async function() {{
|
||||||
console.group("🛠️ Markdown Normalizer Debug");
|
console.group("🛠️ Markdown Normalizer Debug");
|
||||||
|
console.log("Chat ID:", {json.dumps(chat_id)});
|
||||||
console.log("Applied Fixes:", {json.dumps(applied_fixes, ensure_ascii=False)});
|
console.log("Applied Fixes:", {json.dumps(applied_fixes, ensure_ascii=False)});
|
||||||
console.log("Original Content:", {json.dumps(original, ensure_ascii=False)});
|
console.log("Original Content:", {json.dumps(original, ensure_ascii=False)});
|
||||||
console.log("Normalized Content:", {json.dumps(normalized, ensure_ascii=False)});
|
console.log("Normalized Content:", {json.dumps(normalized, ensure_ascii=False)});
|
||||||
@@ -539,11 +583,13 @@ class Filter:
|
|||||||
await self._emit_status(
|
await self._emit_status(
|
||||||
__event_emitter__, normalizer.applied_fixes
|
__event_emitter__, normalizer.applied_fixes
|
||||||
)
|
)
|
||||||
|
chat_ctx = self._get_chat_context(body, __metadata__)
|
||||||
await self._emit_debug_log(
|
await self._emit_debug_log(
|
||||||
__event_call__,
|
__event_call__,
|
||||||
normalizer.applied_fixes,
|
normalizer.applied_fixes,
|
||||||
content,
|
content,
|
||||||
new_content,
|
new_content,
|
||||||
|
chat_id=chat_ctx["chat_id"],
|
||||||
)
|
)
|
||||||
|
|
||||||
return body
|
return body
|
||||||
|
|||||||
@@ -253,7 +253,24 @@ class OpenWebUICommunityClient:
|
|||||||
是否成功
|
是否成功
|
||||||
"""
|
"""
|
||||||
url = f"{self.BASE_URL}/posts/{post_id}/update"
|
url = f"{self.BASE_URL}/posts/{post_id}/update"
|
||||||
response = requests.post(url, headers=self.headers, json=post_data)
|
|
||||||
|
# 仅发送允许更新的字段,避免 422 错误
|
||||||
|
allowed_keys = ["title", "content", "type", "data", "media"]
|
||||||
|
payload = {k: v for k, v in post_data.items() if k in allowed_keys}
|
||||||
|
|
||||||
|
response = requests.post(url, headers=self.headers, json=payload)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
try:
|
||||||
|
error_detail = response.json()
|
||||||
|
print(
|
||||||
|
f" Error: Update failed ({response.status_code}): {json.dumps(error_detail, indent=2)}"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
print(
|
||||||
|
f" Error: Update failed ({response.status_code}): {response.text[:500]}"
|
||||||
|
)
|
||||||
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -282,37 +299,54 @@ class OpenWebUICommunityClient:
|
|||||||
if not post_data:
|
if not post_data:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 确保结构存在
|
# 严格重建 data 结构,避免包含只读字段(如 data.function.id)
|
||||||
if "data" not in post_data:
|
current_function = post_data.get("data", {}).get("function", {})
|
||||||
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"] = {}
|
|
||||||
|
|
||||||
# 更新源代码
|
# 过滤 metadata,移除 openwebui_id 等系统字段
|
||||||
post_data["data"]["function"]["content"] = source_code
|
clean_metadata = {
|
||||||
|
k: v
|
||||||
|
for k, v in (metadata or {}).items()
|
||||||
|
if k not in ["openwebui_id", "post_id"]
|
||||||
|
}
|
||||||
|
|
||||||
|
function_data = {
|
||||||
|
"id": current_function.get("id", ""),
|
||||||
|
"name": metadata.get("title", current_function.get("name", "Plugin")),
|
||||||
|
"type": current_function.get("type", "action"),
|
||||||
|
"content": source_code,
|
||||||
|
"meta": {
|
||||||
|
"description": metadata.get(
|
||||||
|
"description",
|
||||||
|
current_function.get("meta", {}).get("description", ""),
|
||||||
|
),
|
||||||
|
"manifest": clean_metadata,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
post_data["data"] = {"function": function_data}
|
||||||
|
post_data["type"] = "function"
|
||||||
|
|
||||||
# 更新 README(社区页面展示内容)
|
# 更新 README(社区页面展示内容)
|
||||||
if readme_content:
|
if readme_content:
|
||||||
post_data["content"] = readme_content
|
post_data["content"] = readme_content
|
||||||
|
|
||||||
# 更新元数据
|
# 更新标题
|
||||||
if metadata:
|
if metadata and "title" in metadata:
|
||||||
post_data["data"]["function"]["meta"]["manifest"].update(metadata)
|
post_data["title"] = metadata["title"]
|
||||||
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"
|
|
||||||
]
|
|
||||||
|
|
||||||
# 更新图片
|
# 更新图片
|
||||||
if media_urls:
|
if media_urls:
|
||||||
post_data["media"] = media_urls
|
# 将字符串 URL 转换为字典格式 (API 要求)
|
||||||
|
media_list = []
|
||||||
|
for item in media_urls:
|
||||||
|
if isinstance(item, str):
|
||||||
|
media_list.append({"url": item})
|
||||||
|
elif isinstance(item, dict):
|
||||||
|
media_list.append(item)
|
||||||
|
post_data["media"] = media_list
|
||||||
|
else:
|
||||||
|
# 如果没有新图片,保留原有的(如果有)
|
||||||
|
pass
|
||||||
|
|
||||||
return self.update_post(post_id, post_data)
|
return self.update_post(post_id, post_data)
|
||||||
|
|
||||||
@@ -469,10 +503,42 @@ class OpenWebUICommunityClient:
|
|||||||
return True, f"Created new post (ID: {new_post_id})"
|
return True, f"Created new post (ID: {new_post_id})"
|
||||||
return False, "Failed to create new post"
|
return False, "Failed to create new post"
|
||||||
|
|
||||||
|
# 获取远程帖子信息(只需获取一次)
|
||||||
|
remote_post = None
|
||||||
|
if post_id:
|
||||||
|
remote_post = self.get_post(post_id)
|
||||||
|
|
||||||
# 版本检查(仅对更新有效)
|
# 版本检查(仅对更新有效)
|
||||||
if not force and local_version:
|
if not force and local_version and remote_post:
|
||||||
if not self.version_needs_update(post_id, local_version):
|
remote_version = (
|
||||||
return True, f"Skipped: version {local_version} matches remote"
|
remote_post.get("data", {})
|
||||||
|
.get("function", {})
|
||||||
|
.get("meta", {})
|
||||||
|
.get("manifest", {})
|
||||||
|
.get("version")
|
||||||
|
)
|
||||||
|
|
||||||
|
version_changed = local_version != remote_version
|
||||||
|
|
||||||
|
# 检查 README 是否变化
|
||||||
|
readme_changed = False
|
||||||
|
remote_content = remote_post.get("content", "")
|
||||||
|
local_content = readme_content or metadata.get("description", "")
|
||||||
|
|
||||||
|
# 简单的内容比较 (去除首尾空白)
|
||||||
|
if (local_content or "").strip() != (remote_content or "").strip():
|
||||||
|
readme_changed = True
|
||||||
|
|
||||||
|
if not version_changed and not readme_changed:
|
||||||
|
return (
|
||||||
|
True,
|
||||||
|
f"Skipped: version {local_version} matches remote and no README changes",
|
||||||
|
)
|
||||||
|
|
||||||
|
if readme_changed and not version_changed:
|
||||||
|
print(
|
||||||
|
f" ℹ️ Version match ({local_version}) but README changed. Updating..."
|
||||||
|
)
|
||||||
|
|
||||||
# 更新
|
# 更新
|
||||||
success = self.update_plugin(
|
success = self.update_plugin(
|
||||||
@@ -484,7 +550,9 @@ class OpenWebUICommunityClient:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
return True, f"Updated to version {local_version}"
|
if local_version:
|
||||||
|
return True, f"Updated to version {local_version}"
|
||||||
|
return True, "Updated plugin"
|
||||||
return False, "Update failed"
|
return False, "Update failed"
|
||||||
|
|
||||||
def _parse_frontmatter(self, content: str) -> Dict[str, str]:
|
def _parse_frontmatter(self, content: str) -> Dict[str, str]:
|
||||||
|
|||||||
@@ -414,7 +414,8 @@ class OpenWebUIStats:
|
|||||||
"author_header": "| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |",
|
"author_header": "| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |",
|
||||||
"header": "| 📝 发布 | ⬇️ 下载 | 👁️ 浏览 | 👍 点赞 | 💾 收藏 |",
|
"header": "| 📝 发布 | ⬇️ 下载 | 👁️ 浏览 | 👍 点赞 | 💾 收藏 |",
|
||||||
"top6_title": "### 🔥 热门插件 Top 6",
|
"top6_title": "### 🔥 热门插件 Top 6",
|
||||||
"top6_header": "| 排名 | 插件 | 下载 | 浏览 |",
|
"top6_updated": f"> 🕐 自动更新于 {get_beijing_time().strftime('%Y-%m-%d %H:%M')}",
|
||||||
|
"top6_header": "| 排名 | 插件 | 版本 | 下载 | 浏览 | 更新日期 |",
|
||||||
"full_stats": "*完整统计请查看 [社区统计报告](./docs/community-stats.zh.md)*",
|
"full_stats": "*完整统计请查看 [社区统计报告](./docs/community-stats.zh.md)*",
|
||||||
},
|
},
|
||||||
"en": {
|
"en": {
|
||||||
@@ -423,7 +424,8 @@ class OpenWebUIStats:
|
|||||||
"author_header": "| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |",
|
"author_header": "| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |",
|
||||||
"header": "| 📝 Posts | ⬇️ Downloads | 👁️ Views | 👍 Upvotes | 💾 Saves |",
|
"header": "| 📝 Posts | ⬇️ Downloads | 👁️ Views | 👍 Upvotes | 💾 Saves |",
|
||||||
"top6_title": "### 🔥 Top 6 Popular Plugins",
|
"top6_title": "### 🔥 Top 6 Popular Plugins",
|
||||||
"top6_header": "| Rank | Plugin | Downloads | Views |",
|
"top6_updated": f"> 🕐 Auto-updated: {get_beijing_time().strftime('%Y-%m-%d %H:%M')}",
|
||||||
|
"top6_header": "| Rank | Plugin | Version | Downloads | Views | Updated |",
|
||||||
"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)*",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -462,14 +464,16 @@ class OpenWebUIStats:
|
|||||||
# Top 6 热门插件
|
# Top 6 热门插件
|
||||||
lines.append(t["top6_title"])
|
lines.append(t["top6_title"])
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
lines.append(t["top6_updated"])
|
||||||
|
lines.append("")
|
||||||
lines.append(t["top6_header"])
|
lines.append(t["top6_header"])
|
||||||
lines.append("|:---:|------|:---:|:---:|")
|
lines.append("|:---:|------|:---:|:---:|:---:|:---:|")
|
||||||
|
|
||||||
medals = ["🥇", "🥈", "🥉", "4️⃣", "5️⃣", "6️⃣"]
|
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(
|
||||||
f"| {medal} | [{post['title']}]({post['url']}) | {post['downloads']} | {post['views']} |"
|
f"| {medal} | [{post['title']}]({post['url']}) | {post['version']} | {post['downloads']} | {post['views']} | {post['updated_at']} |"
|
||||||
)
|
)
|
||||||
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|||||||
Reference in New Issue
Block a user