feat: 新增插件系统、多种插件类型、开发指南及多语言文档。

This commit is contained in:
fujie
2025-12-20 12:34:49 +08:00
commit eaa6319991
74 changed files with 28409 additions and 0 deletions

View File

@@ -0,0 +1,210 @@
# Smart Mind Map - Mind Mapping Generation Plugin
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.7.2 | **License:** MIT
> **Important**: To ensure the maintainability and usability of all plugins, each plugin should be accompanied by clear and comprehensive documentation to ensure its functionality, configuration, and usage are well explained.
Smart Mind Map is a powerful OpenWebUI action plugin that intelligently analyzes long-form text content and automatically generates interactive mind maps, helping users structure and visualize knowledge.
---
## 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
---
## How It Works
1. **Text Extraction**: Extracts text content from user messages (automatically filters HTML code blocks)
2. **Intelligent Analysis**: Analyzes text structure using the configured LLM model
3. **Markdown Generation**: Converts analysis results to Markmap-compatible Markdown format
4. **Visual Rendering**: Renders the mind map using Markmap.js in an HTML template
5. **Interactive Display**: Presents the mind map to users in an interactive format within the chat interface
---
## Installation and Configuration
### 1. Plugin Installation
1. Download the `思维导图.py` file to your local computer
2. In OpenWebUI Admin Settings, find the "Plugins" section
3. Select "Actions" type
4. Upload the downloaded file
5. Refresh the page, and the plugin will be available
### 2. Model Configuration
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
### 3. Plugin Activation
Select the "Smart Mind Map" action plugin in chat settings to enable it.
---
## Configuration Parameters
You can adjust the following parameters in the plugin's settings (Valves):
| Parameter | Default | Description |
| :--- | :--- | :--- |
| `show_status` | `true` | Whether to display operation status updates in the chat interface (e.g., "Analyzing..."). |
| `LLM_MODEL_ID` | `gemini-2.5-flash` | LLM model ID for text analysis. Recommended to use fast and economical models. |
| `MIN_TEXT_LENGTH` | `100` | Minimum text length (in characters) required for mind map analysis. Text that's too short cannot generate valid mind maps. |
---
## Usage
### Basic Usage
1. Enable the "Smart Mind Map" action in chat settings
2. Input or paste long-form text content (at least 100 characters) in the conversation
3. After sending the message, the plugin will automatically analyze and generate a mind map
4. The mind map will be rendered directly in the chat interface
### 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:
1. Machine Learning - Enables computers to learn from data
2. Natural Language Processing - Understanding and generating human language
3. Computer Vision - Recognizing and processing images
4. Robotics - Creating intelligent systems that can interact with the physical world
```
**Generated Result:**
The plugin will generate an interactive mind map centered on "Artificial Intelligence", including major application areas and their sub-concepts.
### Export Features
Generated mind maps support two export methods:
1. **Copy SVG Code**: Click the "Copy SVG Code" button to copy the mind map in SVG format to the clipboard
2. **Copy Markdown**: Click the "Copy Markdown" button to copy the raw Markdown format to the clipboard
---
## Technical Architecture
### Frontend Rendering
- **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
### Security
- **XSS Protection**: Automatically escapes `</script>` tags to prevent script injection
- **Input Validation**: Checks text length to avoid invalid requests
---
## Troubleshooting
### 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
### 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
### 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
### 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
### 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.)
---
## 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
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
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
---
## 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
---
## License
This plugin is released under the MIT License.
## Contributing
Welcome to submit issue reports and improvement suggestions! Please visit the project repository: [awesome-openwebui](https://github.com/Fu-Jie/awesome-openwebui)
---
## Related Resources
- [Markmap Official Website](https://markmap.js.org/)
- [OpenWebUI Documentation](https://docs.openwebui.com/)
- [D3.js Official Website](https://d3js.org/)

View File

@@ -0,0 +1,210 @@
# 智绘心图 - 思维导图生成插件
**作者:** [Fu-Jie](https://github.com/Fu-Jie) | **版本:** 0.7.2 | **许可证:** MIT
> **重要提示**:为了确保所有插件的可维护性和易用性,每个插件都应附带清晰、完整的文档,以确保其功能、配置和使用方法得到充分说明。
智绘心图是一个强大的 OpenWebUI 动作插件,能够智能分析长篇文本内容,自动生成交互式思维导图,帮助用户结构化和可视化知识。
---
## 核心特性
-**智能文本分析**: 自动识别文本的核心主题、关键概念和层次结构
-**交互式可视化**: 基于 Markmap.js 生成美观的交互式思维导图
-**多语言支持**: 根据用户语言自动调整输出
-**实时渲染**: 在聊天界面中直接渲染思维导图,无需跳转
-**导出功能**: 支持复制 SVG 代码和 Markdown 源码
-**自定义配置**: 可配置 LLM 模型、最小文本长度等参数
---
## 工作原理
1. **文本提取**: 从用户消息中提取文本内容(自动过滤 HTML 代码块)
2. **智能分析**: 使用配置的 LLM 模型分析文本结构
3. **Markdown 生成**: 将分析结果转换为 Markmap 兼容的 Markdown 格式
4. **可视化渲染**: 在 HTML 模板中使用 Markmap.js 渲染思维导图
5. **交互展示**: 在聊天界面中以可交互的形式展示给用户
---
## 安装与配置
### 1. 插件安装
1. 下载 `思维导图.py` 文件到本地
2. 在 OpenWebUI 管理员设置中找到"插件"Plugins部分
3. 选择"动作"Actions类型
4. 上传下载的文件
5. 刷新页面,插件即可使用
### 2. 模型配置
插件需要访问 LLM 模型来分析文本。请确保:
- 您的 OpenWebUI 实例中配置了至少一个可用的 LLM 模型
- 推荐使用快速、经济的模型(如 `gemini-2.5-flash`)来获得最佳体验
- 在插件设置中配置 `LLM_MODEL_ID` 参数
### 3. 插件启用
在聊天设置中选择"智绘心图"动作插件即可启用。
---
## 配置参数
您可以在插件的设置Valves中调整以下参数
| 参数 | 默认值 | 描述 |
| :--- | :--- | :--- |
| `show_status` | `true` | 是否在聊天界面显示操作状态更新(如"正在分析...")。 |
| `LLM_MODEL_ID` | `gemini-2.5-flash` | 用于文本分析的 LLM 模型 ID。推荐使用快速且经济的模型。 |
| `MIN_TEXT_LENGTH` | `100` | 进行思维导图分析所需的最小文本长度(字符数)。文本过短将无法生成有效的导图。 |
---
## 使用方法
### 基本使用
1. 在聊天设置中启用"智绘心图"动作
2. 在对话中输入或粘贴长篇文本内容(至少 100 字符)
3. 发送消息后,插件会自动分析并生成思维导图
4. 思维导图将在聊天界面中直接渲染显示
### 使用示例
**输入文本:**
```
人工智能AI是计算机科学的一个分支致力于创建能够执行通常需要人类智能的任务的系统。
主要应用领域包括:
1. 机器学习 - 使计算机能够从数据中学习
2. 自然语言处理 - 理解和生成人类语言
3. 计算机视觉 - 识别和处理图像
4. 机器人技术 - 创建能够与物理世界交互的智能系统
```
**生成结果:**
插件会生成一个以"人工智能"为中心主题的交互式思维导图,包含主要应用领域及其子概念。
### 导出功能
生成的思维导图支持两种导出方式:
1. **复制 SVG 代码**: 点击"复制 SVG 代码"按钮,可将思维导图的 SVG 格式复制到剪贴板
2. **复制 Markdown**: 点击"复制 Markdown"按钮,可将原始 Markdown 格式复制到剪贴板
---
## 技术架构
### 前端渲染
- **Markmap.js**: 开源的思维导图渲染引擎
- **D3.js**: 数据可视化基础库
- **响应式设计**: 适配不同屏幕尺寸
### 后端处理
- **LLM 集成**: 通过 `generate_chat_completion` 调用配置的模型
- **文本预处理**: 自动过滤 HTML 代码块,提取纯文本内容
- **格式转换**: 将 LLM 输出转换为 Markmap 兼容的 Markdown 格式
### 安全性
- **XSS 防护**: 自动转义 `</script>` 标签,防止脚本注入
- **输入验证**: 检查文本长度,避免无效请求
---
## 故障排除
### 问题:插件无法启动
**解决方案:**
- 检查 OpenWebUI 日志,查看是否有错误信息
- 确认插件已正确上传并启用
- 验证 OpenWebUI 版本是否支持动作插件
### 问题:文本内容过短
**现象:** 提示"文本内容过短,无法进行有效分析"
**解决方案:**
- 确保输入的文本至少包含 100 个字符(默认配置)
- 可以在插件设置中降低 `MIN_TEXT_LENGTH` 参数值
- 提供更详细、结构化的文本内容
### 问题:思维导图未生成
**解决方案:**
- 检查 `LLM_MODEL_ID` 是否配置正确
- 确认配置的模型在 OpenWebUI 中可用
- 查看后端日志,检查是否有 LLM 调用失败的错误
- 验证用户是否有足够的权限访问配置的模型
### 问题:思维导图显示错误
**现象:** 显示"⚠️ 思维导图渲染失败"
**解决方案:**
- 检查浏览器控制台的错误信息
- 确认 Markmap.js 和 D3.js 库是否正确加载
- 验证生成的 Markdown 格式是否符合 Markmap 规范
- 尝试刷新页面重新渲染
### 问题:导出功能不工作
**解决方案:**
- 确认浏览器支持剪贴板 API
- 检查浏览器是否阻止了剪贴板访问权限
- 使用现代浏览器Chrome、Firefox、Edge 等)
---
## 最佳实践
1. **文本准备**
- 提供结构清晰、层次分明的文本内容
- 使用段落、列表等格式帮助 LLM 理解文本结构
- 避免过于冗长或无结构的文本
2. **模型选择**
- 对于日常使用,推荐 `gemini-2.5-flash` 等快速模型
- 对于复杂文本分析,可以使用更强大的模型(如 GPT-4
- 根据需求平衡速度和分析质量
3. **性能优化**
- 合理设置 `MIN_TEXT_LENGTH`,避免处理过短的文本
- 对于特别长的文本,考虑先进行摘要再生成思维导图
- 在生产环境中关闭 `show_status` 以减少界面更新
---
## 更新日志
### v0.7.2 (当前版本)
- 优化文本提取逻辑,自动过滤 HTML 代码块
- 改进错误处理和用户反馈
- 增强导出功能的兼容性
- 优化 UI 样式和交互体验
---
## 许可证
本插件采用 MIT 许可证发布。
## 贡献
欢迎提交问题报告和改进建议!请访问项目仓库:[awesome-openwebui](https://github.com/Fu-Jie/awesome-openwebui)
---
## 相关资源
- [Markmap 官方网站](https://markmap.js.org/)
- [OpenWebUI 文档](https://docs.openwebui.com/)
- [D3.js 官方网站](https://d3js.org/)

View File

@@ -0,0 +1,611 @@
"""
title: Smart Mind Map
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+CiAgPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMyIgZmlsbD0iY3VycmVudENvbG9yIi8+CiAgPGxpbmUgeDE9IjEyIiB5MT0iOSIgeDI9IjEyIiB5Mj0iNCIvPgogIDxjaXJjbGUgY3g9IjEyIiBjeT0iMyIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEyIiB5MT0iMTUiIHgyPSIxMiIgeTI9IjIwIi8+CiAgPGNpcmNsZSBjeD0iMTIiIGN5PSIyMSIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjkiIHkxPSIxMiIgeDI9IjQiIHkyPSIxMiIvPgogIDxjaXJjbGUgY3g9IjMiIGN5PSIxMiIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjE1IiB5MT0iMTIiIHgyPSIyMCIgeTI9IjEyIi8+CiAgPGNpcmNsZSBjeD0iMjEiIGN5PSIxMiIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEwLjUiIHkxPSIxMC41IiB4Mj0iNiIgeTI9IjYiLz4KICA8Y2lyY2xlIGN4PSI1IiBjeT0iNSIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEzLjUiIHkxPSIxMC41IiB4Mj0iMTgiIHkyPSI2Ii8+CiAgPGNpcmNsZSBjeD0iMTkiIGN5PSI1IiByPSIxLjUiLz4KICA8bGluZSB4MT0iMTAuNSIgeTE9IjEzLjUiIHgyPSI2IiB5Mj0iMTgiLz4KICA8Y2lyY2xlIGN4PSI1IiBjeT0iMTkiIHI9IjEuNSIvPgogIDxsaW5lIHgxPSIxMy41IiB5MT0iMTMuNSIgeDI9IjE4IiB5Mj0iMTgiLz4KICA8Y2lyY2xlIGN4PSIxOSIgY3k9IjE5IiByPSIxLjUiLz4KPC9zdmc+
version: 0.7.3
description: 智能分析长文本并生成交互式思维导图,支持 SVG/Markdown 导出。
"""
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any
import logging
import time
import re
from fastapi import Request
from datetime import datetime
import pytz
from open_webui.utils.chat import generate_chat_completion
from open_webui.models.users import Users
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
SYSTEM_PROMPT_MINDMAP_ASSISTANT = """
You are a professional mind map generation assistant, capable of efficiently analyzing long-form text provided by users and structuring its core themes, key concepts, branches, and sub-branches into standard Markdown list syntax for rendering by Markmap.js.
Please strictly follow these guidelines:
- **Language**: All output must be in the language specified by the user.
- **Format**: Your output must strictly be in Markdown list format, wrapped with ```markdown and ```.
- Use `#` to define the central theme (root node).
- Use `-` with two-space indentation to represent branches and sub-branches.
- **Content**:
- Identify the central theme of the text as the `#` heading.
- Identify main concepts as first-level list items.
- Identify supporting details or sub-concepts as nested list items.
- Node content should be concise and clear, avoiding verbosity.
- **Output Markdown syntax only**: Do not include any additional greetings, explanations, or guiding text.
- **If text is too short or cannot generate a valid mind map**: Output a simple Markdown list indicating inability to generate, for example:
```markdown
# Unable to Generate Mind Map
- Reason: Insufficient or unclear text content
```
"""
USER_PROMPT_GENERATE_MINDMAP = """
Please analyze the following long-form text and structure its core themes, key concepts, branches, and sub-branches into standard Markdown list syntax for Markmap.js rendering.
---
**User Context Information:**
User Name: {user_name}
Current Date & Time: {current_date_time_str}
Current Weekday: {current_weekday}
Current Timezone: {current_timezone_str}
User Language: {user_language}
---
**Long-form Text Content:**
{long_text_content}
"""
HTML_TEMPLATE_MINDMAP = """
<!DOCTYPE html>
<html lang="{user_language}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Smart Mind Map: Mind Map Visualization</title>
<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>
<style>
:root {
--primary-color: #1e88e5;
--secondary-color: #43a047;
--background-color: #f4f6f8;
--card-bg-color: #ffffff;
--text-color: #263238;
--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);
--border-radius: 12px;
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
body {
font-family: var(--font-family);
line-height: 1.7;
color: var(--text-color);
margin: 0;
padding: 24px;
background-color: var(--background-color);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.container {
max-width: 1280px;
margin: 20px auto;
background: var(--card-bg-color);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
overflow: hidden;
border: 1px solid var(--border-color);
}
.header {
background: var(--header-gradient);
color: white;
padding: 32px 40px;
text-align: center;
}
.header h1 {
margin: 0;
font-size: 2em;
font-weight: 600;
text-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.user-context {
font-size: 0.85em;
color: var(--muted-text-color);
background-color: #eceff1;
padding: 12px 20px;
display: flex;
justify-content: space-around;
flex-wrap: wrap;
}
.user-context span { margin: 4px 10px; }
.content-area {
padding: 30px 40px;
}
.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: var(--border-radius);
padding: 24px;
min-height: 700px;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid var(--border-color);
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
.download-area {
text-align: center;
padding-top: 30px;
margin-top: 30px;
border-top: 1px solid var(--border-color);
}
.download-btn {
background-color: var(--primary-color);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 1em;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease-in-out;
margin: 0 10px;
display: inline-flex;
align-items: center;
gap: 8px;
}
.download-btn.secondary {
background-color: var(--secondary-color);
}
.download-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
}
.download-btn.copied {
background-color: #2e7d32; /* A darker green for success */
}
.footer {
text-align: center;
padding: 24px;
font-size: 0.85em;
color: #90a4ae;
background-color: #eceff1;
}
.footer a {
color: var(--primary-color);
text-decoration: none;
font-weight: 500;
}
.footer a:hover {
text-decoration: underline;
}
.error-message {
color: #c62828;
background-color: #ffcdd2;
border: 1px solid #ef9a9a;
padding: 20px;
border-radius: var(--border-radius);
font-weight: 500;
font-size: 1.1em;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🧠 Smart Mind Map</h1>
</div>
<div class="user-context">
<span><strong>User:</strong> {user_name}</span>
<span><strong>Analysis Time:</strong> {current_date_time_str}</span>
<span><strong>Weekday:</strong> {current_weekday_zh}</span>
</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="18" height="18" 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">Copy SVG Code</span>
</button>
<button id="download-md-btn-{unique_id}" class="download-btn secondary">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" 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">Copy Markdown</span>
</button>
</div>
</div>
<div class="footer">
<p>© {current_year} Smart Mind Map • Rendering engine powered by <a href="https://markmap.js.org/" target="_blank">Markmap</a></p>
</div>
</div>
<script type="text/template" id="markdown-source-{unique_id}">{markdown_syntax}</script>
<script>
(function() {
const renderMindmap = () => {
const uniqueId = "{unique_id}";
const containerEl = document.getElementById('markmap-container-' + uniqueId);
if (!containerEl || containerEl.dataset.markmapRendered) return;
const sourceEl = document.getElementById('markdown-source-' + uniqueId);
if (!sourceEl) return;
const markdownContent = sourceEl.textContent.trim();
if (!markdownContent) {
containerEl.innerHTML = '<div class="error-message">⚠️ Unable to load mind map: Missing valid content.</div>';
return;
}
try {
const svgEl = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svgEl.style.width = '100%';
svgEl.style.height = '700px';
containerEl.innerHTML = '';
containerEl.appendChild(svgEl);
const { Transformer, Markmap } = window.markmap;
const transformer = new Transformer();
const { root } = transformer.transform(markdownContent);
const style = (id) => `${id} text { font-size: 16px !important; }`;
const options = {
autoFit: true,
style: style
};
Markmap.create(svgEl, options, root);
containerEl.dataset.markmapRendered = 'true';
attachDownloadHandlers(uniqueId);
} catch (error) {
console.error('Markmap rendering error:', error);
containerEl.innerHTML = '<div class="error-message">⚠️ Mind map rendering failed!<br>Reason: ' + error.message + '</div>';
}
};
const attachDownloadHandlers = (uniqueId) => {
const downloadSvgBtn = document.getElementById('download-svg-btn-' + uniqueId);
const downloadMdBtn = document.getElementById('download-md-btn-' + uniqueId);
const containerEl = document.getElementById('markmap-container-' + uniqueId);
const showFeedback = (button, isSuccess) => {
const buttonText = button.querySelector('.btn-text');
const originalText = buttonText.textContent;
button.disabled = true;
if (isSuccess) {
buttonText.textContent = '✅ Copied!';
button.classList.add('copied');
} else {
buttonText.textContent = '❌ Copy Failed';
}
setTimeout(() => {
buttonText.textContent = originalText;
button.disabled = false;
button.classList.remove('copied');
}, 2500);
};
const copyToClipboard = (content, button) => {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(content).then(() => {
showFeedback(button, true);
}, () => {
showFeedback(button, false);
});
} else {
// Fallback for older/insecure contexts
const textArea = document.createElement('textarea');
textArea.value = content;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
showFeedback(button, true);
} catch (err) {
showFeedback(button, false);
}
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);
}
});
}
if (downloadMdBtn) {
downloadMdBtn.addEventListener('click', (event) => {
event.stopPropagation();
const markdownContent = document.getElementById('markdown-source-' + uniqueId).textContent;
copyToClipboard(markdownContent, downloadMdBtn);
});
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', renderMindmap);
} else {
renderMindmap();
}
})();
</script>
</body>
</html>
"""
class Action:
class Valves(BaseModel):
show_status: bool = Field(
default=True,
description="Whether to show action status updates in the chat interface.",
)
LLM_MODEL_ID: str = Field(
default="gemini-2.5-flash",
description="Built-in LLM model ID for text analysis.",
)
MIN_TEXT_LENGTH: int = Field(
default=100,
description="Minimum text length (character count) required for mind map analysis.",
)
def __init__(self):
self.valves = self.Valves()
self.weekday_map = {
"Monday": "Monday",
"Tuesday": "Tuesday",
"Wednesday": "Wednesday",
"Thursday": "Thursday",
"Friday": "Friday",
"Saturday": "Saturday",
"Sunday": "Sunday",
}
def _extract_markdown_syntax(self, llm_output: str) -> str:
match = re.search(r"```markdown\s*(.*?)\s*```", llm_output, re.DOTALL)
if match:
extracted_content = match.group(1).strip()
else:
logger.warning(
"LLM output did not strictly follow the expected Markdown format, treating the entire output as summary."
)
extracted_content = llm_output.strip()
return extracted_content.replace("</script>", "<\\/script>")
async def action(
self,
body: dict,
__user__: Optional[Dict[str, Any]] = None,
__event_emitter__: Optional[Any] = None,
__request__: Optional[Request] = None,
) -> Optional[dict]:
logger.info("Action: Smart Mind Map (v0.7.2) started")
if isinstance(__user__, (list, tuple)):
user_language = (
__user__[0].get("language", "en-US") if __user__ else "en-US"
)
user_name = __user__[0].get("name", "User") if __user__[0] else "User"
user_id = (
__user__[0]["id"]
if __user__ and "id" in __user__[0]
else "unknown_user"
)
elif isinstance(__user__, dict):
user_language = __user__.get("language", "en-US")
user_name = __user__.get("name", "User")
user_id = __user__.get("id", "unknown_user")
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")
current_weekday_zh = self.weekday_map.get(current_weekday_en, "Unknown")
current_year = current_datetime_shanghai.strftime("%Y")
current_timezone_str = "Asia/Shanghai"
except Exception as e:
logger.warning(f"Failed to get timezone info: {e}, using default values.")
now = datetime.now()
current_date_time_str = now.strftime("%Y-%m-%d %H:%M:%S")
current_weekday_zh = "Unknown"
current_year = now.strftime("%Y")
current_timezone_str = "Unknown"
if __event_emitter__:
await __event_emitter__(
{
"type": "notification",
"data": {
"type": "info",
"content": "Smart Mind Map is starting, generating mind map for you...",
},
}
)
messages = body.get("messages")
if (
not messages
or not isinstance(messages, list)
or not messages[-1].get("content")
):
error_message = "Unable to retrieve valid user message content."
if __event_emitter__:
await __event_emitter__(
{
"type": "notification",
"data": {"type": "error", "content": error_message},
}
)
return {
"messages": [{"role": "assistant", "content": f"{error_message}"}]
}
parts = re.split(r"```html.*?```", messages[-1]["content"], flags=re.DOTALL)
long_text_content = ""
if parts:
for part in reversed(parts):
if part.strip():
long_text_content = part.strip()
break
if not long_text_content:
long_text_content = messages[-1]["content"].strip()
if len(long_text_content) < self.valves.MIN_TEXT_LENGTH:
short_text_message = f"Text content is too short ({len(long_text_content)} characters), unable to perform effective analysis. Please provide at least {self.valves.MIN_TEXT_LENGTH} characters of text."
if __event_emitter__:
await __event_emitter__(
{
"type": "notification",
"data": {"type": "warning", "content": short_text_message},
}
)
return {
"messages": [
{"role": "assistant", "content": f"⚠️ {short_text_message}"}
]
}
if self.valves.show_status and __event_emitter__:
await __event_emitter__(
{
"type": "status",
"data": {
"description": "Smart Mind Map: Analyzing text structure in depth...",
"done": False,
"hidden": False,
},
}
)
try:
unique_id = f"id_{int(time.time() * 1000)}"
formatted_user_prompt = USER_PROMPT_GENERATE_MINDMAP.format(
user_name=user_name,
current_date_time_str=current_date_time_str,
current_weekday=current_weekday_zh,
current_timezone_str=current_timezone_str,
user_language=user_language,
long_text_content=long_text_content,
)
llm_payload = {
"model": self.valves.LLM_MODEL_ID,
"messages": [
{"role": "system", "content": SYSTEM_PROMPT_MINDMAP_ASSISTANT},
{"role": "user", "content": formatted_user_prompt},
],
"temperature": 0.5,
"stream": False,
}
user_obj = Users.get_user_by_id(user_id)
if not user_obj:
raise ValueError(f"Unable to get user object, user ID: {user_id}")
llm_response = await generate_chat_completion(
__request__, llm_payload, user_obj
)
if (
not llm_response
or "choices" not in llm_response
or not llm_response["choices"]
):
raise ValueError("LLM response format is incorrect or empty.")
assistant_response_content = llm_response["choices"][0]["message"][
"content"
]
markdown_syntax = self._extract_markdown_syntax(assistant_response_content)
final_html_content = (
HTML_TEMPLATE_MINDMAP.replace("{unique_id}", unique_id)
.replace("{user_language}", user_language)
.replace("{user_name}", user_name)
.replace("{current_date_time_str}", current_date_time_str)
.replace("{current_weekday_zh}", current_weekday_zh)
.replace("{current_year}", current_year)
.replace("{markdown_syntax}", markdown_syntax)
)
html_embed_tag = f"```html\n{final_html_content}\n```"
body["messages"][-1]["content"] = f"{long_text_content}\n\n{html_embed_tag}"
if self.valves.show_status and __event_emitter__:
await __event_emitter__(
{
"type": "status",
"data": {
"description": "Smart Mind Map: Drawing completed!",
"done": True,
"hidden": False,
},
}
)
await __event_emitter__(
{
"type": "notification",
"data": {
"type": "success",
"content": f"Mind map has been generated, {user_name}!",
},
}
)
logger.info("Action: Smart Mind Map (v0.7.2) completed successfully")
except Exception as e:
error_message = f"Smart Mind Map processing failed: {str(e)}"
logger.error(f"Smart Mind Map error: {error_message}", exc_info=True)
user_facing_error = f"Sorry, Smart Mind Map encountered an error during processing: {str(e)}.\nPlease check the Open WebUI backend logs for more details."
body["messages"][-1][
"content"
] = f"{long_text_content}\n\n❌ **Error:** {user_facing_error}"
if __event_emitter__:
if self.valves.show_status:
await __event_emitter__(
{
"type": "status",
"data": {
"description": "Smart Mind Map: Processing failed.",
"done": True,
"hidden": False,
},
}
)
await __event_emitter__(
{
"type": "notification",
"data": {
"type": "error",
"content": f"Smart Mind Map generation failed, {user_name}!",
},
}
)
return body

View File

@@ -0,0 +1,611 @@
"""
title: 智绘心图
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+CiAgPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMyIgZmlsbD0iY3VycmVudENvbG9yIi8+CiAgPGxpbmUgeDE9IjEyIiB5MT0iOSIgeDI9IjEyIiB5Mj0iNCIvPgogIDxjaXJjbGUgY3g9IjEyIiBjeT0iMyIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEyIiB5MT0iMTUiIHgyPSIxMiIgeTI9IjIwIi8+CiAgPGNpcmNsZSBjeD0iMTIiIGN5PSIyMSIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjkiIHkxPSIxMiIgeDI9IjQiIHkyPSIxMiIvPgogIDxjaXJjbGUgY3g9IjMiIGN5PSIxMiIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjE1IiB5MT0iMTIiIHgyPSIyMCIgeTI9IjEyIi8+CiAgPGNpcmNsZSBjeD0iMjEiIGN5PSIxMiIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEwLjUiIHkxPSIxMC41IiB4Mj0iNiIgeTI9IjYiLz4KICA8Y2lyY2xlIGN4PSI1IiBjeT0iNSIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEzLjUiIHkxPSIxMC41IiB4Mj0iMTgiIHkyPSI2Ii8+CiAgPGNpcmNsZSBjeD0iMTkiIGN5PSI1IiByPSIxLjUiLz4KICA8bGluZSB4MT0iMTAuNSIgeTE9IjEzLjUiIHgyPSI2IiB5Mj0iMTgiLz4KICA8Y2lyY2xlIGN4PSI1IiBjeT0iMTkiIHI9IjEuNSIvPgogIDxsaW5lIHgxPSIxMy41IiB5MT0iMTMuNSIgeDI9IjE4IiB5Mj0iMTgiLz4KICA8Y2lyY2xlIGN4PSIxOSIgY3k9IjE5IiByPSIxLjUiLz4KPC9zdmc+
version: 0.7.2
description: 智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。
"""
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any
import logging
import time
import re
from fastapi import Request
from datetime import datetime
import pytz
from open_webui.utils.chat import generate_chat_completion
from open_webui.models.users import Users
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
SYSTEM_PROMPT_MINDMAP_ASSISTANT = """
你是一个专业的思维导图生成助手,能够高效地分析用户提供的长篇文本,并将其核心主题、关键概念、分支和子分支结构化为标准的Markdown列表语法,以便Markmap.js进行渲染。
请严格遵循以下指导原则:
- **语言**: 所有输出必须使用用户指定的语言。
- **格式**: 你的输出必须严格为Markdown列表格式,并用```markdown 和 ``` 包裹。
- 使用 `#` 定义中心主题(根节点)。
- 使用 `-` 和两个空格的缩进表示分支和子分支。
- **内容**:
- 识别文本的中心主题作为 `#` 标题。
- 识别主要概念作为一级列表项。
- 识别支持性细节或子概念作为嵌套的列表项。
- 节点内容应简洁明了,避免冗长。
- **只输出Markdown语法**: 不要包含任何额外的寒暄、解释或引导性文字。
- **如果文本过短或无法生成有效导图**: 请输出一个简单的Markdown列表,表示无法生成,例如:
```markdown
# 无法生成思维导图
- 原因: 文本内容不足或不明确
```
"""
USER_PROMPT_GENERATE_MINDMAP = """
请分析以下长篇文本,并将其核心主题、关键概念、分支和子分支结构化为标准的Markdown列表语法,以供Markmap.js渲染。
---
**用户上下文信息:**
用户姓名: {user_name}
当前日期时间: {current_date_time_str}
当前星期: {current_weekday}
当前时区: {current_timezone_str}
用户语言: {user_language}
---
**长篇文本内容:**
Use code with caution.
Python
{long_text_content}
"""
HTML_TEMPLATE_MINDMAP = """
<!DOCTYPE html>
<html lang="{user_language}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智绘心图: 思维导图</title>
<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>
<style>
:root {
--primary-color: #1e88e5;
--secondary-color: #43a047;
--background-color: #f4f6f8;
--card-bg-color: #ffffff;
--text-color: #263238;
--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);
--border-radius: 12px;
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
body {
font-family: var(--font-family);
line-height: 1.7;
color: var(--text-color);
margin: 0;
padding: 24px;
background-color: var(--background-color);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.container {
max-width: 1280px;
margin: 20px auto;
background: var(--card-bg-color);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
overflow: hidden;
border: 1px solid var(--border-color);
}
.header {
background: var(--header-gradient);
color: white;
padding: 32px 40px;
text-align: center;
}
.header h1 {
margin: 0;
font-size: 2em;
font-weight: 600;
text-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.user-context {
font-size: 0.85em;
color: var(--muted-text-color);
background-color: #eceff1;
padding: 12px 20px;
display: flex;
justify-content: space-around;
flex-wrap: wrap;
}
.user-context span { margin: 4px 10px; }
.content-area {
padding: 30px 40px;
}
.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: var(--border-radius);
padding: 24px;
min-height: 700px;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid var(--border-color);
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
.download-area {
text-align: center;
padding-top: 30px;
margin-top: 30px;
border-top: 1px solid var(--border-color);
}
.download-btn {
background-color: var(--primary-color);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 1em;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease-in-out;
margin: 0 10px;
display: inline-flex;
align-items: center;
gap: 8px;
}
.download-btn.secondary {
background-color: var(--secondary-color);
}
.download-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
}
.download-btn.copied {
background-color: #2e7d32; /* A darker green for success */
}
.footer {
text-align: center;
padding: 24px;
font-size: 0.85em;
color: #90a4ae;
background-color: #eceff1;
}
.footer a {
color: var(--primary-color);
text-decoration: none;
font-weight: 500;
}
.footer a:hover {
text-decoration: underline;
}
.error-message {
color: #c62828;
background-color: #ffcdd2;
border: 1px solid #ef9a9a;
padding: 20px;
border-radius: var(--border-radius);
font-weight: 500;
font-size: 1.1em;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🧠 智绘心图</h1>
</div>
<div class="user-context">
<span><strong>用户:</strong> {user_name}</span>
<span><strong>分析时间:</strong> {current_date_time_str}</span>
<span><strong>星期:</strong> {current_weekday_zh}</span>
</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="18" height="18" 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="18" height="18" 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>
</div>
<div class="footer">
<p>© {current_year} 智绘心图 • 渲染引擎由 <a href="https://markmap.js.org/" target="_blank">Markmap</a> 提供</p>
</div>
</div>
<script type="text/template" id="markdown-source-{unique_id}">{markdown_syntax}</script>
<script>
(function() {
const renderMindmap = () => {
const uniqueId = "{unique_id}";
const containerEl = document.getElementById('markmap-container-' + uniqueId);
if (!containerEl || containerEl.dataset.markmapRendered) return;
const sourceEl = document.getElementById('markdown-source-' + uniqueId);
if (!sourceEl) return;
const markdownContent = sourceEl.textContent.trim();
if (!markdownContent) {
containerEl.innerHTML = '<div class="error-message">⚠️ 无法加载思维导图: 缺少有效内容。</div>';
return;
}
try {
const svgEl = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svgEl.style.width = '100%';
svgEl.style.height = '700px';
containerEl.innerHTML = '';
containerEl.appendChild(svgEl);
const { Transformer, Markmap } = window.markmap;
const transformer = new Transformer();
const { root } = transformer.transform(markdownContent);
const style = (id) => `${id} text { font-size: 16px !important; }`;
const options = {
autoFit: true,
style: style
};
Markmap.create(svgEl, options, root);
containerEl.dataset.markmapRendered = 'true';
attachDownloadHandlers(uniqueId);
} catch (error) {
console.error('Markmap rendering error:', error);
containerEl.innerHTML = '<div class="error-message">⚠️ 思维导图渲染失败!<br>原因: ' + error.message + '</div>';
}
};
const attachDownloadHandlers = (uniqueId) => {
const downloadSvgBtn = document.getElementById('download-svg-btn-' + uniqueId);
const downloadMdBtn = document.getElementById('download-md-btn-' + uniqueId);
const containerEl = document.getElementById('markmap-container-' + uniqueId);
const showFeedback = (button, isSuccess) => {
const buttonText = button.querySelector('.btn-text');
const originalText = buttonText.textContent;
button.disabled = true;
if (isSuccess) {
buttonText.textContent = '✅ 已复制!';
button.classList.add('copied');
} else {
buttonText.textContent = '❌ 复制失败';
}
setTimeout(() => {
buttonText.textContent = originalText;
button.disabled = false;
button.classList.remove('copied');
}, 2500);
};
const copyToClipboard = (content, button) => {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(content).then(() => {
showFeedback(button, true);
}, () => {
showFeedback(button, false);
});
} else {
// Fallback for older/insecure contexts
const textArea = document.createElement('textarea');
textArea.value = content;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
showFeedback(button, true);
} catch (err) {
showFeedback(button, false);
}
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);
}
});
}
if (downloadMdBtn) {
downloadMdBtn.addEventListener('click', (event) => {
event.stopPropagation();
const markdownContent = document.getElementById('markdown-source-' + uniqueId).textContent;
copyToClipboard(markdownContent, downloadMdBtn);
});
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', renderMindmap);
} else {
renderMindmap();
}
})();
</script>
</body>
</html>
"""
class Action:
class Valves(BaseModel):
show_status: bool = Field(
default=True, description="是否在聊天界面显示操作状态更新。"
)
LLM_MODEL_ID: str = Field(
default="gemini-2.5-flash",
description="用于文本分析的内置LLM模型ID。",
)
MIN_TEXT_LENGTH: int = Field(
default=100, description="进行思维导图分析所需的最小文本长度(字符数)。"
)
def __init__(self):
self.valves = self.Valves()
self.weekday_map = {
"Monday": "星期一",
"Tuesday": "星期二",
"Wednesday": "星期三",
"Thursday": "星期四",
"Friday": "星期五",
"Saturday": "星期六",
"Sunday": "星期日",
}
def _extract_markdown_syntax(self, llm_output: str) -> str:
match = re.search(r"```markdown\s*(.*?)\s*```", llm_output, re.DOTALL)
if match:
extracted_content = match.group(1).strip()
else:
logger.warning(
"LLM输出未严格遵循预期Markdown格式将整个输出作为摘要处理。"
)
extracted_content = llm_output.strip()
return extracted_content.replace("</script>", "<\\/script>")
async def action(
self,
body: dict,
__user__: Optional[Dict[str, Any]] = None,
__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")
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")
current_weekday_zh = self.weekday_map.get(current_weekday_en, "未知星期")
current_year = current_datetime_shanghai.strftime("%Y")
current_timezone_str = "Asia/Shanghai"
except Exception as e:
logger.warning(f"获取时区信息失败: {e},使用默认值。")
now = datetime.now()
current_date_time_str = now.strftime("%Y-%m-%d %H:%M:%S")
current_weekday_zh = "未知星期"
current_year = now.strftime("%Y")
current_timezone_str = "未知时区"
if __event_emitter__:
await __event_emitter__(
{
"type": "notification",
"data": {
"type": "info",
"content": "智绘心图已启动,正在为您生成思维导图...",
},
}
)
messages = body.get("messages")
if (
not messages
or not isinstance(messages, list)
or not messages[-1].get("content")
):
error_message = "无法获取有效的用户消息内容。"
if __event_emitter__:
await __event_emitter__(
{
"type": "notification",
"data": {"type": "error", "content": error_message},
}
)
return {
"messages": [{"role": "assistant", "content": f"{error_message}"}]
}
parts = re.split(r"```html.*?```", messages[-1]["content"], flags=re.DOTALL)
long_text_content = ""
if parts:
for part in reversed(parts):
if part.strip():
long_text_content = part.strip()
break
if not long_text_content:
long_text_content = messages[-1]["content"].strip()
if len(long_text_content) < self.valves.MIN_TEXT_LENGTH:
short_text_message = f"文本内容过短({len(long_text_content)}字符),无法进行有效分析。请提供至少{self.valves.MIN_TEXT_LENGTH}字符的文本。"
if __event_emitter__:
await __event_emitter__(
{
"type": "notification",
"data": {"type": "warning", "content": short_text_message},
}
)
return {
"messages": [
{"role": "assistant", "content": f"⚠️ {short_text_message}"}
]
}
if self.valves.show_status and __event_emitter__:
await __event_emitter__(
{
"type": "status",
"data": {
"description": "智绘心图: 深入分析文本结构...",
"done": False,
"hidden": False,
},
}
)
try:
unique_id = f"id_{int(time.time() * 1000)}"
formatted_user_prompt = USER_PROMPT_GENERATE_MINDMAP.format(
user_name=user_name,
current_date_time_str=current_date_time_str,
current_weekday=current_weekday_zh,
current_timezone_str=current_timezone_str,
user_language=user_language,
long_text_content=long_text_content,
)
llm_payload = {
"model": self.valves.LLM_MODEL_ID,
"messages": [
{"role": "system", "content": SYSTEM_PROMPT_MINDMAP_ASSISTANT},
{"role": "user", "content": formatted_user_prompt},
],
"temperature": 0.5,
"stream": False,
}
user_obj = Users.get_user_by_id(user_id)
if not user_obj:
raise ValueError(f"无法获取用户对象用户ID: {user_id}")
llm_response = await generate_chat_completion(
__request__, llm_payload, user_obj
)
if (
not llm_response
or "choices" not in llm_response
or not llm_response["choices"]
):
raise ValueError("LLM响应格式不正确或为空。")
assistant_response_content = llm_response["choices"][0]["message"][
"content"
]
markdown_syntax = self._extract_markdown_syntax(assistant_response_content)
final_html_content = (
HTML_TEMPLATE_MINDMAP.replace("{unique_id}", unique_id)
.replace("{user_language}", user_language)
.replace("{user_name}", user_name)
.replace("{current_date_time_str}", current_date_time_str)
.replace("{current_weekday_zh}", current_weekday_zh)
.replace("{current_year}", current_year)
.replace("{markdown_syntax}", markdown_syntax)
)
html_embed_tag = f"```html\n{final_html_content}\n```"
body["messages"][-1]["content"] = f"{long_text_content}\n\n{html_embed_tag}"
if self.valves.show_status and __event_emitter__:
await __event_emitter__(
{
"type": "status",
"data": {
"description": "智绘心图: 绘制完成!",
"done": True,
"hidden": False,
},
}
)
await __event_emitter__(
{
"type": "notification",
"data": {
"type": "success",
"content": f"思维导图已生成,{user_name}",
},
}
)
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后端日志获取更多详情。"
body["messages"][-1][
"content"
] = f"{long_text_content}\n\n❌ **错误:** {user_facing_error}"
if __event_emitter__:
if self.valves.show_status:
await __event_emitter__(
{
"type": "status",
"data": {
"description": "智绘心图: 处理失败。",
"done": True,
"hidden": False,
},
}
)
await __event_emitter__(
{
"type": "notification",
"data": {
"type": "error",
"content": f"智绘心图生成失败, {user_name}",
},
}
)
return body