+
"""
@@ -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'
'
+
+ # 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('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('summary', '')}
+
+
+
KEY POINTS
+
+
+
-
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('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('summary', '')}
+
+
+
KEY POINTS
+
+
+
-
核心要点
-
- {''.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;
}
-
-
-
-
-
-
- User: {user_name}
- Analysis Time: {current_date_time_str}
- Weekday: {current_weekday_zh}
-
-
-
-
-
-
- Copy SVG Code
-
-
-
- Copy Markdown
-
+"""
+
+CONTENT_TEMPLATE_MINDMAP = """
+
+
+
+ User: {user_name}
+ Time: {current_date_time_str}
+
+
+
+
+
+
+ SVG
+
+
+
+ Markdown
+
+
+
+
-
-
-
-
+
+
+"""
+SCRIPT_TEMPLATE_MINDMAP = """
+
+
+
-
-