Compare commits
6 Commits
v2026.01.0
...
v2026.01.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
856f76cd27 | ||
|
|
28bb9000d8 | ||
|
|
d0b9e46b74 | ||
|
|
a0a4d31715 | ||
|
|
d5f394f5f1 | ||
|
|
a477d2baad |
@@ -25,6 +25,10 @@ Every plugin **MUST** have bilingual versions for both code and documentation:
|
|||||||
- **Valves**: Use `pydantic` for configuration.
|
- **Valves**: Use `pydantic` for configuration.
|
||||||
- **Database**: Re-use `open_webui.internal.db` shared connection.
|
- **Database**: Re-use `open_webui.internal.db` shared connection.
|
||||||
- **User Context**: Use `_get_user_context` helper method.
|
- **User Context**: Use `_get_user_context` helper method.
|
||||||
|
- **Chat API**: For message updates, follow the "OpenWebUI Chat API 更新规范" in `.github/copilot-instructions.md`.
|
||||||
|
- Use Event API for immediate UI updates
|
||||||
|
- Use Chat Persistence API for database storage
|
||||||
|
- Always update both `messages[]` and `history.messages`
|
||||||
|
|
||||||
### Commit Messages
|
### Commit Messages
|
||||||
- **Language**: **English ONLY**. Do not use Chinese in commit messages.
|
- **Language**: **English ONLY**. Do not use Chinese in commit messages.
|
||||||
|
|||||||
196
.github/copilot-instructions.md
vendored
196
.github/copilot-instructions.md
vendored
@@ -949,7 +949,7 @@ async def action(self, body, __event_call__, __metadata__, ...):
|
|||||||
#### 优势
|
#### 优势
|
||||||
|
|
||||||
- **纯 Markdown 输出**:结果是标准的 Markdown 图片语法,无需 HTML 代码块
|
- **纯 Markdown 输出**:结果是标准的 Markdown 图片语法,无需 HTML 代码块
|
||||||
- **自包含**:图片以 Base64 Data URL 嵌入,无外部依赖
|
- **高效存储**:图片上传至 `/api/v1/files`,避免 Base64 字符串膨胀聊天记录
|
||||||
- **持久化**:通过 API 回写,消息重新加载后图片仍然存在
|
- **持久化**:通过 API 回写,消息重新加载后图片仍然存在
|
||||||
- **跨平台**:任何支持 Markdown 图片的客户端都能显示
|
- **跨平台**:任何支持 Markdown 图片的客户端都能显示
|
||||||
- **无服务端渲染依赖**:利用用户浏览器的渲染能力
|
- **无服务端渲染依赖**:利用用户浏览器的渲染能力
|
||||||
@@ -960,7 +960,7 @@ async def action(self, body, __event_call__, __metadata__, ...):
|
|||||||
|------|-------------------------|------------------------|
|
|------|-------------------------|------------------------|
|
||||||
| 输出格式 | HTML 代码块 | Markdown 图片 |
|
| 输出格式 | HTML 代码块 | Markdown 图片 |
|
||||||
| 交互性 | ✅ 支持按钮、动画 | ❌ 静态图片 |
|
| 交互性 | ✅ 支持按钮、动画 | ❌ 静态图片 |
|
||||||
| 外部依赖 | 需要加载 JS 库 | 无(图片自包含) |
|
| 外部依赖 | 需要加载 JS 库 | 依赖 `/api/v1/files` 存储 |
|
||||||
| 持久化 | 依赖浏览器渲染 | ✅ 永久可见 |
|
| 持久化 | 依赖浏览器渲染 | ✅ 永久可见 |
|
||||||
| 文件导出 | 需特殊处理 | ✅ 直接导出 |
|
| 文件导出 | 需特殊处理 | ✅ 直接导出 |
|
||||||
| 适用场景 | 交互式内容 | 信息图、图表快照 |
|
| 适用场景 | 交互式内容 | 信息图、图表快照 |
|
||||||
@@ -970,7 +970,199 @@ async def action(self, body, __event_call__, __metadata__, ...):
|
|||||||
- `plugins/actions/js-render-poc/infographic_markdown.py` - AntV Infographic 生成并嵌入
|
- `plugins/actions/js-render-poc/infographic_markdown.py` - AntV Infographic 生成并嵌入
|
||||||
- `plugins/actions/js-render-poc/js_render_poc.py` - 基础概念验证
|
- `plugins/actions/js-render-poc/js_render_poc.py` - 基础概念验证
|
||||||
|
|
||||||
|
### OpenWebUI Chat API 更新规范 (Chat API Update Specification)
|
||||||
|
|
||||||
|
当插件需要修改消息内容并持久化到数据库时,必须遵循 OpenWebUI 的 Backend-Controlled API 流程。
|
||||||
|
|
||||||
|
When a plugin needs to modify message content and persist it to the database, follow OpenWebUI's Backend-Controlled API flow.
|
||||||
|
|
||||||
|
#### 核心概念 (Core Concepts)
|
||||||
|
|
||||||
|
1. **Event API** (`/api/v1/chats/{chatId}/messages/{messageId}/event`)
|
||||||
|
- 用于**即时更新前端显示**,用户无需刷新页面
|
||||||
|
- 是可选的,部分版本可能不支持
|
||||||
|
- 仅影响当前会话的 UI,不持久化
|
||||||
|
|
||||||
|
2. **Chat Persistence API** (`/api/v1/chats/{chatId}`)
|
||||||
|
- 用于**持久化到数据库**,确保刷新页面后数据仍存在
|
||||||
|
- 必须同时更新 `messages[]` 数组和 `history.messages` 对象
|
||||||
|
- 是消息持久化的唯一可靠方式
|
||||||
|
|
||||||
|
#### 数据结构 (Data Structure)
|
||||||
|
|
||||||
|
OpenWebUI 的 Chat 对象包含两个关键位置存储消息内容:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"chat": {
|
||||||
|
"id": "chat-uuid",
|
||||||
|
"title": "Chat Title",
|
||||||
|
"messages": [ // 1️⃣ 消息数组
|
||||||
|
{ "id": "msg-1", "role": "user", "content": "..." },
|
||||||
|
{ "id": "msg-2", "role": "assistant", "content": "..." }
|
||||||
|
],
|
||||||
|
"history": {
|
||||||
|
"current_id": "msg-2",
|
||||||
|
"messages": { // 2️⃣ 消息索引对象
|
||||||
|
"msg-1": { "id": "msg-1", "role": "user", "content": "..." },
|
||||||
|
"msg-2": { "id": "msg-2", "role": "assistant", "content": "..." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **重要**:修改消息时,**必须同时更新两个位置**,否则可能导致数据不一致。
|
||||||
|
|
||||||
|
#### 标准实现流程 (Standard Implementation)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
(async function() {
|
||||||
|
const chatId = "{chat_id}";
|
||||||
|
const messageId = "{message_id}";
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
|
// 1️⃣ 获取当前 Chat 数据
|
||||||
|
const getResponse = await fetch(`/api/v1/chats/${chatId}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: { "Authorization": `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
const chatData = await getResponse.json();
|
||||||
|
|
||||||
|
// 2️⃣ 使用 map 遍历 messages,只修改目标消息
|
||||||
|
let newContent = "";
|
||||||
|
const updatedMessages = chatData.chat.messages.map(m => {
|
||||||
|
if (m.id === messageId) {
|
||||||
|
const originalContent = m.content || "";
|
||||||
|
newContent = originalContent + "\n\n" + newMarkdown;
|
||||||
|
|
||||||
|
// 3️⃣ 同时更新 history.messages 中对应的消息
|
||||||
|
if (chatData.chat.history && chatData.chat.history.messages) {
|
||||||
|
if (chatData.chat.history.messages[messageId]) {
|
||||||
|
chatData.chat.history.messages[messageId].content = newContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4️⃣ 保留消息的其他属性,只修改 content
|
||||||
|
return { ...m, content: newContent };
|
||||||
|
}
|
||||||
|
return m; // 其他消息原样返回
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5️⃣ 通过 Event API 即时更新前端(可选)
|
||||||
|
try {
|
||||||
|
await fetch(`/api/v1/chats/${chatId}/messages/${messageId}/event`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
type: "chat:message",
|
||||||
|
data: { content: newContent }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Event API 是可选的,继续执行持久化
|
||||||
|
console.log("Event API not available, continuing...");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6️⃣ 持久化到数据库(必须)
|
||||||
|
const updatePayload = {
|
||||||
|
chat: {
|
||||||
|
...chatData.chat, // 保留所有原有属性
|
||||||
|
messages: updatedMessages
|
||||||
|
// history 已在上面原地修改
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await fetch(`/api/v1/chats/${chatId}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(updatePayload)
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 最佳实践 (Best Practices)
|
||||||
|
|
||||||
|
1. **保留原有结构**:使用展开运算符 `...chatData.chat` 和 `...m` 确保不丢失任何原有属性
|
||||||
|
2. **双位置更新**:必须同时更新 `messages[]` 和 `history.messages[id]`
|
||||||
|
3. **错误处理**:Event API 调用应包裹在 try-catch 中,失败时继续持久化
|
||||||
|
4. **重试机制**:对持久化 API 实现重试逻辑,提高可靠性
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 带重试的请求函数
|
||||||
|
const fetchWithRetry = async (url, options, retries = 3) => {
|
||||||
|
for (let i = 0; i < retries; i++) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, options);
|
||||||
|
if (response.ok) return response;
|
||||||
|
if (i < retries - 1) {
|
||||||
|
await new Promise(r => setTimeout(r, 1000 * (i + 1))); // 指数退避
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (i === retries - 1) throw e;
|
||||||
|
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **禁止使用的 API**:不要使用 `/api/v1/chats/{chatId}/share` 作为持久化备用方案,该 API 用于分享功能,不是更新功能
|
||||||
|
|
||||||
|
#### 提取 Chat ID 和 Message ID (Extracting IDs)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _extract_chat_id(self, body: dict, metadata: Optional[dict]) -> str:
|
||||||
|
"""从 body 或 metadata 中提取 chat_id"""
|
||||||
|
if isinstance(body, dict):
|
||||||
|
chat_id = body.get("chat_id")
|
||||||
|
if isinstance(chat_id, str) and chat_id.strip():
|
||||||
|
return chat_id.strip()
|
||||||
|
|
||||||
|
body_metadata = body.get("metadata", {})
|
||||||
|
if isinstance(body_metadata, dict):
|
||||||
|
chat_id = body_metadata.get("chat_id")
|
||||||
|
if isinstance(chat_id, str) and chat_id.strip():
|
||||||
|
return chat_id.strip()
|
||||||
|
|
||||||
|
if isinstance(metadata, dict):
|
||||||
|
chat_id = metadata.get("chat_id")
|
||||||
|
if isinstance(chat_id, str) and chat_id.strip():
|
||||||
|
return chat_id.strip()
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _extract_message_id(self, body: dict, metadata: Optional[dict]) -> str:
|
||||||
|
"""从 body 或 metadata 中提取 message_id"""
|
||||||
|
if isinstance(body, dict):
|
||||||
|
message_id = body.get("id")
|
||||||
|
if isinstance(message_id, str) and message_id.strip():
|
||||||
|
return message_id.strip()
|
||||||
|
|
||||||
|
body_metadata = body.get("metadata", {})
|
||||||
|
if isinstance(body_metadata, dict):
|
||||||
|
message_id = body_metadata.get("message_id")
|
||||||
|
if isinstance(message_id, str) and message_id.strip():
|
||||||
|
return message_id.strip()
|
||||||
|
|
||||||
|
if isinstance(metadata, dict):
|
||||||
|
message_id = metadata.get("message_id")
|
||||||
|
if isinstance(message_id, str) and message_id.strip():
|
||||||
|
return message_id.strip()
|
||||||
|
|
||||||
|
return ""
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 参考实现
|
||||||
|
|
||||||
|
- `plugins/actions/smart-mind-map/smart_mind_map.py` - 思维导图图片模式实现
|
||||||
|
- 官方文档: [Backend-Controlled, UI-Compatible API Flow](https://docs.openwebui.com/tutorials/tips/backend-controlled-ui-compatible-api-flow)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
13
.github/workflows/release.yml
vendored
13
.github/workflows/release.yml
vendored
@@ -180,14 +180,23 @@ jobs:
|
|||||||
|
|
||||||
- name: Determine version
|
- name: Determine version
|
||||||
id: version
|
id: version
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ github.event.inputs.version }}" ]; then
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ github.event.inputs.version }}" ]; then
|
||||||
VERSION="${{ github.event.inputs.version }}"
|
VERSION="${{ github.event.inputs.version }}"
|
||||||
elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
else
|
else
|
||||||
# Auto-generate version based on date and run number
|
# Auto-generate version based on date and daily release count
|
||||||
VERSION="v$(date +'%Y.%m.%d')-${{ github.run_number }}"
|
TODAY=$(date +'%Y.%m.%d')
|
||||||
|
TODAY_PREFIX="v${TODAY}-"
|
||||||
|
|
||||||
|
# Count existing releases with today's date prefix
|
||||||
|
EXISTING_COUNT=$(gh release list --limit 100 | grep -c "^${TODAY_PREFIX}" || echo "0")
|
||||||
|
NEXT_NUM=$((EXISTING_COUNT + 1))
|
||||||
|
|
||||||
|
VERSION="${TODAY_PREFIX}${NEXT_NUM}"
|
||||||
fi
|
fi
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "Release version: $VERSION"
|
echo "Release version: $VERSION"
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -7,31 +7,25 @@ A collection of enhancements, plugins, and prompts for [OpenWebUI](https://githu
|
|||||||
<!-- STATS_START -->
|
<!-- STATS_START -->
|
||||||
## 📊 Community Stats
|
## 📊 Community Stats
|
||||||
|
|
||||||
> 🕐 Auto-updated: 2026-01-06 19:26 (Beijing Time)
|
> 🕐 Auto-updated: 2026-01-06 21:19
|
||||||
|
|
||||||
| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |
|
| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |
|
||||||
|:---:|:---:|:---:|:---:|
|
|:---:|:---:|:---:|:---:|
|
||||||
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **41** | **63** | **17** |
|
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **43** | **62** | **17** |
|
||||||
|
|
||||||
| 📝 Posts | ⬇️ Downloads | 👁️ Views | 👍 Upvotes | 💾 Saves |
|
| 📝 Posts | ⬇️ Downloads | 👁️ Views | 👍 Upvotes | 💾 Saves |
|
||||||
|:---:|:---:|:---:|:---:|:---:|
|
|:---:|:---:|:---:|:---:|:---:|
|
||||||
| **11** | **785** | **8394** | **54** | **46** |
|
| **11** | **794** | **8481** | **54** | **48** |
|
||||||
| **11** | **785** | **8411** | **54** | **47** |
|
|
||||||
|
|
||||||
### 🔥 Top 5 Popular Plugins
|
### 🔥 Top 5 Popular Plugins
|
||||||
|
|
||||||
| Rank | Plugin | Downloads | Views |
|
| Rank | Plugin | Downloads | Views |
|
||||||
|:---:|------|:---:|:---:|
|
|:---:|------|:---:|:---:|
|
||||||
| 🥇 | [Turn Any Text into Beautiful Mind Maps](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 235 | 2095 |
|
| 🥇 | [Turn Any Text into Beautiful Mind Maps](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 240 | 2133 |
|
||||||
| 🥈 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 170 | 455 |
|
| 🥈 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 171 | 459 |
|
||||||
| 🥉 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 112 | 1234 |
|
| 🥉 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 112 | 1236 |
|
||||||
| 4️⃣ | [Flash Card ](https://openwebui.com/posts/flash_card_65a2ea8f) | 75 | 1413 |
|
| 4️⃣ | [Flash Card ](https://openwebui.com/posts/flash_card_65a2ea8f) | 76 | 1421 |
|
||||||
| 5️⃣ | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 65 | 900 |
|
| 5️⃣ | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 65 | 909 |
|
||||||
| 🥇 | [Turn Any Text into Beautiful Mind Maps](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 235 | 2103 |
|
|
||||||
| 🥈 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 170 | 456 |
|
|
||||||
| 🥉 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 112 | 1235 |
|
|
||||||
| 4️⃣ | [Flash Card ](https://openwebui.com/posts/flash_card_65a2ea8f) | 75 | 1414 |
|
|
||||||
| 5️⃣ | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 65 | 904 |
|
|
||||||
|
|
||||||
*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 -->
|
||||||
|
|||||||
16
README_CN.md
16
README_CN.md
@@ -7,25 +7,25 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
|
|||||||
<!-- STATS_START -->
|
<!-- STATS_START -->
|
||||||
## 📊 社区统计
|
## 📊 社区统计
|
||||||
|
|
||||||
> 🕐 自动更新于 2026-01-06 19:26 (北京时间)
|
> 🕐 自动更新于 2026-01-06 21:19
|
||||||
|
|
||||||
| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |
|
| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |
|
||||||
|:---:|:---:|:---:|:---:|
|
|:---:|:---:|:---:|:---:|
|
||||||
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **41** | **63** | **17** |
|
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **43** | **62** | **17** |
|
||||||
|
|
||||||
| 📝 发布 | ⬇️ 下载 | 👁️ 浏览 | 👍 点赞 | 💾 收藏 |
|
| 📝 发布 | ⬇️ 下载 | 👁️ 浏览 | 👍 点赞 | 💾 收藏 |
|
||||||
|:---:|:---:|:---:|:---:|:---:|
|
|:---:|:---:|:---:|:---:|:---:|
|
||||||
| **11** | **785** | **8394** | **54** | **46** |
|
| **11** | **794** | **8481** | **54** | **48** |
|
||||||
|
|
||||||
### 🔥 热门插件 Top 5
|
### 🔥 热门插件 Top 5
|
||||||
|
|
||||||
| 排名 | 插件 | 下载 | 浏览 |
|
| 排名 | 插件 | 下载 | 浏览 |
|
||||||
|:---:|------|:---:|:---:|
|
|:---:|------|:---:|:---:|
|
||||||
| 🥇 | [Turn Any Text into Beautiful Mind Maps](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 235 | 2095 |
|
| 🥇 | [Turn Any Text into Beautiful Mind Maps](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 240 | 2133 |
|
||||||
| 🥈 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 170 | 455 |
|
| 🥈 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 171 | 459 |
|
||||||
| 🥉 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 112 | 1234 |
|
| 🥉 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 112 | 1236 |
|
||||||
| 4️⃣ | [Flash Card ](https://openwebui.com/posts/flash_card_65a2ea8f) | 75 | 1413 |
|
| 4️⃣ | [Flash Card ](https://openwebui.com/posts/flash_card_65a2ea8f) | 76 | 1421 |
|
||||||
| 5️⃣ | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 65 | 900 |
|
| 5️⃣ | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 65 | 909 |
|
||||||
|
|
||||||
*完整统计请查看 [社区统计报告](./docs/community-stats.md)*
|
*完整统计请查看 [社区统计报告](./docs/community-stats.md)*
|
||||||
<!-- STATS_END -->
|
<!-- STATS_END -->
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
# 📊 OpenWebUI Community Stats Report
|
# 📊 OpenWebUI Community Stats Report
|
||||||
|
|
||||||
> 📅 Updated: 2026-01-06 11:08:23
|
> 📅 Updated: 2026-01-06 21:19
|
||||||
|
|
||||||
## 📈 Overview
|
## 📈 Overview
|
||||||
|
|
||||||
| Metric | Value |
|
| Metric | Value |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 📝 Total Posts | 11 |
|
| 📝 Total Posts | 11 |
|
||||||
| ⬇️ Total Downloads | 785 |
|
| ⬇️ Total Downloads | 794 |
|
||||||
| 👁️ Total Views | 8394 |
|
| 👁️ Total Views | 8481 |
|
||||||
| 👍 Total Upvotes | 54 |
|
| 👍 Total Upvotes | 54 |
|
||||||
| 💾 Total Saves | 46 |
|
| 💾 Total Saves | 48 |
|
||||||
| 💬 Total Comments | 13 |
|
| 💬 Total Comments | 13 |
|
||||||
|
|
||||||
## 📂 By Type
|
## 📂 By Type
|
||||||
@@ -22,14 +22,14 @@
|
|||||||
|
|
||||||
| Rank | Title | Type | Version | Downloads | Views | Upvotes | Saves | Updated |
|
| Rank | Title | Type | Version | Downloads | Views | Upvotes | Saves | Updated |
|
||||||
|:---:|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
|:---:|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||||
| 1 | [Turn Any Text into Beautiful Mind Maps](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action | 0.8.2 | 235 | 2095 | 10 | 15 | 2026-01-03 |
|
| 1 | [Turn Any Text into Beautiful Mind Maps](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action | 0.8.2 | 240 | 2133 | 10 | 15 | 2026-01-03 |
|
||||||
| 2 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.6 | 170 | 455 | 3 | 3 | 2026-01-03 |
|
| 2 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.6 | 171 | 459 | 3 | 3 | 2026-01-03 |
|
||||||
| 3 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | filter | 1.1.0 | 112 | 1234 | 5 | 9 | 2025-12-31 |
|
| 3 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | filter | 1.1.0 | 112 | 1236 | 5 | 9 | 2025-12-31 |
|
||||||
| 4 | [Flash Card ](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 75 | 1413 | 8 | 5 | 2026-01-03 |
|
| 4 | [Flash Card ](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 76 | 1421 | 8 | 5 | 2026-01-03 |
|
||||||
| 5 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.3.2 | 65 | 900 | 6 | 7 | 2026-01-03 |
|
| 5 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.3.2 | 65 | 909 | 6 | 8 | 2026-01-03 |
|
||||||
| 6 | [Export to Word (Enhanced Formatting)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.0 | 51 | 499 | 5 | 4 | 2026-01-05 |
|
| 6 | [Export to Word (Enhanced Formatting)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.0 | 51 | 504 | 5 | 4 | 2026-01-05 |
|
||||||
| 7 | [智能信息图](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.3.1 | 33 | 397 | 3 | 0 | 2025-12-29 |
|
| 7 | [智能信息图](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.3.1 | 33 | 398 | 3 | 0 | 2025-12-29 |
|
||||||
| 8 | [智能生成交互式思维导图,帮助用户可视化知识](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.8.0 | 14 | 246 | 2 | 0 | 2025-12-31 |
|
| 8 | [导出为 Word-支持公式、流程图、表格和代码块](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.1 | 15 | 752 | 7 | 1 | 2026-01-05 |
|
||||||
| 9 | [导出为 Word-支持公式、流程图、表格和代码块](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.1 | 13 | 737 | 7 | 1 | 2026-01-05 |
|
| 9 | [智能生成交互式思维导图,帮助用户可视化知识](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.8.0 | 14 | 249 | 2 | 1 | 2025-12-31 |
|
||||||
| 10 | [闪记卡生成插件](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.2 | 12 | 309 | 3 | 1 | 2025-12-31 |
|
| 10 | [闪记卡生成插件](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.2 | 12 | 309 | 3 | 1 | 2025-12-31 |
|
||||||
| 11 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | filter | 1.1.0 | 5 | 109 | 2 | 1 | 2025-12-31 |
|
| 11 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | filter | 1.1.0 | 5 | 111 | 2 | 1 | 2025-12-31 |
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"total_posts": 11,
|
"total_posts": 11,
|
||||||
"total_downloads": 785,
|
"total_downloads": 794,
|
||||||
"total_views": 8394,
|
"total_views": 8481,
|
||||||
"total_upvotes": 54,
|
"total_upvotes": 54,
|
||||||
"total_downvotes": 0,
|
"total_downvotes": 1,
|
||||||
"total_saves": 46,
|
"total_saves": 48,
|
||||||
"total_comments": 13,
|
"total_comments": 13,
|
||||||
"by_type": {
|
"by_type": {
|
||||||
"action": 9,
|
"action": 9,
|
||||||
@@ -18,8 +18,8 @@
|
|||||||
"version": "0.8.2",
|
"version": "0.8.2",
|
||||||
"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": 235,
|
"downloads": 240,
|
||||||
"views": 2095,
|
"views": 2133,
|
||||||
"upvotes": 10,
|
"upvotes": 10,
|
||||||
"saves": 15,
|
"saves": 15,
|
||||||
"comments": 8,
|
"comments": 8,
|
||||||
@@ -34,8 +34,8 @@
|
|||||||
"version": "0.3.6",
|
"version": "0.3.6",
|
||||||
"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": 170,
|
"downloads": 171,
|
||||||
"views": 455,
|
"views": 459,
|
||||||
"upvotes": 3,
|
"upvotes": 3,
|
||||||
"saves": 3,
|
"saves": 3,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "This filter automatically compresses long conversation contexts by intelligently summarizing and removing intermediate messages while preserving critical information, thereby significantly reducing token consumption.",
|
"description": "This filter automatically compresses long conversation contexts by intelligently summarizing and removing intermediate messages while preserving critical information, thereby significantly reducing token consumption.",
|
||||||
"downloads": 112,
|
"downloads": 112,
|
||||||
"views": 1234,
|
"views": 1236,
|
||||||
"upvotes": 5,
|
"upvotes": 5,
|
||||||
"saves": 9,
|
"saves": 9,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
@@ -66,8 +66,8 @@
|
|||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "Quickly generates beautiful flashcards from text, extracting key points and categories.",
|
"description": "Quickly generates beautiful flashcards from text, extracting key points and categories.",
|
||||||
"downloads": 75,
|
"downloads": 76,
|
||||||
"views": 1413,
|
"views": 1421,
|
||||||
"upvotes": 8,
|
"upvotes": 8,
|
||||||
"saves": 5,
|
"saves": 5,
|
||||||
"comments": 2,
|
"comments": 2,
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
"author": "jeff",
|
"author": "jeff",
|
||||||
"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": 65,
|
"downloads": 65,
|
||||||
"views": 900,
|
"views": 909,
|
||||||
"upvotes": 6,
|
"upvotes": 6,
|
||||||
"saves": 8,
|
"saves": 8,
|
||||||
"comments": 2,
|
"comments": 2,
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "Export the current conversation to a formatted Word doc with syntax highlighting, AI-generated titles, and perfect Markdown rendering (tables, quotes, lists).",
|
"description": "Export the current conversation to a formatted Word doc with syntax highlighting, AI-generated titles, and perfect Markdown rendering (tables, quotes, lists).",
|
||||||
"downloads": 51,
|
"downloads": 51,
|
||||||
"views": 499,
|
"views": 504,
|
||||||
"upvotes": 5,
|
"upvotes": 5,
|
||||||
"saves": 4,
|
"saves": 4,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
"author": "jeff",
|
"author": "jeff",
|
||||||
"description": "基于 AntV Infographic 的智能信息图生成插件。支持多种专业模板,自动图标匹配,并提供 SVG/PNG 下载功能。",
|
"description": "基于 AntV Infographic 的智能信息图生成插件。支持多种专业模板,自动图标匹配,并提供 SVG/PNG 下载功能。",
|
||||||
"downloads": 33,
|
"downloads": 33,
|
||||||
"views": 397,
|
"views": 398,
|
||||||
"upvotes": 3,
|
"upvotes": 3,
|
||||||
"saves": 0,
|
"saves": 0,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
@@ -123,6 +123,22 @@
|
|||||||
"updated_at": "2025-12-29",
|
"updated_at": "2025-12-29",
|
||||||
"url": "https://openwebui.com/posts/智能信息图_e04a48ff"
|
"url": "https://openwebui.com/posts/智能信息图_e04a48ff"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "导出为 Word-支持公式、流程图、表格和代码块",
|
||||||
|
"slug": "导出为_word_支持公式流程图表格和代码块_8a6306c0",
|
||||||
|
"type": "action",
|
||||||
|
"version": "0.4.1",
|
||||||
|
"author": "Fu-Jie",
|
||||||
|
"description": "将当前对话内容从 Markdown 转换并导出为 Word (.docx) 文件,支持中英文无乱码。",
|
||||||
|
"downloads": 15,
|
||||||
|
"views": 752,
|
||||||
|
"upvotes": 7,
|
||||||
|
"saves": 1,
|
||||||
|
"comments": 1,
|
||||||
|
"created_at": "2026-01-04",
|
||||||
|
"updated_at": "2026-01-05",
|
||||||
|
"url": "https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "智能生成交互式思维导图,帮助用户可视化知识",
|
"title": "智能生成交互式思维导图,帮助用户可视化知识",
|
||||||
"slug": "智能生成交互式思维导图帮助用户可视化知识_8d4b097b",
|
"slug": "智能生成交互式思维导图帮助用户可视化知识_8d4b097b",
|
||||||
@@ -131,30 +147,14 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"description": "智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。",
|
"description": "智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。",
|
||||||
"downloads": 14,
|
"downloads": 14,
|
||||||
"views": 246,
|
"views": 249,
|
||||||
"upvotes": 2,
|
"upvotes": 2,
|
||||||
"saves": 0,
|
"saves": 1,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
"created_at": "2025-12-31",
|
"created_at": "2025-12-31",
|
||||||
"updated_at": "2025-12-31",
|
"updated_at": "2025-12-31",
|
||||||
"url": "https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b"
|
"url": "https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"title": "导出为 Word-支持公式、流程图、表格和代码块",
|
|
||||||
"slug": "导出为_word_支持公式流程图表格和代码块_8a6306c0",
|
|
||||||
"type": "action",
|
|
||||||
"version": "0.4.1",
|
|
||||||
"author": "Fu-Jie",
|
|
||||||
"description": "将当前对话内容从 Markdown 转换并导出为 Word (.docx) 文件,支持中英文无乱码。",
|
|
||||||
"downloads": 13,
|
|
||||||
"views": 737,
|
|
||||||
"upvotes": 7,
|
|
||||||
"saves": 1,
|
|
||||||
"comments": 1,
|
|
||||||
"created_at": "2026-01-04",
|
|
||||||
"updated_at": "2026-01-05",
|
|
||||||
"url": "https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"title": "闪记卡生成插件",
|
"title": "闪记卡生成插件",
|
||||||
"slug": "闪记卡生成插件_4a31eac3",
|
"slug": "闪记卡生成插件_4a31eac3",
|
||||||
@@ -179,7 +179,7 @@
|
|||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "在 LLM 响应完成后进行上下文摘要和压缩",
|
"description": "在 LLM 响应完成后进行上下文摘要和压缩",
|
||||||
"downloads": 5,
|
"downloads": 5,
|
||||||
"views": 109,
|
"views": 111,
|
||||||
"upvotes": 2,
|
"upvotes": 2,
|
||||||
"saves": 1,
|
"saves": 1,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
@@ -193,10 +193,10 @@
|
|||||||
"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": 41,
|
"followers": 43,
|
||||||
"following": 2,
|
"following": 2,
|
||||||
"total_points": 63,
|
"total_points": 62,
|
||||||
"post_points": 54,
|
"post_points": 53,
|
||||||
"comment_points": 9,
|
"comment_points": 9,
|
||||||
"contributions": 17
|
"contributions": 17
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
# 📊 OpenWebUI 社区统计报告
|
# 📊 OpenWebUI 社区统计报告
|
||||||
|
|
||||||
> 📅 更新时间: 2026-01-06 11:08:23
|
> 📅 更新时间: 2026-01-06 21:19
|
||||||
|
|
||||||
## 📈 总览
|
## 📈 总览
|
||||||
|
|
||||||
| 指标 | 数值 |
|
| 指标 | 数值 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 📝 发布数量 | 11 |
|
| 📝 发布数量 | 11 |
|
||||||
| ⬇️ 总下载量 | 785 |
|
| ⬇️ 总下载量 | 794 |
|
||||||
| 👁️ 总浏览量 | 8394 |
|
| 👁️ 总浏览量 | 8481 |
|
||||||
| 👍 总点赞数 | 54 |
|
| 👍 总点赞数 | 54 |
|
||||||
| 💾 总收藏数 | 46 |
|
| 💾 总收藏数 | 48 |
|
||||||
| 💬 总评论数 | 13 |
|
| 💬 总评论数 | 13 |
|
||||||
|
|
||||||
## 📂 按类型分类
|
## 📂 按类型分类
|
||||||
@@ -22,14 +22,14 @@
|
|||||||
|
|
||||||
| 排名 | 标题 | 类型 | 版本 | 下载 | 浏览 | 点赞 | 收藏 | 更新日期 |
|
| 排名 | 标题 | 类型 | 版本 | 下载 | 浏览 | 点赞 | 收藏 | 更新日期 |
|
||||||
|:---:|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
|:---:|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||||
| 1 | [Turn Any Text into Beautiful Mind Maps](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action | 0.8.2 | 235 | 2095 | 10 | 15 | 2026-01-03 |
|
| 1 | [Turn Any Text into Beautiful Mind Maps](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action | 0.8.2 | 240 | 2133 | 10 | 15 | 2026-01-03 |
|
||||||
| 2 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.6 | 170 | 455 | 3 | 3 | 2026-01-03 |
|
| 2 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.6 | 171 | 459 | 3 | 3 | 2026-01-03 |
|
||||||
| 3 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | filter | 1.1.0 | 112 | 1234 | 5 | 9 | 2025-12-31 |
|
| 3 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | filter | 1.1.0 | 112 | 1236 | 5 | 9 | 2025-12-31 |
|
||||||
| 4 | [Flash Card ](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 75 | 1413 | 8 | 5 | 2026-01-03 |
|
| 4 | [Flash Card ](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 76 | 1421 | 8 | 5 | 2026-01-03 |
|
||||||
| 5 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.3.2 | 65 | 900 | 6 | 7 | 2026-01-03 |
|
| 5 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.3.2 | 65 | 909 | 6 | 8 | 2026-01-03 |
|
||||||
| 6 | [Export to Word (Enhanced Formatting)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.0 | 51 | 499 | 5 | 4 | 2026-01-05 |
|
| 6 | [Export to Word (Enhanced Formatting)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.0 | 51 | 504 | 5 | 4 | 2026-01-05 |
|
||||||
| 7 | [智能信息图](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.3.1 | 33 | 397 | 3 | 0 | 2025-12-29 |
|
| 7 | [智能信息图](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.3.1 | 33 | 398 | 3 | 0 | 2025-12-29 |
|
||||||
| 8 | [智能生成交互式思维导图,帮助用户可视化知识](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.8.0 | 14 | 246 | 2 | 0 | 2025-12-31 |
|
| 8 | [导出为 Word-支持公式、流程图、表格和代码块](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.1 | 15 | 752 | 7 | 1 | 2026-01-05 |
|
||||||
| 9 | [导出为 Word-支持公式、流程图、表格和代码块](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.1 | 13 | 737 | 7 | 1 | 2026-01-05 |
|
| 9 | [智能生成交互式思维导图,帮助用户可视化知识](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.8.0 | 14 | 249 | 2 | 1 | 2025-12-31 |
|
||||||
| 10 | [闪记卡生成插件](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.2 | 12 | 309 | 3 | 1 | 2025-12-31 |
|
| 10 | [闪记卡生成插件](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.2 | 12 | 309 | 3 | 1 | 2025-12-31 |
|
||||||
| 11 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | filter | 1.1.0 | 5 | 109 | 2 | 1 | 2025-12-31 |
|
| 11 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | filter | 1.1.0 | 5 | 111 | 2 | 1 | 2025-12-31 |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Smart Mind Map - Mind Mapping Generation Plugin
|
# Smart Mind Map - Mind Mapping Generation Plugin
|
||||||
|
|
||||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.8.2 | **License:** MIT
|
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.9.1 | **License:** MIT
|
||||||
|
|
||||||
> **Important**: To ensure the maintainability and usability of all plugins, each plugin should be accompanied by clear and comprehensive documentation to ensure its functionality, configuration, and usage are well explained.
|
> **Important**: To ensure the maintainability and usability of all plugins, each plugin should be accompanied by clear and comprehensive documentation to ensure its functionality, configuration, and usage are well explained.
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@ Smart Mind Map is a powerful OpenWebUI action plugin that intelligently analyzes
|
|||||||
- ✅ **Real-time Rendering**: Renders mind maps directly in the chat interface without navigation
|
- ✅ **Real-time Rendering**: Renders mind maps directly in the chat interface without navigation
|
||||||
- ✅ **Export Capabilities**: Supports PNG, SVG code, and Markdown source export
|
- ✅ **Export Capabilities**: Supports PNG, SVG code, and Markdown source export
|
||||||
- ✅ **Customizable Configuration**: Configurable LLM model, minimum text length, and other parameters
|
- ✅ **Customizable Configuration**: Configurable LLM model, minimum text length, and other parameters
|
||||||
|
- ✅ **Image Output Mode**: Generate static SVG images embedded directly in Markdown (no interactive HTML)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -80,6 +81,7 @@ You can adjust the following parameters in the plugin's settings (Valves):
|
|||||||
| `MIN_TEXT_LENGTH` | `100` | Minimum text length (in characters) required for mind map analysis. Text that's too short cannot generate valid mind maps. |
|
| `MIN_TEXT_LENGTH` | `100` | Minimum text length (in characters) required for mind map analysis. Text that's too short cannot generate valid mind maps. |
|
||||||
| `CLEAR_PREVIOUS_HTML` | `false` | Whether to clear previous plugin-generated HTML content when generating a new mind map. |
|
| `CLEAR_PREVIOUS_HTML` | `false` | Whether to clear previous plugin-generated HTML content when generating a new mind map. |
|
||||||
| `MESSAGE_COUNT` | `1` | Number of recent messages to use for mind map generation (1-5). |
|
| `MESSAGE_COUNT` | `1` | Number of recent messages to use for mind map generation (1-5). |
|
||||||
|
| `OUTPUT_MODE` | `html` | Output mode: `html` for interactive HTML (default), or `image` to embed as static Markdown image. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -277,6 +279,32 @@ This plugin uses only OpenWebUI's built-in dependencies. **No additional package
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### v0.9.1
|
||||||
|
|
||||||
|
**New Feature: Image Output Mode**
|
||||||
|
|
||||||
|
- Added `OUTPUT_MODE` configuration parameter with two options:
|
||||||
|
- `html` (default): Interactive HTML mind map with full control panel
|
||||||
|
- `image`: Static SVG image embedded directly in Markdown (uploaded to `/api/v1/files`)
|
||||||
|
- Image mode features:
|
||||||
|
- Auto-responsive width (adapts to chat container)
|
||||||
|
- Automatic theme detection (light/dark)
|
||||||
|
- Persistent storage via Chat API (survives page refresh)
|
||||||
|
- Efficient file storage (no huge base64 strings in chat history)
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
|
||||||
|
- Implemented robust Chat API update mechanism with retry logic
|
||||||
|
- Fixed message persistence using both `messages[]` and `history.messages`
|
||||||
|
- Added Event API for immediate frontend updates
|
||||||
|
- Removed unnecessary `SVG_WIDTH` and `SVG_HEIGHT` parameters (now auto-calculated)
|
||||||
|
|
||||||
|
**Technical Details:**
|
||||||
|
|
||||||
|
- Image mode uses `__event_call__` to execute JavaScript in the browser
|
||||||
|
- SVG is rendered offline, converted to Blob, and uploaded to OpenWebUI Files API
|
||||||
|
- Updates chat message with `/api/v1/files/{id}/content` URL via OpenWebUI Backend-Controlled API flow
|
||||||
|
|
||||||
### v0.8.2
|
### v0.8.2
|
||||||
|
|
||||||
- Removed debug messages from output
|
- Removed debug messages from output
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 思维导图 - 思维导图生成插件
|
# 思维导图 - 思维导图生成插件
|
||||||
|
|
||||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie) | **版本:** 0.8.2 | **许可证:** MIT
|
**作者:** [Fu-Jie](https://github.com/Fu-Jie) | **版本:** 0.9.1 | **许可证:** MIT
|
||||||
|
|
||||||
> **重要提示**:为了确保所有插件的可维护性和易用性,每个插件都应附带清晰、完整的文档,以确保其功能、配置和使用方法得到充分说明。
|
> **重要提示**:为了确保所有插件的可维护性和易用性,每个插件都应附带清晰、完整的文档,以确保其功能、配置和使用方法得到充分说明。
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
- ✅ **实时渲染**:在聊天界面中直接渲染思维导图,无需跳转
|
- ✅ **实时渲染**:在聊天界面中直接渲染思维导图,无需跳转
|
||||||
- ✅ **导出功能**:支持 PNG、SVG 代码和 Markdown 源码导出
|
- ✅ **导出功能**:支持 PNG、SVG 代码和 Markdown 源码导出
|
||||||
- ✅ **自定义配置**:可配置 LLM 模型、最小文本长度等参数
|
- ✅ **自定义配置**:可配置 LLM 模型、最小文本长度等参数
|
||||||
|
- ✅ **图片输出模式**:生成静态 SVG 图片直接嵌入 Markdown(无交互式 HTML)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -80,6 +81,7 @@
|
|||||||
| `MIN_TEXT_LENGTH` | `100` | 进行思维导图分析所需的最小文本长度(字符数)。文本过短将无法生成有效的导图。 |
|
| `MIN_TEXT_LENGTH` | `100` | 进行思维导图分析所需的最小文本长度(字符数)。文本过短将无法生成有效的导图。 |
|
||||||
| `CLEAR_PREVIOUS_HTML` | `false` | 在生成新的思维导图时,是否清除之前由插件生成的 HTML 内容。 |
|
| `CLEAR_PREVIOUS_HTML` | `false` | 在生成新的思维导图时,是否清除之前由插件生成的 HTML 内容。 |
|
||||||
| `MESSAGE_COUNT` | `1` | 用于生成思维导图的最近消息数量(1-5)。 |
|
| `MESSAGE_COUNT` | `1` | 用于生成思维导图的最近消息数量(1-5)。 |
|
||||||
|
| `OUTPUT_MODE` | `html` | 输出模式:`html` 为交互式 HTML(默认),`image` 为嵌入静态 Markdown 图片。 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -277,6 +279,32 @@
|
|||||||
|
|
||||||
## 更新日志
|
## 更新日志
|
||||||
|
|
||||||
|
### v0.9.1
|
||||||
|
|
||||||
|
**新功能:图片输出模式**
|
||||||
|
|
||||||
|
- 新增 `OUTPUT_MODE` 配置参数,支持两种模式:
|
||||||
|
- `html`(默认):交互式 HTML 思维导图,带完整控制面板
|
||||||
|
- `image`:静态 SVG 图片直接嵌入 Markdown(上传至 `/api/v1/files`)
|
||||||
|
- 图片模式特性:
|
||||||
|
- 自动响应式宽度(适应聊天容器)
|
||||||
|
- 自动主题检测(亮色/暗色)
|
||||||
|
- 通过 Chat API 持久化存储(刷新页面后保留)
|
||||||
|
- 高效文件存储(聊天记录中无超长 Base64 字符串)
|
||||||
|
|
||||||
|
**改进项:**
|
||||||
|
|
||||||
|
- 实现健壮的 Chat API 更新机制,带重试逻辑
|
||||||
|
- 修复消息持久化,同时更新 `messages[]` 和 `history.messages`
|
||||||
|
- 添加 Event API 实现即时前端更新
|
||||||
|
- 移除不必要的 `SVG_WIDTH` 和 `SVG_HEIGHT` 参数(现已自动计算)
|
||||||
|
|
||||||
|
**技术细节:**
|
||||||
|
|
||||||
|
- 图片模式使用 `__event_call__` 在浏览器中执行 JavaScript
|
||||||
|
- SVG 离屏渲染,转换为 Blob,并上传至 OpenWebUI Files API
|
||||||
|
- 通过 OpenWebUI Backend-Controlled API 流程更新聊天消息为 `/api/v1/files/{id}/content` URL
|
||||||
|
|
||||||
### v0.8.2
|
### v0.8.2
|
||||||
|
|
||||||
- 移除输出中的调试信息
|
- 移除输出中的调试信息
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Smart Mind Map
|
|||||||
author: Fu-Jie
|
author: Fu-Jie
|
||||||
author_url: https://github.com/Fu-Jie
|
author_url: https://github.com/Fu-Jie
|
||||||
funding_url: https://github.com/Fu-Jie/awesome-openwebui
|
funding_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||||
version: 0.9.0
|
version: 0.9.1
|
||||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4=
|
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4=
|
||||||
description: Intelligently analyzes text content and generates interactive mind maps to help users structure and visualize knowledge.
|
description: Intelligently analyzes text content and generates interactive mind maps to help users structure and visualize knowledge.
|
||||||
"""
|
"""
|
||||||
@@ -790,14 +790,6 @@ class Action:
|
|||||||
default="html",
|
default="html",
|
||||||
description="Output mode: 'html' for interactive HTML (default), or 'image' to embed as Markdown image.",
|
description="Output mode: 'html' for interactive HTML (default), or 'image' to embed as Markdown image.",
|
||||||
)
|
)
|
||||||
SVG_WIDTH: int = Field(
|
|
||||||
default=1200,
|
|
||||||
description="Width of the SVG canvas in pixels (for image mode).",
|
|
||||||
)
|
|
||||||
SVG_HEIGHT: int = Field(
|
|
||||||
default=800,
|
|
||||||
description="Height of the SVG canvas in pixels (for image mode).",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.valves = self.Valves()
|
self.valves = self.Valves()
|
||||||
@@ -959,27 +951,83 @@ class Action:
|
|||||||
chat_id: str,
|
chat_id: str,
|
||||||
message_id: str,
|
message_id: str,
|
||||||
markdown_syntax: str,
|
markdown_syntax: str,
|
||||||
svg_width: int,
|
|
||||||
svg_height: int,
|
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Generate JavaScript code for frontend SVG rendering and image embedding"""
|
"""Generate JavaScript code for frontend SVG rendering and image embedding"""
|
||||||
|
|
||||||
# Escape the syntax for JS embedding
|
# Escape the syntax for JS embedding
|
||||||
syntax_escaped = (
|
syntax_escaped = (
|
||||||
markdown_syntax
|
markdown_syntax.replace("\\", "\\\\")
|
||||||
.replace("\\", "\\\\")
|
|
||||||
.replace("`", "\\`")
|
.replace("`", "\\`")
|
||||||
.replace("${", "\\${")
|
.replace("${", "\\${")
|
||||||
.replace("</script>", "<\\/script>")
|
.replace("</script>", "<\\/script>")
|
||||||
)
|
)
|
||||||
|
|
||||||
return f"""
|
return f"""
|
||||||
(async function() {{
|
(async function() {{
|
||||||
const uniqueId = "{unique_id}";
|
const uniqueId = "{unique_id}";
|
||||||
const chatId = "{chat_id}";
|
const chatId = "{chat_id}";
|
||||||
const messageId = "{message_id}";
|
const messageId = "{message_id}";
|
||||||
const defaultWidth = {svg_width};
|
const defaultWidth = 1200;
|
||||||
const defaultHeight = {svg_height};
|
const defaultHeight = 800;
|
||||||
|
|
||||||
|
// Theme detection - check parent document for OpenWebUI theme
|
||||||
|
const detectTheme = () => {{
|
||||||
|
try {{
|
||||||
|
// 1. Check parent document's html/body class or data-theme
|
||||||
|
const html = document.documentElement;
|
||||||
|
const body = document.body;
|
||||||
|
const htmlClass = html ? html.className : '';
|
||||||
|
const bodyClass = body ? body.className : '';
|
||||||
|
const htmlDataTheme = html ? html.getAttribute('data-theme') : '';
|
||||||
|
|
||||||
|
if (htmlDataTheme === 'dark' || bodyClass.includes('dark') || htmlClass.includes('dark')) {{
|
||||||
|
return 'dark';
|
||||||
|
}}
|
||||||
|
if (htmlDataTheme === 'light' || bodyClass.includes('light') || htmlClass.includes('light')) {{
|
||||||
|
return 'light';
|
||||||
|
}}
|
||||||
|
|
||||||
|
// 2. Check meta theme-color
|
||||||
|
const metas = document.querySelectorAll('meta[name="theme-color"]');
|
||||||
|
if (metas.length > 0) {{
|
||||||
|
const color = metas[metas.length - 1].content.trim();
|
||||||
|
const m = color.match(/^#?([0-9a-f]{{6}})$/i);
|
||||||
|
if (m) {{
|
||||||
|
const hex = m[1];
|
||||||
|
const r = parseInt(hex.slice(0, 2), 16);
|
||||||
|
const g = parseInt(hex.slice(2, 4), 16);
|
||||||
|
const b = parseInt(hex.slice(4, 6), 16);
|
||||||
|
const luma = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
|
||||||
|
return luma < 0.5 ? 'dark' : 'light';
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
// 3. Check system preference
|
||||||
|
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {{
|
||||||
|
return 'dark';
|
||||||
|
}}
|
||||||
|
|
||||||
|
return 'light';
|
||||||
|
}} catch (e) {{
|
||||||
|
return 'light';
|
||||||
|
}}
|
||||||
|
}};
|
||||||
|
|
||||||
|
const currentTheme = detectTheme();
|
||||||
|
console.log("[MindMap Image] Detected theme:", currentTheme);
|
||||||
|
|
||||||
|
// Theme-based colors
|
||||||
|
const colors = currentTheme === 'dark' ? {{
|
||||||
|
background: '#1f2937',
|
||||||
|
text: '#e5e7eb',
|
||||||
|
link: '#94a3b8',
|
||||||
|
nodeStroke: '#64748b'
|
||||||
|
}} : {{
|
||||||
|
background: '#ffffff',
|
||||||
|
text: '#1f2937',
|
||||||
|
link: '#546e7a',
|
||||||
|
nodeStroke: '#94a3b8'
|
||||||
|
}};
|
||||||
|
|
||||||
// Auto-detect chat container width for responsive sizing
|
// Auto-detect chat container width for responsive sizing
|
||||||
let svgWidth = defaultWidth;
|
let svgWidth = defaultWidth;
|
||||||
@@ -1054,7 +1102,7 @@ class Action:
|
|||||||
svgEl.setAttribute('height', svgHeight);
|
svgEl.setAttribute('height', svgHeight);
|
||||||
svgEl.style.width = svgWidth + 'px';
|
svgEl.style.width = svgWidth + 'px';
|
||||||
svgEl.style.height = svgHeight + 'px';
|
svgEl.style.height = svgHeight + 'px';
|
||||||
svgEl.style.backgroundColor = '#ffffff';
|
svgEl.style.backgroundColor = colors.background;
|
||||||
container.appendChild(svgEl);
|
container.appendChild(svgEl);
|
||||||
|
|
||||||
// Transform markdown to tree
|
// Transform markdown to tree
|
||||||
@@ -1082,23 +1130,23 @@ class Action:
|
|||||||
clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
||||||
clonedSvg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
|
clonedSvg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
|
||||||
|
|
||||||
// Add background rect
|
// Add background rect with theme color
|
||||||
const bgRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
const bgRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||||
bgRect.setAttribute('width', '100%');
|
bgRect.setAttribute('width', '100%');
|
||||||
bgRect.setAttribute('height', '100%');
|
bgRect.setAttribute('height', '100%');
|
||||||
bgRect.setAttribute('fill', '#ffffff');
|
bgRect.setAttribute('fill', colors.background);
|
||||||
clonedSvg.insertBefore(bgRect, clonedSvg.firstChild);
|
clonedSvg.insertBefore(bgRect, clonedSvg.firstChild);
|
||||||
|
|
||||||
// Add inline styles
|
// Add inline styles with theme colors
|
||||||
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
|
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
text {{ font-family: sans-serif; font-size: 14px; fill: #000000; }}
|
text {{ font-family: sans-serif; font-size: 14px; fill: ${{colors.text}}; }}
|
||||||
foreignObject, .markmap-foreign, .markmap-foreign div {{ color: #000000; font-family: sans-serif; font-size: 14px; }}
|
foreignObject, .markmap-foreign, .markmap-foreign div {{ color: ${{colors.text}}; font-family: sans-serif; font-size: 14px; }}
|
||||||
h1 {{ font-size: 22px; font-weight: 700; margin: 0; }}
|
h1 {{ font-size: 22px; font-weight: 700; margin: 0; }}
|
||||||
h2 {{ font-size: 18px; font-weight: 600; margin: 0; }}
|
h2 {{ font-size: 18px; font-weight: 600; margin: 0; }}
|
||||||
strong {{ font-weight: 700; }}
|
strong {{ font-weight: 700; }}
|
||||||
.markmap-link {{ stroke: #546e7a; fill: none; }}
|
.markmap-link {{ stroke: ${{colors.link}}; fill: none; }}
|
||||||
.markmap-node circle, .markmap-node rect {{ stroke: #94a3b8; }}
|
.markmap-node circle, .markmap-node rect {{ stroke: ${{colors.nodeStroke}}; }}
|
||||||
`;
|
`;
|
||||||
clonedSvg.insertBefore(style, bgRect.nextSibling);
|
clonedSvg.insertBefore(style, bgRect.nextSibling);
|
||||||
|
|
||||||
@@ -1110,7 +1158,7 @@ class Action:
|
|||||||
const textEl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
const textEl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||||
textEl.setAttribute('x', fo.getAttribute('x') || '0');
|
textEl.setAttribute('x', fo.getAttribute('x') || '0');
|
||||||
textEl.setAttribute('y', (parseFloat(fo.getAttribute('y') || '0') + 14).toString());
|
textEl.setAttribute('y', (parseFloat(fo.getAttribute('y') || '0') + 14).toString());
|
||||||
textEl.setAttribute('fill', '#000000');
|
textEl.setAttribute('fill', colors.text);
|
||||||
textEl.setAttribute('font-family', 'sans-serif');
|
textEl.setAttribute('font-family', 'sans-serif');
|
||||||
textEl.setAttribute('font-size', '14');
|
textEl.setAttribute('font-size', '14');
|
||||||
textEl.textContent = text.trim();
|
textEl.textContent = text.trim();
|
||||||
@@ -1120,20 +1168,61 @@ class Action:
|
|||||||
|
|
||||||
// Serialize SVG to string
|
// Serialize SVG to string
|
||||||
const svgData = new XMLSerializer().serializeToString(clonedSvg);
|
const svgData = new XMLSerializer().serializeToString(clonedSvg);
|
||||||
const svgBase64 = btoa(unescape(encodeURIComponent(svgData)));
|
|
||||||
const dataUrl = 'data:image/svg+xml;base64,' + svgBase64;
|
|
||||||
|
|
||||||
console.log("[MindMap Image] Data URL generated, length:", dataUrl.length);
|
// Cleanup container
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
document.body.removeChild(container);
|
document.body.removeChild(container);
|
||||||
|
|
||||||
// Generate markdown image
|
// Convert SVG string to Blob
|
||||||
const markdownImage = ``;
|
const blob = new Blob([svgData], {{ type: 'image/svg+xml' }});
|
||||||
|
const file = new File([blob], `mindmap-${{uniqueId}}.svg`, {{ type: 'image/svg+xml' }});
|
||||||
|
|
||||||
|
// Upload file to OpenWebUI API
|
||||||
|
console.log("[MindMap Image] Uploading SVG file...");
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const uploadResponse = await fetch('/api/v1/files/', {{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {{
|
||||||
|
'Authorization': `Bearer ${{token}}`
|
||||||
|
}},
|
||||||
|
body: formData
|
||||||
|
}});
|
||||||
|
|
||||||
|
if (!uploadResponse.ok) {{
|
||||||
|
throw new Error(`Upload failed: ${{uploadResponse.statusText}}`);
|
||||||
|
}}
|
||||||
|
|
||||||
|
const fileData = await uploadResponse.json();
|
||||||
|
const fileId = fileData.id;
|
||||||
|
const imageUrl = `/api/v1/files/${{fileId}}/content`;
|
||||||
|
|
||||||
|
console.log("[MindMap Image] File uploaded, ID:", fileId);
|
||||||
|
|
||||||
|
// Generate markdown image with file URL
|
||||||
|
const markdownImage = ``;
|
||||||
|
|
||||||
// Update message via API
|
// Update message via API
|
||||||
if (chatId && messageId) {{
|
if (chatId && messageId) {{
|
||||||
const token = localStorage.getItem("token");
|
|
||||||
|
// Helper function with retry logic
|
||||||
|
const fetchWithRetry = async (url, options, retries = 3) => {{
|
||||||
|
for (let i = 0; i < retries; i++) {{
|
||||||
|
try {{
|
||||||
|
const response = await fetch(url, options);
|
||||||
|
if (response.ok) return response;
|
||||||
|
if (i < retries - 1) {{
|
||||||
|
console.log(`[MindMap Image] Retry ${{i + 1}}/${{retries}} for ${{url}}`);
|
||||||
|
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
|
||||||
|
}}
|
||||||
|
}} catch (e) {{
|
||||||
|
if (i === retries - 1) throw e;
|
||||||
|
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
return null;
|
||||||
|
}};
|
||||||
|
|
||||||
// Get current chat data
|
// Get current chat data
|
||||||
const getResponse = await fetch(`/api/v1/chats/${{chatId}}`, {{
|
const getResponse = await fetch(`/api/v1/chats/${{chatId}}`, {{
|
||||||
@@ -1146,24 +1235,26 @@ class Action:
|
|||||||
}}
|
}}
|
||||||
|
|
||||||
const chatData = await getResponse.json();
|
const chatData = await getResponse.json();
|
||||||
let originalContent = "";
|
|
||||||
let updatedMessages = [];
|
let updatedMessages = [];
|
||||||
|
let newContent = "";
|
||||||
|
|
||||||
if (chatData.chat && chatData.chat.messages) {{
|
if (chatData.chat && chatData.chat.messages) {{
|
||||||
updatedMessages = chatData.chat.messages.map(m => {{
|
updatedMessages = chatData.chat.messages.map(m => {{
|
||||||
if (m.id === messageId) {{
|
if (m.id === messageId) {{
|
||||||
originalContent = m.content || "";
|
const originalContent = m.content || "";
|
||||||
// Remove existing mindmap images
|
// Remove existing mindmap images (both base64 and file URL patterns)
|
||||||
const mindmapPattern = /\\n*!\\[🧠[^\\]]*\\]\\(data:image\\/[^)]+\\)/g;
|
const mindmapPattern = /\\n*!\\[🧠[^\\]]*\\]\\((?:data:image\\/[^)]+|(?:\\/api\\/v1\\/files\\/[^)]+))\\)/g;
|
||||||
let cleanedContent = originalContent.replace(mindmapPattern, "");
|
let cleanedContent = originalContent.replace(mindmapPattern, "");
|
||||||
cleanedContent = cleanedContent.replace(/\\n{{3,}}/g, "\\n\\n").trim();
|
cleanedContent = cleanedContent.replace(/\\n{{3,}}/g, "\\n\\n").trim();
|
||||||
// Append new image
|
// Append new image
|
||||||
const newContent = cleanedContent + "\\n\\n" + markdownImage;
|
newContent = cleanedContent + "\\n\\n" + markdownImage;
|
||||||
|
|
||||||
// Critical: Update content in both messages array AND history object
|
// Critical: Update content in both messages array AND history object
|
||||||
// The history object is often the source of truth for the database
|
// The history object is the source of truth for the database
|
||||||
if (chatData.chat.history && chatData.chat.history.messages && chatData.chat.history.messages[messageId]) {{
|
if (chatData.chat.history && chatData.chat.history.messages) {{
|
||||||
chatData.chat.history.messages[messageId].content = newContent;
|
if (chatData.chat.history.messages[messageId]) {{
|
||||||
|
chatData.chat.history.messages[messageId].content = newContent;
|
||||||
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
return {{ ...m, content: newContent }};
|
return {{ ...m, content: newContent }};
|
||||||
@@ -1172,28 +1263,40 @@ class Action:
|
|||||||
}});
|
}});
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// First: Update frontend display via event API (for immediate visual feedback)
|
if (!newContent) {{
|
||||||
await fetch(`/api/v1/chats/${{chatId}}/messages/${{messageId}}/event`, {{
|
console.warn("[MindMap Image] Could not find message to update");
|
||||||
method: "POST",
|
return;
|
||||||
headers: {{
|
}}
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": `Bearer ${{token}}`
|
|
||||||
}},
|
|
||||||
body: JSON.stringify({{
|
|
||||||
type: "chat:message",
|
|
||||||
data: {{ content: updatedMessages.find(m => m.id === messageId)?.content || "" }}
|
|
||||||
}})
|
|
||||||
}});
|
|
||||||
|
|
||||||
// Second: Persist to database by updating the entire chat
|
// Try to update frontend display via event API (optional, may not exist in all versions)
|
||||||
|
try {{
|
||||||
|
await fetch(`/api/v1/chats/${{chatId}}/messages/${{messageId}}/event`, {{
|
||||||
|
method: "POST",
|
||||||
|
headers: {{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${{token}}`
|
||||||
|
}},
|
||||||
|
body: JSON.stringify({{
|
||||||
|
type: "chat:message",
|
||||||
|
data: {{ content: newContent }}
|
||||||
|
}})
|
||||||
|
}});
|
||||||
|
}} catch (eventErr) {{
|
||||||
|
// Event API is optional, continue with persistence
|
||||||
|
console.log("[MindMap Image] Event API not available, continuing...");
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Persist to database by updating the entire chat object
|
||||||
|
// This follows the OpenWebUI Backend-Controlled API Flow
|
||||||
const updatePayload = {{
|
const updatePayload = {{
|
||||||
chat: {{
|
chat: {{
|
||||||
...chatData.chat,
|
...chatData.chat,
|
||||||
messages: updatedMessages
|
messages: updatedMessages
|
||||||
|
// history is already updated in-place above
|
||||||
}}
|
}}
|
||||||
}};
|
}};
|
||||||
|
|
||||||
const persistResponse = await fetch(`/api/v1/chats/${{chatId}}`, {{
|
const persistResponse = await fetchWithRetry(`/api/v1/chats/${{chatId}}`, {{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {{
|
headers: {{
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -1202,22 +1305,13 @@ class Action:
|
|||||||
body: JSON.stringify(updatePayload)
|
body: JSON.stringify(updatePayload)
|
||||||
}});
|
}});
|
||||||
|
|
||||||
if (persistResponse.ok) {{
|
if (persistResponse && persistResponse.ok) {{
|
||||||
console.log("[MindMap Image] ✅ Message persisted successfully!");
|
console.log("[MindMap Image] ✅ Message persisted successfully!");
|
||||||
}} else {{
|
}} else {{
|
||||||
console.error("[MindMap Image] Persist API error:", persistResponse.status);
|
console.error("[MindMap Image] ❌ Failed to persist message after retries");
|
||||||
// Try alternative update method
|
|
||||||
const altResponse = await fetch(`/api/v1/chats/${{chatId}}/share`, {{
|
|
||||||
method: "POST",
|
|
||||||
headers: {{
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": `Bearer ${{token}}`
|
|
||||||
}}
|
|
||||||
}});
|
|
||||||
console.log("[MindMap Image] Alt persist attempted:", altResponse.status);
|
|
||||||
}}
|
}}
|
||||||
}} else {{
|
}} else {{
|
||||||
console.warn("[MindMap Image] ⚠️ Missing chatId or messageId");
|
console.warn("[MindMap Image] ⚠️ Missing chatId or messageId, cannot persist");
|
||||||
}}
|
}}
|
||||||
|
|
||||||
}} catch (error) {{
|
}} catch (error) {{
|
||||||
@@ -1235,7 +1329,7 @@ class Action:
|
|||||||
__metadata__: Optional[dict] = None,
|
__metadata__: Optional[dict] = None,
|
||||||
__request__: Optional[Request] = None,
|
__request__: Optional[Request] = None,
|
||||||
) -> Optional[dict]:
|
) -> Optional[dict]:
|
||||||
logger.info("Action: Smart Mind Map (v0.8.0) started")
|
logger.info("Action: Smart Mind Map (v0.9.1) started")
|
||||||
user_ctx = self._get_user_context(__user__)
|
user_ctx = self._get_user_context(__user__)
|
||||||
user_language = user_ctx["user_language"]
|
user_language = user_ctx["user_language"]
|
||||||
user_name = user_ctx["user_name"]
|
user_name = user_ctx["user_name"]
|
||||||
@@ -1422,30 +1516,28 @@ class Action:
|
|||||||
# Image mode: use JavaScript to render and embed as Markdown image
|
# Image mode: use JavaScript to render and embed as Markdown image
|
||||||
chat_id = self._extract_chat_id(body, __metadata__)
|
chat_id = self._extract_chat_id(body, __metadata__)
|
||||||
message_id = self._extract_message_id(body, __metadata__)
|
message_id = self._extract_message_id(body, __metadata__)
|
||||||
|
|
||||||
await self._emit_status(
|
await self._emit_status(
|
||||||
__event_emitter__,
|
__event_emitter__,
|
||||||
"Smart Mind Map: Rendering image...",
|
"Smart Mind Map: Rendering image...",
|
||||||
False,
|
False,
|
||||||
)
|
)
|
||||||
|
|
||||||
if __event_call__:
|
if __event_call__:
|
||||||
js_code = self._generate_image_js_code(
|
js_code = self._generate_image_js_code(
|
||||||
unique_id=unique_id,
|
unique_id=unique_id,
|
||||||
chat_id=chat_id,
|
chat_id=chat_id,
|
||||||
message_id=message_id,
|
message_id=message_id,
|
||||||
markdown_syntax=markdown_syntax,
|
markdown_syntax=markdown_syntax,
|
||||||
svg_width=self.valves.SVG_WIDTH,
|
|
||||||
svg_height=self.valves.SVG_HEIGHT,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await __event_call__(
|
await __event_call__(
|
||||||
{
|
{
|
||||||
"type": "execute",
|
"type": "execute",
|
||||||
"data": {"code": js_code},
|
"data": {"code": js_code},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
await self._emit_status(
|
await self._emit_status(
|
||||||
__event_emitter__, "Smart Mind Map: Image generated!", True
|
__event_emitter__, "Smart Mind Map: Image generated!", True
|
||||||
)
|
)
|
||||||
@@ -1454,9 +1546,9 @@ class Action:
|
|||||||
f"Mind map image has been generated, {user_name}!",
|
f"Mind map image has been generated, {user_name}!",
|
||||||
"success",
|
"success",
|
||||||
)
|
)
|
||||||
logger.info("Action: Smart Mind Map (v0.9.0) completed in image mode")
|
logger.info("Action: Smart Mind Map (v0.9.1) completed in image mode")
|
||||||
return body
|
return body
|
||||||
|
|
||||||
# HTML mode (default): embed as HTML block
|
# HTML mode (default): embed as HTML block
|
||||||
html_embed_tag = f"```html\n{final_html}\n```"
|
html_embed_tag = f"```html\n{final_html}\n```"
|
||||||
body["messages"][-1]["content"] = f"{long_text_content}\n\n{html_embed_tag}"
|
body["messages"][-1]["content"] = f"{long_text_content}\n\n{html_embed_tag}"
|
||||||
@@ -1469,7 +1561,7 @@ class Action:
|
|||||||
f"Mind map has been generated, {user_name}!",
|
f"Mind map has been generated, {user_name}!",
|
||||||
"success",
|
"success",
|
||||||
)
|
)
|
||||||
logger.info("Action: Smart Mind Map (v0.9.0) completed in HTML mode")
|
logger.info("Action: Smart Mind Map (v0.9.1) completed in HTML mode")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_message = f"Smart Mind Map processing failed: {str(e)}"
|
error_message = f"Smart Mind Map processing failed: {str(e)}"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: 思维导图
|
|||||||
author: Fu-Jie
|
author: Fu-Jie
|
||||||
author_url: https://github.com/Fu-Jie
|
author_url: https://github.com/Fu-Jie
|
||||||
funding_url: https://github.com/Fu-Jie/awesome-openwebui
|
funding_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||||
version: 0.9.0
|
version: 0.9.1
|
||||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4=
|
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4=
|
||||||
description: 智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。
|
description: 智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。
|
||||||
"""
|
"""
|
||||||
@@ -789,14 +789,6 @@ class Action:
|
|||||||
default="html",
|
default="html",
|
||||||
description="输出模式: 'html' 为交互式HTML(默认),'image' 为嵌入Markdown图片。",
|
description="输出模式: 'html' 为交互式HTML(默认),'image' 为嵌入Markdown图片。",
|
||||||
)
|
)
|
||||||
SVG_WIDTH: int = Field(
|
|
||||||
default=1200,
|
|
||||||
description="SVG画布宽度(像素,用于图片模式)。",
|
|
||||||
)
|
|
||||||
SVG_HEIGHT: int = Field(
|
|
||||||
default=800,
|
|
||||||
description="SVG画布高度(像素,用于图片模式)。",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.valves = self.Valves()
|
self.valves = self.Valves()
|
||||||
@@ -870,9 +862,7 @@ class Action:
|
|||||||
if match:
|
if match:
|
||||||
extracted_content = match.group(1).strip()
|
extracted_content = match.group(1).strip()
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning("LLM输出未严格遵循预期Markdown格式,将整个输出作为摘要处理。")
|
||||||
"LLM输出未严格遵循预期Markdown格式,将整个输出作为摘要处理。"
|
|
||||||
)
|
|
||||||
extracted_content = llm_output.strip()
|
extracted_content = llm_output.strip()
|
||||||
return extracted_content.replace("</script>", "<\\/script>")
|
return extracted_content.replace("</script>", "<\\/script>")
|
||||||
|
|
||||||
@@ -958,27 +948,83 @@ class Action:
|
|||||||
chat_id: str,
|
chat_id: str,
|
||||||
message_id: str,
|
message_id: str,
|
||||||
markdown_syntax: str,
|
markdown_syntax: str,
|
||||||
svg_width: int,
|
|
||||||
svg_height: int,
|
|
||||||
) -> str:
|
) -> str:
|
||||||
"""生成用于前端 SVG 渲染和图片嵌入的 JavaScript 代码"""
|
"""生成用于前端 SVG 渲染和图片嵌入的 JavaScript 代码"""
|
||||||
|
|
||||||
# 转义语法以便嵌入 JS
|
# 转义语法以便嵌入 JS
|
||||||
syntax_escaped = (
|
syntax_escaped = (
|
||||||
markdown_syntax
|
markdown_syntax.replace("\\", "\\\\")
|
||||||
.replace("\\", "\\\\")
|
|
||||||
.replace("`", "\\`")
|
.replace("`", "\\`")
|
||||||
.replace("${", "\\${")
|
.replace("${", "\\${")
|
||||||
.replace("</script>", "<\\/script>")
|
.replace("</script>", "<\\/script>")
|
||||||
)
|
)
|
||||||
|
|
||||||
return f"""
|
return f"""
|
||||||
(async function() {{
|
(async function() {{
|
||||||
const uniqueId = "{unique_id}";
|
const uniqueId = "{unique_id}";
|
||||||
const chatId = "{chat_id}";
|
const chatId = "{chat_id}";
|
||||||
const messageId = "{message_id}";
|
const messageId = "{message_id}";
|
||||||
const defaultWidth = {svg_width};
|
const defaultWidth = 1200;
|
||||||
const defaultHeight = {svg_height};
|
const defaultHeight = 800;
|
||||||
|
|
||||||
|
// 主题检测 - 检查 OpenWebUI 当前主题
|
||||||
|
const detectTheme = () => {{
|
||||||
|
try {{
|
||||||
|
// 1. 检查 html/body 的 class 或 data-theme 属性
|
||||||
|
const html = document.documentElement;
|
||||||
|
const body = document.body;
|
||||||
|
const htmlClass = html ? html.className : '';
|
||||||
|
const bodyClass = body ? body.className : '';
|
||||||
|
const htmlDataTheme = html ? html.getAttribute('data-theme') : '';
|
||||||
|
|
||||||
|
if (htmlDataTheme === 'dark' || bodyClass.includes('dark') || htmlClass.includes('dark')) {{
|
||||||
|
return 'dark';
|
||||||
|
}}
|
||||||
|
if (htmlDataTheme === 'light' || bodyClass.includes('light') || htmlClass.includes('light')) {{
|
||||||
|
return 'light';
|
||||||
|
}}
|
||||||
|
|
||||||
|
// 2. 检查 meta theme-color
|
||||||
|
const metas = document.querySelectorAll('meta[name="theme-color"]');
|
||||||
|
if (metas.length > 0) {{
|
||||||
|
const color = metas[metas.length - 1].content.trim();
|
||||||
|
const m = color.match(/^#?([0-9a-f]{{6}})$/i);
|
||||||
|
if (m) {{
|
||||||
|
const hex = m[1];
|
||||||
|
const r = parseInt(hex.slice(0, 2), 16);
|
||||||
|
const g = parseInt(hex.slice(2, 4), 16);
|
||||||
|
const b = parseInt(hex.slice(4, 6), 16);
|
||||||
|
const luma = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
|
||||||
|
return luma < 0.5 ? 'dark' : 'light';
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
// 3. 检查系统偏好
|
||||||
|
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {{
|
||||||
|
return 'dark';
|
||||||
|
}}
|
||||||
|
|
||||||
|
return 'light';
|
||||||
|
}} catch (e) {{
|
||||||
|
return 'light';
|
||||||
|
}}
|
||||||
|
}};
|
||||||
|
|
||||||
|
const currentTheme = detectTheme();
|
||||||
|
console.log("[思维导图图片] 检测到主题:", currentTheme);
|
||||||
|
|
||||||
|
// 基于主题的颜色配置
|
||||||
|
const colors = currentTheme === 'dark' ? {{
|
||||||
|
background: '#1f2937',
|
||||||
|
text: '#e5e7eb',
|
||||||
|
link: '#94a3b8',
|
||||||
|
nodeStroke: '#64748b'
|
||||||
|
}} : {{
|
||||||
|
background: '#ffffff',
|
||||||
|
text: '#1f2937',
|
||||||
|
link: '#546e7a',
|
||||||
|
nodeStroke: '#94a3b8'
|
||||||
|
}};
|
||||||
|
|
||||||
// 自动检测聊天容器宽度以实现自适应
|
// 自动检测聊天容器宽度以实现自适应
|
||||||
let svgWidth = defaultWidth;
|
let svgWidth = defaultWidth;
|
||||||
@@ -1053,7 +1099,7 @@ class Action:
|
|||||||
svgEl.setAttribute('height', svgHeight);
|
svgEl.setAttribute('height', svgHeight);
|
||||||
svgEl.style.width = svgWidth + 'px';
|
svgEl.style.width = svgWidth + 'px';
|
||||||
svgEl.style.height = svgHeight + 'px';
|
svgEl.style.height = svgHeight + 'px';
|
||||||
svgEl.style.backgroundColor = '#ffffff';
|
svgEl.style.backgroundColor = colors.background;
|
||||||
container.appendChild(svgEl);
|
container.appendChild(svgEl);
|
||||||
|
|
||||||
// 将 markdown 转换为树结构
|
// 将 markdown 转换为树结构
|
||||||
@@ -1081,23 +1127,23 @@ class Action:
|
|||||||
clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
||||||
clonedSvg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
|
clonedSvg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
|
||||||
|
|
||||||
// 添加背景矩形
|
// 添加背景矩形(使用主题颜色)
|
||||||
const bgRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
const bgRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||||
bgRect.setAttribute('width', '100%');
|
bgRect.setAttribute('width', '100%');
|
||||||
bgRect.setAttribute('height', '100%');
|
bgRect.setAttribute('height', '100%');
|
||||||
bgRect.setAttribute('fill', '#ffffff');
|
bgRect.setAttribute('fill', colors.background);
|
||||||
clonedSvg.insertBefore(bgRect, clonedSvg.firstChild);
|
clonedSvg.insertBefore(bgRect, clonedSvg.firstChild);
|
||||||
|
|
||||||
// 添加内联样式
|
// 添加内联样式(使用主题颜色)
|
||||||
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
|
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
text {{ font-family: sans-serif; font-size: 14px; fill: #000000; }}
|
text {{ font-family: sans-serif; font-size: 14px; fill: ${{colors.text}}; }}
|
||||||
foreignObject, .markmap-foreign, .markmap-foreign div {{ color: #000000; font-family: sans-serif; font-size: 14px; }}
|
foreignObject, .markmap-foreign, .markmap-foreign div {{ color: ${{colors.text}}; font-family: sans-serif; font-size: 14px; }}
|
||||||
h1 {{ font-size: 22px; font-weight: 700; margin: 0; }}
|
h1 {{ font-size: 22px; font-weight: 700; margin: 0; }}
|
||||||
h2 {{ font-size: 18px; font-weight: 600; margin: 0; }}
|
h2 {{ font-size: 18px; font-weight: 600; margin: 0; }}
|
||||||
strong {{ font-weight: 700; }}
|
strong {{ font-weight: 700; }}
|
||||||
.markmap-link {{ stroke: #546e7a; fill: none; }}
|
.markmap-link {{ stroke: ${{colors.link}}; fill: none; }}
|
||||||
.markmap-node circle, .markmap-node rect {{ stroke: #94a3b8; }}
|
.markmap-node circle, .markmap-node rect {{ stroke: ${{colors.nodeStroke}}; }}
|
||||||
`;
|
`;
|
||||||
clonedSvg.insertBefore(style, bgRect.nextSibling);
|
clonedSvg.insertBefore(style, bgRect.nextSibling);
|
||||||
|
|
||||||
@@ -1109,7 +1155,7 @@ class Action:
|
|||||||
const textEl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
const textEl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||||
textEl.setAttribute('x', fo.getAttribute('x') || '0');
|
textEl.setAttribute('x', fo.getAttribute('x') || '0');
|
||||||
textEl.setAttribute('y', (parseFloat(fo.getAttribute('y') || '0') + 14).toString());
|
textEl.setAttribute('y', (parseFloat(fo.getAttribute('y') || '0') + 14).toString());
|
||||||
textEl.setAttribute('fill', '#000000');
|
textEl.setAttribute('fill', colors.text);
|
||||||
textEl.setAttribute('font-family', 'sans-serif');
|
textEl.setAttribute('font-family', 'sans-serif');
|
||||||
textEl.setAttribute('font-size', '14');
|
textEl.setAttribute('font-size', '14');
|
||||||
textEl.textContent = text.trim();
|
textEl.textContent = text.trim();
|
||||||
@@ -1119,21 +1165,63 @@ class Action:
|
|||||||
|
|
||||||
// 序列化 SVG 为字符串
|
// 序列化 SVG 为字符串
|
||||||
const svgData = new XMLSerializer().serializeToString(clonedSvg);
|
const svgData = new XMLSerializer().serializeToString(clonedSvg);
|
||||||
const svgBase64 = btoa(unescape(encodeURIComponent(svgData)));
|
|
||||||
const dataUrl = 'data:image/svg+xml;base64,' + svgBase64;
|
|
||||||
|
|
||||||
console.log("[思维导图图片] Data URL 已生成,长度:", dataUrl.length);
|
// 清理容器
|
||||||
|
|
||||||
// 清理
|
|
||||||
document.body.removeChild(container);
|
document.body.removeChild(container);
|
||||||
|
|
||||||
// 生成 markdown 图片
|
// 将 SVG 字符串转换为 Blob
|
||||||
const markdownImage = ``;
|
const blob = new Blob([svgData], {{ type: 'image/svg+xml' }});
|
||||||
|
const file = new File([blob], `mindmap-${{uniqueId}}.svg`, {{ type: 'image/svg+xml' }});
|
||||||
|
|
||||||
|
// 上传文件到 OpenWebUI API
|
||||||
|
console.log("[思维导图图片] 正在上传 SVG 文件...");
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const uploadResponse = await fetch('/api/v1/files/', {{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {{
|
||||||
|
'Authorization': `Bearer ${{token}}`
|
||||||
|
}},
|
||||||
|
body: formData
|
||||||
|
}});
|
||||||
|
|
||||||
|
if (!uploadResponse.ok) {{
|
||||||
|
throw new Error(`上传失败: ${{uploadResponse.statusText}}`);
|
||||||
|
}}
|
||||||
|
|
||||||
|
const fileData = await uploadResponse.json();
|
||||||
|
const fileId = fileData.id;
|
||||||
|
const imageUrl = `/api/v1/files/${{fileId}}/content`;
|
||||||
|
|
||||||
|
console.log("[思维导图图片] 文件已上传, ID:", fileId);
|
||||||
|
|
||||||
|
// 生成包含文件 URL 的 markdown 图片
|
||||||
|
const markdownImage = ``;
|
||||||
|
|
||||||
// 通过 API 更新消息
|
// 通过 API 更新消息
|
||||||
if (chatId && messageId) {{
|
if (chatId && messageId) {{
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
|
// 带重试逻辑的请求函数
|
||||||
|
const fetchWithRetry = async (url, options, retries = 3) => {{
|
||||||
|
for (let i = 0; i < retries; i++) {{
|
||||||
|
try {{
|
||||||
|
const response = await fetch(url, options);
|
||||||
|
if (response.ok) return response;
|
||||||
|
if (i < retries - 1) {{
|
||||||
|
console.log(`[思维导图图片] 重试 ${{i + 1}}/${{retries}}: ${{url}}`);
|
||||||
|
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
|
||||||
|
}}
|
||||||
|
}} catch (e) {{
|
||||||
|
if (i === retries - 1) throw e;
|
||||||
|
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
return null;
|
||||||
|
}};
|
||||||
|
|
||||||
// 获取当前聊天数据
|
// 获取当前聊天数据
|
||||||
const getResponse = await fetch(`/api/v1/chats/${{chatId}}`, {{
|
const getResponse = await fetch(`/api/v1/chats/${{chatId}}`, {{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -1145,24 +1233,26 @@ class Action:
|
|||||||
}}
|
}}
|
||||||
|
|
||||||
const chatData = await getResponse.json();
|
const chatData = await getResponse.json();
|
||||||
let originalContent = "";
|
|
||||||
let updatedMessages = [];
|
let updatedMessages = [];
|
||||||
|
let newContent = "";
|
||||||
|
|
||||||
if (chatData.chat && chatData.chat.messages) {{
|
if (chatData.chat && chatData.chat.messages) {{
|
||||||
updatedMessages = chatData.chat.messages.map(m => {{
|
updatedMessages = chatData.chat.messages.map(m => {{
|
||||||
if (m.id === messageId) {{
|
if (m.id === messageId) {{
|
||||||
originalContent = m.content || "";
|
const originalContent = m.content || "";
|
||||||
// 移除已有的思维导图图片
|
// 移除已有的思维导图图片 (包括 base64 和文件 URL 格式)
|
||||||
const mindmapPattern = /\\n*!\\[🧠[^\\]]*\\]\\(data:image\\/[^)]+\\)/g;
|
const mindmapPattern = /\\n*!\\[🧠[^\\]]*\\]\\((?:data:image\\/[^)]+|(?:\\/api\\/v1\\/files\\/[^)]+))\\)/g;
|
||||||
let cleanedContent = originalContent.replace(mindmapPattern, "");
|
let cleanedContent = originalContent.replace(mindmapPattern, "");
|
||||||
cleanedContent = cleanedContent.replace(/\\n{{3,}}/g, "\\n\\n").trim();
|
cleanedContent = cleanedContent.replace(/\\n{{3,}}/g, "\\n\\n").trim();
|
||||||
// 追加新图片
|
// 追加新图片
|
||||||
const newContent = cleanedContent + "\\n\\n" + markdownImage;
|
newContent = cleanedContent + "\\n\\n" + markdownImage;
|
||||||
|
|
||||||
// 关键: 同时更新 messages 数组和 history 对象中的内容
|
// 关键: 同时更新 messages 数组和 history 对象中的内容
|
||||||
// history 对象通常是数据库的单一真值来源
|
// history 对象是数据库的单一真值来源
|
||||||
if (chatData.chat.history && chatData.chat.history.messages && chatData.chat.history.messages[messageId]) {{
|
if (chatData.chat.history && chatData.chat.history.messages) {{
|
||||||
chatData.chat.history.messages[messageId].content = newContent;
|
if (chatData.chat.history.messages[messageId]) {{
|
||||||
|
chatData.chat.history.messages[messageId].content = newContent;
|
||||||
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
return {{ ...m, content: newContent }};
|
return {{ ...m, content: newContent }};
|
||||||
@@ -1171,28 +1261,40 @@ class Action:
|
|||||||
}});
|
}});
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// 第一步: 通过事件 API 更新前端显示(立即视觉反馈)
|
if (!newContent) {{
|
||||||
await fetch(`/api/v1/chats/${{chatId}}/messages/${{messageId}}/event`, {{
|
console.warn("[思维导图图片] 找不到要更新的消息");
|
||||||
method: "POST",
|
return;
|
||||||
headers: {{
|
}}
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": `Bearer ${{token}}`
|
|
||||||
}},
|
|
||||||
body: JSON.stringify({{
|
|
||||||
type: "chat:message",
|
|
||||||
data: {{ content: updatedMessages.find(m => m.id === messageId)?.content || "" }}
|
|
||||||
}})
|
|
||||||
}});
|
|
||||||
|
|
||||||
// 第二步: 通过更新整个聊天来持久化到数据库
|
// 尝试通过事件 API 更新前端显示(可选,部分版本可能不支持)
|
||||||
|
try {{
|
||||||
|
await fetch(`/api/v1/chats/${{chatId}}/messages/${{messageId}}/event`, {{
|
||||||
|
method: "POST",
|
||||||
|
headers: {{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${{token}}`
|
||||||
|
}},
|
||||||
|
body: JSON.stringify({{
|
||||||
|
type: "chat:message",
|
||||||
|
data: {{ content: newContent }}
|
||||||
|
}})
|
||||||
|
}});
|
||||||
|
}} catch (eventErr) {{
|
||||||
|
// 事件 API 是可选的,继续执行持久化
|
||||||
|
console.log("[思维导图图片] 事件 API 不可用,继续执行...");
|
||||||
|
}}
|
||||||
|
|
||||||
|
// 通过更新整个聊天对象来持久化到数据库
|
||||||
|
// 遵循 OpenWebUI 后端控制的 API 流程
|
||||||
const updatePayload = {{
|
const updatePayload = {{
|
||||||
chat: {{
|
chat: {{
|
||||||
...chatData.chat,
|
...chatData.chat,
|
||||||
messages: updatedMessages
|
messages: updatedMessages
|
||||||
|
// history 已在上面原地更新
|
||||||
}}
|
}}
|
||||||
}};
|
}};
|
||||||
|
|
||||||
const persistResponse = await fetch(`/api/v1/chats/${{chatId}}`, {{
|
const persistResponse = await fetchWithRetry(`/api/v1/chats/${{chatId}}`, {{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {{
|
headers: {{
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -1201,22 +1303,13 @@ class Action:
|
|||||||
body: JSON.stringify(updatePayload)
|
body: JSON.stringify(updatePayload)
|
||||||
}});
|
}});
|
||||||
|
|
||||||
if (persistResponse.ok) {{
|
if (persistResponse && persistResponse.ok) {{
|
||||||
console.log("[思维导图图片] ✅ 消息已持久化保存!");
|
console.log("[思维导图图片] ✅ 消息已持久化保存!");
|
||||||
}} else {{
|
}} else {{
|
||||||
console.error("[思维导图图片] 持久化 API 错误:", persistResponse.status);
|
console.error("[思维导图图片] ❌ 重试后仍然无法持久化消息");
|
||||||
// 尝试备用更新方法
|
|
||||||
const altResponse = await fetch(`/api/v1/chats/${{chatId}}/share`, {{
|
|
||||||
method: "POST",
|
|
||||||
headers: {{
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": `Bearer ${{token}}`
|
|
||||||
}}
|
|
||||||
}});
|
|
||||||
console.log("[思维导图图片] 备用持久化尝试:", altResponse.status);
|
|
||||||
}}
|
}}
|
||||||
}} else {{
|
}} else {{
|
||||||
console.warn("[思维导图图片] ⚠️ 缺少 chatId 或 messageId");
|
console.warn("[思维导图图片] ⚠️ 缺少 chatId 或 messageId,无法持久化");
|
||||||
}}
|
}}
|
||||||
|
|
||||||
}} catch (error) {{
|
}} catch (error) {{
|
||||||
@@ -1234,7 +1327,7 @@ class Action:
|
|||||||
__metadata__: Optional[dict] = None,
|
__metadata__: Optional[dict] = None,
|
||||||
__request__: Optional[Request] = None,
|
__request__: Optional[Request] = None,
|
||||||
) -> Optional[dict]:
|
) -> Optional[dict]:
|
||||||
logger.info("Action: 思维导图 (v12 - Final Feedback Fix) started")
|
logger.info("Action: 思维导图 (v0.9.1) started")
|
||||||
user_ctx = self._get_user_context(__user__)
|
user_ctx = self._get_user_context(__user__)
|
||||||
user_language = user_ctx["user_language"]
|
user_language = user_ctx["user_language"]
|
||||||
user_name = user_ctx["user_name"]
|
user_name = user_ctx["user_name"]
|
||||||
@@ -1416,30 +1509,28 @@ class Action:
|
|||||||
# 图片模式: 使用 JavaScript 渲染并嵌入为 Markdown 图片
|
# 图片模式: 使用 JavaScript 渲染并嵌入为 Markdown 图片
|
||||||
chat_id = self._extract_chat_id(body, __metadata__)
|
chat_id = self._extract_chat_id(body, __metadata__)
|
||||||
message_id = self._extract_message_id(body, __metadata__)
|
message_id = self._extract_message_id(body, __metadata__)
|
||||||
|
|
||||||
await self._emit_status(
|
await self._emit_status(
|
||||||
__event_emitter__,
|
__event_emitter__,
|
||||||
"思维导图: 正在渲染图片...",
|
"思维导图: 正在渲染图片...",
|
||||||
False,
|
False,
|
||||||
)
|
)
|
||||||
|
|
||||||
if __event_call__:
|
if __event_call__:
|
||||||
js_code = self._generate_image_js_code(
|
js_code = self._generate_image_js_code(
|
||||||
unique_id=unique_id,
|
unique_id=unique_id,
|
||||||
chat_id=chat_id,
|
chat_id=chat_id,
|
||||||
message_id=message_id,
|
message_id=message_id,
|
||||||
markdown_syntax=markdown_syntax,
|
markdown_syntax=markdown_syntax,
|
||||||
svg_width=self.valves.SVG_WIDTH,
|
|
||||||
svg_height=self.valves.SVG_HEIGHT,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await __event_call__(
|
await __event_call__(
|
||||||
{
|
{
|
||||||
"type": "execute",
|
"type": "execute",
|
||||||
"data": {"code": js_code},
|
"data": {"code": js_code},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
await self._emit_status(
|
await self._emit_status(
|
||||||
__event_emitter__, "思维导图: 图片已生成!", True
|
__event_emitter__, "思维导图: 图片已生成!", True
|
||||||
)
|
)
|
||||||
@@ -1448,9 +1539,9 @@ class Action:
|
|||||||
f"思维导图图片已生成,{user_name}!",
|
f"思维导图图片已生成,{user_name}!",
|
||||||
"success",
|
"success",
|
||||||
)
|
)
|
||||||
logger.info("Action: 思维导图 (v0.9.0) 图片模式完成")
|
logger.info("Action: 思维导图 (v0.9.1) 图片模式完成")
|
||||||
return body
|
return body
|
||||||
|
|
||||||
# HTML 模式(默认): 嵌入为 HTML 块
|
# HTML 模式(默认): 嵌入为 HTML 块
|
||||||
html_embed_tag = f"```html\n{final_html}\n```"
|
html_embed_tag = f"```html\n{final_html}\n```"
|
||||||
body["messages"][-1]["content"] = f"{long_text_content}\n\n{html_embed_tag}"
|
body["messages"][-1]["content"] = f"{long_text_content}\n\n{html_embed_tag}"
|
||||||
@@ -1459,7 +1550,7 @@ class Action:
|
|||||||
await self._emit_notification(
|
await self._emit_notification(
|
||||||
__event_emitter__, f"思维导图已生成,{user_name}!", "success"
|
__event_emitter__, f"思维导图已生成,{user_name}!", "success"
|
||||||
)
|
)
|
||||||
logger.info("Action: 思维导图 (v0.9.0) HTML 模式完成")
|
logger.info("Action: 思维导图 (v0.9.1) HTML 模式完成")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_message = f"思维导图处理失败: {str(e)}"
|
error_message = f"思维导图处理失败: {str(e)}"
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ class OpenWebUIStats:
|
|||||||
texts = {
|
texts = {
|
||||||
"zh": {
|
"zh": {
|
||||||
"title": "# 📊 OpenWebUI 社区统计报告",
|
"title": "# 📊 OpenWebUI 社区统计报告",
|
||||||
"updated": f"> 📅 更新时间 (北京): {get_beijing_time().strftime('%Y-%m-%d %H:%M')}",
|
"updated": f"> 📅 更新时间: {get_beijing_time().strftime('%Y-%m-%d %H:%M')}",
|
||||||
"overview_title": "## 📈 总览",
|
"overview_title": "## 📈 总览",
|
||||||
"overview_header": "| 指标 | 数值 |",
|
"overview_header": "| 指标 | 数值 |",
|
||||||
"posts": "📝 发布数量",
|
"posts": "📝 发布数量",
|
||||||
@@ -265,7 +265,7 @@ class OpenWebUIStats:
|
|||||||
},
|
},
|
||||||
"en": {
|
"en": {
|
||||||
"title": "# 📊 OpenWebUI Community Stats Report",
|
"title": "# 📊 OpenWebUI Community Stats Report",
|
||||||
"updated": f"> 📅 Updated (Beijing Time): {get_beijing_time().strftime('%Y-%m-%d %H:%M')}",
|
"updated": f"> 📅 Updated: {get_beijing_time().strftime('%Y-%m-%d %H:%M')}",
|
||||||
"overview_title": "## 📈 Overview",
|
"overview_title": "## 📈 Overview",
|
||||||
"overview_header": "| Metric | Value |",
|
"overview_header": "| Metric | Value |",
|
||||||
"posts": "📝 Total Posts",
|
"posts": "📝 Total Posts",
|
||||||
@@ -346,7 +346,7 @@ class OpenWebUIStats:
|
|||||||
texts = {
|
texts = {
|
||||||
"zh": {
|
"zh": {
|
||||||
"title": "## 📊 社区统计",
|
"title": "## 📊 社区统计",
|
||||||
"updated": f"> 🕐 自动更新于 {get_beijing_time().strftime('%Y-%m-%d %H:%M')} (北京时间)",
|
"updated": f"> 🕐 自动更新于 {get_beijing_time().strftime('%Y-%m-%d %H:%M')}",
|
||||||
"author_header": "| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |",
|
"author_header": "| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |",
|
||||||
"header": "| 📝 发布 | ⬇️ 下载 | 👁️ 浏览 | 👍 点赞 | 💾 收藏 |",
|
"header": "| 📝 发布 | ⬇️ 下载 | 👁️ 浏览 | 👍 点赞 | 💾 收藏 |",
|
||||||
"top5_title": "### 🔥 热门插件 Top 5",
|
"top5_title": "### 🔥 热门插件 Top 5",
|
||||||
@@ -355,7 +355,7 @@ class OpenWebUIStats:
|
|||||||
},
|
},
|
||||||
"en": {
|
"en": {
|
||||||
"title": "## 📊 Community Stats",
|
"title": "## 📊 Community Stats",
|
||||||
"updated": f"> 🕐 Auto-updated: {get_beijing_time().strftime('%Y-%m-%d %H:%M')} (Beijing Time)",
|
"updated": f"> 🕐 Auto-updated: {get_beijing_time().strftime('%Y-%m-%d %H:%M')}",
|
||||||
"author_header": "| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |",
|
"author_header": "| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |",
|
||||||
"header": "| 📝 Posts | ⬇️ Downloads | 👁️ Views | 👍 Upvotes | 💾 Saves |",
|
"header": "| 📝 Posts | ⬇️ Downloads | 👁️ Views | 👍 Upvotes | 💾 Saves |",
|
||||||
"top5_title": "### 🔥 Top 5 Popular Plugins",
|
"top5_title": "### 🔥 Top 5 Popular Plugins",
|
||||||
|
|||||||
Reference in New Issue
Block a user