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

@@ -51,6 +51,16 @@ The plugin requires access to an LLM model for text analysis. Please ensure:
Select the "Smart Mind Map" action plugin in chat settings to enable it. 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 ## Configuration Parameters
@@ -77,6 +87,7 @@ You can adjust the following parameters in the plugin's settings (Valves):
### Usage Example ### Usage Example
**Input Text:** **Input Text:**
``` ```
Artificial Intelligence (AI) is a branch of computer science dedicated to creating systems capable of performing tasks that typically require human intelligence. 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: Main application areas include:
@@ -124,6 +135,7 @@ Generated mind maps support two export methods:
### Issue: Plugin Won't Start ### Issue: Plugin Won't Start
**Solution:** **Solution:**
- Check OpenWebUI logs for error messages - Check OpenWebUI logs for error messages
- Confirm the plugin is correctly uploaded and enabled - Confirm the plugin is correctly uploaded and enabled
- Verify OpenWebUI version supports action plugins - Verify OpenWebUI version supports action plugins
@@ -133,6 +145,7 @@ Generated mind maps support two export methods:
**Symptom:** Prompt shows "Text content is too short for effective analysis" **Symptom:** Prompt shows "Text content is too short for effective analysis"
**Solution:** **Solution:**
- Ensure input text contains at least 100 characters (default configuration) - Ensure input text contains at least 100 characters (default configuration)
- Lower the `MIN_TEXT_LENGTH` parameter value in plugin settings - Lower the `MIN_TEXT_LENGTH` parameter value in plugin settings
- Provide more detailed, structured text content - Provide more detailed, structured text content
@@ -140,6 +153,7 @@ Generated mind maps support two export methods:
### Issue: Mind Map Not Generated ### Issue: Mind Map Not Generated
**Solution:** **Solution:**
- Check if `LLM_MODEL_ID` is configured correctly - Check if `LLM_MODEL_ID` is configured correctly
- Confirm the configured model is available in OpenWebUI - Confirm the configured model is available in OpenWebUI
- Review backend logs for LLM call failures - Review backend logs for LLM call failures
@@ -150,6 +164,7 @@ Generated mind maps support two export methods:
**Symptom:** Shows "⚠️ Mind map rendering failed" **Symptom:** Shows "⚠️ Mind map rendering failed"
**Solution:** **Solution:**
- Check browser console for error messages - Check browser console for error messages
- Confirm Markmap.js and D3.js libraries are loading correctly - Confirm Markmap.js and D3.js libraries are loading correctly
- Verify generated Markdown format conforms to Markmap specifications - Verify generated Markdown format conforms to Markmap specifications
@@ -158,6 +173,7 @@ Generated mind maps support two export methods:
### Issue: Export Function Not Working ### Issue: Export Function Not Working
**Solution:** **Solution:**
- Confirm browser supports Clipboard API - Confirm browser supports Clipboard API
- Check if browser is blocking clipboard access permissions - Check if browser is blocking clipboard access permissions
- Use modern browsers (Chrome, Firefox, Edge, etc.) - Use modern browsers (Chrome, Firefox, Edge, etc.)
@@ -186,6 +202,7 @@ Generated mind maps support two export methods:
## Changelog ## Changelog
### v0.7.2 (Current Version) ### v0.7.2 (Current Version)
- Optimized text extraction logic, automatically filters HTML code blocks - Optimized text extraction logic, automatically filters HTML code blocks
- Improved error handling and user feedback - Improved error handling and user feedback
- Enhanced export functionality compatibility - Enhanced export functionality compatibility

View File

@@ -1,10 +1,10 @@
# 智绘心图 - 思维导图生成插件 # 思维导图 - 思维导图生成插件
**作者:** [Fu-Jie](https://github.com/Fu-Jie) | **版本:** 0.7.2 | **许可证:** MIT **作者:** [Fu-Jie](https://github.com/Fu-Jie) | **版本:** 0.7.2 | **许可证:** MIT
> **重要提示**:为了确保所有插件的可维护性和易用性,每个插件都应附带清晰、完整的文档,以确保其功能、配置和使用方法得到充分说明。 > **重要提示**:为了确保所有插件的可维护性和易用性,每个插件都应附带清晰、完整的文档,以确保其功能、配置和使用方法得到充分说明。
智绘心图是一个强大的 OpenWebUI 动作插件,能够智能分析长篇文本内容,自动生成交互式思维导图,帮助用户结构化和可视化知识。 思维导图是一个强大的 OpenWebUI 动作插件,能够智能分析长篇文本内容,自动生成交互式思维导图,帮助用户结构化和可视化知识。
--- ---
@@ -49,7 +49,17 @@
### 3. 插件启用 ### 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 字符) 2. 在对话中输入或粘贴长篇文本内容(至少 100 字符)
3. 发送消息后,插件会自动分析并生成思维导图 3. 发送消息后,插件会自动分析并生成思维导图
4. 思维导图将在聊天界面中直接渲染显示 4. 思维导图将在聊天界面中直接渲染显示
@@ -77,6 +87,7 @@
### 使用示例 ### 使用示例
**输入文本:** **输入文本:**
``` ```
人工智能AI是计算机科学的一个分支致力于创建能够执行通常需要人类智能的任务的系统。 人工智能AI是计算机科学的一个分支致力于创建能够执行通常需要人类智能的任务的系统。
主要应用领域包括: 主要应用领域包括:
@@ -124,6 +135,7 @@
### 问题:插件无法启动 ### 问题:插件无法启动
**解决方案:** **解决方案:**
- 检查 OpenWebUI 日志,查看是否有错误信息 - 检查 OpenWebUI 日志,查看是否有错误信息
- 确认插件已正确上传并启用 - 确认插件已正确上传并启用
- 验证 OpenWebUI 版本是否支持动作插件 - 验证 OpenWebUI 版本是否支持动作插件
@@ -133,6 +145,7 @@
**现象:** 提示"文本内容过短,无法进行有效分析" **现象:** 提示"文本内容过短,无法进行有效分析"
**解决方案:** **解决方案:**
- 确保输入的文本至少包含 100 个字符(默认配置) - 确保输入的文本至少包含 100 个字符(默认配置)
- 可以在插件设置中降低 `MIN_TEXT_LENGTH` 参数值 - 可以在插件设置中降低 `MIN_TEXT_LENGTH` 参数值
- 提供更详细、结构化的文本内容 - 提供更详细、结构化的文本内容
@@ -140,6 +153,7 @@
### 问题:思维导图未生成 ### 问题:思维导图未生成
**解决方案:** **解决方案:**
- 检查 `LLM_MODEL_ID` 是否配置正确 - 检查 `LLM_MODEL_ID` 是否配置正确
- 确认配置的模型在 OpenWebUI 中可用 - 确认配置的模型在 OpenWebUI 中可用
- 查看后端日志,检查是否有 LLM 调用失败的错误 - 查看后端日志,检查是否有 LLM 调用失败的错误
@@ -150,6 +164,7 @@
**现象:** 显示"⚠️ 思维导图渲染失败" **现象:** 显示"⚠️ 思维导图渲染失败"
**解决方案:** **解决方案:**
- 检查浏览器控制台的错误信息 - 检查浏览器控制台的错误信息
- 确认 Markmap.js 和 D3.js 库是否正确加载 - 确认 Markmap.js 和 D3.js 库是否正确加载
- 验证生成的 Markdown 格式是否符合 Markmap 规范 - 验证生成的 Markdown 格式是否符合 Markmap 规范
@@ -158,6 +173,7 @@
### 问题:导出功能不工作 ### 问题:导出功能不工作
**解决方案:** **解决方案:**
- 确认浏览器支持剪贴板 API - 确认浏览器支持剪贴板 API
- 检查浏览器是否阻止了剪贴板访问权限 - 检查浏览器是否阻止了剪贴板访问权限
- 使用现代浏览器Chrome、Firefox、Edge 等) - 使用现代浏览器Chrome、Firefox、Edge 等)
@@ -186,6 +202,7 @@
## 更新日志 ## 更新日志
### v0.7.2 (当前版本) ### v0.7.2 (当前版本)
- 优化文本提取逻辑,自动过滤 HTML 代码块 - 优化文本提取逻辑,自动过滤 HTML 代码块
- 改进错误处理和用户反馈 - 改进错误处理和用户反馈
- 增强导出功能的兼容性 - 增强导出功能的兼容性

View File

@@ -1,18 +1,20 @@
""" """
title: 智绘心 title: 思维导
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4= icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4=
version: 0.7.4 version: 0.8.0
description: 智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。 description: 智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。
""" """
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any
import logging import logging
import time import os
import re import re
import time
from datetime import datetime, timezone
from typing import Any, Dict, Optional
from zoneinfo import ZoneInfo
from fastapi import Request from fastapi import Request
from datetime import datetime from pydantic import BaseModel, Field
import pytz
from open_webui.utils.chat import generate_chat_completion from open_webui.utils.chat import generate_chat_completion
from open_webui.models.users import Users from open_webui.models.users import Users
@@ -75,24 +77,20 @@ HTML_WRAPPER_TEMPLATE = """
} }
#main-container { #main-container {
display: flex; display: flex;
flex-wrap: wrap; flex-direction: column;
gap: 20px; gap: 20px;
align-items: flex-start; align-items: stretch;
width: 100%; width: 100%;
} }
.plugin-item { .plugin-item {
flex: 1 1 400px; /* 默认宽度,允许伸缩 */ width: 100%;
min-width: 300px;
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: visible;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.plugin-item:hover { .plugin-item:hover {
transform: translateY(-2px); transform: translateY(-2px);
} }
@media (max-width: 768px) {
.plugin-item { flex: 1 1 100%; }
}
/* STYLES_INSERTION_POINT */ /* STYLES_INSERTION_POINT */
</style> </style>
</head> </head>
@@ -115,13 +113,24 @@ CSS_TEMPLATE_MINDMAP = """
--muted-text-color: #546e7a; --muted-text-color: #546e7a;
--border-color: #e0e0e0; --border-color: #e0e0e0;
--header-gradient: linear-gradient(135deg, var(--secondary-color), var(--primary-color)); --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; --border-radius: 12px;
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --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 { .mindmap-container-wrapper {
font-family: var(--font-family); font-family: var(--font-family);
line-height: 1.7; line-height: 1.6;
color: var(--text-color); color: var(--text-color);
margin: 0; margin: 0;
padding: 0; padding: 0;
@@ -130,99 +139,110 @@ CSS_TEMPLATE_MINDMAP = """
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: var(--background-color);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
} }
.header { .header {
background: var(--header-gradient); background: var(--header-gradient);
color: white; color: white;
padding: 20px 24px; padding: 18px 20px;
text-align: center; text-align: center;
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
} }
.header h1 { .header h1 {
margin: 0; margin: 0;
font-size: 1.5em; font-size: 1.4em;
font-weight: 600; font-weight: 600;
text-shadow: 0 1px 3px rgba(0,0,0,0.2); letter-spacing: 0.3px;
} }
.user-context { .user-context {
font-size: 0.8em; font-size: 0.85em;
color: var(--muted-text-color); color: var(--muted-text-color);
background-color: #eceff1; background-color: rgba(255, 255, 255, 0.6);
padding: 8px 16px; padding: 8px 14px;
display: flex; display: flex;
justify-content: space-around; justify-content: space-between;
flex-wrap: wrap; flex-wrap: wrap;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
gap: 6px;
} }
.user-context span { margin: 2px 8px; } .theme-dark .user-context {
background-color: rgba(31, 41, 55, 0.7);
}
.user-context span { margin: 2px 6px; }
.content-area { .content-area {
padding: 20px; padding: 16px;
flex-grow: 1; flex-grow: 1;
background: var(--card-bg-color);
} }
.markmap-container { .markmap-container {
position: relative; position: relative;
background-color: #fff; background-color: var(--card-bg-color);
background-image: radial-gradient(var(--border-color) 0.5px, transparent 0.5px); border-radius: 10px;
background-size: 20px 20px; padding: 12px;
border-radius: 8px;
padding: 16px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border: 1px solid var(--border-color); 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 { .control-rows {
text-align: center; display: flex;
padding-top: 20px; flex-wrap: wrap;
margin-top: 20px; gap: 10px;
border-top: 1px solid var(--border-color); 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); background-color: var(--primary-color);
color: white; color: white;
border: none; border: none;
padding: 8px 16px; padding: 8px 12px;
border-radius: 6px; border-radius: 8px;
font-size: 0.9em; font-size: 0.9em;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease-in-out; transition: background-color 0.15s ease, transform 0.15s ease;
margin: 0 6px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
} }
.download-btn.secondary { .control-btn.secondary { background-color: var(--secondary-color); }
background-color: var(--secondary-color); .control-btn.neutral { background-color: #64748b; }
} .control-btn:hover { transform: translateY(-1px); }
.download-btn:hover { .control-btn.copied { background-color: #2e7d32; }
transform: translateY(-1px); .control-btn:disabled { opacity: 0.6; cursor: not-allowed; }
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.download-btn.copied {
background-color: #2e7d32;
}
.footer { .footer {
text-align: center; text-align: center;
padding: 16px; padding: 12px;
font-size: 0.8em; font-size: 0.85em;
color: #90a4ae; color: var(--muted-text-color);
background-color: #eceff1; background-color: var(--card-bg-color);
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
border-bottom-left-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
} }
.footer a { .footer a {
color: var(--primary-color); color: var(--primary-color);
text-decoration: none; text-decoration: none;
font-weight: 500; font-weight: 500;
} }
.footer a:hover { .footer a:hover { text-decoration: underline; }
text-decoration: underline;
}
.error-message { .error-message {
color: #c62828; color: #c62828;
background-color: #ffcdd2; background-color: #ffcdd2;
border: 1px solid #ef9a9a; border: 1px solid #ef9a9a;
padding: 16px; padding: 14px;
border-radius: 8px; border-radius: 8px;
font-weight: 500; font-weight: 500;
font-size: 1em; font-size: 1em;
@@ -240,16 +260,30 @@ CONTENT_TEMPLATE_MINDMAP = """
</div> </div>
<div class="content-area"> <div class="content-area">
<div class="markmap-container" id="markmap-container-{unique_id}"></div> <div class="markmap-container" id="markmap-container-{unique_id}"></div>
<div class="download-area"> <div class="control-rows">
<button id="download-svg-btn-{unique_id}" class="download-btn"> <div class="btn-group">
<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> <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> <span class="btn-text">SVG</span>
</button> </button>
<button id="download-md-btn-{unique_id}" class="download-btn secondary"> <button id="download-md-btn-{unique_id}" class="control-btn neutral">
<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> <span class="btn-text">Markdown</span>
</button> </button>
</div> </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>
<div class="footer"> <div class="footer">
<p>© {current_year} 智能思维导图 • <a href="https://markmap.js.org/" target="_blank">Markmap</a></p> <p>© {current_year} 智能思维导图 • <a href="https://markmap.js.org/" target="_blank">Markmap</a></p>
@@ -260,13 +294,140 @@ CONTENT_TEMPLATE_MINDMAP = """
""" """
SCRIPT_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> <script>
(function() { (function() {
const renderMindmap = () => {
const uniqueId = "{unique_id}"; 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 containerEl = document.getElementById('markmap-container-' + uniqueId); const containerEl = document.getElementById('markmap-container-' + uniqueId);
if (!containerEl || containerEl.dataset.markmapRendered) return; if (!containerEl || containerEl.dataset.markmapRendered) return;
@@ -279,11 +440,11 @@ SCRIPT_TEMPLATE_MINDMAP = """
return; return;
} }
try { ensureMarkmapReady().then(() => {
const svgEl = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svgEl.style.width = '100%'; svgEl.style.width = '100%';
svgEl.style.height = 'auto'; svgEl.style.height = '100%';
svgEl.style.minHeight = '300px'; svgEl.style.minHeight = '60vh';
containerEl.innerHTML = ''; containerEl.innerHTML = '';
containerEl.appendChild(svgEl); containerEl.appendChild(svgEl);
@@ -292,54 +453,60 @@ SCRIPT_TEMPLATE_MINDMAP = """
const { root } = transformer.transform(markdownContent); const { root } = transformer.transform(markdownContent);
const style = (id) => `${id} text { font-size: 14px !important; }`; const style = (id) => `${id} text { font-size: 14px !important; }`;
const options = { const options = {
autoFit: true, autoFit: true,
style: style style: style,
initialExpandLevel: Infinity
}; };
Markmap.create(svgEl, options, root);
const markmapInstance = Markmap.create(svgEl, options, root);
containerEl.dataset.markmapRendered = 'true'; containerEl.dataset.markmapRendered = 'true';
attachDownloadHandlers(uniqueId); setupControls({
containerEl,
svgEl,
markmapInstance,
root,
});
} catch (error) { }).catch((error) => {
console.error('Markmap rendering error:', error); console.error('Markmap loading error:', error);
containerEl.innerHTML = '<div class="error-message">⚠️ 思维导图渲染失败!<br>原因:' + error.message + '</div>'; 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 downloadSvgBtn = document.getElementById('download-svg-btn-' + uniqueId);
const downloadPngBtn = document.getElementById('download-png-btn-' + uniqueId);
const downloadMdBtn = document.getElementById('download-md-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 wrapper = containerEl.closest('.mindmap-container-wrapper');
const buttonText = button.querySelector('.btn-text'); let currentTheme = setTheme(wrapper);
const showFeedback = (button, textOk = '完成', textFail = '失败') => {
if (!button) return;
const buttonText = button.querySelector('.btn-text') || button;
const originalText = buttonText.textContent; const originalText = buttonText.textContent;
button.disabled = true; button.disabled = true;
if (isSuccess) { buttonText.textContent = textOk;
buttonText.textContent = '';
button.classList.add('copied'); button.classList.add('copied');
} else {
buttonText.textContent = '';
}
setTimeout(() => { setTimeout(() => {
buttonText.textContent = originalText; buttonText.textContent = originalText;
button.disabled = false; button.disabled = false;
button.classList.remove('copied'); button.classList.remove('copied');
}, 2500); }, 1800);
}; };
const copyToClipboard = (content, button) => { const copyToClipboard = (content, button) => {
if (navigator.clipboard && window.isSecureContext) { if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(content).then(() => { navigator.clipboard.writeText(content).then(() => showFeedback(button), () => showFeedback(button, '失败', '失败'));
showFeedback(button, true);
}, () => {
showFeedback(button, false);
});
} else { } else {
const textArea = document.createElement('textarea'); const textArea = document.createElement('textarea');
textArea.value = content; textArea.value = content;
@@ -350,34 +517,130 @@ SCRIPT_TEMPLATE_MINDMAP = """
textArea.select(); textArea.select();
try { try {
document.execCommand('copy'); document.execCommand('copy');
showFeedback(button, true); showFeedback(button);
} catch (err) { } catch (err) {
showFeedback(button, false); showFeedback(button, '失败', '失败');
} }
document.body.removeChild(textArea); document.body.removeChild(textArea);
} }
}; };
if (downloadSvgBtn) { const handleDownloadSVG = () => {
downloadSvgBtn.addEventListener('click', (event) => { const svg = containerEl.querySelector('svg');
event.stopPropagation(); if (!svg) return;
const svgEl = containerEl.querySelector('svg'); const svgData = new XMLSerializer().serializeToString(svg);
if (svgEl) {
const svgData = new XMLSerializer().serializeToString(svgEl);
copyToClipboard(svgData, downloadSvgBtn); copyToClipboard(svgData, downloadSvgBtn);
} };
});
}
if (downloadMdBtn) { const handleDownloadMD = () => {
downloadMdBtn.addEventListener('click', (event) => { const markdownContent = document.getElementById('markdown-source-' + uniqueId)?.textContent || '';
event.stopPropagation(); if (!markdownContent) return;
const markdownContent = document.getElementById('markdown-source-' + uniqueId).textContent;
copyToClipboard(markdownContent, downloadMdBtn); 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') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', renderMindmap); document.addEventListener('DOMContentLoaded', renderMindmap);
} else { } else {
@@ -422,6 +685,21 @@ class Action:
"Sunday": "星期日", "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: def _extract_markdown_syntax(self, llm_output: str) -> str:
match = re.search(r"```markdown\s*(.*?)\s*```", llm_output, re.DOTALL) match = re.search(r"```markdown\s*(.*?)\s*```", llm_output, re.DOTALL)
if match: if match:
@@ -516,33 +794,21 @@ class Action:
__event_emitter__: Optional[Any] = None, __event_emitter__: Optional[Any] = None,
__request__: Optional[Request] = None, __request__: Optional[Request] = None,
) -> Optional[dict]: ) -> Optional[dict]:
logger.info("Action: 智绘心图 (v12 - Final Feedback Fix) started") logger.info("Action: 思维导图 (v12 - Final Feedback Fix) started")
user_ctx = self._get_user_context(__user__)
if isinstance(__user__, (list, tuple)): user_language = user_ctx["user_language"]
user_language = ( user_name = user_ctx["user_name"]
__user__[0].get("language", "zh-CN") if __user__ else "zh-CN" user_id = user_ctx["user_id"]
)
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")
try: try:
shanghai_tz = pytz.timezone("Asia/Shanghai") tz_env = os.environ.get("TZ")
current_datetime_shanghai = datetime.now(shanghai_tz) tzinfo = ZoneInfo(tz_env) if tz_env else None
current_date_time_str = current_datetime_shanghai.strftime( now_dt = datetime.now(tzinfo or timezone.utc)
"%Y年%m月%d%H:%M:%S" current_date_time_str = now_dt.strftime("%Y年%m月%d%H:%M:%S")
) current_weekday_en = now_dt.strftime("%A")
current_weekday_en = current_datetime_shanghai.strftime("%A")
current_weekday_zh = self.weekday_map.get(current_weekday_en, "未知星期") current_weekday_zh = self.weekday_map.get(current_weekday_en, "未知星期")
current_year = current_datetime_shanghai.strftime("%Y") current_year = now_dt.strftime("%Y")
current_timezone_str = "Asia/Shanghai" current_timezone_str = tz_env or "UTC"
except Exception as e: except Exception as e:
logger.warning(f"获取时区信息失败: {e},使用默认值。") logger.warning(f"获取时区信息失败: {e},使用默认值。")
now = datetime.now() now = datetime.now()
@@ -552,7 +818,7 @@ class Action:
current_timezone_str = "未知时区" current_timezone_str = "未知时区"
await self._emit_notification( await self._emit_notification(
__event_emitter__, "智绘心图已启动,正在为您生成思维导图...", "info" __event_emitter__, "思维导图已启动,正在为您生成思维导图...", "info"
) )
messages = body.get("messages") messages = body.get("messages")
@@ -612,7 +878,7 @@ class Action:
} }
await self._emit_status( await self._emit_status(
__event_emitter__, "智绘心图: 深入分析文本结构...", False __event_emitter__, "思维导图: 深入分析文本结构...", False
) )
try: try:
@@ -708,23 +974,23 @@ class Action:
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}"
await self._emit_status(__event_emitter__, "智绘心图: 绘制完成!", True) await self._emit_status(__event_emitter__, "思维导图: 绘制完成!", True)
await self._emit_notification( await self._emit_notification(
__event_emitter__, f"思维导图已生成,{user_name}", "success" __event_emitter__, f"思维导图已生成,{user_name}", "success"
) )
logger.info("Action: 智绘心图 (v12) completed successfully") logger.info("Action: 思维导图 (v12) completed successfully")
except Exception as e: except Exception as e:
error_message = f"智绘心图处理失败: {str(e)}" error_message = f"思维导图处理失败: {str(e)}"
logger.error(f"智绘心图错误: {error_message}", exc_info=True) logger.error(f"思维导图错误: {error_message}", exc_info=True)
user_facing_error = f"抱歉,智绘心图在处理时遇到错误: {str(e)}\n请检查Open WebUI后端日志获取更多详情。" user_facing_error = f"抱歉,思维导图在处理时遇到错误: {str(e)}\n请检查Open WebUI后端日志获取更多详情。"
body["messages"][-1][ body["messages"][-1][
"content" "content"
] = f"{long_text_content}\n\n❌ **错误:** {user_facing_error}" ] = 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( await self._emit_notification(
__event_emitter__, f"智绘心图生成失败, {user_name}", "error" __event_emitter__, f"思维导图生成失败, {user_name}", "error"
) )
return body return body

View File

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