diff --git a/plugins/actions/smart-mind-map/smart_mind_map.py b/plugins/actions/smart-mind-map/smart_mind_map.py
index d54d79b..858391b 100644
--- a/plugins/actions/smart-mind-map/smart_mind_map.py
+++ b/plugins/actions/smart-mind-map/smart_mind_map.py
@@ -4,7 +4,7 @@ author: Fu-Jie
author_url: https://github.com/Fu-Jie/awesome-openwebui
funding_url: https://github.com/open-webui
funding_url: https://github.com/Fu-Jie/awesome-openwebui
-version: 0.9.2
+version: 0.9.3
openwebui_id: 3094c59a-b4dd-4e0c-9449-15e2dd547dc4
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4=
description: Intelligently analyzes text content and generates interactive mind maps to help users structure and visualize knowledge.
@@ -14,6 +14,7 @@ import logging
import os
import re
import time
+import json
from datetime import datetime, timezone
from typing import Any, Callable, Awaitable, Dict, Optional
from zoneinfo import ZoneInfo
@@ -29,6 +30,438 @@ logging.basicConfig(
)
logger = logging.getLogger(__name__)
+TRANSLATIONS = {
+ "en-US": {
+ "status_starting": "Smart Mind Map is starting, generating mind map for you...",
+ "error_no_content": "Unable to retrieve valid user message content.",
+ "error_text_too_short": "Text content is too short ({len} characters), unable to perform effective analysis. Please provide at least {min_len} characters of text.",
+ "status_analyzing": "Smart Mind Map: Analyzing text structure in depth...",
+ "status_drawing": "Smart Mind Map: Drawing completed!",
+ "notification_success": "Mind map has been generated, {user_name}!",
+ "error_processing": "Smart Mind Map processing failed: {error}",
+ "error_user_facing": "Sorry, Smart Mind Map encountered an error during processing: {error}.\nPlease check the Open WebUI backend logs for more details.",
+ "status_failed": "Smart Mind Map: Processing failed.",
+ "notification_failed": "Smart Mind Map generation failed, {user_name}!",
+ "status_rendering_image": "Smart Mind Map: Rendering image...",
+ "status_image_generated": "Smart Mind Map: Image generated!",
+ "notification_image_success": "Mind map image has been generated, {user_name}!",
+ "ui_title": "🧠 Smart Mind Map",
+ "ui_user": "User:",
+ "ui_time": "Time:",
+ "ui_download_png": "PNG",
+ "ui_download_svg": "SVG",
+ "ui_download_md": "Markdown",
+ "ui_zoom_out": "-",
+ "ui_zoom_reset": "Reset",
+ "ui_zoom_in": "+",
+ "ui_depth_select": "Expand Level",
+ "ui_depth_all": "Expand All",
+ "ui_depth_2": "Level 2",
+ "ui_depth_3": "Level 3",
+ "ui_fullscreen": "Fullscreen",
+ "ui_theme": "Theme",
+ "ui_footer": "© {year} Smart Mind Map • Markmap",
+ "html_error_missing_content": "⚠️ Unable to load mind map: Missing valid content.",
+ "html_error_load_failed": "⚠️ Resource loading failed, please try again later.",
+ "js_done": "Done",
+ "js_failed": "Failed",
+ "js_generating": "Generating...",
+ "js_filename": "mindmap.png",
+ "js_upload_failed": "Upload failed: ",
+ "md_image_alt": "🧠 Mind Map"
+ },
+ "zh-CN": {
+ "status_starting": "思维导图已启动,正在为您生成思维导图...",
+ "error_no_content": "无法获取有效的用户消息内容。",
+ "error_text_too_short": "文本内容过短({len}字符),无法进行有效分析。请提供至少{min_len}字符的文本。",
+ "status_analyzing": "思维导图:深入分析文本结构...",
+ "status_drawing": "思维导图:绘制完成!",
+ "notification_success": "思维导图已生成,{user_name}!",
+ "error_processing": "思维导图处理失败:{error}",
+ "error_user_facing": "抱歉,思维导图在处理时遇到错误:{error}。\n请检查Open WebUI后端日志获取更多详情。",
+ "status_failed": "思维导图:处理失败。",
+ "notification_failed": "思维导图生成失败,{user_name}!",
+ "status_rendering_image": "思维导图:正在渲染图片...",
+ "status_image_generated": "思维导图:图片已生成!",
+ "notification_image_success": "思维导图图片已生成,{user_name}!",
+ "ui_title": "🧠 智能思维导图",
+ "ui_user": "用户:",
+ "ui_time": "时间:",
+ "ui_download_png": "PNG",
+ "ui_download_svg": "SVG",
+ "ui_download_md": "Markdown",
+ "ui_zoom_out": "缩小",
+ "ui_zoom_reset": "重置",
+ "ui_zoom_in": "放大",
+ "ui_depth_select": "展开层级",
+ "ui_depth_all": "全部展开",
+ "ui_depth_2": "展开 2 级",
+ "ui_depth_3": "展开 3 级",
+ "ui_fullscreen": "全屏",
+ "ui_theme": "主题",
+ "ui_footer": "© {year} 智能思维导图 • Markmap",
+ "html_error_missing_content": "⚠️ 无法加载思维导图:缺少有效内容。",
+ "html_error_load_failed": "⚠️ 资源加载失败,请稍后重试。",
+ "js_done": "完成",
+ "js_failed": "失败",
+ "js_generating": "生成中...",
+ "js_filename": "思维导图.png",
+ "js_upload_failed": "上传失败:",
+ "md_image_alt": "🧠 思维导图"
+ },
+ "zh-HK": {
+ "status_starting": "思維導圖已啟動,正在為您生成思維導圖...",
+ "error_no_content": "無法獲取有效的用戶消息內容。",
+ "error_text_too_short": "文本內容過短({len}字元),無法進行有效分析。請提供至少{min_len}字元的文本。",
+ "status_analyzing": "思維導圖:深入分析文本結構...",
+ "status_drawing": "思維導圖:繪製完成!",
+ "notification_success": "思維導圖已生成,{user_name}!",
+ "error_processing": "思維導圖處理失敗:{error}",
+ "error_user_facing": "抱歉,思維導圖在處理時遇到錯誤:{error}。\n請檢查Open WebUI後端日誌獲取更多詳情。",
+ "status_failed": "思維導圖:處理失敗。",
+ "notification_failed": "思維導圖生成失敗,{user_name}!",
+ "status_rendering_image": "思維導圖:正在渲染圖片...",
+ "status_image_generated": "思維導圖:圖片已生成!",
+ "notification_image_success": "思維導圖圖片已生成,{user_name}!",
+ "ui_title": "🧠 智能思維導圖",
+ "ui_user": "用戶:",
+ "ui_time": "時間:",
+ "ui_download_png": "PNG",
+ "ui_download_svg": "SVG",
+ "ui_download_md": "Markdown",
+ "ui_zoom_out": "縮小",
+ "ui_zoom_reset": "重置",
+ "ui_zoom_in": "放大",
+ "ui_depth_select": "展開層級",
+ "ui_depth_all": "全部展開",
+ "ui_depth_2": "展開 2 級",
+ "ui_depth_3": "展開 3 級",
+ "ui_fullscreen": "全屏",
+ "ui_theme": "主題",
+ "ui_footer": "© {year} 智能思維導圖 • Markmap",
+ "html_error_missing_content": "⚠️ 無法加載思維導圖:缺少有效內容。",
+ "html_error_load_failed": "⚠️ 資源加載失敗,請稍後重試。",
+ "js_done": "完成",
+ "js_failed": "失敗",
+ "js_generating": "生成中...",
+ "js_filename": "思維導圖.png",
+ "js_upload_failed": "上傳失敗:",
+ "md_image_alt": "🧠 思維導圖"
+ },
+ "ko-KR": {
+ "status_starting": "스마트 마인드맵이 시작되었습니다, 마인드맵을 생성 중입니다...",
+ "error_no_content": "유효한 사용자 메시지 내용을 가져올 수 없습니다.",
+ "error_text_too_short": "텍스트 내용이 너무 짧아({len}자), 효과적인 분석을 수행할 수 없습니다. 최소 {min_len}자 이상의 텍스트를 제공해 주세요.",
+ "status_analyzing": "스마트 마인드맵: 텍스트 구조 심층 분석 중...",
+ "status_drawing": "스마트 마인드맵: 그리기 완료!",
+ "notification_success": "마인드맵이 생성되었습니다, {user_name}님!",
+ "error_processing": "스마트 마인드맵 처리 실패: {error}",
+ "error_user_facing": "죄송합니다, 스마트 마인드맵 처리 중 오류가 발생했습니다: {error}.\n자세한 내용은 Open WebUI 백엔드 로그를 확인해 주세요.",
+ "status_failed": "스마트 마인드맵: 처리 실패.",
+ "notification_failed": "스마트 마인드맵 생성 실패, {user_name}님!",
+ "status_rendering_image": "스마트 마인드맵: 이미지 렌더링 중...",
+ "status_image_generated": "스마트 마인드맵: 이미지 생성됨!",
+ "notification_image_success": "마인드맵 이미지가 생성되었습니다, {user_name}님!",
+ "ui_title": "🧠 스마트 마인드맵",
+ "ui_user": "사용자:",
+ "ui_time": "시간:",
+ "ui_download_png": "PNG",
+ "ui_download_svg": "SVG",
+ "ui_download_md": "Markdown",
+ "ui_zoom_out": "-",
+ "ui_zoom_reset": "초기화",
+ "ui_zoom_in": "+",
+ "ui_depth_select": "레벨 확장",
+ "ui_depth_all": "모두 확장",
+ "ui_depth_2": "레벨 2",
+ "ui_depth_3": "레벨 3",
+ "ui_fullscreen": "전체 화면",
+ "ui_theme": "테마",
+ "ui_footer": "© {year} 스마트 마인드맵 • Markmap",
+ "html_error_missing_content": "⚠️ 마인드맵을 로드할 수 없습니다: 유효한 내용이 없습니다.",
+ "html_error_load_failed": "⚠️ 리소스 로드 실패, 나중에 다시 시도해 주세요.",
+ "js_done": "완료",
+ "js_failed": "실패",
+ "js_generating": "생성 중...",
+ "js_filename": "mindmap.png",
+ "js_upload_failed": "업로드 실패: ",
+ "md_image_alt": "🧠 마인드맵"
+ },
+ "ja-JP": {
+ "status_starting": "スマートマインドマップが起動しました。マインドマップを生成しています...",
+ "error_no_content": "有効なユーザーメッセージの内容を取得できませんでした。",
+ "error_text_too_short": "テキストの内容が短すぎるため({len}文字)、効果的な分析を実行できません。少なくとも{min_len}文字のテキストを提供してください。",
+ "status_analyzing": "スマートマインドマップ:テキスト構造を詳細に分析中...",
+ "status_drawing": "スマートマインドマップ:描画完了!",
+ "notification_success": "マインドマップが生成されました、{user_name}さん!",
+ "error_processing": "スマートマインドマップ処理失敗:{error}",
+ "error_user_facing": "申し訳ありません、スマートマインドマップの処理中にエラーが発生しました:{error}。\n詳細については、Open WebUIバックエンドログを確認してください。",
+ "status_failed": "スマートマインドマップ:処理失敗。",
+ "notification_failed": "スマートマインドマップ生成失敗、{user_name}さん!",
+ "status_rendering_image": "スマートマインドマップ:画像レンダリング中...",
+ "status_image_generated": "スマートマインドマップ:画像生成完了!",
+ "notification_image_success": "マインドマップ画像が生成されました、{user_name}さん!",
+ "ui_title": "🧠 スマートマインドマップ",
+ "ui_user": "ユーザー:",
+ "ui_time": "時間:",
+ "ui_download_png": "PNG",
+ "ui_download_svg": "SVG",
+ "ui_download_md": "Markdown",
+ "ui_zoom_out": "-",
+ "ui_zoom_reset": "リセット",
+ "ui_zoom_in": "+",
+ "ui_depth_select": "レベル展開",
+ "ui_depth_all": "すべて展開",
+ "ui_depth_2": "レベル2",
+ "ui_depth_3": "レベル3",
+ "ui_fullscreen": "全画面",
+ "ui_theme": "テーマ",
+ "ui_footer": "© {year} スマートマインドマップ • Markmap",
+ "html_error_missing_content": "⚠️ マインドマップを読み込めません:有効なコンテンツがありません。",
+ "html_error_load_failed": "⚠️ リソースの読み込みに失敗しました。後でもう一度お試しください。",
+ "js_done": "完了",
+ "js_failed": "失敗",
+ "js_generating": "生成中...",
+ "js_filename": "mindmap.png",
+ "js_upload_failed": "アップロード失敗:",
+ "md_image_alt": "🧠 マインドマップ"
+ },
+ "fr-FR": {
+ "status_starting": "Smart Mind Map démarre, génération de la carte heuristique en cours...",
+ "error_no_content": "Impossible de récupérer le contenu valide du message utilisateur.",
+ "error_text_too_short": "Le contenu du texte est trop court ({len} caractères), impossible d'effectuer une analyse efficace. Veuillez fournir au moins {min_len} caractères de texte.",
+ "status_analyzing": "Smart Mind Map : Analyse approfondie de la structure du texte...",
+ "status_drawing": "Smart Mind Map : Dessin terminé !",
+ "notification_success": "La carte heuristique a été générée, {user_name} !",
+ "error_processing": "Échec du traitement de Smart Mind Map : {error}",
+ "error_user_facing": "Désolé, Smart Mind Map a rencontré une erreur lors du traitement : {error}.\nVeuillez vérifier les journaux backend d'Open WebUI pour plus de détails.",
+ "status_failed": "Smart Mind Map : Échec du traitement.",
+ "notification_failed": "Échec de la génération de la carte heuristique, {user_name} !",
+ "status_rendering_image": "Smart Mind Map : Rendu de l'image...",
+ "status_image_generated": "Smart Mind Map : Image générée !",
+ "notification_image_success": "L'image de la carte heuristique a été générée, {user_name} !",
+ "ui_title": "🧠 Smart Mind Map",
+ "ui_user": "Utilisateur :",
+ "ui_time": "Heure :",
+ "ui_download_png": "PNG",
+ "ui_download_svg": "SVG",
+ "ui_download_md": "Markdown",
+ "ui_zoom_out": "-",
+ "ui_zoom_reset": "Rénitialiser",
+ "ui_zoom_in": "+",
+ "ui_depth_select": "Niveau d'expansion",
+ "ui_depth_all": "Tout développer",
+ "ui_depth_2": "Niveau 2",
+ "ui_depth_3": "Niveau 3",
+ "ui_fullscreen": "Plein écran",
+ "ui_theme": "Thème",
+ "ui_footer": "© {year} Smart Mind Map • Markmap",
+ "html_error_missing_content": "⚠️ Impossible de charger la carte heuristique : contenu valide manquant.",
+ "html_error_load_failed": "⚠️ Échec du chargement des ressources, veuillez réessayer plus tard.",
+ "js_done": "Terminé",
+ "js_failed": "Échec",
+ "js_generating": "Génération...",
+ "js_filename": "carte_heuristique.png",
+ "js_upload_failed": "Échec du téléchargement : ",
+ "md_image_alt": "🧠 Carte Heuristique"
+ },
+ "de-DE": {
+ "status_starting": "Smart Mind Map startet, Mindmap wird für Sie erstellt...",
+ "error_no_content": "Gültiger Inhalt der Benutzernachricht konnte nicht abgerufen werden.",
+ "error_text_too_short": "Der Textinhalt ist zu kurz ({len} Zeichen), eine effektive Analyse ist nicht möglich. Bitte geben Sie mindestens {min_len} Zeichen Text an.",
+ "status_analyzing": "Smart Mind Map: Detaillierte Analyse der Textstruktur...",
+ "status_drawing": "Smart Mind Map: Zeichnen abgeschlossen!",
+ "notification_success": "Mindmap wurde erstellt, {user_name}!",
+ "error_processing": "Smart Mind Map Verarbeitung fehlgeschlagen: {error}",
+ "error_user_facing": "Entschuldigung, bei der Verarbeitung von Smart Mind Map ist ein Fehler aufgetreten: {error}.\nBitte überprüfen Sie die Open WebUI Backend-Protokolle für weitere Details.",
+ "status_failed": "Smart Mind Map: Verarbeitung fehlgeschlagen.",
+ "notification_failed": "Erstellung der Mindmap fehlgeschlagen, {user_name}!",
+ "status_rendering_image": "Smart Mind Map: Bild wird gerendert...",
+ "status_image_generated": "Smart Mind Map: Bild erstellt!",
+ "notification_image_success": "Mindmap-Bild wurde erstellt, {user_name}!",
+ "ui_title": "🧠 Smart Mind Map",
+ "ui_user": "Benutzer:",
+ "ui_time": "Zeit:",
+ "ui_download_png": "PNG",
+ "ui_download_svg": "SVG",
+ "ui_download_md": "Markdown",
+ "ui_zoom_out": "-",
+ "ui_zoom_reset": "Zurücksetzen",
+ "ui_zoom_in": "+",
+ "ui_depth_select": "Ebene erweitern",
+ "ui_depth_all": "Alles erweitern",
+ "ui_depth_2": "Ebene 2",
+ "ui_depth_3": "Ebene 3",
+ "ui_fullscreen": "Vollbild",
+ "ui_theme": "Thema",
+ "ui_footer": "© {year} Smart Mind Map • Markmap",
+ "html_error_missing_content": "⚠️ Mindmap kann nicht geladen werden: Gültiger Inhalt fehlt.",
+ "html_error_load_failed": "⚠️ Ressourcenladen fehlgeschlagen, bitte versuchen Sie es später erneut.",
+ "js_done": "Fertig",
+ "js_failed": "Fehlgeschlagen",
+ "js_generating": "Generiere...",
+ "js_filename": "mindmap.png",
+ "js_upload_failed": "Upload fehlgeschlagen: ",
+ "md_image_alt": "🧠 Mindmap"
+ },
+ "es-ES": {
+ "status_starting": "Smart Mind Map se está iniciando, generando mapa mental para usted...",
+ "error_no_content": "No se puede recuperar el contenido válido del mensaje del usuario.",
+ "error_text_too_short": "El contenido del texto es demasiado corto ({len} caracteres), no se puede realizar un análisis efectivo. Proporcione al menos {min_len} caracteres de texto.",
+ "status_analyzing": "Smart Mind Map: Analizando la estructura del texto en profundidad...",
+ "status_drawing": "Smart Mind Map: ¡Dibujo completado!",
+ "notification_success": "¡El mapa mental ha sido generado, {user_name}!",
+ "error_processing": "Falló el procesamiento de Smart Mind Map: {error}",
+ "error_user_facing": "Lo sentimos, Smart Mind Map encontró un error durante el procesamiento: {error}.\nConsulte los registros del backend de Open WebUI para más detalles.",
+ "status_failed": "Smart Mind Map: Procesamiento fallido.",
+ "notification_failed": "¡La generación del mapa mental falló, {user_name}!",
+ "status_rendering_image": "Smart Mind Map: Renderizando imagen...",
+ "status_image_generated": "Smart Mind Map: ¡Imagen generada!",
+ "notification_image_success": "¡La imagen del mapa mental ha sido generada, {user_name}!",
+ "ui_title": "🧠 Smart Mind Map",
+ "ui_user": "Usuario:",
+ "ui_time": "Hora:",
+ "ui_download_png": "PNG",
+ "ui_download_svg": "SVG",
+ "ui_download_md": "Markdown",
+ "ui_zoom_out": "-",
+ "ui_zoom_reset": "Restablecer",
+ "ui_zoom_in": "+",
+ "ui_depth_select": "Expandir Nivel",
+ "ui_depth_all": "Expandir Todo",
+ "ui_depth_2": "Nivel 2",
+ "ui_depth_3": "Nivel 3",
+ "ui_fullscreen": "Pantalla completa",
+ "ui_theme": "Tema",
+ "ui_footer": "© {year} Smart Mind Map • Markmap",
+ "html_error_missing_content": "⚠️ No se puede cargar el mapa mental: Falta contenido válido.",
+ "html_error_load_failed": "⚠️ Falló la carga de recursos, inténtelo de nuevo más tarde.",
+ "js_done": "Hecho",
+ "js_failed": "Fallido",
+ "js_generating": "Generando...",
+ "js_filename": "mapa_mental.png",
+ "js_upload_failed": "Carga fallida: ",
+ "md_image_alt": "🧠 Mapa Mental"
+ },
+ "it-IT": {
+ "status_starting": "Smart Mind Map si sta avviando, generazione mappa mentale in corso...",
+ "error_no_content": "Impossibile recuperare il contenuto valido del messaggio utente.",
+ "error_text_too_short": "Il testo è troppo breve ({len} caratteri), impossibile eseguire un'analisi efficace. Fornire almeno {min_len} caratteri di testo.",
+ "status_analyzing": "Smart Mind Map: Analisi approfondita della struttura del testo...",
+ "status_drawing": "Smart Mind Map: Disegno completato!",
+ "notification_success": "La mappa mentale è stata generata, {user_name}!",
+ "error_processing": "Elaborazione Smart Mind Map fallita: {error}",
+ "error_user_facing": "Spiacenti, Smart Mind Map ha riscontrato un errore durante l'elaborazione: {error}.\nControllare i log del backend di Open WebUI per ulteriori dettagli.",
+ "status_failed": "Smart Mind Map: Elaborazione fallita.",
+ "notification_failed": "Generazione mappa mentale fallita, {user_name}!",
+ "status_rendering_image": "Smart Mind Map: Rendering immagine...",
+ "status_image_generated": "Smart Mind Map: Immagine generata!",
+ "notification_image_success": "L'immagine della mappa mentale è stata generata, {user_name}!",
+ "ui_title": "🧠 Smart Mind Map",
+ "ui_user": "Utente:",
+ "ui_time": "Ora:",
+ "ui_download_png": "PNG",
+ "ui_download_svg": "SVG",
+ "ui_download_md": "Markdown",
+ "ui_zoom_out": "-",
+ "ui_zoom_reset": "Reimposta",
+ "ui_zoom_in": "+",
+ "ui_depth_select": "Espandi Livello",
+ "ui_depth_all": "Espandi Tutto",
+ "ui_depth_2": "Livello 2",
+ "ui_depth_3": "Livello 3",
+ "ui_fullscreen": "Schermo intero",
+ "ui_theme": "Tema",
+ "ui_footer": "© {year} Smart Mind Map • Markmap",
+ "html_error_missing_content": "⚠️ Impossibile caricare la mappa mentale: Contenuto valido mancante.",
+ "html_error_load_failed": "⚠️ Caricamento risorse fallito, riprovare più tardi.",
+ "js_done": "Fatto",
+ "js_failed": "Fallito",
+ "js_generating": "Generazione...",
+ "js_filename": "mappa_mentale.png",
+ "js_upload_failed": "Caricamento fallito: ",
+ "md_image_alt": "🧠 Mappa Mentale"
+ },
+ "vi-VN": {
+ "status_starting": "Smart Mind Map đang khởi động, đang tạo sơ đồ tư duy cho bạn...",
+ "error_no_content": "Không thể lấy nội dung tin nhắn người dùng hợp lệ.",
+ "error_text_too_short": "Nội dung văn bản quá ngắn ({len} ký tự), không thể thực hiện phân tích hiệu quả. Vui lòng cung cấp ít nhất {min_len} ký tự văn bản.",
+ "status_analyzing": "Smart Mind Map: Phân tích sâu cấu trúc văn bản...",
+ "status_drawing": "Smart Mind Map: Vẽ hoàn tất!",
+ "notification_success": "Sơ đồ tư duy đã được tạo, {user_name}!",
+ "error_processing": "Xử lý Smart Mind Map thất bại: {error}",
+ "error_user_facing": "Xin lỗi, Smart Mind Map đã gặp lỗi trong quá trình xử lý: {error}.\nVui lòng kiểm tra nhật ký backend Open WebUI để biết thêm chi tiết.",
+ "status_failed": "Smart Mind Map: Xử lý thất bại.",
+ "notification_failed": "Tạo sơ đồ tư duy thất bại, {user_name}!",
+ "status_rendering_image": "Smart Mind Map: Đang render hình ảnh...",
+ "status_image_generated": "Smart Mind Map: Hình ảnh đã tạo!",
+ "notification_image_success": "Hình ảnh sơ đồ tư duy đã được tạo, {user_name}!",
+ "ui_title": "🧠 Smart Mind Map",
+ "ui_user": "Người dùng:",
+ "ui_time": "Thời gian:",
+ "ui_download_png": "PNG",
+ "ui_download_svg": "SVG",
+ "ui_download_md": "Markdown",
+ "ui_zoom_out": "-",
+ "ui_zoom_reset": "Đặt lại",
+ "ui_zoom_in": "+",
+ "ui_depth_select": "Mở rộng Cấp độ",
+ "ui_depth_all": "Mở rộng Tất cả",
+ "ui_depth_2": "Cấp độ 2",
+ "ui_depth_3": "Cấp độ 3",
+ "ui_fullscreen": "Toàn màn hình",
+ "ui_theme": "Chủ đề",
+ "ui_footer": "© {year} Smart Mind Map • Markmap",
+ "html_error_missing_content": "⚠️ Không thể tải sơ đồ tư duy: Thiếu nội dung hợp lệ.",
+ "html_error_load_failed": "⚠️ Tải tài nguyên thất bại, vui lòng thử lại sau.",
+ "js_done": "Xong",
+ "js_failed": "Thất bại",
+ "js_generating": "Đang tạo...",
+ "js_filename": "sodo_tuduy.png",
+ "js_upload_failed": "Tải lên thất bại: ",
+ "md_image_alt": "🧠 Sơ đồ Tư duy"
+ },
+ "id-ID": {
+ "status_starting": "Smart Mind Map sedang dimulai, membuat peta pikiran untuk Anda...",
+ "error_no_content": "Tidak dapat mengambil konten pesan pengguna yang valid.",
+ "error_text_too_short": "Konten teks terlalu pendek ({len} karakter), tidak dapat melakukan analisis efektif. Harap berikan setidaknya {min_len} karakter teks.",
+ "status_analyzing": "Smart Mind Map: Menganalisis struktur teks secara mendalam...",
+ "status_drawing": "Smart Mind Map: Menggambar selesai!",
+ "notification_success": "Peta pikiran telah dibuat, {user_name}!",
+ "error_processing": "Pemrosesan Smart Mind Map gagal: {error}",
+ "error_user_facing": "Maaf, Smart Mind Map mengalami kesalahan saat memproses: {error}.\nSilakan periksa log backend Open WebUI untuk detail lebih lanjut.",
+ "status_failed": "Smart Mind Map: Pemrosesan gagal.",
+ "notification_failed": "Pembuatan peta pikiran gagal, {user_name}!",
+ "status_rendering_image": "Smart Mind Map: Merender gambar...",
+ "status_image_generated": "Smart Mind Map: Gambar dibuat!",
+ "notification_image_success": "Gambar peta pikiran telah dibuat, {user_name}!",
+ "ui_title": "🧠 Smart Mind Map",
+ "ui_user": "Pengguna:",
+ "ui_time": "Waktu:",
+ "ui_download_png": "PNG",
+ "ui_download_svg": "SVG",
+ "ui_download_md": "Markdown",
+ "ui_zoom_out": "-",
+ "ui_zoom_reset": "Atur Ulang",
+ "ui_zoom_in": "+",
+ "ui_depth_select": "Perluas Level",
+ "ui_depth_all": "Perluas Semua",
+ "ui_depth_2": "Level 2",
+ "ui_depth_3": "Level 3",
+ "ui_fullscreen": "Layar Penuh",
+ "ui_theme": "Tema",
+ "ui_footer": "© {year} Smart Mind Map • Markmap",
+ "html_error_missing_content": "⚠️ Tidak dapat memuat peta pikiran: Konten valid hilang.",
+ "html_error_load_failed": "⚠️ Gagal memuat sumber daya, silakan coba lagi nanti.",
+ "js_done": "Selesai",
+ "js_failed": "Gagal",
+ "js_generating": "Membuat...",
+ "js_filename": "peta_pikiran.png",
+ "js_upload_failed": "Unggah gagal: ",
+ "md_image_alt": "🧠 Peta Pikiran"
+ }
+}
+
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.
@@ -51,8 +484,6 @@ Please strictly follow these guidelines:
```
"""
-import json
-
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.
@@ -72,7 +503,7 @@ User Language: {user_language}
HTML_WRAPPER_TEMPLATE = """
-
+
@@ -296,54 +727,55 @@ CSS_TEMPLATE_MINDMAP = """
CONTENT_TEMPLATE_MINDMAP = """
- User: {user_name}
- Time: {current_date_time_str}
+ {t_ui_user} {user_name}
+ {t_ui_time} {current_date_time_str}
-
-
-
+
+
+
-
"""
-SCRIPT_TEMPLATE_MINDMAP = """
+SCRIPT_TEMPLATE_MINDMAP = r"""
", "<\\/script>")
)
+ # Prepare i18n for this specific context
+ i18n_data = {}
+ target_lang = lang
+ if target_lang not in TRANSLATIONS and target_lang in self.fallback_map:
+ target_lang = self.fallback_map[target_lang]
+ if target_lang not in TRANSLATIONS:
+ target_lang = "en-US"
+
+ full_trans = TRANSLATIONS.get(target_lang, TRANSLATIONS["en-US"])
+ # We only need specific keys for the JS image generation part
+ keys = ["js_upload_failed", "md_image_alt"]
+ for k in keys:
+ i18n_data[k] = full_trans.get(k, TRANSLATIONS["en-US"].get(k, k))
+
+ i18n_json = json.dumps(i18n_data, ensure_ascii=False)
+
return f"""
(async function() {{
const uniqueId = "{unique_id}";
const chatId = "{chat_id}";
const messageId = "{message_id}";
+ const i18n = {i18n_json};
const defaultWidth = 1200;
const defaultHeight = 800;
- // Theme detection - check parent document for OpenWebUI theme
+ // Theme detection ... (Same as before)
const detectTheme = () => {{
try {{
- // 1. Check parent document's html/body class or data-theme
const html = document.documentElement;
const body = document.body;
const htmlClass = html ? html.className : '';
@@ -1053,8 +1552,6 @@ class Action:
if (htmlDataTheme === 'light' || bodyClass.includes('light') || htmlClass.includes('light')) {{
return 'light';
}}
-
- // 2. Check meta theme-color
const metas = document.querySelectorAll('meta[name="theme-color"]');
if (metas.length > 0) {{
const color = metas[metas.length - 1].content.trim();
@@ -1068,12 +1565,9 @@ class Action:
return luma < 0.5 ? 'dark' : 'light';
}}
}}
-
- // 3. Check system preference
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {{
return 'dark';
}}
-
return 'light';
}} catch (e) {{
return 'light';
@@ -1083,7 +1577,6 @@ class Action:
const currentTheme = detectTheme();
console.log("[MindMap Image] Detected theme:", currentTheme);
- // Theme-based colors
const colors = currentTheme === 'dark' ? {{
background: '#1f2937',
text: '#e5e7eb',
@@ -1096,28 +1589,19 @@ class Action:
nodeStroke: '#94a3b8'
}};
- // Auto-detect chat container width for responsive sizing
let svgWidth = defaultWidth;
let svgHeight = defaultHeight;
const chatContainer = document.getElementById('chat-container');
if (chatContainer) {{
const containerWidth = chatContainer.clientWidth;
if (containerWidth > 100) {{
- // Use container width with some padding (90% of container)
svgWidth = Math.floor(containerWidth * 0.9);
- // Maintain aspect ratio based on default dimensions
svgHeight = Math.floor(svgWidth * (defaultHeight / defaultWidth));
- console.log("[MindMap Image] Auto-detected container width:", containerWidth, "-> SVG:", svgWidth, "x", svgHeight);
}}
}}
- console.log("[MindMap Image] Starting render...");
- console.log("[MindMap Image] chatId:", chatId, "messageId:", messageId);
-
try {{
- // Load D3 if not loaded
if (typeof d3 === 'undefined') {{
- console.log("[MindMap Image] Loading D3...");
await new Promise((resolve, reject) => {{
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/d3@7';
@@ -1127,9 +1611,7 @@ class Action:
}});
}}
- // Load markmap-lib if not loaded
if (!window.markmap || !window.markmap.Transformer) {{
- console.log("[MindMap Image] Loading markmap-lib...");
await new Promise((resolve, reject) => {{
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/markmap-lib@0.17';
@@ -1139,9 +1621,7 @@ class Action:
}});
}}
- // Load markmap-view if not loaded
if (!window.markmap || !window.markmap.Markmap) {{
- console.log("[MindMap Image] Loading markmap-view...");
await new Promise((resolve, reject) => {{
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/markmap-view@0.17';
@@ -1153,17 +1633,13 @@ class Action:
const {{ Transformer, Markmap }} = window.markmap;
- // Get markdown syntax
let syntaxContent = `{syntax_escaped}`;
- console.log("[MindMap Image] Syntax length:", syntaxContent.length);
- // Create offscreen container
const container = document.createElement('div');
container.id = 'mindmap-offscreen-' + uniqueId;
container.style.cssText = 'position:absolute;left:-9999px;top:-9999px;width:' + svgWidth + 'px;height:' + svgHeight + 'px;';
document.body.appendChild(container);
- // Create SVG element
const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svgEl.setAttribute('width', svgWidth);
svgEl.setAttribute('height', svgHeight);
@@ -1172,11 +1648,9 @@ class Action:
svgEl.style.backgroundColor = colors.background;
container.appendChild(svgEl);
- // Transform markdown to tree
const transformer = new Transformer();
const {{ root }} = transformer.transform(syntaxContent);
- // Create markmap instance
const options = {{
autoFit: true,
initialExpandLevel: Infinity,
@@ -1184,27 +1658,22 @@ class Action:
pan: false
}};
- console.log("[MindMap Image] Rendering markmap...");
const markmapInstance = Markmap.create(svgEl, options, root);
- // Wait for render to complete
await new Promise(resolve => setTimeout(resolve, 1500));
markmapInstance.fit();
await new Promise(resolve => setTimeout(resolve, 500));
- // Clone and prepare SVG for export
const clonedSvg = svgEl.cloneNode(true);
clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
clonedSvg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
- // Add background rect with theme color
const bgRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
bgRect.setAttribute('width', '100%');
bgRect.setAttribute('height', '100%');
bgRect.setAttribute('fill', colors.background);
clonedSvg.insertBefore(bgRect, clonedSvg.firstChild);
- // Add inline styles with theme colors
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
style.textContent = `
text {{ font-family: sans-serif; font-size: 14px; fill: ${{colors.text}}; }}
@@ -1217,7 +1686,6 @@ class Action:
`;
clonedSvg.insertBefore(style, bgRect.nextSibling);
- // Convert foreignObject to text for better compatibility
const foreignObjects = clonedSvg.querySelectorAll('foreignObject');
foreignObjects.forEach(fo => {{
const text = fo.textContent || '';
@@ -1233,18 +1701,13 @@ class Action:
fo.parentNode.replaceChild(g, fo);
}});
- // Serialize SVG to string
const svgData = new XMLSerializer().serializeToString(clonedSvg);
- // Cleanup container
document.body.removeChild(container);
- // Convert SVG string to Blob
const blob = new Blob([svgData], {{ type: 'image/svg+xml' }});
const file = new File([blob], `mindmap-${{uniqueId}}.svg`, {{ type: 'image/svg+xml' }});
- // Upload file to OpenWebUI API
- console.log("[MindMap Image] Uploading SVG file...");
const token = localStorage.getItem("token");
const formData = new FormData();
formData.append('file', file);
@@ -1258,29 +1721,22 @@ class Action:
}});
if (!uploadResponse.ok) {{
- throw new Error(`Upload failed: ${{uploadResponse.statusText}}`);
+ throw new Error(i18n.js_upload_failed + uploadResponse.statusText);
}}
const fileData = await uploadResponse.json();
const fileId = fileData.id;
const imageUrl = `/api/v1/files/${{fileId}}/content`;
- console.log("[MindMap Image] File uploaded, ID:", fileId);
+ const markdownImage = ``;
- // Generate markdown image with file URL
- const markdownImage = ``;
-
- // Update message via API
if (chatId && messageId) {{
-
- // Helper function with retry logic
const fetchWithRetry = async (url, options, retries = 3) => {{
for (let i = 0; i < retries; i++) {{
try {{
const response = await fetch(url, options);
if (response.ok) return response;
if (i < retries - 1) {{
- console.log(`[MindMap Image] Retry ${{i + 1}}/${{retries}} for ${{url}}`);
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
}}
}} catch (e) {{
@@ -1291,7 +1747,6 @@ class Action:
return null;
}};
- // Get current chat data
const getResponse = await fetch(`/api/v1/chats/${{chatId}}`, {{
method: "GET",
headers: {{ "Authorization": `Bearer ${{token}}` }}
@@ -1309,15 +1764,11 @@ class Action:
updatedMessages = chatData.chat.messages.map(m => {{
if (m.id === messageId) {{
const originalContent = m.content || "";
- // Remove existing mindmap images (both base64 and file URL patterns)
- const mindmapPattern = /\\n*!\\[🧠[^\\]]*\\]\\((?:data:image\\/[^)]+|(?:\\/api\\/v1\\/files\\/[^)]+))\\)/g;
+ const mindmapPattern = /\\n*!\\[[^[\\]]*\\]\\((?:data:image\\/[^)]+|(?:\\/api\\/v1\\/files\\/[^)]+))\\)/g;
let cleanedContent = originalContent.replace(mindmapPattern, "");
cleanedContent = cleanedContent.replace(/\\n{{3,}}/g, "\\n\\n").trim();
- // Append new image
newContent = cleanedContent + "\\n\\n" + markdownImage;
- // Critical: Update content in both messages array AND history object
- // The history object is the source of truth for the database
if (chatData.chat.history && chatData.chat.history.messages) {{
if (chatData.chat.history.messages[messageId]) {{
chatData.chat.history.messages[messageId].content = newContent;
@@ -1331,11 +1782,9 @@ class Action:
}}
if (!newContent) {{
- console.warn("[MindMap Image] Could not find message to update");
return;
}}
- // Try to update frontend display via event API (optional, may not exist in all versions)
try {{
await fetch(`/api/v1/chats/${{chatId}}/messages/${{messageId}}/event`, {{
method: "POST",
@@ -1349,21 +1798,16 @@ class Action:
}})
}});
}} catch (eventErr) {{
- // Event API is optional, continue with persistence
- console.log("[MindMap Image] Event API not available, continuing...");
}}
- // Persist to database by updating the entire chat object
- // This follows the OpenWebUI Backend-Controlled API Flow
const updatePayload = {{
chat: {{
...chatData.chat,
messages: updatedMessages
- // history is already updated in-place above
}}
}};
- const persistResponse = await fetchWithRetry(`/api/v1/chats/${{chatId}}`, {{
+ await fetchWithRetry(`/api/v1/chats/${{chatId}}`, {{
method: "POST",
headers: {{
"Content-Type": "application/json",
@@ -1371,14 +1815,6 @@ class Action:
}},
body: JSON.stringify(updatePayload)
}});
-
- if (persistResponse && persistResponse.ok) {{
- console.log("[MindMap Image] ✅ Message persisted successfully!");
- }} else {{
- console.error("[MindMap Image] ❌ Failed to persist message after retries");
- }}
- }} else {{
- console.warn("[MindMap Image] ⚠️ Missing chatId or messageId, cannot persist");
}}
}} catch (error) {{
@@ -1396,7 +1832,7 @@ class Action:
__metadata__: Optional[dict] = None,
__request__: Optional[Request] = None,
) -> Optional[dict]:
- logger.info("Action: Smart Mind Map (v0.9.2) started")
+ logger.info("Action: Smart Mind Map (v0.9.3) started")
user_ctx = await self._get_user_context(__user__, __event_call__)
user_language = user_ctx["user_language"]
user_name = user_ctx["user_name"]
@@ -1406,28 +1842,35 @@ class Action:
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("%B %d, %Y %H:%M:%S")
+
+ # Localize date time
+ current_date_time_str = self._format_date(user_language, now_dt)
+
current_weekday_en = now_dt.strftime("%A")
+ # We don't have weekday map for all languages, so use English or simple fallback?
+ # Or just ignore it as it is used in prompt only.
+ # I will keep English weekday for the LLM prompt.
current_weekday_zh = self.weekday_map.get(current_weekday_en, "Unknown")
+
current_year = now_dt.strftime("%Y")
current_timezone_str = tz_env or "UTC"
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("%B %d, %Y %H:%M:%S")
+ 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"
await self._emit_notification(
__event_emitter__,
- "Smart Mind Map is starting, generating mind map for you...",
+ self._get_translation(user_language, "status_starting"),
"info",
)
messages = body.get("messages")
if not messages or not isinstance(messages, list):
- error_message = "Unable to retrieve valid user message content."
+ error_message = self._get_translation(user_language, "error_no_content")
await self._emit_notification(__event_emitter__, error_message, "error")
return {
"messages": [{"role": "assistant", "content": f"❌ {error_message}"}]
@@ -1442,16 +1885,10 @@ class Action:
for i, msg in enumerate(recent_messages, 1):
text_content = self._extract_text_content(msg.get("content"))
if text_content:
- role = msg.get("role", "unknown")
- role_label = (
- "User"
- if role == "user"
- else "Assistant" if role == "assistant" else role
- )
aggregated_parts.append(f"{text_content}")
if not aggregated_parts:
- error_message = "Unable to retrieve valid user message content."
+ error_message = self._get_translation(user_language, "error_no_content")
await self._emit_notification(__event_emitter__, error_message, "error")
return {
"messages": [{"role": "assistant", "content": f"❌ {error_message}"}]
@@ -1471,7 +1908,12 @@ class Action:
long_text_content = original_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."
+ short_text_message = self._get_translation(
+ user_language,
+ "error_text_too_short",
+ len=len(long_text_content),
+ min_len=self.valves.MIN_TEXT_LENGTH
+ )
await self._emit_notification(
__event_emitter__, short_text_message, "warning"
)
@@ -1483,7 +1925,7 @@ class Action:
await self._emit_status(
__event_emitter__,
- "Smart Mind Map: Analyzing text structure in depth...",
+ self._get_translation(user_language, "status_analyzing"),
False,
)
@@ -1534,15 +1976,44 @@ class Action:
markdown_syntax = self._extract_markdown_syntax(assistant_response_content)
# Prepare content components
+ # Resolve translations for UI
+ ui_trans = {}
+ target_lang = user_language
+ if target_lang not in TRANSLATIONS and target_lang in self.fallback_map:
+ target_lang = self.fallback_map[target_lang]
+ if target_lang not in TRANSLATIONS:
+ target_lang = "en-US"
+
+ full_trans = TRANSLATIONS.get(target_lang, TRANSLATIONS["en-US"])
+ for k in full_trans:
+ if k.startswith("ui_"):
+ # For ui_footer which has {year}
+ if k == "ui_footer":
+ ui_trans[f"t_{k}"] = full_trans[k].format(year=current_year)
+ else:
+ ui_trans[f"t_{k}"] = full_trans[k]
+
content_html = (
- CONTENT_TEMPLATE_MINDMAP.replace("{unique_id}", unique_id)
- .replace("{user_name}", user_name)
- .replace("{current_date_time_str}", current_date_time_str)
- .replace("{current_year}", current_year)
- .replace("{markdown_syntax}", markdown_syntax)
+ CONTENT_TEMPLATE_MINDMAP.format(
+ unique_id=unique_id,
+ user_name=user_name,
+ current_date_time_str=current_date_time_str,
+ markdown_syntax=markdown_syntax,
+ **ui_trans
+ )
)
- script_html = SCRIPT_TEMPLATE_MINDMAP.replace("{unique_id}", unique_id)
+ # Prepare JS i18n
+ js_trans = {}
+ for k in full_trans:
+ if k.startswith("js_") or k.startswith("html_"):
+ js_trans[k] = full_trans[k]
+
+ script_html = SCRIPT_TEMPLATE_MINDMAP.replace(
+ "{unique_id}", unique_id
+ ).replace(
+ "{i18n_json}", json.dumps(js_trans, ensure_ascii=False)
+ )
# Extract existing HTML if any
existing_html_block = ""
@@ -1587,7 +2058,7 @@ class Action:
await self._emit_status(
__event_emitter__,
- "Smart Mind Map: Rendering image...",
+ self._get_translation(user_language, "status_rendering_image"),
False,
)
@@ -1597,6 +2068,7 @@ class Action:
chat_id=chat_id,
message_id=message_id,
markdown_syntax=markdown_syntax,
+ lang=user_language,
)
await __event_call__(
@@ -1607,14 +2079,14 @@ class Action:
)
await self._emit_status(
- __event_emitter__, "Smart Mind Map: Image generated!", True
+ __event_emitter__, self._get_translation(user_language, "status_image_generated"), True
)
await self._emit_notification(
__event_emitter__,
- f"Mind map image has been generated, {user_name}!",
+ self._get_translation(user_language, "notification_image_success", user_name=user_name),
"success",
)
- logger.info("Action: Smart Mind Map (v0.9.1) completed in image mode")
+ logger.info("Action: Smart Mind Map (v0.9.3) completed in image mode")
return body
# HTML mode (default): embed as HTML block
@@ -1622,29 +2094,30 @@ class Action:
body["messages"][-1]["content"] = f"{long_text_content}\n\n{html_embed_tag}"
await self._emit_status(
- __event_emitter__, "Smart Mind Map: Drawing completed!", True
+ __event_emitter__, self._get_translation(user_language, "status_drawing"), True
)
await self._emit_notification(
__event_emitter__,
- f"Mind map has been generated, {user_name}!",
+ self._get_translation(user_language, "notification_success", user_name=user_name),
"success",
)
- logger.info("Action: Smart Mind Map (v0.9.1) completed in HTML mode")
+ logger.info("Action: Smart Mind Map (v0.9.3) completed in HTML mode")
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."
+ user_facing_error = self._get_translation(user_language, "error_user_facing", error=str(e))
+
body["messages"][-1][
"content"
] = f"{long_text_content}\n\n❌ **Error:** {user_facing_error}"
await self._emit_status(
- __event_emitter__, "Smart Mind Map: Processing failed.", True
+ __event_emitter__, self._get_translation(user_language, "status_failed"), True
)
await self._emit_notification(
__event_emitter__,
- f"Smart Mind Map generation failed, {user_name}!",
+ self._get_translation(user_language, "notification_failed", user_name=user_name),
"error",
)
diff --git a/plugins/actions/smart-mind-map/smart_mind_map_cn.py b/plugins/actions/smart-mind-map/smart_mind_map_cn.py
deleted file mode 100644
index fb53dea..0000000
--- a/plugins/actions/smart-mind-map/smart_mind_map_cn.py
+++ /dev/null
@@ -1,1617 +0,0 @@
-"""
-title: 思维导图
-author: Fu-Jie
-author_url: https://github.com/Fu-Jie/awesome-openwebui
-funding_url: https://github.com/open-webui
-version: 0.9.2
-openwebui_id: 8d4b097b-219b-4dd2-b509-05fbe6388335
-icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4=
-description: 智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。
-"""
-
-import logging
-import os
-import re
-import time
-from datetime import datetime, timezone
-from typing import Any, Callable, Awaitable, Dict, Optional
-from zoneinfo import ZoneInfo
-
-from fastapi import Request
-from pydantic import BaseModel, Field
-
-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
- # 无法生成思维导图
- - 原因: 文本内容不足或不明确
- ```
-"""
-
-import json
-
-USER_PROMPT_GENERATE_MINDMAP = """
-请分析以下长篇文本,并将其核心主题、关键概念、分支和子分支结构化为标准的Markdown列表语法,以供Markmap.js渲染。
-
----
-**用户上下文信息:**
-用户姓名: {user_name}
-当前日期时间: {current_date_time_str}
-当前星期: {current_weekday}
-当前时区: {current_timezone_str}
-用户语言: {user_language}
----
-
-**长篇文本内容:**
-{long_text_content}
-"""
-
-HTML_WRAPPER_TEMPLATE = """
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-"""
-
-CSS_TEMPLATE_MINDMAP = """
- :root {
- --primary-color: #1e88e5;
- --secondary-color: #43a047;
- --background-color: #f4f6f8;
- --card-bg-color: #ffffff;
- --text-color: #000000;
- --link-color: #546e7a;
- --node-stroke-color: #90a4ae;
- --muted-text-color: #546e7a;
- --border-color: #e0e0e0;
- --header-gradient: linear-gradient(135deg, var(--secondary-color), var(--primary-color));
- --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: #ffffff;
- --link-color: #cbd5e1;
- --node-stroke-color: #94a3b8;
- --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.6;
- color: var(--text-color);
- margin: 0;
- padding: 0;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- 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: 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.4em;
- font-weight: 600;
- letter-spacing: 0.3px;
- }
- .user-context {
- font-size: 0.85em;
- color: var(--muted-text-color);
- background-color: rgba(255, 255, 255, 0.6);
- padding: 8px 14px;
- display: flex;
- justify-content: space-between;
- flex-wrap: wrap;
- border-bottom: 1px solid var(--border-color);
- gap: 6px;
- }
- .theme-dark .user-context {
- background-color: rgba(31, 41, 55, 0.7);
- }
- .user-context span { margin: 2px 6px; }
- .content-area {
- padding: 16px;
- flex-grow: 1;
- background: var(--card-bg-color);
- }
- .markmap-container {
- position: relative;
- background-color: var(--card-bg-color);
- border-radius: 10px;
- padding: 12px;
- display: flex;
- justify-content: center;
- align-items: center;
- border: 1px solid var(--border-color);
- width: 100%;
- min-height: 60vh;
- overflow: visible;
- }
- .markmap-container svg {
- width: 100%;
- height: 100%;
- }
- .markmap-container svg text {
- fill: var(--text-color) !important;
- font-family: var(--font-family);
- }
- .markmap-container svg foreignObject,
- .markmap-container svg .markmap-foreign,
- .markmap-container svg .markmap-foreign div {
- color: var(--text-color) !important;
- font-family: var(--font-family);
- }
- .markmap-container svg .markmap-link {
- stroke: var(--link-color) !important;
- }
- .markmap-container svg .markmap-node circle,
- .markmap-container svg .markmap-node rect {
- stroke: var(--node-stroke-color) !important;
- }
- .control-rows {
- display: flex;
- flex-wrap: wrap;
- gap: 10px;
- justify-content: center;
- margin-top: 12px;
- }
- .btn-group {
- display: inline-flex;
- gap: 6px;
- align-items: center;
- }
- .control-btn {
- background-color: var(--primary-color);
- color: white;
- border: none;
- padding: 8px 12px;
- border-radius: 8px;
- font-size: 0.9em;
- font-weight: 500;
- cursor: pointer;
- transition: background-color 0.15s ease, transform 0.15s ease;
- display: inline-flex;
- align-items: center;
- gap: 6px;
- height: 36px;
- box-sizing: border-box;
- }
- select.control-btn {
- appearance: none;
- padding-right: 28px;
- background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23FFFFFF%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E");
- background-repeat: no-repeat;
- background-position: right 8px center;
- background-size: 10px;
- }
- .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: 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; }
- .error-message {
- color: #c62828;
- background-color: #ffcdd2;
- border: 1px solid #ef9a9a;
- padding: 14px;
- border-radius: 8px;
- font-weight: 500;
- font-size: 1em;
- }
-"""
-
-CONTENT_TEMPLATE_MINDMAP = """
-
-
-
- 用户: {user_name}
- 时间: {current_date_time_str}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-"""
-
-SCRIPT_TEMPLATE_MINDMAP = """
-
-"""
-
-
-class Action:
- class Valves(BaseModel):
- SHOW_STATUS: bool = Field(
- default=True, description="是否在聊天界面显示操作状态更新。"
- )
- MODEL_ID: str = Field(
- default="",
- description="用于文本分析的内置LLM模型ID。如果为空,则使用当前对话的模型。",
- )
- MIN_TEXT_LENGTH: int = Field(
- default=100,
- description="进行思维导图分析所需的最小文本长度(字符数)。",
- )
- CLEAR_PREVIOUS_HTML: bool = Field(
- default=False,
- description="是否强制清除旧的插件结果(如果为 True,则不合并,直接覆盖)。",
- )
- MESSAGE_COUNT: int = Field(
- default=1,
- description="用于生成的最近消息数量。设置为1仅使用最后一条消息,更大值可包含更多上下文。",
- )
- OUTPUT_MODE: str = Field(
- default="html",
- description="输出模式: 'html' 为交互式HTML(默认),'image' 为嵌入Markdown图片。",
- )
- SHOW_DEBUG_LOG: bool = Field(
- default=False,
- description="是否在浏览器控制台打印调试日志。",
- )
-
- def __init__(self):
- self.valves = self.Valves()
- self.weekday_map = {
- "Monday": "星期一",
- "Tuesday": "星期二",
- "Wednesday": "星期三",
- "Thursday": "星期四",
- "Friday": "星期五",
- "Saturday": "星期六",
- "Sunday": "星期日",
- }
-
- async def _get_user_context(
- self,
- __user__: Optional[Dict[str, Any]],
- __event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
- ) -> 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 = {}
-
- user_id = user_data.get("id", "unknown_user")
- user_name = user_data.get("name", "User")
- user_language = user_data.get("language", "en-US")
-
- if __event_call__:
- try:
- js_code = """
- return (
- localStorage.getItem('locale') ||
- localStorage.getItem('language') ||
- navigator.language ||
- 'en-US'
- );
- """
- frontend_lang = await __event_call__(
- {"type": "execute", "data": {"code": js_code}}
- )
- if frontend_lang and isinstance(frontend_lang, str):
- user_language = frontend_lang
- except Exception as e:
- logger.warning(f"Failed to retrieve frontend language: {e}")
-
- return {
- "user_id": user_id,
- "user_name": user_name,
- "user_language": user_language,
- }
-
- def _get_chat_context(
- self, body: dict, __metadata__: Optional[dict] = None
- ) -> Dict[str, str]:
- """
- 统一提取聊天上下文信息 (chat_id, message_id)。
- 优先从 body 中提取,其次从 metadata 中提取。
- """
- chat_id = ""
- message_id = ""
-
- # 1. 尝试从 body 获取
- if isinstance(body, dict):
- chat_id = body.get("chat_id", "")
- message_id = body.get("id", "") # message_id 在 body 中通常是 id
-
- # 再次检查 body.metadata
- if not chat_id or not message_id:
- body_metadata = body.get("metadata", {})
- if isinstance(body_metadata, dict):
- if not chat_id:
- chat_id = body_metadata.get("chat_id", "")
- if not message_id:
- message_id = body_metadata.get("message_id", "")
-
- # 2. 尝试从 __metadata__ 获取 (作为补充)
- if __metadata__ and isinstance(__metadata__, dict):
- if not chat_id:
- chat_id = __metadata__.get("chat_id", "")
- if not message_id:
- message_id = __metadata__.get("message_id", "")
-
- return {
- "chat_id": str(chat_id).strip(),
- "message_id": str(message_id).strip(),
- }
-
- 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>")
-
- async def _emit_status(self, emitter, description: str, done: bool = False):
- """发送状态更新事件。"""
- if self.valves.SHOW_STATUS and emitter:
- await emitter(
- {"type": "status", "data": {"description": description, "done": done}}
- )
-
- async def _emit_notification(self, emitter, content: str, ntype: str = "info"):
- """发送通知事件 (info/success/warning/error)。"""
- if emitter:
- await emitter(
- {"type": "notification", "data": {"type": ntype, "content": content}}
- )
-
- async def _emit_debug_log(self, emitter, title: str, data: dict):
- """在浏览器控制台打印结构化调试日志"""
- if not self.valves.SHOW_DEBUG_LOG or not emitter:
- return
-
- try:
- js_code = f"""
- (async function() {{
- console.group("🛠️ {title}");
- console.log({json.dumps(data, ensure_ascii=False)});
- console.groupEnd();
- }})();
- """
-
- await emitter({"type": "execute", "data": {"code": js_code}})
- except Exception as e:
- print(f"Error emitting debug log: {e}")
-
- def _remove_existing_html(self, content: str) -> str:
- """移除内容中已有的插件生成 HTML 代码块 (通过标记识别)。"""
- pattern = r"```html\s*[\s\S]*?```"
- return re.sub(pattern, "", content).strip()
-
- def _extract_text_content(self, content) -> str:
- """从消息内容中提取文本,支持多模态消息格式"""
- if isinstance(content, str):
- return content
- elif isinstance(content, list):
- # 多模态消息: [{"type": "text", "text": "..."}, {"type": "image_url", ...}]
- text_parts = []
- for item in content:
- if isinstance(item, dict) and item.get("type") == "text":
- text_parts.append(item.get("text", ""))
- elif isinstance(item, str):
- text_parts.append(item)
- return "\n".join(text_parts)
- return str(content) if content else ""
-
- def _merge_html(
- self,
- existing_html_code: str,
- new_content: str,
- new_styles: str = "",
- new_scripts: str = "",
- user_language: str = "zh-CN",
- ) -> str:
- """
- 将新内容合并到现有的 HTML 容器中,或者创建一个新的容器。
- """
- if (
- "" in existing_html_code
- and "" in existing_html_code
- ):
- base_html = existing_html_code
- base_html = re.sub(r"^```html\s*", "", base_html)
- base_html = re.sub(r"\s*```$", "", base_html)
- else:
- base_html = HTML_WRAPPER_TEMPLATE.replace("{user_language}", user_language)
-
- wrapped_content = f'\n{new_content}\n
'
-
- if new_styles:
- base_html = base_html.replace(
- "/* STYLES_INSERTION_POINT */",
- f"{new_styles}\n/* STYLES_INSERTION_POINT */",
- )
-
- base_html = base_html.replace(
- "",
- f"{wrapped_content}\n",
- )
-
- if new_scripts:
- base_html = base_html.replace(
- "",
- f"{new_scripts}\n",
- )
-
- return base_html.strip()
-
- def _generate_image_js_code(
- self,
- unique_id: str,
- chat_id: str,
- message_id: str,
- markdown_syntax: str,
- ) -> str:
- """生成用于前端 SVG 渲染和图片嵌入的 JavaScript 代码"""
-
- # 转义语法以便嵌入 JS
- syntax_escaped = (
- markdown_syntax.replace("\\", "\\\\")
- .replace("`", "\\`")
- .replace("${", "\\${")
- .replace("", "<\\/script>")
- )
-
- return f"""
-(async function() {{
- const uniqueId = "{unique_id}";
- const chatId = "{chat_id}";
- const messageId = "{message_id}";
- const defaultWidth = 1200;
- const defaultHeight = 800;
-
- // 主题检测 - 检查 OpenWebUI 当前主题
- const detectTheme = () => {{
- try {{
- // 1. 检查 html/body 的 class 或 data-theme 属性
- const html = document.documentElement;
- const body = document.body;
- const htmlClass = html ? html.className : '';
- const bodyClass = body ? body.className : '';
- const htmlDataTheme = html ? html.getAttribute('data-theme') : '';
-
- if (htmlDataTheme === 'dark' || bodyClass.includes('dark') || htmlClass.includes('dark')) {{
- return 'dark';
- }}
- if (htmlDataTheme === 'light' || bodyClass.includes('light') || htmlClass.includes('light')) {{
- return 'light';
- }}
-
- // 2. 检查 meta theme-color
- const metas = document.querySelectorAll('meta[name="theme-color"]');
- if (metas.length > 0) {{
- const color = metas[metas.length - 1].content.trim();
- const m = color.match(/^#?([0-9a-f]{{6}})$/i);
- if (m) {{
- const hex = m[1];
- const r = parseInt(hex.slice(0, 2), 16);
- const g = parseInt(hex.slice(2, 4), 16);
- const b = parseInt(hex.slice(4, 6), 16);
- const luma = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
- return luma < 0.5 ? 'dark' : 'light';
- }}
- }}
-
- // 3. 检查系统偏好
- if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {{
- return 'dark';
- }}
-
- return 'light';
- }} catch (e) {{
- return 'light';
- }}
- }};
-
- const currentTheme = detectTheme();
- console.log("[思维导图图片] 检测到主题:", currentTheme);
-
- // 基于主题的颜色配置
- const colors = currentTheme === 'dark' ? {{
- background: '#1f2937',
- text: '#e5e7eb',
- link: '#94a3b8',
- nodeStroke: '#64748b'
- }} : {{
- background: '#ffffff',
- text: '#1f2937',
- link: '#546e7a',
- nodeStroke: '#94a3b8'
- }};
-
- // 自动检测聊天容器宽度以实现自适应
- let svgWidth = defaultWidth;
- let svgHeight = defaultHeight;
- const chatContainer = document.getElementById('chat-container');
- if (chatContainer) {{
- const containerWidth = chatContainer.clientWidth;
- if (containerWidth > 100) {{
- // 使用容器宽度的90%(留出边距)
- svgWidth = Math.floor(containerWidth * 0.9);
- // 根据默认尺寸保持宽高比
- svgHeight = Math.floor(svgWidth * (defaultHeight / defaultWidth));
- console.log("[思维导图图片] 自动检测容器宽度:", containerWidth, "-> SVG:", svgWidth, "x", svgHeight);
- }}
- }}
-
- console.log("[思维导图图片] 开始渲染...");
- console.log("[思维导图图片] chatId:", chatId, "messageId:", messageId);
-
- try {{
- // 加载 D3
- if (typeof d3 === 'undefined') {{
- console.log("[思维导图图片] 正在加载 D3...");
- await new Promise((resolve, reject) => {{
- const script = document.createElement('script');
- script.src = 'https://cdn.jsdelivr.net/npm/d3@7';
- script.onload = resolve;
- script.onerror = reject;
- document.head.appendChild(script);
- }});
- }}
-
- // 加载 markmap-lib
- if (!window.markmap || !window.markmap.Transformer) {{
- console.log("[思维导图图片] 正在加载 markmap-lib...");
- await new Promise((resolve, reject) => {{
- const script = document.createElement('script');
- script.src = 'https://cdn.jsdelivr.net/npm/markmap-lib@0.17';
- script.onload = resolve;
- script.onerror = reject;
- document.head.appendChild(script);
- }});
- }}
-
- // 加载 markmap-view
- if (!window.markmap || !window.markmap.Markmap) {{
- console.log("[思维导图图片] 正在加载 markmap-view...");
- await new Promise((resolve, reject) => {{
- const script = document.createElement('script');
- script.src = 'https://cdn.jsdelivr.net/npm/markmap-view@0.17';
- script.onload = resolve;
- script.onerror = reject;
- document.head.appendChild(script);
- }});
- }}
-
- const {{ Transformer, Markmap }} = window.markmap;
-
- // 获取 markdown 语法
- let syntaxContent = `{syntax_escaped}`;
- console.log("[思维导图图片] 语法长度:", syntaxContent.length);
-
- // 创建离屏容器
- const container = document.createElement('div');
- container.id = 'mindmap-offscreen-' + uniqueId;
- container.style.cssText = 'position:absolute;left:-9999px;top:-9999px;width:' + svgWidth + 'px;height:' + svgHeight + 'px;';
- document.body.appendChild(container);
-
- // 创建 SVG 元素
- const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
- svgEl.setAttribute('width', svgWidth);
- svgEl.setAttribute('height', svgHeight);
- svgEl.style.width = svgWidth + 'px';
- svgEl.style.height = svgHeight + 'px';
- svgEl.style.backgroundColor = colors.background;
- container.appendChild(svgEl);
-
- // 将 markdown 转换为树结构
- const transformer = new Transformer();
- const {{ root }} = transformer.transform(syntaxContent);
-
- // 创建 markmap 实例
- const options = {{
- autoFit: true,
- initialExpandLevel: Infinity,
- zoom: false,
- pan: false
- }};
-
- console.log("[思维导图图片] 正在渲染 markmap...");
- const markmapInstance = Markmap.create(svgEl, options, root);
-
- // 等待渲染完成
- await new Promise(resolve => setTimeout(resolve, 1500));
- markmapInstance.fit();
- await new Promise(resolve => setTimeout(resolve, 500));
-
- // 克隆并准备 SVG 导出
- const clonedSvg = svgEl.cloneNode(true);
- clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
- clonedSvg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
-
- // 添加背景矩形(使用主题颜色)
- const bgRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
- bgRect.setAttribute('width', '100%');
- bgRect.setAttribute('height', '100%');
- bgRect.setAttribute('fill', colors.background);
- clonedSvg.insertBefore(bgRect, clonedSvg.firstChild);
-
- // 添加内联样式(使用主题颜色)
- const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
- style.textContent = `
- text {{ font-family: sans-serif; font-size: 14px; fill: ${{colors.text}}; }}
- foreignObject, .markmap-foreign, .markmap-foreign div {{ color: ${{colors.text}}; font-family: sans-serif; font-size: 14px; }}
- h1 {{ font-size: 22px; font-weight: 700; margin: 0; }}
- h2 {{ font-size: 18px; font-weight: 600; margin: 0; }}
- strong {{ font-weight: 700; }}
- .markmap-link {{ stroke: ${{colors.link}}; fill: none; }}
- .markmap-node circle, .markmap-node rect {{ stroke: ${{colors.nodeStroke}}; }}
- `;
- clonedSvg.insertBefore(style, bgRect.nextSibling);
-
- // 将 foreignObject 转换为 text 以提高兼容性
- const foreignObjects = clonedSvg.querySelectorAll('foreignObject');
- foreignObjects.forEach(fo => {{
- const text = fo.textContent || '';
- const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
- const textEl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
- textEl.setAttribute('x', fo.getAttribute('x') || '0');
- textEl.setAttribute('y', (parseFloat(fo.getAttribute('y') || '0') + 14).toString());
- textEl.setAttribute('fill', colors.text);
- textEl.setAttribute('font-family', 'sans-serif');
- textEl.setAttribute('font-size', '14');
- textEl.textContent = text.trim();
- g.appendChild(textEl);
- fo.parentNode.replaceChild(g, fo);
- }});
-
- // 序列化 SVG 为字符串
- const svgData = new XMLSerializer().serializeToString(clonedSvg);
-
- // 清理容器
- document.body.removeChild(container);
-
- // 将 SVG 字符串转换为 Blob
- const blob = new Blob([svgData], {{ type: 'image/svg+xml' }});
- const file = new File([blob], `mindmap-${{uniqueId}}.svg`, {{ type: 'image/svg+xml' }});
-
- // 上传文件到 OpenWebUI API
- console.log("[思维导图图片] 正在上传 SVG 文件...");
- const token = localStorage.getItem("token");
- const formData = new FormData();
- formData.append('file', file);
-
- const uploadResponse = await fetch('/api/v1/files/', {{
- method: 'POST',
- headers: {{
- 'Authorization': `Bearer ${{token}}`
- }},
- body: formData
- }});
-
- if (!uploadResponse.ok) {{
- throw new Error(`上传失败: ${{uploadResponse.statusText}}`);
- }}
-
- const fileData = await uploadResponse.json();
- const fileId = fileData.id;
- const imageUrl = `/api/v1/files/${{fileId}}/content`;
-
- console.log("[思维导图图片] 文件已上传, ID:", fileId);
-
- // 生成包含文件 URL 的 markdown 图片
- const markdownImage = ``;
-
- // 通过 API 更新消息
- if (chatId && messageId) {{
- const token = localStorage.getItem("token");
-
- // 带重试逻辑的请求函数
- const fetchWithRetry = async (url, options, retries = 3) => {{
- for (let i = 0; i < retries; i++) {{
- try {{
- const response = await fetch(url, options);
- if (response.ok) return response;
- if (i < retries - 1) {{
- console.log(`[思维导图图片] 重试 ${{i + 1}}/${{retries}}: ${{url}}`);
- await new Promise(r => setTimeout(r, 1000 * (i + 1)));
- }}
- }} catch (e) {{
- if (i === retries - 1) throw e;
- await new Promise(r => setTimeout(r, 1000 * (i + 1)));
- }}
- }}
- return null;
- }};
-
- // 获取当前聊天数据
- const getResponse = await fetch(`/api/v1/chats/${{chatId}}`, {{
- method: "GET",
- headers: {{ "Authorization": `Bearer ${{token}}` }}
- }});
-
- if (!getResponse.ok) {{
- throw new Error("获取聊天数据失败: " + getResponse.status);
- }}
-
- const chatData = await getResponse.json();
- let updatedMessages = [];
- let newContent = "";
-
- if (chatData.chat && chatData.chat.messages) {{
- updatedMessages = chatData.chat.messages.map(m => {{
- if (m.id === messageId) {{
- const originalContent = m.content || "";
- // 移除已有的思维导图图片 (包括 base64 和文件 URL 格式)
- const mindmapPattern = /\\n*!\\[🧠[^\\]]*\\]\\((?:data:image\\/[^)]+|(?:\\/api\\/v1\\/files\\/[^)]+))\\)/g;
- let cleanedContent = originalContent.replace(mindmapPattern, "");
- cleanedContent = cleanedContent.replace(/\\n{{3,}}/g, "\\n\\n").trim();
- // 追加新图片
- newContent = cleanedContent + "\\n\\n" + markdownImage;
-
- // 关键: 同时更新 messages 数组和 history 对象中的内容
- // history 对象是数据库的单一真值来源
- if (chatData.chat.history && chatData.chat.history.messages) {{
- if (chatData.chat.history.messages[messageId]) {{
- chatData.chat.history.messages[messageId].content = newContent;
- }}
- }}
-
- return {{ ...m, content: newContent }};
- }}
- return m;
- }});
- }}
-
- if (!newContent) {{
- console.warn("[思维导图图片] 找不到要更新的消息");
- return;
- }}
-
- // 尝试通过事件 API 更新前端显示(可选,部分版本可能不支持)
- try {{
- await fetch(`/api/v1/chats/${{chatId}}/messages/${{messageId}}/event`, {{
- method: "POST",
- headers: {{
- "Content-Type": "application/json",
- "Authorization": `Bearer ${{token}}`
- }},
- body: JSON.stringify({{
- type: "chat:message",
- data: {{ content: newContent }}
- }})
- }});
- }} catch (eventErr) {{
- // 事件 API 是可选的,继续执行持久化
- console.log("[思维导图图片] 事件 API 不可用,继续执行...");
- }}
-
- // 通过更新整个聊天对象来持久化到数据库
- // 遵循 OpenWebUI 后端控制的 API 流程
- const updatePayload = {{
- chat: {{
- ...chatData.chat,
- messages: updatedMessages
- // history 已在上面原地更新
- }}
- }};
-
- const persistResponse = await fetchWithRetry(`/api/v1/chats/${{chatId}}`, {{
- method: "POST",
- headers: {{
- "Content-Type": "application/json",
- "Authorization": `Bearer ${{token}}`
- }},
- body: JSON.stringify(updatePayload)
- }});
-
- if (persistResponse && persistResponse.ok) {{
- console.log("[思维导图图片] ✅ 消息已持久化保存!");
- }} else {{
- console.error("[思维导图图片] ❌ 重试后仍然无法持久化消息");
- }}
- }} else {{
- console.warn("[思维导图图片] ⚠️ 缺少 chatId 或 messageId,无法持久化");
- }}
-
- }} catch (error) {{
- console.error("[思维导图图片] 错误:", error);
- }}
-}})();
-"""
-
- async def action(
- self,
- body: dict,
- __user__: Optional[Dict[str, Any]] = None,
- __event_emitter__: Optional[Any] = None,
- __event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
- __metadata__: Optional[dict] = None,
- __request__: Optional[Request] = None,
- ) -> Optional[dict]:
- logger.info("Action: 思维导图 (v0.9.2) started")
- user_ctx = await self._get_user_context(__user__, __event_call__)
- user_language = user_ctx["user_language"]
- user_name = user_ctx["user_name"]
- user_id = user_ctx["user_id"]
-
- try:
- 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 = now_dt.strftime("%Y")
- current_timezone_str = tz_env or "UTC"
- 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 = "未知时区"
-
- await self._emit_notification(
- __event_emitter__, "思维导图已启动,正在为您生成思维导图...", "info"
- )
-
- messages = body.get("messages")
- if not messages or not isinstance(messages, list):
- error_message = "无法获取有效的用户消息内容。"
- await self._emit_notification(__event_emitter__, error_message, "error")
- return {
- "messages": [{"role": "assistant", "content": f"❌ {error_message}"}]
- }
-
- # Get last N messages based on MESSAGE_COUNT
- message_count = min(self.valves.MESSAGE_COUNT, len(messages))
- recent_messages = messages[-message_count:]
-
- # Aggregate content from selected messages with labels
- aggregated_parts = []
- for i, msg in enumerate(recent_messages, 1):
- text_content = self._extract_text_content(msg.get("content"))
- if text_content:
- role = msg.get("role", "unknown")
- role_label = (
- "用户"
- if role == "user"
- else "助手" if role == "assistant" else role
- )
- aggregated_parts.append(f"{text_content}")
-
- if not aggregated_parts:
- error_message = "无法获取有效的用户消息内容。"
- await self._emit_notification(__event_emitter__, error_message, "error")
- return {
- "messages": [{"role": "assistant", "content": f"❌ {error_message}"}]
- }
-
- original_content = "\n\n---\n\n".join(aggregated_parts)
-
- parts = re.split(r"```html.*?```", original_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 = original_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}字符的文本。"
- await self._emit_notification(
- __event_emitter__, short_text_message, "warning"
- )
- return {
- "messages": [
- {"role": "assistant", "content": f"⚠️ {short_text_message}"}
- ]
- }
-
- await self._emit_status(
- __event_emitter__, "思维导图: 深入分析文本结构...", 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,
- )
-
- # 确定使用的模型
- target_model = self.valves.MODEL_ID
- if not target_model:
- target_model = body.get("model")
-
- llm_payload = {
- "model": target_model,
- "messages": [
- {"role": "system", "content": SYSTEM_PROMPT_MINDMAP_ASSISTANT},
- {"role": "user", "content": formatted_user_prompt},
- ],
- "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)
-
- # Prepare content components
- content_html = (
- CONTENT_TEMPLATE_MINDMAP.replace("{unique_id}", unique_id)
- .replace("{user_name}", user_name)
- .replace("{current_date_time_str}", current_date_time_str)
- .replace("{current_year}", current_year)
- .replace("{markdown_syntax}", markdown_syntax)
- )
-
- script_html = SCRIPT_TEMPLATE_MINDMAP.replace("{unique_id}", unique_id)
-
- # Extract existing HTML if any
- existing_html_block = ""
- match = re.search(
- r"```html\s*([\s\S]*?)```",
- long_text_content,
- )
- if match:
- existing_html_block = match.group(1)
-
- if self.valves.CLEAR_PREVIOUS_HTML:
- long_text_content = self._remove_existing_html(long_text_content)
- final_html = self._merge_html(
- "", content_html, CSS_TEMPLATE_MINDMAP, script_html, user_language
- )
- else:
- # If we found existing HTML, we remove the old block from text and merge into it
- if existing_html_block:
- long_text_content = self._remove_existing_html(long_text_content)
- final_html = self._merge_html(
- existing_html_block,
- content_html,
- CSS_TEMPLATE_MINDMAP,
- script_html,
- user_language,
- )
- else:
- final_html = self._merge_html(
- "",
- content_html,
- CSS_TEMPLATE_MINDMAP,
- script_html,
- user_language,
- )
-
- # 检查输出模式
- if self.valves.OUTPUT_MODE == "image":
- # 图片模式: 使用 JavaScript 渲染并嵌入为 Markdown 图片
- chat_ctx = self._get_chat_context(body, __metadata__)
- chat_id = chat_ctx["chat_id"]
- message_id = chat_ctx["message_id"]
-
- await self._emit_status(
- __event_emitter__,
- "思维导图: 正在渲染图片...",
- False,
- )
-
- if __event_call__:
- js_code = self._generate_image_js_code(
- unique_id=unique_id,
- chat_id=chat_id,
- message_id=message_id,
- markdown_syntax=markdown_syntax,
- )
-
- await __event_call__(
- {
- "type": "execute",
- "data": {"code": js_code},
- }
- )
-
- await self._emit_status(
- __event_emitter__, "思维导图: 图片已生成!", True
- )
- await self._emit_notification(
- __event_emitter__,
- f"思维导图图片已生成,{user_name}!",
- "success",
- )
- logger.info("Action: 思维导图 (v0.9.1) 图片模式完成")
- return body
-
- # HTML 模式(默认): 嵌入为 HTML 块
- 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_notification(
- __event_emitter__, f"思维导图已生成,{user_name}!", "success"
- )
- logger.info("Action: 思维导图 (v0.9.1) HTML 模式完成")
-
- 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}"
-
- await self._emit_status(__event_emitter__, "思维导图: 处理失败。", True)
- await self._emit_notification(
- __event_emitter__, f"思维导图生成失败, {user_name}!", "error"
- )
-
- return body