diff --git a/plugins/actions/ACTION_PLUGIN_TEMPLATE.py b/plugins/actions/ACTION_PLUGIN_TEMPLATE.py index 33c51c2..7b99038 100644 --- a/plugins/actions/ACTION_PLUGIN_TEMPLATE.py +++ b/plugins/actions/ACTION_PLUGIN_TEMPLATE.py @@ -48,26 +48,52 @@ Content to process: {content} """ -# HTML Template for rendering the result in the chat -HTML_TEMPLATE = """ +# HTML Wrapper Template (supports multiple plugins and grid layout) +HTML_WRAPPER_TEMPLATE = """ - [Plugin Title] -
-

[Result Title]

-
{result_content}
+
+
+ """ @@ -89,7 +115,7 @@ class Action: ) CLEAR_PREVIOUS_HTML: bool = Field( default=False, - description="Whether to clear existing plugin-generated HTML content in the message before appending new results (identified by marker).", + description="Whether to force clear previous plugin results (if True, overwrites instead of merging).", ) # Add other configuration fields as needed # MAX_TEXT_LENGTH: int = Field(default=2000, description="...") @@ -122,7 +148,7 @@ class Action: now = datetime.now() return { - "current_date_time_str": now.strftime("%Y-%m-%d %H:%M:%S"), + "current_date_time_str": now.strftime("%B %d, %Y %H:%M:%S"), "current_weekday": now.strftime("%A"), "current_year": now.strftime("%Y"), "current_timezone_str": str(now.tzinfo) if now.tzinfo else "Unknown", @@ -149,6 +175,55 @@ class Action: pattern = r"```html\s*[\s\S]*?```" return re.sub(pattern, "", content).strip() + def _merge_html( + self, + existing_html_code: str, + new_content: str, + new_styles: str = "", + new_scripts: str = "", + user_language: str = "en-US", + ) -> str: + """ + Merges new content into an existing HTML container, or creates a new one. + """ + # Check for compatible container marker + if ( + "" in existing_html_code + and "" in existing_html_code + ): + base_html = existing_html_code + # Remove code block markers ```html ... ``` for processing + base_html = re.sub(r"^```html\s*", "", base_html) + base_html = re.sub(r"\s*```$", "", base_html) + else: + # Initialize new container + base_html = HTML_WRAPPER_TEMPLATE.replace("{user_language}", user_language) + + # Wrap new content + wrapped_content = f'
\n{new_content}\n
' + + # Inject Styles + if new_styles: + base_html = base_html.replace( + "/* STYLES_INSERTION_POINT */", + f"{new_styles}\n/* STYLES_INSERTION_POINT */", + ) + + # Inject Content + base_html = base_html.replace( + "", + f"{wrapped_content}\n", + ) + + # Inject Scripts + if new_scripts: + base_html = base_html.replace( + "", + f"{new_scripts}\n", + ) + + return base_html.strip() + async def _emit_status( self, emitter: Optional[Callable[[Any], Awaitable[None]]], diff --git a/plugins/actions/ACTION_PLUGIN_TEMPLATE_CN.py b/plugins/actions/ACTION_PLUGIN_TEMPLATE_CN.py index 1fa974e..e83c2cf 100644 --- a/plugins/actions/ACTION_PLUGIN_TEMPLATE_CN.py +++ b/plugins/actions/ACTION_PLUGIN_TEMPLATE_CN.py @@ -48,26 +48,52 @@ USER_PROMPT_TEMPLATE = """ {content} """ -# 用于在聊天中渲染结果的 HTML 模板 -HTML_TEMPLATE = """ +# HTML 容器模板 (支持多插件共存与网格布局) +HTML_WRAPPER_TEMPLATE = """ - [插件标题] -
-

[结果标题]

-
{result_content}
+
+
+ """ @@ -89,7 +115,7 @@ class Action: ) CLEAR_PREVIOUS_HTML: bool = Field( default=False, - description="是否在追加新结果前清除消息中已有的插件生成 HTML 内容 (通过标记识别)。", + description="是否强制清除旧的插件结果(如果为 True,则不合并,直接覆盖)。", ) # 根据需要添加其他配置字段 # MAX_TEXT_LENGTH: int = Field(default=2000, description="...") @@ -121,11 +147,21 @@ class Action: except Exception: now = datetime.now() + weekday_map = { + "Monday": "星期一", + "Tuesday": "星期二", + "Wednesday": "星期三", + "Thursday": "星期四", + "Friday": "星期五", + "Saturday": "星期六", + "Sunday": "星期日", + } + weekday_en = now.strftime("%A") return { - "current_date_time_str": now.strftime("%Y-%m-%d %H:%M:%S"), - "current_weekday": now.strftime("%A"), + "current_date_time_str": now.strftime("%Y年%m月%d日 %H:%M:%S"), + "current_weekday": weekday_map.get(weekday_en, weekday_en), "current_year": now.strftime("%Y"), - "current_timezone_str": str(now.tzinfo) if now.tzinfo else "Unknown", + "current_timezone_str": str(now.tzinfo) if now.tzinfo else "未知时区", } def _process_llm_output(self, llm_output: str) -> Any: @@ -150,6 +186,55 @@ class Action: pattern = r"```html\s*[\s\S]*?```" return re.sub(pattern, "", content).strip() + 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 + # 移除代码块标记 ```html ... ``` 以便处理 + 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() + async def _emit_status( self, emitter: Optional[Callable[[Any], Awaitable[None]]], diff --git a/plugins/actions/knowledge-card/knowledge_card.py b/plugins/actions/knowledge-card/knowledge_card.py index bab5fc6..c32e8fb 100644 --- a/plugins/actions/knowledge-card/knowledge_card.py +++ b/plugins/actions/knowledge-card/knowledge_card.py @@ -19,6 +19,55 @@ from open_webui.models.users import Users logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) +HTML_WRAPPER_TEMPLATE = """ + + + + + + + + + +
+ +
+ + + +""" + class Action: class Valves(BaseModel): @@ -44,7 +93,7 @@ class Action: ) clear_previous_html: bool = Field( default=False, - description="Whether to clear existing plugin-generated HTML content in the message before appending new results (identified by marker).", + description="Whether to force clear previous plugin results (if True, overwrites instead of merging).", ) def __init__(self): @@ -176,16 +225,44 @@ Important Principles: ) return body - # 2. Generate HTML - html_card = self.generate_html_card(card_data) + # 2. Generate HTML components + card_content, card_style = self.generate_html_card_components(card_data) # 3. Append to message + # Extract existing HTML if any + existing_html_block = "" + match = re.search( + r"```html\s*([\s\S]*?)```", + body["messages"][-1]["content"], + ) + if match: + existing_html_block = match.group(1) + if self.valves.clear_previous_html: body["messages"][-1]["content"] = self._remove_existing_html( body["messages"][-1]["content"] ) + final_html = self._merge_html( + "", card_content, card_style, "", self.valves.language + ) + else: + if existing_html_block: + body["messages"][-1]["content"] = self._remove_existing_html( + body["messages"][-1]["content"] + ) + final_html = self._merge_html( + existing_html_block, + card_content, + card_style, + "", + self.valves.language, + ) + else: + final_html = self._merge_html( + "", card_content, card_style, "", self.valves.language + ) - html_embed_tag = f"```html\n{html_card}\n```" + html_embed_tag = f"```html\n{final_html}\n```" body["messages"][-1]["content"] += f"\n\n{html_embed_tag}" if self.valves.show_status: @@ -220,11 +297,51 @@ Important Principles: pattern = r"```html\s*[\s\S]*?```" return re.sub(pattern, "", content).strip() - def generate_html_card(self, data): + def _merge_html( + self, + existing_html_code: str, + new_content: str, + new_styles: str = "", + new_scripts: str = "", + user_language: str = "en-US", + ) -> str: + """ + Merges new content into an existing HTML container, or creates a new one. + """ + 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_html_card_components(self, data): # Enhanced CSS with premium styling style = """ - - """ - # Enhanced HTML structure - html = f""" - - - - - {style} - - -
-
-
-
-
{data.get('category', 'General Knowledge')}
-

{data.get('title', 'Flash Card')}

-
-
-
- {data.get('summary', '')} + # Generate tags HTML + tags_html = "" + if "tags" in data and data["tags"]: + for tag in data["tags"]: + tags_html += f'
#{tag}
' + + # Generate key points HTML + points_html = "" + if "key_points" in data and data["key_points"]: + for point in data["key_points"]: + points_html += f"
  • {point}
  • " + + # Build the card HTML structure + content = f""" +
    +
    +
    +
    +
    {data.get('category', 'Knowledge')}
    +

    {data.get('title', 'Flash Card')}

    +
    +
    +
    + {data.get('summary', '')} +
    + +
    KEY POINTS
    +
      + {points_html} +
    +
    + -
    Key Points
    -
      - {''.join([f'
    • {point}
    • ' for point in data.get('key_points', [])])} -
    -
    -
    -
    - -""" - return html + """ + + return content, style diff --git a/plugins/actions/knowledge-card/闪记卡.py b/plugins/actions/knowledge-card/闪记卡.py index f4fcd20..f2ff2c4 100644 --- a/plugins/actions/knowledge-card/闪记卡.py +++ b/plugins/actions/knowledge-card/闪记卡.py @@ -19,6 +19,55 @@ from open_webui.models.users import Users logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) +HTML_WRAPPER_TEMPLATE = """ + + + + + + + + + +
    + +
    + + + +""" + class Action: class Valves(BaseModel): @@ -41,7 +90,7 @@ class Action: ) clear_previous_html: bool = Field( default=False, - description="是否在追加新结果前清除消息中已有的插件生成 HTML 内容 (通过标记识别)。", + description="是否强制清除旧的插件结果(如果为 True,则不合并,直接覆盖)。", ) def __init__(self): @@ -173,21 +222,44 @@ class Action: ) return body - # 2. Generate HTML - html_card = self.generate_html_card(card_data) + # 2. Generate HTML components + card_content, card_style = self.generate_html_card_components(card_data) # 3. Append to message - # We append it to the user message so it shows up as part of the interaction - # Or we can append it to the assistant response if we were a Pipe, but this is an Action. - # Actions usually modify the input or trigger a side effect. - # To show the card, we can append it to the message content. + # Extract existing HTML if any + existing_html_block = "" + match = re.search( + r"```html\s*([\s\S]*?)```", + body["messages"][-1]["content"], + ) + if match: + existing_html_block = match.group(1) if self.valves.clear_previous_html: body["messages"][-1]["content"] = self._remove_existing_html( body["messages"][-1]["content"] ) + final_html = self._merge_html( + "", card_content, card_style, "", self.valves.language + ) + else: + if existing_html_block: + body["messages"][-1]["content"] = self._remove_existing_html( + body["messages"][-1]["content"] + ) + final_html = self._merge_html( + existing_html_block, + card_content, + card_style, + "", + self.valves.language, + ) + else: + final_html = self._merge_html( + "", card_content, card_style, "", self.valves.language + ) - html_embed_tag = f"```html\n{html_card}\n```" + html_embed_tag = f"```html\n{final_html}\n```" body["messages"][-1]["content"] += f"\n\n{html_embed_tag}" if self.valves.show_status: @@ -222,11 +294,51 @@ class Action: pattern = r"```html\s*[\s\S]*?```" return re.sub(pattern, "", content).strip() - def generate_html_card(self, data): + 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_html_card_components(self, data): # Enhanced CSS with premium styling style = """ - - """ - # Enhanced HTML structure - html = f""" - - - - - {style} - - -
    -
    -
    -
    -
    {data.get('category', '通用知识')}
    -

    {data.get('title', '知识卡片')}

    -
    -
    -
    - {data.get('summary', '')} + # Generate tags HTML + tags_html = "" + if "tags" in data and data["tags"]: + for tag in data["tags"]: + tags_html += f'#{tag}' + + # Generate key points HTML + points_html = "" + if "key_points" in data and data["key_points"]: + for point in data["key_points"]: + points_html += f"
  • {point}
  • " + + # Build the card HTML structure + content = f""" +
    +
    +
    +
    +
    {data.get('category', 'Knowledge')}
    +

    {data.get('title', 'Flash Card')}

    +
    +
    +
    + {data.get('summary', '')} +
    + +
    KEY POINTS
    +
      + {points_html} +
    +
    + -
    核心要点
    -
      - {''.join([f'
    • {point}
    • ' for point in data.get('key_points', [])])} -
    -
    -
    -
    - -""" - return html + """ + + return content, style diff --git a/plugins/actions/smart-mind-map/smart_mind_map.py b/plugins/actions/smart-mind-map/smart_mind_map.py index e21bc37..4ad5d25 100644 --- a/plugins/actions/smart-mind-map/smart_mind_map.py +++ b/plugins/actions/smart-mind-map/smart_mind_map.py @@ -59,18 +59,56 @@ User Language: {user_language} {long_text_content} """ -HTML_TEMPLATE_MINDMAP = """ +HTML_WRAPPER_TEMPLATE = """ - Smart Mind Map: Mind Map Visualization - - - + + +
    + +
    + + + +""" + +CSS_TEMPLATE_MINDMAP = """ :root { --primary-color: #1e88e5; --secondary-color: #43a047; @@ -84,101 +122,98 @@ HTML_TEMPLATE_MINDMAP = """ --border-radius: 12px; --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } - body { + .mindmap-container-wrapper { font-family: var(--font-family); line-height: 1.7; color: var(--text-color); margin: 0; - padding: 24px; - background-color: var(--background-color); + padding: 0; + background-color: var(--card-bg-color); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - } - .container { - max-width: 1280px; - margin: 20px auto; - background: var(--card-bg-color); - border-radius: var(--border-radius); - box-shadow: var(--shadow); - overflow: hidden; - border: 1px solid var(--border-color); + height: 100%; + display: flex; + flex-direction: column; } .header { background: var(--header-gradient); color: white; - padding: 32px 40px; + padding: 20px 24px; text-align: center; } .header h1 { margin: 0; - font-size: 2em; + font-size: 1.5em; font-weight: 600; text-shadow: 0 1px 3px rgba(0,0,0,0.2); } .user-context { - font-size: 0.85em; + font-size: 0.8em; color: var(--muted-text-color); background-color: #eceff1; - padding: 12px 20px; + padding: 8px 16px; display: flex; justify-content: space-around; flex-wrap: wrap; + border-bottom: 1px solid var(--border-color); } - .user-context span { margin: 4px 10px; } + .user-context span { margin: 2px 8px; } .content-area { - padding: 30px 40px; + padding: 20px; + flex-grow: 1; } .markmap-container { position: relative; background-color: #fff; background-image: radial-gradient(var(--border-color) 0.5px, transparent 0.5px); background-size: 20px 20px; - border-radius: var(--border-radius); - padding: 24px; - min-height: 700px; + border-radius: 8px; + padding: 16px; + min-height: 500px; display: flex; justify-content: center; align-items: center; border: 1px solid var(--border-color); - box-shadow: 0 4px 12px rgba(0,0,0,0.05); + box-shadow: inset 0 2px 6px rgba(0,0,0,0.03); } .download-area { text-align: center; - padding-top: 30px; - margin-top: 30px; + padding-top: 20px; + margin-top: 20px; border-top: 1px solid var(--border-color); } .download-btn { background-color: var(--primary-color); color: white; border: none; - padding: 12px 24px; - border-radius: 8px; - font-size: 1em; + padding: 8px 16px; + border-radius: 6px; + font-size: 0.9em; font-weight: 500; cursor: pointer; transition: all 0.2s ease-in-out; - margin: 0 10px; + margin: 0 6px; display: inline-flex; align-items: center; - gap: 8px; + gap: 6px; } .download-btn.secondary { background-color: var(--secondary-color); } .download-btn:hover { - transform: translateY(-2px); - box-shadow: 0 6px 12px rgba(0,0,0,0.15); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0,0,0,0.1); } .download-btn.copied { - background-color: #2e7d32; /* A darker green for success */ + background-color: #2e7d32; } .footer { text-align: center; - padding: 24px; - font-size: 0.85em; + padding: 16px; + font-size: 0.8em; color: #90a4ae; background-color: #eceff1; + border-top: 1px solid var(--border-color); } .footer a { color: var(--primary-color); @@ -192,43 +227,47 @@ HTML_TEMPLATE_MINDMAP = """ color: #c62828; background-color: #ffcdd2; border: 1px solid #ef9a9a; - padding: 20px; - border-radius: var(--border-radius); + padding: 16px; + border-radius: 8px; font-weight: 500; - font-size: 1.1em; + font-size: 1em; } - - - -
    -
    -

    🧠 Smart Mind Map

    -
    -
    - User: {user_name} - Analysis Time: {current_date_time_str} - Weekday: {current_weekday_zh} -
    -
    -
    -
    - - +""" + +CONTENT_TEMPLATE_MINDMAP = """ +
    +
    +

    🧠 Smart Mind Map

    +
    +
    + User: {user_name} + Time: {current_date_time_str} +
    +
    +
    +
    + + +
    +
    +
    - -
    - - + + +""" +SCRIPT_TEMPLATE_MINDMAP = """ + + + - - """ @@ -372,7 +408,7 @@ class Action: ) CLEAR_PREVIOUS_HTML: bool = Field( default=False, - description="Whether to clear existing plugin-generated HTML content in the message before appending new results (identified by marker).", + description="Whether to force clear previous plugin results (if True, overwrites instead of merging).", ) def __init__(self): @@ -403,6 +439,48 @@ class Action: pattern = r"```html\s*[\s\S]*?```" return re.sub(pattern, "", content).strip() + def _merge_html( + self, + existing_html_code: str, + new_content: str, + new_styles: str = "", + new_scripts: str = "", + user_language: str = "en-US", + ) -> str: + """ + Merges new content into an existing HTML container, or creates a new one. + """ + 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() + async def action( self, body: dict, @@ -431,7 +509,7 @@ class Action: shanghai_tz = pytz.timezone("Asia/Shanghai") current_datetime_shanghai = datetime.now(shanghai_tz) current_date_time_str = current_datetime_shanghai.strftime( - "%Y-%m-%d %H:%M:%S" + "%B %d, %Y %H:%M:%S" ) current_weekday_en = current_datetime_shanghai.strftime("%A") current_weekday_zh = self.weekday_map.get(current_weekday_en, "Unknown") @@ -440,7 +518,7 @@ class Action: except Exception as e: logger.warning(f"Failed to get timezone info: {e}, using default values.") now = datetime.now() - current_date_time_str = now.strftime("%Y-%m-%d %H:%M:%S") + current_date_time_str = now.strftime("%B %d, %Y %H:%M:%S") current_weekday_zh = "Unknown" current_year = now.strftime("%Y") current_timezone_str = "Unknown" @@ -558,20 +636,52 @@ class Action: ] markdown_syntax = self._extract_markdown_syntax(assistant_response_content) - final_html_content = ( - HTML_TEMPLATE_MINDMAP.replace("{unique_id}", unique_id) - .replace("{user_language}", user_language) + # 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_weekday_zh}", current_weekday_zh) .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, + ) - html_embed_tag = f"```html\n{final_html_content}\n```" + html_embed_tag = f"```html\n{final_html}\n```" body["messages"][-1]["content"] = f"{long_text_content}\n\n{html_embed_tag}" if self.valves.show_status and __event_emitter__: diff --git a/plugins/actions/smart-mind-map/思维导图.py b/plugins/actions/smart-mind-map/思维导图.py index e9843dd..b518e1c 100644 --- a/plugins/actions/smart-mind-map/思维导图.py +++ b/plugins/actions/smart-mind-map/思维导图.py @@ -56,23 +56,59 @@ USER_PROMPT_GENERATE_MINDMAP = """ --- **长篇文本内容:** -Use code with caution. -Python {long_text_content} """ -HTML_TEMPLATE_MINDMAP = """ +HTML_WRAPPER_TEMPLATE = """ - 智绘心图: 思维导图 - - - + + +
    + +
    + + + +""" + +CSS_TEMPLATE_MINDMAP = """ :root { --primary-color: #1e88e5; --secondary-color: #43a047; @@ -86,101 +122,98 @@ HTML_TEMPLATE_MINDMAP = """ --border-radius: 12px; --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } - body { + .mindmap-container-wrapper { font-family: var(--font-family); line-height: 1.7; color: var(--text-color); margin: 0; - padding: 24px; - background-color: var(--background-color); + padding: 0; + background-color: var(--card-bg-color); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - } - .container { - max-width: 1280px; - margin: 20px auto; - background: var(--card-bg-color); - border-radius: var(--border-radius); - box-shadow: var(--shadow); - overflow: hidden; - border: 1px solid var(--border-color); + height: 100%; + display: flex; + flex-direction: column; } .header { background: var(--header-gradient); color: white; - padding: 32px 40px; + padding: 20px 24px; text-align: center; } .header h1 { margin: 0; - font-size: 2em; + font-size: 1.5em; font-weight: 600; text-shadow: 0 1px 3px rgba(0,0,0,0.2); } .user-context { - font-size: 0.85em; + font-size: 0.8em; color: var(--muted-text-color); background-color: #eceff1; - padding: 12px 20px; + padding: 8px 16px; display: flex; justify-content: space-around; flex-wrap: wrap; + border-bottom: 1px solid var(--border-color); } - .user-context span { margin: 4px 10px; } + .user-context span { margin: 2px 8px; } .content-area { - padding: 30px 40px; + padding: 20px; + flex-grow: 1; } .markmap-container { position: relative; background-color: #fff; background-image: radial-gradient(var(--border-color) 0.5px, transparent 0.5px); background-size: 20px 20px; - border-radius: var(--border-radius); - padding: 24px; - min-height: 700px; + border-radius: 8px; + padding: 16px; + min-height: 500px; display: flex; justify-content: center; align-items: center; border: 1px solid var(--border-color); - box-shadow: 0 4px 12px rgba(0,0,0,0.05); + box-shadow: inset 0 2px 6px rgba(0,0,0,0.03); } .download-area { text-align: center; - padding-top: 30px; - margin-top: 30px; + padding-top: 20px; + margin-top: 20px; border-top: 1px solid var(--border-color); } .download-btn { background-color: var(--primary-color); color: white; border: none; - padding: 12px 24px; - border-radius: 8px; - font-size: 1em; + padding: 8px 16px; + border-radius: 6px; + font-size: 0.9em; font-weight: 500; cursor: pointer; transition: all 0.2s ease-in-out; - margin: 0 10px; + margin: 0 6px; display: inline-flex; align-items: center; - gap: 8px; + gap: 6px; } .download-btn.secondary { background-color: var(--secondary-color); } .download-btn:hover { - transform: translateY(-2px); - box-shadow: 0 6px 12px rgba(0,0,0,0.15); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0,0,0,0.1); } .download-btn.copied { - background-color: #2e7d32; /* A darker green for success */ + background-color: #2e7d32; } .footer { text-align: center; - padding: 24px; - font-size: 0.85em; + padding: 16px; + font-size: 0.8em; color: #90a4ae; background-color: #eceff1; + border-top: 1px solid var(--border-color); } .footer a { color: var(--primary-color); @@ -194,43 +227,47 @@ HTML_TEMPLATE_MINDMAP = """ color: #c62828; background-color: #ffcdd2; border: 1px solid #ef9a9a; - padding: 20px; - border-radius: var(--border-radius); + padding: 16px; + border-radius: 8px; font-weight: 500; - font-size: 1.1em; + font-size: 1em; } - - - -
    -
    -

    🧠 智绘心图

    -
    -
    - 用户: {user_name} - 分析时间: {current_date_time_str} - 星期: {current_weekday_zh} -
    -
    -
    -
    - - +""" + +CONTENT_TEMPLATE_MINDMAP = """ +
    +
    +

    🧠 智能思维导图

    +
    +
    + 用户: {user_name} + 时间: {current_date_time_str} +
    +
    +
    +
    + + +
    +
    +
    - -
    - - + + +""" +SCRIPT_TEMPLATE_MINDMAP = """ + + + - - """ @@ -369,11 +403,11 @@ class Action: ) MIN_TEXT_LENGTH: int = Field( default=100, - description="进行思维导图分析所需的最小文本长度(字符数)。", + description="进行思维导图分析所需的最小文本长度(字符数)。", ) CLEAR_PREVIOUS_HTML: bool = Field( default=False, - description="是否在追加新结果前清除消息中已有的插件生成 HTML 内容 (通过标记识别)。", + description="是否强制清除旧的插件结果(如果为 True,则不合并,直接覆盖)。", ) def __init__(self): @@ -404,6 +438,48 @@ class Action: pattern = r"```html\s*[\s\S]*?```" return re.sub(pattern, "", content).strip() + 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() + async def action( self, body: dict, @@ -432,7 +508,7 @@ class Action: shanghai_tz = pytz.timezone("Asia/Shanghai") current_datetime_shanghai = datetime.now(shanghai_tz) current_date_time_str = current_datetime_shanghai.strftime( - "%Y-%m-%d %H:%M:%S" + "%Y年%m月%d日 %H:%M:%S" ) current_weekday_en = current_datetime_shanghai.strftime("%A") current_weekday_zh = self.weekday_map.get(current_weekday_en, "未知星期") @@ -441,7 +517,7 @@ class Action: except Exception as e: logger.warning(f"获取时区信息失败: {e},使用默认值。") now = datetime.now() - current_date_time_str = now.strftime("%Y-%m-%d %H:%M:%S") + current_date_time_str = now.strftime("%Y年%m月%d日 %H:%M:%S") current_weekday_zh = "未知星期" current_year = now.strftime("%Y") current_timezone_str = "未知时区" @@ -558,20 +634,52 @@ class Action: ] markdown_syntax = self._extract_markdown_syntax(assistant_response_content) - final_html_content = ( - HTML_TEMPLATE_MINDMAP.replace("{unique_id}", unique_id) - .replace("{user_language}", user_language) + # 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_weekday_zh}", current_weekday_zh) .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, + ) - html_embed_tag = f"```html\n{final_html_content}\n```" + html_embed_tag = f"```html\n{final_html}\n```" body["messages"][-1]["content"] = f"{long_text_content}\n\n{html_embed_tag}" if self.valves.show_status and __event_emitter__: diff --git a/plugins/actions/summary/summary.py b/plugins/actions/summary/summary.py index c782da2..2828c84 100644 --- a/plugins/actions/summary/summary.py +++ b/plugins/actions/summary/summary.py @@ -27,6 +27,58 @@ logging.basicConfig( ) logger = logging.getLogger(__name__) +# ================================================================= +# HTML Wrapper Template (supports multiple plugins and grid layout) +# ================================================================= +HTML_WRAPPER_TEMPLATE = """ + + + + + + + + + +
    + +
    + + + +""" + # ================================================================= # Internal LLM Prompts # ================================================================= @@ -93,15 +145,7 @@ Please conduct a deep and comprehensive analysis, focusing on actionable advice. # Frontend HTML Template (Jinja2 Syntax) # ================================================================= -HTML_TEMPLATE = """ - - - - - - - Deep Reading: Deep Analysis Report - - - -
    -
    -

    📖 Deep Reading: Deep Analysis Report

    -
    -
    - User: {{ user_name }} - Analysis Time: {{ current_date_time_str }} - Weekday: {{ current_weekday }} -
    -
    -
    -

    📝Detailed Summary

    -
    {{ summary_html | safe }}
    +""" + +CONTENT_TEMPLATE_SUMMARY = """ +
    +
    +

    📖 Deep Reading: Analysis Report

    -
    -

    💡Key Information Points

    -
    {{ keypoints_html | safe }}
    +
    + User: {user_name} + Time: {current_date_time_str}
    -
    -

    🎯Actionable Advice

    -
    {{ actions_html | safe }}
    +
    +
    +

    📝Detailed Summary

    +
    {summary_html}
    +
    +
    +

    💡Key Information Points

    +
    {keypoints_html}
    +
    +
    +

    🎯Actionable Advice

    +
    {actions_html}
    +
    +
    +
    - -
    - -""" +""" class Action: @@ -295,7 +323,7 @@ class Action: ) CLEAR_PREVIOUS_HTML: bool = Field( default=False, - description="Whether to clear existing plugin-generated HTML content in the message before appending new results (identified by marker).", + description="Whether to force clear previous plugin results (if True, overwrites instead of merging).", ) def __init__(self): @@ -358,12 +386,64 @@ class Action: pattern = r"```html\s*[\s\S]*?```" return re.sub(pattern, "", content).strip() - def _build_html(self, context: dict) -> str: + def _merge_html( + self, + existing_html_code: str, + new_content: str, + new_styles: str = "", + new_scripts: str = "", + user_language: str = "en-US", + ) -> str: """ - Build final HTML content using Jinja2 template and context data. + Merges new content into an existing HTML container, or creates a new one. """ - template = Template(HTML_TEMPLATE) - return template.render(context) + 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 _build_content_html(self, context: dict) -> str: + """ + Build content HTML using context data. + """ + return ( + CONTENT_TEMPLATE_SUMMARY.replace( + "{user_name}", context.get("user_name", "User") + ) + .replace( + "{current_date_time_str}", context.get("current_date_time_str", "") + ) + .replace("{current_year}", context.get("current_year", "")) + .replace("{summary_html}", context.get("summary_html", "")) + .replace("{keypoints_html}", context.get("keypoints_html", "")) + .replace("{actions_html}", context.get("actions_html", "")) + ) async def action( self, @@ -390,7 +470,7 @@ class Action: user_id = __user__.get("id", "unknown_user") now = datetime.now() - current_date_time_str = now.strftime("%Y-%m-%d %H:%M:%S") + current_date_time_str = now.strftime("%B %d, %Y %H:%M:%S") current_weekday = now.strftime("%A") current_year = now.strftime("%Y") current_timezone_str = "Unknown Timezone" @@ -497,12 +577,38 @@ class Action: **processed_content, } - final_html_content = self._build_html(context) + content_html = self._build_content_html(context) + + # Extract existing HTML if any + existing_html_block = "" + match = re.search( + r"```html\s*([\s\S]*?)```", + original_content, + ) + if match: + existing_html_block = match.group(1) if self.valves.CLEAR_PREVIOUS_HTML: original_content = self._remove_existing_html(original_content) + final_html = self._merge_html( + "", content_html, CSS_TEMPLATE_SUMMARY, "", user_language + ) + else: + if existing_html_block: + original_content = self._remove_existing_html(original_content) + final_html = self._merge_html( + existing_html_block, + content_html, + CSS_TEMPLATE_SUMMARY, + "", + user_language, + ) + else: + final_html = self._merge_html( + "", content_html, CSS_TEMPLATE_SUMMARY, "", user_language + ) - html_embed_tag = f"```html\n{final_html_content}\n```" + html_embed_tag = f"```html\n{final_html}\n```" body["messages"][-1]["content"] = f"{original_content}\n\n{html_embed_tag}" if self.valves.show_status and __event_emitter__: diff --git a/plugins/actions/summary/精读.py b/plugins/actions/summary/精读.py index f6b8a68..d23835e 100644 --- a/plugins/actions/summary/精读.py +++ b/plugins/actions/summary/精读.py @@ -24,6 +24,58 @@ logging.basicConfig( ) logger = logging.getLogger(__name__) +# ================================================================= +# HTML 容器模板 (支持多插件共存与网格布局) +# ================================================================= +HTML_WRAPPER_TEMPLATE = """ + + + + + + + + + +
    + +
    + + + +""" + # ================================================================= # 内部 LLM 提示词设计 # ================================================================= @@ -90,15 +142,7 @@ USER_PROMPT_GENERATE_SUMMARY = """ # 前端 HTML 模板 (Jinja2 语法) # ================================================================= -HTML_TEMPLATE = """ - - - - - - - 精读:深度分析报告 - - - -
    -
    -

    📖 精读:深度分析报告

    -
    -
    - 用户: {{ user_name }} - 分析时间: {{ current_date_time_str }} - 星期: {{ current_weekday }} -
    -
    -
    -

    📝详细摘要

    -
    {{ summary_html | safe }}
    +""" + +CONTENT_TEMPLATE_SUMMARY = """ +
    +
    +

    📖 精读:深度分析报告

    -
    -

    💡关键信息点

    -
    {{ keypoints_html | safe }}
    +
    + 用户: {user_name} + 时间: {current_date_time_str}
    -
    -

    🎯行动建议

    -
    {{ actions_html | safe }}
    +
    +
    +

    📝详细摘要

    +
    {summary_html}
    +
    +
    +

    💡关键信息点

    +
    {keypoints_html}
    +
    +
    +

    🎯行动建议

    +
    {actions_html}
    +
    +
    +
    - -
    - -""" +""" class Action: @@ -290,11 +318,20 @@ class Action: ) CLEAR_PREVIOUS_HTML: bool = Field( default=False, - description="是否在追加新结果前清除消息中已有的插件生成 HTML 内容 (通过标记识别)。", + description="是否强制清除旧的插件结果(如果为 True,则不合并,直接覆盖)。", ) def __init__(self): self.valves = self.Valves() + self.weekday_map = { + "Monday": "星期一", + "Tuesday": "星期二", + "Wednesday": "星期三", + "Thursday": "星期四", + "Friday": "星期五", + "Saturday": "星期六", + "Sunday": "星期日", + } def _process_llm_output(self, llm_output: str) -> Dict[str, str]: """ @@ -347,12 +384,64 @@ class Action: pattern = r"```html\s*[\s\S]*?```" return re.sub(pattern, "", content).strip() - def _build_html(self, context: dict) -> str: + def _merge_html( + self, + existing_html_code: str, + new_content: str, + new_styles: str = "", + new_scripts: str = "", + user_language: str = "zh-CN", + ) -> str: """ - 使用 Jinja2 模板和上下文数据构建最终的HTML内容。 + 将新内容合并到现有的 HTML 容器中,或者创建一个新的容器。 """ - template = Template(HTML_TEMPLATE) - return template.render(context) + 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 _build_content_html(self, context: dict) -> str: + """ + 使用上下文数据构建内容 HTML。 + """ + return ( + CONTENT_TEMPLATE_SUMMARY.replace( + "{user_name}", context.get("user_name", "用户") + ) + .replace( + "{current_date_time_str}", context.get("current_date_time_str", "") + ) + .replace("{current_year}", context.get("current_year", "")) + .replace("{summary_html}", context.get("summary_html", "")) + .replace("{keypoints_html}", context.get("keypoints_html", "")) + .replace("{actions_html}", context.get("actions_html", "")) + ) async def action( self, @@ -379,8 +468,9 @@ class Action: user_id = __user__.get("id", "unknown_user") now = datetime.now() - current_date_time_str = now.strftime("%Y-%m-%d %H:%M:%S") - current_weekday = now.strftime("%A") + current_date_time_str = now.strftime("%Y年%m月%d日 %H:%M:%S") + current_weekday_en = now.strftime("%A") + current_weekday = self.weekday_map.get(current_weekday_en, current_weekday_en) current_year = now.strftime("%Y") current_timezone_str = "未知时区" @@ -486,12 +576,38 @@ class Action: **processed_content, } - final_html_content = self._build_html(context) + content_html = self._build_content_html(context) + + # Extract existing HTML if any + existing_html_block = "" + match = re.search( + r"```html\s*([\s\S]*?)```", + original_content, + ) + if match: + existing_html_block = match.group(1) if self.valves.CLEAR_PREVIOUS_HTML: original_content = self._remove_existing_html(original_content) + final_html = self._merge_html( + "", content_html, CSS_TEMPLATE_SUMMARY, "", user_language + ) + else: + if existing_html_block: + original_content = self._remove_existing_html(original_content) + final_html = self._merge_html( + existing_html_block, + content_html, + CSS_TEMPLATE_SUMMARY, + "", + user_language, + ) + else: + final_html = self._merge_html( + "", content_html, CSS_TEMPLATE_SUMMARY, "", user_language + ) - html_embed_tag = f"```html\n{final_html_content}\n```" + html_embed_tag = f"```html\n{final_html}\n```" body["messages"][-1]["content"] = f"{original_content}\n\n{html_embed_tag}" if self.valves.show_status and __event_emitter__: