feat: Update smart mind map plugin with enhanced features and improved UI

- Renamed plugin title from "智绘心图" to "思维导图" and updated version to 0.8.0.
- Refactored user context extraction for better handling of user data.
- Improved CSS styles for better responsiveness and aesthetics.
- Added new control buttons for downloading and zooming functionalities.
- Enhanced JavaScript for dynamic theme detection and improved SVG handling.
- Updated documentation to reflect changes in plugin functionality and naming.
This commit is contained in:
fujie
2025-12-30 23:53:26 +08:00
parent 4e5646ae94
commit 73df5a0818
7 changed files with 768 additions and 280 deletions

View File

@@ -466,6 +466,165 @@ async def generate_title_using_ai(
---
## 🎭 iframe 主题检测规范 (iframe Theme Detection)
当插件在 iframe 中运行(特别是使用 `srcdoc` 属性)时,需要检测应用程序的主题以保持视觉一致性。
### 检测优先级 (Priority Order)
按以下顺序尝试检测主题,直到找到有效结果:
1. **显式切换** (Explicit Toggle) - 用户手动点击主题按钮
2. **父文档 Meta 标签** (Parent Meta Theme-Color) - 从 `window.parent.document``<meta name="theme-color">` 读取
3. **父文档 Class/Data-Theme** (Parent HTML/Body Class) - 检查父文档 html/body 的 class 或 data-theme 属性
4. **系统偏好** (System Preference) - `prefers-color-scheme: dark` 媒体查询
### 核心实现代码 (Implementation)
```javascript
// 1. 颜色亮度解析(支持 hex 和 rgb
const parseColorLuma = (colorStr) => {
if (!colorStr) return null;
// hex #rrggbb or rrggbb
let m = colorStr.match(/^#?([0-9a-f]{6})$/i);
if (m) {
const hex = m[1];
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
}
// rgb(r, g, b) or rgba(r, g, b, a)
m = colorStr.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
if (m) {
const r = parseInt(m[1], 10);
const g = parseInt(m[2], 10);
const b = parseInt(m[3], 10);
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
}
return null;
};
// 2. 从 meta 标签提取主题
const getThemeFromMeta = (doc, scope = 'self') => {
const metas = Array.from((doc || document).querySelectorAll('meta[name="theme-color"]'));
if (!metas.length) return null;
const color = metas[metas.length - 1].content.trim();
const luma = parseColorLuma(color);
if (luma === null) return null;
return luma < 0.5 ? 'dark' : 'light';
};
// 3. 安全地访问父文档
const getParentDocumentSafe = () => {
try {
if (!window.parent || window.parent === window) return null;
const pDoc = window.parent.document;
void pDoc.title; // 触发跨域检查
return pDoc;
} catch (err) {
console.log(`Parent document not accessible: ${err.name}`);
return null;
}
};
// 4. 从父文档的 class/data-theme 检测主题
const getThemeFromParentClass = () => {
try {
if (!window.parent || window.parent === window) return null;
const pDoc = window.parent.document;
const html = pDoc.documentElement;
const body = pDoc.body;
const htmlClass = html ? html.className : '';
const bodyClass = body ? body.className : '';
const htmlDataTheme = html ? html.getAttribute('data-theme') : '';
if (htmlDataTheme === 'dark' || bodyClass.includes('dark') || htmlClass.includes('dark'))
return 'dark';
if (htmlDataTheme === 'light' || bodyClass.includes('light') || htmlClass.includes('light'))
return 'light';
return null;
} catch (err) {
return null;
}
};
// 5. 主题设置及检测
const setTheme = (wrapperEl, explicitTheme) => {
const parentDoc = getParentDocumentSafe();
const metaThemeParent = parentDoc ? getThemeFromMeta(parentDoc, 'parent') : null;
const parentClassTheme = getThemeFromParentClass();
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
// 按优先级选择
const chosen = explicitTheme || metaThemeParent || parentClassTheme || (prefersDark ? 'dark' : 'light');
wrapperEl.classList.toggle('theme-dark', chosen === 'dark');
return chosen;
};
```
### CSS 变量定义 (CSS Variables)
使用 CSS 变量实现主题切换,避免硬编码颜色:
```css
:root {
--primary-color: #1e88e5;
--background-color: #f4f6f8;
--text-color: #263238;
--border-color: #e0e0e0;
}
.theme-dark {
--primary-color: #64b5f6;
--background-color: #111827;
--text-color: #e5e7eb;
--border-color: #374151;
}
.container {
background-color: var(--background-color);
color: var(--text-color);
border-color: var(--border-color);
}
```
### 调试与日志 (Debugging)
添加详细日志便于排查主题检测问题:
```javascript
console.log(`[plugin] [parent] meta theme-color count: ${metas.length}`);
console.log(`[plugin] [parent] meta theme-color picked: "${color}"`);
console.log(`[plugin] [parent] meta theme-color luma=${luma.toFixed(3)}, inferred=${inferred}`);
console.log(`[plugin] parent html.class="${htmlClass}", data-theme="${htmlDataTheme}"`);
console.log(`[plugin] final chosen theme: ${chosen}`);
```
### 最佳实践 (Best Practices)
- 仅尝试访问**父文档**的主题信息,不依赖 srcdoc iframe 自身的 meta通常为空
- 在跨域 iframe 中使用 class/data-theme 作为备选方案
- 使用 try-catch 包裹所有父文档访问,避免跨域异常中断
- 提供用户手动切换主题的按钮作为最高优先级
- 记录详细日志便于用户反馈主题检测问题
### OpenWebUI Configuration Requirement (OpenWebUI Configuration)
For iframe plugins to access parent document theme information, users need to configure:
1. **Enable Artifact Same-Origin Access** - In User Settings: **Interface****Artifacts** → Check **iframe Sandbox Allow Same Origin**
2. **Configure Sandbox Attributes** - Ensure iframe's sandbox attribute includes both `allow-same-origin` and `allow-scripts`
3. **Verify Meta Tag** - Ensure OpenWebUI page head contains `<meta name="theme-color" content="#color">` tag
**Important Notes**:
- Same-origin access allows iframe to read theme information via `window.parent.document`
- Cross-origin iframes cannot access parent document and should implement class/data-theme detection as fallback
- Using same-origin access in srcdoc iframe is safe (origin is null, doesn't bypass CORS policy)
- Users can provide manual theme toggle button in plugin as highest priority option
---
## ✅ 开发检查清单 (Development Checklist)
开发新插件时,请确保完成以下检查:

View File

@@ -1,8 +1,8 @@
# Open WebUI Action 插件开发范例:智绘心
# Open WebUI Action 插件开发范例:思维导
## 引言
智绘心图” (`smart-mind-map`) 是一个功能强大的 Open WebUI Action 插件。它通过分析用户提供的文本利用大语言模型LLM提取关键信息并最终生成一个可交互的、可视化的思维导图。本文档将深入解析其源码 (`思维导图.py`),提炼其中蕴含的插件开发知识与最佳实践,为开发者提供一个高质量的参考范例。
思维导图” (`smart-mind-map`) 是一个功能强大的 Open WebUI Action 插件。它通过分析用户提供的文本利用大语言模型LLM提取关键信息并最终生成一个可交互的、可视化的思维导图。本文档将深入解析其源码 (`思维导图.py`),提炼其中蕴含的插件开发知识与最佳实践,为开发者提供一个高质量的参考范例。
## 核心开发知识点
@@ -22,15 +22,18 @@
Open WebUI 通过文件顶部的特定格式注释来识别和展示插件信息。
**代码示例 (`思维导图.py`):**
```python
"""
title: 智绘心
title: 思维导
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+CiAgPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMyIgZmlsbD0iY3VycmVudENvbG9yIi8+CiAgPGxpbmUgeDE9IjEyIiB5MT0iOSIgeDI9IjEyIiB5Mj0iNCIvPgogIDxjaXJjbGUgY3g9IjEyIiBjeT0iMyIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEyIiB5MT0iMTUiIHgyPSIxMiIgeTI9IjIwIi8+CiAgPGNpcmNsZSBjeD0iMTIiIGN5PSIyMSIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjkiIHkxPSIxMiIgeDI9IjQiIHkyPSIxMiIvPgogIDxjaXJjbGUgY3g9IjMiIGN5PSIxMiIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjE1IiB5MT0iMTIiIHgyPSIyMCIgeTI9IjEyIi8+CiAgPGNpcmNsZSBjeD0iMjEiIGN5PSIxMiIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEwLjUiIHkxPSྡ1LjUiIHgyPSI2IiB5Mj0iNiIvPgogIDxjaXJjbGUgY3g9IjUiIGN5PSI1Iigcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEzLjUiIHkxPSྡ5LjUgeDI9IjE1IiB5Mj0iNiIvPgogIDxjaXJjbGUgY3g9IjE5IiBjeT0iNSIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9ྡ1LjUgeTE9ྡ3MuNSB4Mj0iNiIgeTI9IjE4Ii8+CiAgPGNpcmNsZSBjeD0iNSIgY3k9IjE5IiByPSྡ1LjUiLz4KICA8bGluZSB4MT0ྡzIuNSB5MT0ྡzIuNSB4Mj0iNSIgeTI9IjE4Ii8+CiAgPGNpcmNsZSBjeD0ྡ5IiBjeT0ྡ5IiByPSྡ1LjUiLz4KPC9zdmc+Cg==
version: 0.7.2
description: 智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。
"""
```
**知识点**:
- `title`: 插件在 UI 中显示的名称。
- `icon_url`: 插件的图标,支持 base64 编码的 SVG以实现无依赖的矢量图标。
- `version`: 插件的版本号。
@@ -43,6 +46,7 @@ description: 智能分析文本内容,生成交互式思维导图,帮助用户
通过在 `Action` 类内部定义一个 `Valves` Pydantic 模型,可以为插件创建可在 Web UI 中配置的参数。
**代码示例 (`思维导图.py`):**
```python
class Action:
class Valves(BaseModel):
@@ -60,7 +64,9 @@ class Action:
def __init__(self):
self.valves = self.Valves()
```
**知识点**:
- `Valves` 类继承自 `pydantic.BaseModel`
- 每个字段都是一个配置项,`default` 是默认值,`description` 会在 UI 中作为提示信息显示。
-`__init__` 中实例化 `self.valves`,之后可以通过 `self.valves.PARAMETER_NAME` 来访问配置值。
@@ -72,6 +78,7 @@ class Action:
`action` 方法是插件的执行入口,它是一个异步函数,接收 Open WebUI 传入的上下文信息。
**代码示例 (`思维导图.py`):**
```python
async def action(
self,
@@ -83,7 +90,9 @@ class Action:
# ... 插件逻辑 ...
return body
```
**知识点**:
- `body`: 包含当前聊天上下文的字典,最重要的是 `body.get("messages")`,它包含了完整的消息历史。
- `__user__`: 包含当前用户信息的字典,如 `id`, `name`, `language` 等。插件中演示了如何兼容其为 `dict``list` 的情况。
- `__event_emitter__`: 一个可调用的异步函数,用于向前端发送事件,是实现实时反馈的关键。
@@ -97,6 +106,7 @@ class Action:
使用 `__event_emitter__` 可以极大地提升用户体验,让用户了解插件的执行进度。
**代码示例 (`思维导图.py`):**
```python
# 发送通知 (Toast)
await __event_emitter__(
@@ -104,7 +114,7 @@ await __event_emitter__(
"type": "notification",
"data": {
"type": "info", # 'info', 'success', 'warning', 'error'
"content": "智绘心图已启动,正在为您生成思维导图...",
"content": "思维导图已启动,正在为您生成思维导图...",
},
}
)
@@ -114,7 +124,7 @@ await __event_emitter__(
{
"type": "status",
"data": {
"description": "智绘心图: 深入分析文本结构...",
"description": "思维导图: 深入分析文本结构...",
"done": False, # False 表示进行中
"hidden": False,
},
@@ -126,14 +136,16 @@ await __event_emitter__(
{
"type": "status",
"data": {
"description": "智绘心图: 绘制完成!",
"description": "思维导图: 绘制完成!",
"done": True, # True 表示已完成
"hidden": False, # True 可以让成功状态自动隐藏
},
}
)
```
**知识点**:
- **通知 (`notification`)**: 在屏幕角落弹出短暂的提示信息,适合用于触发、成功或失败的即时反馈。
- **状态 (`status`)**: 在聊天输入框上方显示一个持久的状态条,适合展示多步骤任务的当前进度。`done: True` 会标记任务完成。
@@ -141,9 +153,10 @@ await __event_emitter__(
### 5. 与 LLM 交互
插件的核心功能通常依赖于 LLM。`智绘心图` 演示了如何构建一个结构化的 Prompt 并调用 LLM。
插件的核心功能通常依赖于 LLM。`思维导图` 演示了如何构建一个结构化的 Prompt 并调用 LLM。
**代码示例 (`思维导图.py`):**
```python
# 1. 构建动态 Prompt
SYSTEM_PROMPT_MINDMAP_ASSISTANT = "..." # 系统指令
@@ -179,7 +192,9 @@ llm_response = await generate_chat_completion(
assistant_response_content = llm_response["choices"][0]["message"]["content"]
markdown_syntax = self._extract_markdown_syntax(assistant_response_content)
```
**知识点**:
- **Prompt 工程**: 将系统指令和用户指令分离。在用户指令中动态注入上下文信息(如用户名、时间、语言),可以使 LLM 的输出更具个性化和准确性。
- **调用工具**: 使用 `open_webui.utils.chat.generate_chat_completion` 是与 Open WebUI 内置 LLM 服务交互的标准方式。
- **用户上下文**: 调用 `generate_chat_completion` 需要传递 `user_obj`,这可能用于权限控制、计费或模型特定的用户标识。通过 `open_webui.models.users.Users.get_user_by_id` 获取该对象。
@@ -192,6 +207,7 @@ markdown_syntax = self._extract_markdown_syntax(assistant_response_content)
Action 插件的一大亮点是能够生成 HTML从而在聊天界面中渲染丰富的交互式内容。
**代码示例 (`思维导图.py`):**
```python
# 1. 定义 HTML 模板
HTML_TEMPLATE_MINDMAP = """
@@ -227,9 +243,11 @@ final_html_content =
html_embed_tag = f"```html\n{final_html_content}\n```"
body["messages"][-1]["content"] = f"{long_text_content}\n\n{html_embed_tag}"
```
**知识点**:
- **HTML 模板**: 将静态 HTML/CSS/JS 代码定义为模板字符串,使用占位符(如 `{unique_id}`)来注入动态数据。
- **嵌入 JS**: 可以在 HTML 中直接嵌入 JavaScript 代码,用于处理前端交互逻辑,如渲染图表、绑定按钮事件等。`智绘心图` 的 JS 代码负责调用 Markmap.js 库来渲染思维导图,并实现了“复制 SVG”和“复制 Markdown”的按钮功能。
- **嵌入 JS**: 可以在 HTML 中直接嵌入 JavaScript 代码,用于处理前端交互逻辑,如渲染图表、绑定按钮事件等。`思维导图` 的 JS 代码负责调用 Markmap.js 库来渲染思维导图,并实现了“复制 SVG”和“复制 Markdown”的按钮功能。
- **唯一 ID**: 使用 `unique_id` 是一个好习惯,可以防止在同一页面上多次使用该插件时发生 DOM 元素 ID 冲突。
- **响应格式**: 最终的 HTML 内容需要被包裹在 ````html\n...\n```` 代码块中Open WebUI 的前端会自动识别并渲染它。
- **内容追加**: 插件将生成的 HTML 追加到原始用户输入之后,而不是替换它,保留了上下文。
@@ -241,6 +259,7 @@ body["messages"][-1]["content"] = f"{long_text_content}\n\n{html_embed_tag}"
一个生产级的插件必须具备良好的健壮性。
**代码示例 (`思维导图.py`):**
```python
# 输入验证
if len(long_text_content) < self.valves.MIN_TEXT_LENGTH:
@@ -251,8 +270,8 @@ if len(long_text_content) < self.valves.MIN_TEXT_LENGTH:
try:
# ... 核心逻辑 ...
except Exception as e:
error_message = f"智绘心图处理失败: {str(e)}"
logger.error(f"智绘心图错误: {error_message}", exc_info=True)
error_message = f"思维导图处理失败: {str(e)}"
logger.error(f"思维导图错误: {error_message}", exc_info=True)
# 向前端发送错误通知
if __event_emitter__:
@@ -267,7 +286,9 @@ logger = logging.getLogger(__name__)
logger.info("Action started")
logger.error("Error occurred", exc_info=True)
```
**知识点**:
- **输入验证**: 在执行核心逻辑前,对输入(如文本长度)进行检查,可以避免不必要的资源消耗和潜在错误。
- **`try...except` 块**: 将主要逻辑包裹在 `try` 块中,并捕获 `Exception`,确保任何意外失败都能被优雅地处理。
- **用户友好的错误反馈**: 在 `except` 块中,不仅要记录详细的错误日志(`logger.error`),还要通过 `EventEmitter` 和聊天消息向用户提供清晰、可操作的错误提示。
@@ -277,9 +298,10 @@ logger.error("Error occurred", exc_info=True)
### 总结
`智绘心图` 插件是一个优秀的 Open WebUI Action 开发学习案例。它全面展示了如何利用 Action 插件的各项功能,构建一个交互性强、用户体验好、功能完整且健壮的 AI 应用。
`思维导图` 插件是一个优秀的 Open WebUI Action 开发学习案例。它全面展示了如何利用 Action 插件的各项功能,构建一个交互性强、用户体验好、功能完整且健壮的 AI 应用。
**最佳实践总结**:
- **明确元数据**: 为你的插件提供清晰的 `title`, `icon`, `description`
- **提供配置**: 使用 `Valves` 让插件更灵活。
- **善用反馈**: 积极使用 `EventEmitter` 提供实时状态和通知。
@@ -288,4 +310,4 @@ logger.error("Error occurred", exc_info=True)
- **防御性编程**: 始终考虑输入验证和错误处理。
- **详细日志**: 记录日志是排查问题的关键。
通过学习和借鉴`智绘心图`的设计模式,开发者可以更高效地构建出属于自己的高质量 Open WebUI 插件。
通过学习和借鉴`思维导图`的设计模式,开发者可以更高效地构建出属于自己的高质量 Open WebUI 插件。

View File

@@ -8,7 +8,7 @@
| 插件名称 | 描述 | 版本 | 文档 |
| :----------- | :----------------------------------- | :---- | :---------------------------------------------------------------------------- |
| **智绘心** | 智能分析文本内容,生成交互式思维导图 | 0.7.2 | [中文](./smart-mind-map/README_CN.md) / [English](./smart-mind-map/README.md) |
| **思维导** | 智能分析文本内容,生成交互式思维导图 | 0.7.2 | [中文](./smart-mind-map/README_CN.md) / [English](./smart-mind-map/README.md) |
## 🎯 什么是动作插件?

View File

@@ -10,12 +10,12 @@ Smart Mind Map is a powerful OpenWebUI action plugin that intelligently analyzes
## Core Features
- **Intelligent Text Analysis**: Automatically identifies core themes, key concepts, and hierarchical structures
- **Interactive Visualization**: Generates beautiful interactive mind maps based on Markmap.js
- **Multi-language Support**: Automatically adjusts output based on user language
- **Real-time Rendering**: Renders mind maps directly in the chat interface without navigation
- **Export Capabilities**: Supports copying SVG code and Markdown source
- **Customizable Configuration**: Configurable LLM model, minimum text length, and other parameters
-**Intelligent Text Analysis**: Automatically identifies core themes, key concepts, and hierarchical structures
-**Interactive Visualization**: Generates beautiful interactive mind maps based on Markmap.js
-**Multi-language Support**: Automatically adjusts output based on user language
-**Real-time Rendering**: Renders mind maps directly in the chat interface without navigation
-**Export Capabilities**: Supports copying SVG code and Markdown source
-**Customizable Configuration**: Configurable LLM model, minimum text length, and other parameters
---
@@ -43,14 +43,24 @@ Smart Mind Map is a powerful OpenWebUI action plugin that intelligently analyzes
The plugin requires access to an LLM model for text analysis. Please ensure:
- Your OpenWebUI instance has at least one available LLM model configured
- Recommended to use fast, economical models (e.g., `gemini-2.5-flash`) for the best experience
- Configure the `LLM_MODEL_ID` parameter in the plugin settings
- Your OpenWebUI instance has at least one available LLM model configured
- Recommended to use fast, economical models (e.g., `gemini-2.5-flash`) for the best experience
- Configure the `LLM_MODEL_ID` parameter in the plugin settings
### 3. Plugin Activation
Select the "Smart Mind Map" action plugin in chat settings to enable it.
### 4. Theme Color Consistency (Optional)
To keep the mind map visually consistent with the OpenWebUI theme colors, enable same-origin access for artifacts in OpenWebUI:
- **Configuration Location**: In OpenWebUI User Settings: **Interface****Artifacts****iframe Sandbox Allow Same Origin**
- **Enable Option**: Check the "Allow same-origin access for artifacts" / "iframe sandbox allow-same-origin" option
- **Sandbox Attributes**: Ensure the iframe's sandbox attribute includes both `allow-same-origin` and `allow-scripts`
Once enabled, the mind map will automatically detect and apply the current OpenWebUI theme (light/dark) without any manual configuration.
---
## Configuration Parameters
@@ -77,6 +87,7 @@ You can adjust the following parameters in the plugin's settings (Valves):
### Usage Example
**Input Text:**
```
Artificial Intelligence (AI) is a branch of computer science dedicated to creating systems capable of performing tasks that typically require human intelligence.
Main application areas include:
@@ -102,20 +113,20 @@ Generated mind maps support two export methods:
### Frontend Rendering
- **Markmap.js**: Open-source mind mapping rendering engine
- **D3.js**: Data visualization foundation library
- **Responsive Design**: Adapts to different screen sizes
- **Markmap.js**: Open-source mind mapping rendering engine
- **D3.js**: Data visualization foundation library
- **Responsive Design**: Adapts to different screen sizes
### Backend Processing
- **LLM Integration**: Calls configured models via `generate_chat_completion`
- **Text Preprocessing**: Automatically filters HTML code blocks, extracts plain text content
- **Format Conversion**: Converts LLM output to Markmap-compatible Markdown format
- **LLM Integration**: Calls configured models via `generate_chat_completion`
- **Text Preprocessing**: Automatically filters HTML code blocks, extracts plain text content
- **Format Conversion**: Converts LLM output to Markmap-compatible Markdown format
### Security
- **XSS Protection**: Automatically escapes `</script>` tags to prevent script injection
- **Input Validation**: Checks text length to avoid invalid requests
- **XSS Protection**: Automatically escapes `</script>` tags to prevent script injection
- **Input Validation**: Checks text length to avoid invalid requests
---
@@ -124,72 +135,78 @@ Generated mind maps support two export methods:
### Issue: Plugin Won't Start
**Solution:**
- Check OpenWebUI logs for error messages
- Confirm the plugin is correctly uploaded and enabled
- Verify OpenWebUI version supports action plugins
- Check OpenWebUI logs for error messages
- Confirm the plugin is correctly uploaded and enabled
- Verify OpenWebUI version supports action plugins
### Issue: Text Content Too Short
**Symptom:** Prompt shows "Text content is too short for effective analysis"
**Solution:**
- Ensure input text contains at least 100 characters (default configuration)
- Lower the `MIN_TEXT_LENGTH` parameter value in plugin settings
- Provide more detailed, structured text content
- Ensure input text contains at least 100 characters (default configuration)
- Lower the `MIN_TEXT_LENGTH` parameter value in plugin settings
- Provide more detailed, structured text content
### Issue: Mind Map Not Generated
**Solution:**
- Check if `LLM_MODEL_ID` is configured correctly
- Confirm the configured model is available in OpenWebUI
- Review backend logs for LLM call failures
- Verify user has sufficient permissions to access the configured model
- Check if `LLM_MODEL_ID` is configured correctly
- Confirm the configured model is available in OpenWebUI
- Review backend logs for LLM call failures
- Verify user has sufficient permissions to access the configured model
### Issue: Mind Map Display Error
**Symptom:** Shows "⚠️ Mind map rendering failed"
**Solution:**
- Check browser console for error messages
- Confirm Markmap.js and D3.js libraries are loading correctly
- Verify generated Markdown format conforms to Markmap specifications
- Try refreshing the page to re-render
- Check browser console for error messages
- Confirm Markmap.js and D3.js libraries are loading correctly
- Verify generated Markdown format conforms to Markmap specifications
- Try refreshing the page to re-render
### Issue: Export Function Not Working
**Solution:**
- Confirm browser supports Clipboard API
- Check if browser is blocking clipboard access permissions
- Use modern browsers (Chrome, Firefox, Edge, etc.)
- Confirm browser supports Clipboard API
- Check if browser is blocking clipboard access permissions
- Use modern browsers (Chrome, Firefox, Edge, etc.)
---
## Best Practices
1. **Text Preparation**
- Provide text content with clear structure and distinct hierarchies
- Use paragraphs, lists, and other formatting to help LLM understand text structure
- Avoid excessively lengthy or unstructured text
- Provide text content with clear structure and distinct hierarchies
- Use paragraphs, lists, and other formatting to help LLM understand text structure
- Avoid excessively lengthy or unstructured text
2. **Model Selection**
- For daily use, recommend fast models like `gemini-2.5-flash`
- For complex text analysis, use more powerful models (e.g., GPT-4)
- Balance speed and analysis quality based on needs
- For daily use, recommend fast models like `gemini-2.5-flash`
- For complex text analysis, use more powerful models (e.g., GPT-4)
- Balance speed and analysis quality based on needs
3. **Performance Optimization**
- Set `MIN_TEXT_LENGTH` appropriately to avoid processing text that's too short
- For particularly long texts, consider summarizing before generating mind maps
- Disable `show_status` in production environments to reduce interface updates
- Set `MIN_TEXT_LENGTH` appropriately to avoid processing text that's too short
- For particularly long texts, consider summarizing before generating mind maps
- Disable `show_status` in production environments to reduce interface updates
---
## Changelog
### v0.7.2 (Current Version)
- Optimized text extraction logic, automatically filters HTML code blocks
- Improved error handling and user feedback
- Enhanced export functionality compatibility
- Optimized UI styling and interactive experience
- Optimized text extraction logic, automatically filters HTML code blocks
- Improved error handling and user feedback
- Enhanced export functionality compatibility
- Optimized UI styling and interactive experience
---
@@ -205,6 +222,6 @@ Welcome to submit issue reports and improvement suggestions! Please visit the pr
## Related Resources
- [Markmap Official Website](https://markmap.js.org/)
- [OpenWebUI Documentation](https://docs.openwebui.com/)
- [D3.js Official Website](https://d3js.org/)
- [Markmap Official Website](https://markmap.js.org/)
- [OpenWebUI Documentation](https://docs.openwebui.com/)
- [D3.js Official Website](https://d3js.org/)

View File

@@ -1,21 +1,21 @@
# 智绘心图 - 思维导图生成插件
# 思维导图 - 思维导图生成插件
**作者:** [Fu-Jie](https://github.com/Fu-Jie) | **版本:** 0.7.2 | **许可证:** MIT
> **重要提示**:为了确保所有插件的可维护性和易用性,每个插件都应附带清晰、完整的文档,以确保其功能、配置和使用方法得到充分说明。
智绘心图是一个强大的 OpenWebUI 动作插件,能够智能分析长篇文本内容,自动生成交互式思维导图,帮助用户结构化和可视化知识。
思维导图是一个强大的 OpenWebUI 动作插件,能够智能分析长篇文本内容,自动生成交互式思维导图,帮助用户结构化和可视化知识。
---
## 核心特性
- **智能文本分析**: 自动识别文本的核心主题、关键概念和层次结构
- **交互式可视化**: 基于 Markmap.js 生成美观的交互式思维导图
- **多语言支持**: 根据用户语言自动调整输出
- **实时渲染**: 在聊天界面中直接渲染思维导图,无需跳转
- **导出功能**: 支持复制 SVG 代码和 Markdown 源码
- **自定义配置**: 可配置 LLM 模型、最小文本长度等参数
-**智能文本分析**: 自动识别文本的核心主题、关键概念和层次结构
-**交互式可视化**: 基于 Markmap.js 生成美观的交互式思维导图
-**多语言支持**: 根据用户语言自动调整输出
-**实时渲染**: 在聊天界面中直接渲染思维导图,无需跳转
-**导出功能**: 支持复制 SVG 代码和 Markdown 源码
-**自定义配置**: 可配置 LLM 模型、最小文本长度等参数
---
@@ -43,13 +43,23 @@
插件需要访问 LLM 模型来分析文本。请确保:
- 您的 OpenWebUI 实例中配置了至少一个可用的 LLM 模型
- 推荐使用快速、经济的模型(如 `gemini-2.5-flash`)来获得最佳体验
- 在插件设置中配置 `LLM_MODEL_ID` 参数
- 您的 OpenWebUI 实例中配置了至少一个可用的 LLM 模型
- 推荐使用快速、经济的模型(如 `gemini-2.5-flash`)来获得最佳体验
- 在插件设置中配置 `LLM_MODEL_ID` 参数
### 3. 插件启用
在聊天设置中选择"智绘心图"动作插件即可启用。
在聊天设置中选择"思维导图"动作插件即可启用。
### 4. 主题颜色风格一致性(可选)
为了使思维导图与 OpenWebUI 主题颜色风格保持一致,需要在 OpenWebUI 中启用 artifact 的同源访问:
- **配置位置**:在 OpenWebUI 用户设置中找到"界面"→"产物"部分Settings → Interface → Products/Artifacts
- **启用选项**:勾选 "iframe 沙盒允许同源访问"Allow same-origin access for artifacts / iframe sandbox allow-same-origin
- **沙箱属性**:确保 iframe 的 sandbox 属性包含 `allow-same-origin``allow-scripts`
启用后,思维导图会自动检测并应用 OpenWebUI 的当前主题(亮色/暗色),无需手动配置。
---
@@ -69,7 +79,7 @@
### 基本使用
1. 在聊天设置中启用"智绘心图"动作
1. 在聊天设置中启用"思维导图"动作
2. 在对话中输入或粘贴长篇文本内容(至少 100 字符)
3. 发送消息后,插件会自动分析并生成思维导图
4. 思维导图将在聊天界面中直接渲染显示
@@ -77,6 +87,7 @@
### 使用示例
**输入文本:**
```
人工智能AI是计算机科学的一个分支致力于创建能够执行通常需要人类智能的任务的系统。
主要应用领域包括:
@@ -102,20 +113,20 @@
### 前端渲染
- **Markmap.js**: 开源的思维导图渲染引擎
- **D3.js**: 数据可视化基础库
- **响应式设计**: 适配不同屏幕尺寸
- **Markmap.js**: 开源的思维导图渲染引擎
- **D3.js**: 数据可视化基础库
- **响应式设计**: 适配不同屏幕尺寸
### 后端处理
- **LLM 集成**: 通过 `generate_chat_completion` 调用配置的模型
- **文本预处理**: 自动过滤 HTML 代码块,提取纯文本内容
- **格式转换**: 将 LLM 输出转换为 Markmap 兼容的 Markdown 格式
- **LLM 集成**: 通过 `generate_chat_completion` 调用配置的模型
- **文本预处理**: 自动过滤 HTML 代码块,提取纯文本内容
- **格式转换**: 将 LLM 输出转换为 Markmap 兼容的 Markdown 格式
### 安全性
- **XSS 防护**: 自动转义 `</script>` 标签,防止脚本注入
- **输入验证**: 检查文本长度,避免无效请求
- **XSS 防护**: 自动转义 `</script>` 标签,防止脚本注入
- **输入验证**: 检查文本长度,避免无效请求
---
@@ -124,72 +135,78 @@
### 问题:插件无法启动
**解决方案:**
- 检查 OpenWebUI 日志,查看是否有错误信息
- 确认插件已正确上传并启用
- 验证 OpenWebUI 版本是否支持动作插件
- 检查 OpenWebUI 日志,查看是否有错误信息
- 确认插件已正确上传并启用
- 验证 OpenWebUI 版本是否支持动作插件
### 问题:文本内容过短
**现象:** 提示"文本内容过短,无法进行有效分析"
**解决方案:**
- 确保输入的文本至少包含 100 个字符(默认配置)
- 可以在插件设置中降低 `MIN_TEXT_LENGTH` 参数值
- 提供更详细、结构化的文本内容
- 确保输入的文本至少包含 100 个字符(默认配置)
- 可以在插件设置中降低 `MIN_TEXT_LENGTH` 参数值
- 提供更详细、结构化的文本内容
### 问题:思维导图未生成
**解决方案:**
- 检查 `LLM_MODEL_ID` 是否配置正确
- 确认配置的模型在 OpenWebUI 中可用
- 查看后端日志,检查是否有 LLM 调用失败的错误
- 验证用户是否有足够的权限访问配置的模型
- 检查 `LLM_MODEL_ID` 是否配置正确
- 确认配置的模型在 OpenWebUI 中可用
- 查看后端日志,检查是否有 LLM 调用失败的错误
- 验证用户是否有足够的权限访问配置的模型
### 问题:思维导图显示错误
**现象:** 显示"⚠️ 思维导图渲染失败"
**解决方案:**
- 检查浏览器控制台的错误信息
- 确认 Markmap.js 和 D3.js 库是否正确加载
- 验证生成的 Markdown 格式是否符合 Markmap 规范
- 尝试刷新页面重新渲染
- 检查浏览器控制台的错误信息
- 确认 Markmap.js 和 D3.js 库是否正确加载
- 验证生成的 Markdown 格式是否符合 Markmap 规范
- 尝试刷新页面重新渲染
### 问题:导出功能不工作
**解决方案:**
- 确认浏览器支持剪贴板 API
- 检查浏览器是否阻止了剪贴板访问权限
- 使用现代浏览器Chrome、Firefox、Edge 等)
- 确认浏览器支持剪贴板 API
- 检查浏览器是否阻止了剪贴板访问权限
- 使用现代浏览器Chrome、Firefox、Edge 等)
---
## 最佳实践
1. **文本准备**
- 提供结构清晰、层次分明的文本内容
- 使用段落、列表等格式帮助 LLM 理解文本结构
- 避免过于冗长或无结构的文本
- 提供结构清晰、层次分明的文本内容
- 使用段落、列表等格式帮助 LLM 理解文本结构
- 避免过于冗长或无结构的文本
2. **模型选择**
- 对于日常使用,推荐 `gemini-2.5-flash` 等快速模型
- 对于复杂文本分析,可以使用更强大的模型(如 GPT-4
- 根据需求平衡速度和分析质量
- 对于日常使用,推荐 `gemini-2.5-flash` 等快速模型
- 对于复杂文本分析,可以使用更强大的模型(如 GPT-4
- 根据需求平衡速度和分析质量
3. **性能优化**
- 合理设置 `MIN_TEXT_LENGTH`,避免处理过短的文本
- 对于特别长的文本,考虑先进行摘要再生成思维导图
- 在生产环境中关闭 `show_status` 以减少界面更新
- 合理设置 `MIN_TEXT_LENGTH`,避免处理过短的文本
- 对于特别长的文本,考虑先进行摘要再生成思维导图
- 在生产环境中关闭 `show_status` 以减少界面更新
---
## 更新日志
### v0.7.2 (当前版本)
- 优化文本提取逻辑,自动过滤 HTML 代码块
- 改进错误处理和用户反馈
- 增强导出功能的兼容性
- 优化 UI 样式和交互体验
- 优化文本提取逻辑,自动过滤 HTML 代码块
- 改进错误处理和用户反馈
- 增强导出功能的兼容性
- 优化 UI 样式和交互体验
---
@@ -205,6 +222,6 @@
## 相关资源
- [Markmap 官方网站](https://markmap.js.org/)
- [OpenWebUI 文档](https://docs.openwebui.com/)
- [D3.js 官方网站](https://d3js.org/)
- [Markmap 官方网站](https://markmap.js.org/)
- [OpenWebUI 文档](https://docs.openwebui.com/)
- [D3.js 官方网站](https://d3js.org/)

View File

@@ -1,18 +1,20 @@
"""
title: 智绘心
title: 思维导
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4=
version: 0.7.4
version: 0.8.0
description: 智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。
"""
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any
import logging
import time
import os
import re
import time
from datetime import datetime, timezone
from typing import Any, Dict, Optional
from zoneinfo import ZoneInfo
from fastapi import Request
from datetime import datetime
import pytz
from pydantic import BaseModel, Field
from open_webui.utils.chat import generate_chat_completion
from open_webui.models.users import Users
@@ -75,24 +77,20 @@ HTML_WRAPPER_TEMPLATE = """
}
#main-container {
display: flex;
flex-wrap: wrap;
flex-direction: column;
gap: 20px;
align-items: flex-start;
align-items: stretch;
width: 100%;
}
.plugin-item {
flex: 1 1 400px; /* 默认宽度,允许伸缩 */
min-width: 300px;
width: 100%;
border-radius: 12px;
overflow: hidden;
overflow: visible;
transition: all 0.3s ease;
}
.plugin-item:hover {
transform: translateY(-2px);
}
@media (max-width: 768px) {
.plugin-item { flex: 1 1 100%; }
}
/* STYLES_INSERTION_POINT */
</style>
</head>
@@ -115,13 +113,24 @@ CSS_TEMPLATE_MINDMAP = """
--muted-text-color: #546e7a;
--border-color: #e0e0e0;
--header-gradient: linear-gradient(135deg, var(--secondary-color), var(--primary-color));
--shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
--shadow: 0 10px 20px rgba(0, 0, 0, 0.06);
--border-radius: 12px;
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
.theme-dark {
--primary-color: #64b5f6;
--secondary-color: #81c784;
--background-color: #111827;
--card-bg-color: #1f2937;
--text-color: #e5e7eb;
--muted-text-color: #9ca3af;
--border-color: #374151;
--header-gradient: linear-gradient(135deg, #0ea5e9, #22c55e);
--shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
}
.mindmap-container-wrapper {
font-family: var(--font-family);
line-height: 1.7;
line-height: 1.6;
color: var(--text-color);
margin: 0;
padding: 0;
@@ -130,99 +139,110 @@ CSS_TEMPLATE_MINDMAP = """
height: 100%;
display: flex;
flex-direction: column;
background: var(--background-color);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
}
.header {
background: var(--header-gradient);
color: white;
padding: 20px 24px;
padding: 18px 20px;
text-align: center;
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
}
.header h1 {
margin: 0;
font-size: 1.5em;
font-size: 1.4em;
font-weight: 600;
text-shadow: 0 1px 3px rgba(0,0,0,0.2);
letter-spacing: 0.3px;
}
.user-context {
font-size: 0.8em;
font-size: 0.85em;
color: var(--muted-text-color);
background-color: #eceff1;
padding: 8px 16px;
background-color: rgba(255, 255, 255, 0.6);
padding: 8px 14px;
display: flex;
justify-content: space-around;
justify-content: space-between;
flex-wrap: wrap;
border-bottom: 1px solid var(--border-color);
gap: 6px;
}
.user-context span { margin: 2px 8px; }
.content-area {
padding: 20px;
.theme-dark .user-context {
background-color: rgba(31, 41, 55, 0.7);
}
.user-context span { margin: 2px 6px; }
.content-area {
padding: 16px;
flex-grow: 1;
background: var(--card-bg-color);
}
.markmap-container {
position: relative;
background-color: #fff;
background-image: radial-gradient(var(--border-color) 0.5px, transparent 0.5px);
background-size: 20px 20px;
border-radius: 8px;
padding: 16px;
background-color: var(--card-bg-color);
border-radius: 10px;
padding: 12px;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid var(--border-color);
box-shadow: inset 0 2px 6px rgba(0,0,0,0.03);
width: 100%;
min-height: 60vh;
overflow: visible;
}
.download-area {
text-align: center;
padding-top: 20px;
margin-top: 20px;
border-top: 1px solid var(--border-color);
.control-rows {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin-top: 12px;
}
.download-btn {
.btn-group {
display: inline-flex;
gap: 6px;
align-items: center;
}
.control-btn {
background-color: var(--primary-color);
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
padding: 8px 12px;
border-radius: 8px;
font-size: 0.9em;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease-in-out;
margin: 0 6px;
transition: background-color 0.15s ease, transform 0.15s ease;
display: inline-flex;
align-items: center;
gap: 6px;
}
.download-btn.secondary {
background-color: var(--secondary-color);
}
.download-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.download-btn.copied {
background-color: #2e7d32;
}
.control-btn.secondary { background-color: var(--secondary-color); }
.control-btn.neutral { background-color: #64748b; }
.control-btn:hover { transform: translateY(-1px); }
.control-btn.copied { background-color: #2e7d32; }
.control-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.footer {
text-align: center;
padding: 16px;
font-size: 0.8em;
color: #90a4ae;
background-color: #eceff1;
padding: 12px;
font-size: 0.85em;
color: var(--muted-text-color);
background-color: var(--card-bg-color);
border-top: 1px solid var(--border-color);
border-bottom-left-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
}
.footer a {
color: var(--primary-color);
text-decoration: none;
font-weight: 500;
}
.footer a:hover {
text-decoration: underline;
}
.footer a:hover { text-decoration: underline; }
.error-message {
color: #c62828;
background-color: #ffcdd2;
border: 1px solid #ef9a9a;
padding: 16px;
padding: 14px;
border-radius: 8px;
font-weight: 500;
font-size: 1em;
@@ -240,15 +260,29 @@ CONTENT_TEMPLATE_MINDMAP = """
</div>
<div class="content-area">
<div class="markmap-container" id="markmap-container-{unique_id}"></div>
<div class="download-area">
<button id="download-svg-btn-{unique_id}" class="download-btn">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
<span class="btn-text">SVG</span>
</button>
<button id="download-md-btn-{unique_id}" class="download-btn secondary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
<span class="btn-text">Markdown</span>
</button>
<div class="control-rows">
<div class="btn-group">
<button id="download-png-btn-{unique_id}" class="control-btn secondary">
<span class="btn-text">PNG</span>
</button>
<button id="download-svg-btn-{unique_id}" class="control-btn">
<span class="btn-text">SVG</span>
</button>
<button id="download-md-btn-{unique_id}" class="control-btn neutral">
<span class="btn-text">Markdown</span>
</button>
</div>
<div class="btn-group">
<button id="zoom-out-btn-{unique_id}" class="control-btn neutral" title="缩小">-</button>
<button id="zoom-reset-btn-{unique_id}" class="control-btn neutral" title="重置">重置</button>
<button id="zoom-in-btn-{unique_id}" class="control-btn neutral" title="放大">+</button>
</div>
<div class="btn-group">
<button id="expand-all-btn-{unique_id}" class="control-btn secondary">展开全部</button>
<button id="collapse-all-btn-{unique_id}" class="control-btn neutral">折叠</button>
<button id="fullscreen-btn-{unique_id}" class="control-btn">全屏</button>
<button id="theme-toggle-btn-{unique_id}" class="control-btn neutral">主题</button>
</div>
</div>
</div>
<div class="footer">
@@ -260,13 +294,140 @@ CONTENT_TEMPLATE_MINDMAP = """
"""
SCRIPT_TEMPLATE_MINDMAP = """
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script src="https://cdn.jsdelivr.net/npm/markmap-lib@0.17"></script>
<script src="https://cdn.jsdelivr.net/npm/markmap-view@0.17"></script>
<script>
(function() {
const uniqueId = "{unique_id}";
const loadScriptOnce = (src, checkFn) => {
if (checkFn()) return Promise.resolve();
return new Promise((resolve, reject) => {
const existing = document.querySelector(`script[data-src="${src}"]`);
if (existing) {
existing.addEventListener('load', () => resolve());
existing.addEventListener('error', () => reject(new Error('加载失败: ' + src)));
return;
}
const script = document.createElement('script');
script.src = src;
script.async = true;
script.dataset.src = src;
script.onload = () => resolve();
script.onerror = () => reject(new Error('加载失败: ' + src));
document.head.appendChild(script);
});
};
const ensureMarkmapReady = () =>
loadScriptOnce('https://cdn.jsdelivr.net/npm/d3@7', () => window.d3)
.then(() => loadScriptOnce('https://cdn.jsdelivr.net/npm/markmap-lib@0.17', () => window.markmap && window.markmap.Transformer))
.then(() => loadScriptOnce('https://cdn.jsdelivr.net/npm/markmap-view@0.17', () => window.markmap && window.markmap.Markmap));
const parseColorLuma = (colorStr) => {
if (!colorStr) return null;
// hex #rrggbb or rrggbb
let m = colorStr.match(/^#?([0-9a-f]{6})$/i);
if (m) {
const hex = m[1];
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
}
// rgb(r, g, b) or rgba(r, g, b, a)
m = colorStr.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
if (m) {
const r = parseInt(m[1], 10);
const g = parseInt(m[2], 10);
const b = parseInt(m[3], 10);
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
}
return null;
};
const getThemeFromMeta = (doc, scope = 'self') => {
const metas = Array.from((doc || document).querySelectorAll('meta[name="theme-color"]'));
console.log(`[mindmap ${uniqueId}] [${scope}] meta theme-color count: ${metas.length}`);
if (!metas.length) return null;
const color = metas[metas.length - 1].content.trim();
console.log(`[mindmap ${uniqueId}] [${scope}] meta theme-color picked: "${color}"`);
const luma = parseColorLuma(color);
if (luma === null) {
console.log(`[mindmap ${uniqueId}] [${scope}] meta theme-color invalid format, skip.`);
return null;
}
const inferred = luma < 0.5 ? 'dark' : 'light';
console.log(`[mindmap ${uniqueId}] [${scope}] meta theme-color luma=${luma.toFixed(3)}, inferred=${inferred}`);
return inferred;
};
const getParentDocumentSafe = () => {
try {
if (!window.parent || window.parent === window) {
console.log(`[mindmap ${uniqueId}] no parent window or same as self`);
return null;
}
const pDoc = window.parent.document;
// Access a property to trigger potential DOMException on cross-origin
void pDoc.title;
console.log(`[mindmap ${uniqueId}] parent document accessible, title="${pDoc.title}"`);
return pDoc;
} catch (err) {
console.log(`[mindmap ${uniqueId}] parent document not accessible: ${err.name} - ${err.message}`);
return null;
}
};
const getThemeFromParentClass = () => {
try {
if (!window.parent || window.parent === window) return null;
const pDoc = window.parent.document;
const html = pDoc.documentElement;
const body = pDoc.body;
const htmlClass = html ? html.className : '';
const bodyClass = body ? body.className : '';
const htmlDataTheme = html ? html.getAttribute('data-theme') : '';
console.log(`[mindmap ${uniqueId}] parent html.class="${htmlClass}", body.class="${bodyClass}", data-theme="${htmlDataTheme}"`);
if (htmlDataTheme === 'dark' || bodyClass.includes('dark') || htmlClass.includes('dark')) return 'dark';
if (htmlDataTheme === 'light' || bodyClass.includes('light') || htmlClass.includes('light')) return 'light';
return null;
} catch (err) {
console.log(`[mindmap ${uniqueId}] parent class not accessible: ${err.name}`);
return null;
}
};
const getThemeFromBodyBg = () => {
try {
const bg = getComputedStyle(document.body).backgroundColor;
console.log(`[mindmap ${uniqueId}] self body bg: "${bg}"`);
const luma = parseColorLuma(bg);
if (luma !== null) {
const inferred = luma < 0.5 ? 'dark' : 'light';
console.log(`[mindmap ${uniqueId}] body bg luma=${luma.toFixed(3)}, inferred=${inferred}`);
return inferred;
}
} catch (err) {
console.log(`[mindmap ${uniqueId}] body bg detection error: ${err}`);
}
return null;
};
const setTheme = (wrapperEl, explicitTheme) => {
console.log(`[mindmap ${uniqueId}] --- theme detection start ---`);
const parentDoc = getParentDocumentSafe();
const metaThemeParent = parentDoc ? getThemeFromMeta(parentDoc, 'parent') : null;
const parentClassTheme = getThemeFromParentClass();
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
// Priority: explicit > metaParent > parentClass > prefers-color-scheme
const chosen = explicitTheme || metaThemeParent || parentClassTheme || (prefersDark ? 'dark' : 'light');
console.log(`[mindmap ${uniqueId}] setTheme -> explicit=${explicitTheme || 'none'}, metaParent=${metaThemeParent || 'none'}, parentClass=${parentClassTheme || 'none'}, prefersDark=${prefersDark}, chosen=${chosen}`);
console.log(`[mindmap ${uniqueId}] --- theme detection end ---`);
wrapperEl.classList.toggle('theme-dark', chosen === 'dark');
return chosen;
};
const renderMindmap = () => {
const uniqueId = "{unique_id}";
const containerEl = document.getElementById('markmap-container-' + uniqueId);
if (!containerEl || containerEl.dataset.markmapRendered) return;
@@ -279,67 +440,73 @@ SCRIPT_TEMPLATE_MINDMAP = """
return;
}
try {
const svgEl = document.createElementNS("http://www.w3.org/2000/svg", "svg");
ensureMarkmapReady().then(() => {
const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svgEl.style.width = '100%';
svgEl.style.height = 'auto';
svgEl.style.minHeight = '300px';
containerEl.innerHTML = '';
svgEl.style.height = '100%';
svgEl.style.minHeight = '60vh';
containerEl.innerHTML = '';
containerEl.appendChild(svgEl);
const { Transformer, Markmap } = window.markmap;
const transformer = new Transformer();
const { root } = transformer.transform(markdownContent);
const style = (id) => `${id} text { font-size: 14px !important; }`;
const options = {
const options = {
autoFit: true,
style: style
style: style,
initialExpandLevel: Infinity
};
Markmap.create(svgEl, options, root);
containerEl.dataset.markmapRendered = 'true';
attachDownloadHandlers(uniqueId);
} catch (error) {
console.error('Markmap rendering error:', error);
containerEl.innerHTML = '<div class="error-message">⚠️ 思维导图渲染失败!<br>原因:' + error.message + '</div>';
}
const markmapInstance = Markmap.create(svgEl, options, root);
containerEl.dataset.markmapRendered = 'true';
setupControls({
containerEl,
svgEl,
markmapInstance,
root,
});
}).catch((error) => {
console.error('Markmap loading error:', error);
containerEl.innerHTML = '<div class="error-message">⚠️ 资源加载失败,请稍后重试。</div>';
});
};
const attachDownloadHandlers = (uniqueId) => {
const setupControls = ({ containerEl, svgEl, markmapInstance, root }) => {
const downloadSvgBtn = document.getElementById('download-svg-btn-' + uniqueId);
const downloadPngBtn = document.getElementById('download-png-btn-' + uniqueId);
const downloadMdBtn = document.getElementById('download-md-btn-' + uniqueId);
const containerEl = document.getElementById('markmap-container-' + uniqueId);
const zoomInBtn = document.getElementById('zoom-in-btn-' + uniqueId);
const zoomOutBtn = document.getElementById('zoom-out-btn-' + uniqueId);
const zoomResetBtn = document.getElementById('zoom-reset-btn-' + uniqueId);
const expandAllBtn = document.getElementById('expand-all-btn-' + uniqueId);
const collapseAllBtn = document.getElementById('collapse-all-btn-' + uniqueId);
const fullscreenBtn = document.getElementById('fullscreen-btn-' + uniqueId);
const themeToggleBtn = document.getElementById('theme-toggle-btn-' + uniqueId);
const showFeedback = (button, isSuccess) => {
const buttonText = button.querySelector('.btn-text');
const wrapper = containerEl.closest('.mindmap-container-wrapper');
let currentTheme = setTheme(wrapper);
const showFeedback = (button, textOk = '完成', textFail = '失败') => {
if (!button) return;
const buttonText = button.querySelector('.btn-text') || button;
const originalText = buttonText.textContent;
button.disabled = true;
if (isSuccess) {
buttonText.textContent = '';
button.classList.add('copied');
} else {
buttonText.textContent = '';
}
buttonText.textContent = textOk;
button.classList.add('copied');
setTimeout(() => {
buttonText.textContent = originalText;
button.disabled = false;
button.classList.remove('copied');
}, 2500);
}, 1800);
};
const copyToClipboard = (content, button) => {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(content).then(() => {
showFeedback(button, true);
}, () => {
showFeedback(button, false);
});
navigator.clipboard.writeText(content).then(() => showFeedback(button), () => showFeedback(button, '失败', '失败'));
} else {
const textArea = document.createElement('textarea');
textArea.value = content;
@@ -350,32 +517,128 @@ SCRIPT_TEMPLATE_MINDMAP = """
textArea.select();
try {
document.execCommand('copy');
showFeedback(button, true);
showFeedback(button);
} catch (err) {
showFeedback(button, false);
showFeedback(button, '失败', '失败');
}
document.body.removeChild(textArea);
}
};
if (downloadSvgBtn) {
downloadSvgBtn.addEventListener('click', (event) => {
event.stopPropagation();
const svgEl = containerEl.querySelector('svg');
if (svgEl) {
const svgData = new XMLSerializer().serializeToString(svgEl);
copyToClipboard(svgData, downloadSvgBtn);
}
});
}
const handleDownloadSVG = () => {
const svg = containerEl.querySelector('svg');
if (!svg) return;
const svgData = new XMLSerializer().serializeToString(svg);
copyToClipboard(svgData, downloadSvgBtn);
};
if (downloadMdBtn) {
downloadMdBtn.addEventListener('click', (event) => {
event.stopPropagation();
const markdownContent = document.getElementById('markdown-source-' + uniqueId).textContent;
copyToClipboard(markdownContent, downloadMdBtn);
});
}
const handleDownloadMD = () => {
const markdownContent = document.getElementById('markdown-source-' + uniqueId)?.textContent || '';
if (!markdownContent) return;
copyToClipboard(markdownContent, downloadMdBtn);
};
const handleDownloadPNG = () => {
const svg = containerEl.querySelector('svg');
if (!svg) return;
const serializer = new XMLSerializer();
const svgData = serializer.serializeToString(svg);
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(svgBlob);
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const rect = svg.getBoundingClientRect();
canvas.width = Math.max(rect.width, 1200);
canvas.height = Math.max(rect.height, 800);
const ctx = canvas.getContext('2d');
ctx.fillStyle = getComputedStyle(containerEl).getPropertyValue('--card-bg-color') || '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
canvas.toBlob((blob) => {
if (!blob) return;
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'mindmap.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
showFeedback(downloadPngBtn);
}, 'image/png');
URL.revokeObjectURL(url);
};
img.onerror = () => showFeedback(downloadPngBtn, '失败', '失败');
img.src = url;
};
let baseTransform = '';
let currentScale = 1;
const minScale = 0.6;
const maxScale = 2.4;
const step = 0.2;
const updateBaseTransform = () => {
const g = svgEl.querySelector('g');
if (g) {
baseTransform = g.getAttribute('transform') || 'translate(0,0)';
}
};
const applyScale = () => {
const g = svgEl.querySelector('g');
if (!g) return;
const translatePart = (baseTransform.match(/translate\([^)]*\)/) || ['translate(0,0)'])[0];
g.setAttribute('transform', `${translatePart} scale(${currentScale})`);
};
const handleZoom = (direction) => {
if (direction === 'reset') {
currentScale = 1;
markmapInstance.fit();
updateBaseTransform();
applyScale();
return;
}
currentScale = Math.min(maxScale, Math.max(minScale, currentScale + (direction === 'in' ? step : -step)));
applyScale();
};
const handleExpand = (level) => {
markmapInstance.setOptions({ initialExpandLevel: level });
markmapInstance.setData(root);
markmapInstance.fit();
currentScale = 1;
updateBaseTransform();
applyScale();
};
const handleFullscreen = () => {
const el = containerEl;
if (!document.fullscreenElement) {
(el.requestFullscreen && el.requestFullscreen());
} else {
document.exitFullscreen && document.exitFullscreen();
}
};
const handleThemeToggle = () => {
currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
setTheme(wrapper, currentTheme);
};
updateBaseTransform();
downloadSvgBtn?.addEventListener('click', (e) => { e.stopPropagation(); handleDownloadSVG(); });
downloadMdBtn?.addEventListener('click', (e) => { e.stopPropagation(); handleDownloadMD(); });
downloadPngBtn?.addEventListener('click', (e) => { e.stopPropagation(); handleDownloadPNG(); });
zoomInBtn?.addEventListener('click', (e) => { e.stopPropagation(); handleZoom('in'); });
zoomOutBtn?.addEventListener('click', (e) => { e.stopPropagation(); handleZoom('out'); });
zoomResetBtn?.addEventListener('click', (e) => { e.stopPropagation(); handleZoom('reset'); });
expandAllBtn?.addEventListener('click', (e) => { e.stopPropagation(); handleExpand(Infinity); });
collapseAllBtn?.addEventListener('click', (e) => { e.stopPropagation(); handleExpand(1); });
fullscreenBtn?.addEventListener('click', (e) => { e.stopPropagation(); handleFullscreen(); });
themeToggleBtn?.addEventListener('click', (e) => { e.stopPropagation(); handleThemeToggle(); });
};
if (document.readyState === 'loading') {
@@ -422,6 +685,21 @@ class Action:
"Sunday": "星期日",
}
def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
"""Extract basic user context with safe fallbacks."""
if isinstance(__user__, (list, tuple)):
user_data = __user__[0] if __user__ else {}
elif isinstance(__user__, dict):
user_data = __user__
else:
user_data = {}
return {
"user_id": user_data.get("id", "unknown_user"),
"user_name": user_data.get("name", "用户"),
"user_language": user_data.get("language", "zh-CN"),
}
def _extract_markdown_syntax(self, llm_output: str) -> str:
match = re.search(r"```markdown\s*(.*?)\s*```", llm_output, re.DOTALL)
if match:
@@ -516,33 +794,21 @@ class Action:
__event_emitter__: Optional[Any] = None,
__request__: Optional[Request] = None,
) -> Optional[dict]:
logger.info("Action: 智绘心图 (v12 - Final Feedback Fix) started")
if isinstance(__user__, (list, tuple)):
user_language = (
__user__[0].get("language", "zh-CN") if __user__ else "zh-CN"
)
user_name = __user__[0].get("name", "用户") if __user__[0] else "用户"
user_id = (
__user__[0]["id"]
if __user__ and "id" in __user__[0]
else "unknown_user"
)
elif isinstance(__user__, dict):
user_language = __user__.get("language", "zh-CN")
user_name = __user__.get("name", "用户")
user_id = __user__.get("id", "unknown_user")
logger.info("Action: 思维导图 (v12 - Final Feedback Fix) started")
user_ctx = self._get_user_context(__user__)
user_language = user_ctx["user_language"]
user_name = user_ctx["user_name"]
user_id = user_ctx["user_id"]
try:
shanghai_tz = pytz.timezone("Asia/Shanghai")
current_datetime_shanghai = datetime.now(shanghai_tz)
current_date_time_str = current_datetime_shanghai.strftime(
"%Y年%m月%d%H:%M:%S"
)
current_weekday_en = current_datetime_shanghai.strftime("%A")
tz_env = os.environ.get("TZ")
tzinfo = ZoneInfo(tz_env) if tz_env else None
now_dt = datetime.now(tzinfo or timezone.utc)
current_date_time_str = now_dt.strftime("%Y年%m月%d%H:%M:%S")
current_weekday_en = now_dt.strftime("%A")
current_weekday_zh = self.weekday_map.get(current_weekday_en, "未知星期")
current_year = current_datetime_shanghai.strftime("%Y")
current_timezone_str = "Asia/Shanghai"
current_year = now_dt.strftime("%Y")
current_timezone_str = tz_env or "UTC"
except Exception as e:
logger.warning(f"获取时区信息失败: {e},使用默认值。")
now = datetime.now()
@@ -552,7 +818,7 @@ class Action:
current_timezone_str = "未知时区"
await self._emit_notification(
__event_emitter__, "智绘心图已启动,正在为您生成思维导图...", "info"
__event_emitter__, "思维导图已启动,正在为您生成思维导图...", "info"
)
messages = body.get("messages")
@@ -612,7 +878,7 @@ class Action:
}
await self._emit_status(
__event_emitter__, "智绘心图: 深入分析文本结构...", False
__event_emitter__, "思维导图: 深入分析文本结构...", False
)
try:
@@ -708,23 +974,23 @@ class Action:
html_embed_tag = f"```html\n{final_html}\n```"
body["messages"][-1]["content"] = f"{long_text_content}\n\n{html_embed_tag}"
await self._emit_status(__event_emitter__, "智绘心图: 绘制完成!", True)
await self._emit_status(__event_emitter__, "思维导图: 绘制完成!", True)
await self._emit_notification(
__event_emitter__, f"思维导图已生成,{user_name}", "success"
)
logger.info("Action: 智绘心图 (v12) completed successfully")
logger.info("Action: 思维导图 (v12) completed successfully")
except Exception as e:
error_message = f"智绘心图处理失败: {str(e)}"
logger.error(f"智绘心图错误: {error_message}", exc_info=True)
user_facing_error = f"抱歉,智绘心图在处理时遇到错误: {str(e)}\n请检查Open WebUI后端日志获取更多详情。"
error_message = f"思维导图处理失败: {str(e)}"
logger.error(f"思维导图错误: {error_message}", exc_info=True)
user_facing_error = f"抱歉,思维导图在处理时遇到错误: {str(e)}\n请检查Open WebUI后端日志获取更多详情。"
body["messages"][-1][
"content"
] = f"{long_text_content}\n\n❌ **错误:** {user_facing_error}"
await self._emit_status(__event_emitter__, "智绘心图: 处理失败。", True)
await self._emit_status(__event_emitter__, "思维导图: 处理失败。", True)
await self._emit_notification(
__event_emitter__, f"智绘心图生成失败, {user_name}", "error"
__event_emitter__, f"思维导图生成失败, {user_name}", "error"
)
return body

View File

@@ -5,28 +5,34 @@
这12个插件为 OpenWebUI 带来了全方位的功能增强,显著提升了用户体验和生产力:
### 📊 可视化能力增强
- **智能信息图**:将文本内容自动转换为专业的 AntV 可视化图表,支持多种模板(流程图、对比图、象限图等),并可导出 SVG/PNG/HTML
- **思维导图**:基于 Markmap 的交互式思维导图生成,帮助结构化知识和可视化思维
### 💾 数据处理能力
- **Excel 导出**:一键将对话中的 Markdown 表格导出为符合中国规范的 Excel 文件,自动识别数据类型并应用合适的对齐和格式
- **多模态文件处理**:支持 PDF、Office 文档、音视频等多种格式的智能分析,自动上传并调用 Gemini 进行内容理解
### 🧠 学习与分析增强
- **闪记卡**:快速提炼文本核心要点为精美的记忆卡片,支持分类标签和关键点提取
- **精读分析**:深度分析长文本,自动生成摘要、关键信息点和可执行的行动建议
### ⚡ 性能与上下文优化
- **异步上下文压缩**:自动压缩对话历史并生成摘要,支持数据库持久化,有效管理超长对话
- **上下文增强**自动注入环境变量、优化模型功能适配、智能清洗输出内容修复代码块、LaTeX 等)
- **多模型回答合并**:将多个 AI 模型的回答合并为统一上下文,提升 MoE模型混合专家场景的效果
### 🎬 专业场景支持
- **字幕增强**:自动识别视频+字幕需求,调用专门的字幕精修专家生成高质量 SRT 字幕
- **智能路由**:根据模型类型自动选择最佳处理方式(直连 Gemini 或通过分析器)
- **提示词优化**MoE 场景下自动优化提示词,提取原始问题和各模型回答
### 🔧 开发者体验
- **数据库去重**:自动记录已分析文件,避免重复处理,节省资源
- **会话持久化**:基于 Chat ID 维护跨多轮对话的上下文
- **智能追问**:支持针对已上传文档的纯文本追问,无需重复上传
@@ -37,7 +43,7 @@
1. **📊 智能信息图 (infographic/信息图.py)** - 基于 AntV Infographic 的智能信息图生成插件,支持多种专业模板与 SVG/PNG 下载
2. **🧠 智绘心图 (smart-mind-map/思维导图.py)** - 智能分析文本内容生成交互式思维导图,帮助用户结构化和可视化知识
2. **🧠 思维导图 (smart-mind-map/思维导图.py)** - 智能分析文本内容生成交互式思维导图,帮助用户结构化和可视化知识
3. **📊 导出为 Excel (export_to_excel/导出为Excel.py)** - 将对话历史中的 Markdown 表格导出为符合中国规范的 Excel 文件
@@ -68,6 +74,7 @@
---
**统计信息:**
- Actions: 5个插件
- Filters: 5个插件
- Pipelines: 1个插件