From fa636c7bc585ac12ff4bf195465e012c249aefdd Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:33:04 +0000 Subject: [PATCH] feat: Add full i18n support to Smart Mind Map plugin - Consolidated smart_mind_map.py and smart_mind_map_cn.py into a single file. - Added TRANSLATIONS dictionary supporting 17 languages (en-US, ko-KR, fr-FR, es-AR, en-CA, fr-CA, ja-JP, de-DE, zh-HK, it-IT, zh-CN, en-GB, es-MX, id-ID, es-ES, de-AT, en-AU, vi-VN). - Implemented automatic language detection with fallback to browser/local storage. - Added localized date formatting for various locales. - Updated HTML/JS templates to use injected translations. - Fixed SyntaxWarning in regex strings. - Verified frontend rendering with Playwright. Co-authored-by: Fu-Jie <33599649+Fu-Jie@users.noreply.github.com> --- .../actions/smart-mind-map/smart_mind_map.py | 773 ++++++-- .../smart-mind-map/smart_mind_map_cn.py | 1617 ----------------- 2 files changed, 623 insertions(+), 1767 deletions(-) delete mode 100644 plugins/actions/smart-mind-map/smart_mind_map_cn.py 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 = """
-

🧠 Smart Mind Map

+

{t_ui_title}

- 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 = `![${{i18n.md_image_alt}}](${{imageUrl}})`; - // Generate markdown image with file URL - const markdownImage = `![🧠 Mind Map](${{imageUrl}})`; - - // 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 = `![🧠 思维导图](${{imageUrl}})`; - - // 通过 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