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:
159
.github/copilot-instructions.md
vendored
159
.github/copilot-instructions.md
vendored
@@ -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)
|
||||
|
||||
开发新插件时,请确保完成以下检查:
|
||||
|
||||
@@ -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 插件。
|
||||
|
||||
@@ -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) |
|
||||
|
||||
## 🎯 什么是动作插件?
|
||||
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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; }
|
||||
.theme-dark .user-context {
|
||||
background-color: rgba(31, 41, 55, 0.7);
|
||||
}
|
||||
.user-context span { margin: 2px 6px; }
|
||||
.content-area {
|
||||
padding: 20px;
|
||||
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,11 +440,11 @@ 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';
|
||||
svgEl.style.height = '100%';
|
||||
svgEl.style.minHeight = '60vh';
|
||||
containerEl.innerHTML = '';
|
||||
containerEl.appendChild(svgEl);
|
||||
|
||||
@@ -292,54 +453,60 @@ SCRIPT_TEMPLATE_MINDMAP = """
|
||||
const { root } = transformer.transform(markdownContent);
|
||||
|
||||
const style = (id) => `${id} text { font-size: 14px !important; }`;
|
||||
|
||||
const options = {
|
||||
autoFit: true,
|
||||
style: style
|
||||
style: style,
|
||||
initialExpandLevel: Infinity
|
||||
};
|
||||
Markmap.create(svgEl, options, root);
|
||||
|
||||
const markmapInstance = Markmap.create(svgEl, options, root);
|
||||
containerEl.dataset.markmapRendered = 'true';
|
||||
|
||||
attachDownloadHandlers(uniqueId);
|
||||
setupControls({
|
||||
containerEl,
|
||||
svgEl,
|
||||
markmapInstance,
|
||||
root,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Markmap rendering error:', error);
|
||||
containerEl.innerHTML = '<div class="error-message">⚠️ 思维导图渲染失败!<br>原因:' + error.message + '</div>';
|
||||
}
|
||||
}).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
|
||||
|
||||
@@ -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个插件
|
||||
|
||||
Reference in New Issue
Block a user