diff --git a/plugins/actions/ACTION_PLUGIN_TEMPLATE.py b/plugins/actions/ACTION_PLUGIN_TEMPLATE.py index 7b99038..addf720 100644 --- a/plugins/actions/ACTION_PLUGIN_TEMPLATE.py +++ b/plugins/actions/ACTION_PLUGIN_TEMPLATE.py @@ -101,11 +101,11 @@ HTML_WRAPPER_TEMPLATE = """ class Action: class Valves(BaseModel): - show_status: bool = Field( + SHOW_STATUS: bool = Field( default=True, description="Whether to show operation status updates in the chat interface.", ) - LLM_MODEL_ID: str = Field( + MODEL_ID: str = Field( default="", description="Built-in LLM Model ID used for processing. If empty, uses the current conversation's model.", ) @@ -231,7 +231,7 @@ class Action: done: bool = False, ): """Emits a status update event.""" - if self.valves.show_status and emitter: + if self.valves.SHOW_STATUS and emitter: await emitter( {"type": "status", "data": {"description": description, "done": done}} ) @@ -301,7 +301,7 @@ class Action: ) # 5. Determine Model - target_model = self.valves.LLM_MODEL_ID + target_model = self.valves.MODEL_ID if not target_model: target_model = body.get("model") # Note: No hardcoded fallback here, relies on system/user context @@ -362,4 +362,9 @@ class Action: # Append error to chat (optional) body["messages"][-1]["content"] += f"\n\n❌ **Error**: {error_msg}" + await self._emit_status(__event_emitter__, "Processing failed.", done=True) + await self._emit_notification( + __event_emitter__, "Action failed, please check logs.", "error" + ) + return body diff --git a/plugins/actions/ACTION_PLUGIN_TEMPLATE_CN.py b/plugins/actions/ACTION_PLUGIN_TEMPLATE_CN.py index e83c2cf..50d0ddb 100644 --- a/plugins/actions/ACTION_PLUGIN_TEMPLATE_CN.py +++ b/plugins/actions/ACTION_PLUGIN_TEMPLATE_CN.py @@ -101,11 +101,11 @@ HTML_WRAPPER_TEMPLATE = """ class Action: class Valves(BaseModel): - show_status: bool = Field( + SHOW_STATUS: bool = Field( default=True, description="是否在聊天界面显示操作状态更新。", ) - LLM_MODEL_ID: str = Field( + MODEL_ID: str = Field( default="", description="用于处理的内置 LLM 模型 ID。如果为空,则使用当前对话的模型。", ) @@ -242,7 +242,7 @@ class Action: done: bool = False, ): """发送状态更新事件。""" - if self.valves.show_status and emitter: + if self.valves.SHOW_STATUS and emitter: await emitter( {"type": "status", "data": {"description": description, "done": done}} ) @@ -312,7 +312,7 @@ class Action: ) # 5. 确定模型 - target_model = self.valves.LLM_MODEL_ID + target_model = self.valves.MODEL_ID if not target_model: target_model = body.get("model") # 注意: 这里没有硬编码的回退,依赖于系统/用户上下文 diff --git a/plugins/actions/PLUGIN_DEVELOPMENT_GUIDE.md b/plugins/actions/PLUGIN_DEVELOPMENT_GUIDE.md new file mode 100644 index 0000000..d689ead --- /dev/null +++ b/plugins/actions/PLUGIN_DEVELOPMENT_GUIDE.md @@ -0,0 +1,292 @@ +# OpenWebUI HTML Action 插件开发指南 + +> 本文档定义了开发 OpenWebUI Action 插件的标准规范和最佳实践。 + +## 📐 核心技术规范 + +### 1. Valves 配置规范 (Pydantic BaseModel) + +**命名规则**: 所有字段必须使用 **大写+下划线** (UPPER_CASE)。 + +```python +class Valves(BaseModel): + SHOW_STATUS: bool = Field(default=True, description="是否显示状态更新") + MODEL_ID: str = Field(default="", description="指定模型 ID,留空则使用当前对话模型") + MIN_TEXT_LENGTH: int = Field(default=50, description="最小字符限制") + CLEAR_PREVIOUS_HTML: bool = Field(default=False, description="是否清除旧结果") + # 根据需要添加更多配置... +``` + +**常用字段参考**: +| 字段名 | 类型 | 说明 | +|--------|------|------| +| `SHOW_STATUS` | `bool` | 控制是否显示状态更新 | +| `MODEL_ID` | `str` | 允许用户指定 LLM 模型 | +| `MIN_TEXT_LENGTH` | `int` | 设置触发分析的最小字符数 | +| `MAX_TEXT_LENGTH` | `int` | 设置推荐的最大字符数 | +| `CLEAR_PREVIOUS_HTML` | `bool` | 控制是覆盖还是合并旧的插件输出 | +| `LANGUAGE` | `str` | 目标语言 (如 'zh', 'en') | + +--- + +### 2. 事件发送规范 (Event Emission) + +**禁止直接调用** `await __event_emitter__`。必须实现并使用以下辅助方法: + +```python +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}} + ) +``` + +**使用示例**: +```python +# 开始处理 +await self._emit_status(__event_emitter__, "正在分析文本...", done=False) + +# 处理完成 +await self._emit_status(__event_emitter__, "分析完成!", done=True) +await self._emit_notification(__event_emitter__, "报告已生成", "success") + +# 发生错误 +await self._emit_status(__event_emitter__, "处理失败。", done=True) +await self._emit_notification(__event_emitter__, f"错误: {str(e)}", "error") +``` + +--- + +### 3. 日志与调试 + +- **严禁使用** `print()` +- 必须使用 Python 标准库 `logging` + +```python +import logging + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +# 记录关键路径 +logger.info(f"Action: {__name__} started") + +# 记录异常 (包含堆栈信息) +logger.error(f"处理失败: {str(e)}", exc_info=True) +``` + +--- + +### 4. HTML 注入逻辑 + +#### 4.1 HTML 包装器 +使用统一的注释标记插件内容: +```html + +
+ + + +
+``` + +#### 4.2 合并机制 +必须实现以下方法,支持在同一条消息中多次运行插件: + +```python +def _remove_existing_html(self, content: str) -> str: + """移除已存在的插件 HTML 块。""" + pattern = r"```html\s*[\s\S]*?```" + return re.sub(pattern, "", content).strip() + +def _merge_html( + self, + existing_html: str, + new_content: str, + new_styles: str = "", + new_scripts: str = "", + user_language: str = "en-US", +) -> str: + """合并新内容到已有 HTML 容器,或创建新容器。""" + # 实现逻辑... +``` + +#### 4.3 注入流程 +```python +# 1. 提取已有 HTML +existing_html_block = "" +match = re.search( + r"```html\s*([\s\S]*?)```", + original_content, +) +if match: + existing_html_block = match.group(1) + +# 2. 根据配置决定是否清除旧内容 +if self.valves.CLEAR_PREVIOUS_HTML: + original_content = self._remove_existing_html(original_content) + final_html = self._merge_html("", new_content, new_styles, "", user_language) +else: + # 合并到已有 HTML + final_html = self._merge_html(existing_html_block, new_content, new_styles, "", user_language) + +# 3. 注入到消息 +html_embed_tag = f"```html\n{final_html}\n```" +body["messages"][-1]["content"] = f"{original_content}\n\n{html_embed_tag}" +``` + +--- + +## 📝 完整代码结构模板 + +```python +""" +title: 插件名称 +author: 作者名 +version: 0.1.0 +description: 插件描述 +""" + +import logging +import re +import json +import time +from typing import Optional, Dict, Any, Callable, Awaitable +from pydantic import BaseModel, Field +from starlette.requests import Request + +from open_webui.apps.webui.models.users import Users +from open_webui.main import generate_chat_completion + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +# ============ 提示词模板 ============ +SYSTEM_PROMPT = """你是一个专业的...""" +USER_PROMPT_TEMPLATE = """请分析以下内容: {content}""" + +# ============ HTML 模板 ============ +HTML_WRAPPER_TEMPLATE = """ +
+ + +
+""" + +class Action: + class Valves(BaseModel): + SHOW_STATUS: bool = Field(default=True, description="是否显示状态更新") + MODEL_ID: str = Field(default="", description="指定模型 ID") + MIN_TEXT_LENGTH: int = Field(default=50, description="最小字符限制") + CLEAR_PREVIOUS_HTML: bool = Field(default=False, description="是否清除旧结果") + + def __init__(self): + self.valves = self.Valves() + + # ========== 事件发送辅助方法 ========== + 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"): + if emitter: + await emitter({"type": "notification", "data": {"type": ntype, "content": content}}) + + # ========== HTML 处理辅助方法 ========== + def _remove_existing_html(self, content: str) -> str: + pattern = r"```html\s*[\s\S]*?```" + return re.sub(pattern, "", content).strip() + + def _merge_html(self, existing_html: str, new_content: str, new_styles: str = "", new_scripts: str = "", user_language: str = "en-US") -> str: + # 实现合并逻辑... + pass + + # ========== 主入口 ========== + async def action( + self, + body: dict, + __user__: Optional[Dict[str, Any]] = None, + __event_emitter__: Optional[Callable[[Any], Awaitable[None]]] = None, + __request__: Optional[Request] = None, + ) -> Optional[dict]: + logger.info(f"Action: {__name__} started") + + # 1. 输入校验 + messages = body.get("messages", []) + if not messages or not messages[-1].get("content"): + return body + + original_content = messages[-1]["content"] + + if len(original_content) < self.valves.MIN_TEXT_LENGTH: + await self._emit_notification(__event_emitter__, "文本过短", "warning") + return body + + # 2. 发送开始状态 + await self._emit_status(__event_emitter__, "正在处理...", done=False) + + try: + # 3. 调用 LLM + user_id = __user__.get("id") if __user__ else "default" + user_obj = Users.get_user_by_id(user_id) + + target_model = self.valves.MODEL_ID or body.get("model") + + payload = { + "model": target_model, + "messages": [ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": USER_PROMPT_TEMPLATE.format(content=original_content)}, + ], + "stream": False, + } + + llm_response = await generate_chat_completion(__request__, payload, user_obj) + result = llm_response["choices"][0]["message"]["content"] + + # 4. 生成 HTML + # ... + + # 5. 注入到消息 + html_embed_tag = f"```html\n{final_html}\n```" + body["messages"][-1]["content"] = f"{original_content}\n\n{html_embed_tag}" + + # 6. 发送成功通知 + await self._emit_status(__event_emitter__, "处理完成!", done=True) + await self._emit_notification(__event_emitter__, "操作成功", "success") + + except Exception as e: + logger.error(f"处理失败: {e}", exc_info=True) + await self._emit_status(__event_emitter__, "处理失败", done=True) + await self._emit_notification(__event_emitter__, f"错误: {str(e)}", "error") + + return body +``` + +--- + +## 🎨 UI 设计原则 + +1. **响应式**: 确保 HTML 在移动端和桌面端都能完美显示 +2. **交互性**: 适当添加 JavaScript 交互(如点击展开、切换视图、复制内容) +3. **本地化**: 根据 `__user__.get("language")` 自动适配中英文界面 +4. **美学设计**: 优先使用现代 UI 设计 + - 毛玻璃效果 (backdrop-filter) + - 渐变色 (linear-gradient) + - 圆角卡片 (border-radius) + - Google Fonts 字体 + +--- + +## 📚 参考模板 + +- [英文模板](./ACTION_PLUGIN_TEMPLATE.py) +- [中文模板](./ACTION_PLUGIN_TEMPLATE_CN.py) diff --git a/plugins/actions/knowledge-card/knowledge_card.py b/plugins/actions/knowledge-card/knowledge_card.py index c32e8fb..7cc9e8e 100644 --- a/plugins/actions/knowledge-card/knowledge_card.py +++ b/plugins/actions/knowledge-card/knowledge_card.py @@ -71,27 +71,27 @@ HTML_WRAPPER_TEMPLATE = """ class Action: class Valves(BaseModel): - model_id: str = Field( + MODEL_ID: str = Field( default="", description="Model ID used for generating card content. If empty, uses the current model.", ) - min_text_length: int = Field( + MIN_TEXT_LENGTH: int = Field( default=50, description="Minimum text length required to generate a flashcard (characters).", ) - max_text_length: int = Field( + MAX_TEXT_LENGTH: int = Field( default=2000, description="Recommended maximum text length. For longer texts, deep analysis tools are recommended.", ) - language: str = Field( + LANGUAGE: str = Field( default="en", description="Target language for card content (e.g., 'en', 'zh').", ) - show_status: bool = Field( + SHOW_STATUS: bool = Field( default=True, description="Whether to show status updates in the chat interface.", ) - clear_previous_html: bool = Field( + CLEAR_PREVIOUS_HTML: bool = Field( default=False, description="Whether to force clear previous plugin results (if True, overwrites instead of merging).", ) @@ -106,7 +106,7 @@ class Action: __event_emitter__: Optional[Any] = None, __request__: Optional[Any] = None, ) -> Optional[dict]: - print(f"action:{__name__} triggered") + logger.info(f"Action: {__name__} triggered") if not __event_emitter__: return body @@ -121,49 +121,34 @@ class Action: # Check text length text_length = len(target_message) - if text_length < self.valves.min_text_length: - if __event_emitter__: - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "warning", - "content": f"Text too short ({text_length} chars), recommended at least {self.valves.min_text_length} chars.", - }, - } - ) + if text_length < self.valves.MIN_TEXT_LENGTH: + await self._emit_notification( + __event_emitter__, + f"Text too short ({text_length} chars), recommended at least {self.valves.MIN_TEXT_LENGTH} chars.", + "warning", + ) return body - if text_length > self.valves.max_text_length: - if __event_emitter__: - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "info", - "content": f"Text quite long ({text_length} chars), consider using 'Deep Reading' for deep analysis.", - }, - } - ) + if text_length > self.valves.MAX_TEXT_LENGTH: + await self._emit_notification( + __event_emitter__, + f"Text quite long ({text_length} chars), consider using 'Deep Reading' for deep analysis.", + "info", + ) # Notify user that we are generating the card - if self.valves.show_status: - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "info", - "content": "⚡ Generating Flash Card...", - }, - } - ) + await self._emit_notification( + __event_emitter__, "⚡ Generating Flash Card...", "info" + ) try: # 1. Extract information using LLM user_id = __user__.get("id") if __user__ else "default" user_obj = Users.get_user_by_id(user_id) - model = self.valves.model_id if self.valves.model_id else body.get("model") + target_model = ( + self.valves.MODEL_ID if self.valves.MODEL_ID else body.get("model") + ) system_prompt = f""" You are a Flash Card Generation Expert, specializing in creating knowledge cards suitable for learning and memorization. Your task is to distill text into concise, easy-to-remember flashcards. @@ -178,7 +163,7 @@ Please extract the following fields and return them in JSON format: 4. "tags": List 2-4 classification tags (1-3 words each). 5. "category": Choose a main category (e.g., Concept, Skill, Fact, Method, etc.). -Target Language: {self.valves.language} +Target Language: {self.valves.LANGUAGE} Important Principles: - **Minimalism**: Refine each point to the extreme. @@ -191,7 +176,7 @@ Important Principles: prompt = f"Please refine the following text into a learning flashcard:\n\n{target_message}" payload = { - "model": model, + "model": target_model, "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": prompt}, @@ -213,16 +198,11 @@ Important Principles: card_data = json.loads(content) except Exception as e: logger.error(f"Failed to parse JSON: {e}, content: {content}") - if self.valves.show_status: - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "error", - "content": "Failed to generate card data, please try again.", - }, - } - ) + await self._emit_notification( + __event_emitter__, + "Failed to generate card data, please try again.", + "error", + ) return body # 2. Generate HTML components @@ -238,12 +218,12 @@ Important Principles: if match: existing_html_block = match.group(1) - if self.valves.clear_previous_html: + 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 + "", card_content, card_style, "", self.valves.LANGUAGE ) else: if existing_html_block: @@ -255,43 +235,43 @@ Important Principles: card_content, card_style, "", - self.valves.language, + self.valves.LANGUAGE, ) else: final_html = self._merge_html( - "", card_content, card_style, "", self.valves.language + "", card_content, card_style, "", self.valves.LANGUAGE ) html_embed_tag = f"```html\n{final_html}\n```" body["messages"][-1]["content"] += f"\n\n{html_embed_tag}" - if self.valves.show_status: - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "success", - "content": "⚡ Flash Card generated successfully!", - }, - } - ) + await self._emit_notification( + __event_emitter__, "⚡ Flash Card generated successfully!", "success" + ) return body except Exception as e: logger.error(f"Error generating knowledge card: {e}") - if self.valves.show_status: - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "error", - "content": f"Error generating knowledge card: {str(e)}", - }, - } - ) + await self._emit_notification( + __event_emitter__, f"Error generating knowledge card: {str(e)}", "error" + ) return body + async def _emit_status(self, emitter, description: str, done: bool = False): + """Emits a status update event.""" + 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"): + """Emits a notification event (info/success/warning/error).""" + if emitter: + await emitter( + {"type": "notification", "data": {"type": ntype, "content": content}} + ) + def _remove_existing_html(self, content: str) -> str: """Removes existing plugin-generated HTML code blocks from the content.""" pattern = r"```html\s*[\s\S]*?```" diff --git a/plugins/actions/knowledge-card/闪记卡.py b/plugins/actions/knowledge-card/闪记卡.py index f2ff2c4..48119fd 100644 --- a/plugins/actions/knowledge-card/闪记卡.py +++ b/plugins/actions/knowledge-card/闪记卡.py @@ -71,24 +71,24 @@ HTML_WRAPPER_TEMPLATE = """ class Action: class Valves(BaseModel): - model_id: str = Field( + MODEL_ID: str = Field( default="", description="用于生成卡片内容的模型 ID。如果为空,则使用当前模型。", ) - min_text_length: int = Field( + MIN_TEXT_LENGTH: int = Field( default=50, description="生成闪记卡所需的最小文本长度(字符数)。" ) - max_text_length: int = Field( + MAX_TEXT_LENGTH: int = Field( default=2000, description="建议的最大文本长度。超过此长度建议使用深度分析工具。", ) - language: str = Field( + LANGUAGE: str = Field( default="zh", description="卡片内容的目标语言 (例如 'zh', 'en')。" ) - show_status: bool = Field( + SHOW_STATUS: bool = Field( default=True, description="是否在聊天界面显示状态更新。" ) - clear_previous_html: bool = Field( + CLEAR_PREVIOUS_HTML: bool = Field( default=False, description="是否强制清除旧的插件结果(如果为 True,则不合并,直接覆盖)。", ) @@ -103,7 +103,7 @@ class Action: __event_emitter__: Optional[Any] = None, __request__: Optional[Any] = None, ) -> Optional[dict]: - print(f"action:{__name__} triggered") + logger.info(f"Action: {__name__} 触发") if not __event_emitter__: return body @@ -118,49 +118,32 @@ class Action: # Check text length text_length = len(target_message) - if text_length < self.valves.min_text_length: - if __event_emitter__: - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "warning", - "content": f"文本过短({text_length}字符),建议至少{self.valves.min_text_length}字符。", - }, - } - ) + if text_length < self.valves.MIN_TEXT_LENGTH: + await self._emit_notification( + __event_emitter__, + f"文本内容过短 ({text_length} 字符),建议至少 {self.valves.MIN_TEXT_LENGTH} 字符。", + "warning", + ) return body - if text_length > self.valves.max_text_length: - if __event_emitter__: - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "info", - "content": f"文本较长({text_length}字符),建议使用'墨海拾贝'进行深度分析。", - }, - } - ) + if text_length > self.valves.MAX_TEXT_LENGTH: + await self._emit_notification( + __event_emitter__, + f"文本较长({text_length}字符),建议使用'墨海拾贝'进行深度分析。", + "info", + ) # Notify user that we are generating the card - if self.valves.show_status: - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "info", - "content": "⚡ 正在生成闪记卡...", - }, - } - ) + await self._emit_notification(__event_emitter__, "⚡ 正在生成闪记卡...", "info") try: # 1. Extract information using LLM user_id = __user__.get("id") if __user__ else "default" user_obj = Users.get_user_by_id(user_id) - model = self.valves.model_id if self.valves.model_id else body.get("model") + target_model = ( + self.valves.MODEL_ID if self.valves.MODEL_ID else body.get("model") + ) system_prompt = f""" 你是一个闪记卡生成专家,专注于创建适合学习和记忆的知识卡片。你的任务是将文本提炼成简洁、易记的学习卡片。 @@ -175,7 +158,7 @@ class Action: 4. "tags": 列出 2-4 个分类标签(每个 2-5 字) 5. "category": 选择一个主分类(如:概念、技能、事实、方法等) -目标语言: {self.valves.language} +目标语言: {self.valves.LANGUAGE} 重要原则: - **极简主义**: 每个要点都要精炼到极致 @@ -188,7 +171,7 @@ class Action: prompt = f"请将以下文本提炼成一张学习记忆卡片:\n\n{target_message}" payload = { - "model": model, + "model": target_model, "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": prompt}, @@ -210,16 +193,9 @@ class Action: card_data = json.loads(content) except Exception as e: logger.error(f"Failed to parse JSON: {e}, content: {content}") - if self.valves.show_status: - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "error", - "content": "生成卡片数据失败,请重试。", - }, - } - ) + await self._emit_notification( + __event_emitter__, "生成卡片数据失败,请重试。", "error" + ) return body # 2. Generate HTML components @@ -235,12 +211,12 @@ class Action: if match: existing_html_block = match.group(1) - if self.valves.clear_previous_html: + 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 + "", card_content, card_style, "", self.valves.LANGUAGE ) else: if existing_html_block: @@ -252,43 +228,43 @@ class Action: card_content, card_style, "", - self.valves.language, + self.valves.LANGUAGE, ) else: final_html = self._merge_html( - "", card_content, card_style, "", self.valves.language + "", card_content, card_style, "", self.valves.LANGUAGE ) html_embed_tag = f"```html\n{final_html}\n```" body["messages"][-1]["content"] += f"\n\n{html_embed_tag}" - if self.valves.show_status: - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "success", - "content": "⚡ 闪记卡生成成功!", - }, - } - ) + await self._emit_notification( + __event_emitter__, "⚡ 闪记卡生成成功!", "success" + ) return body except Exception as e: logger.error(f"Error generating knowledge card: {e}") - if self.valves.show_status: - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "error", - "content": f"生成知识卡片时出错: {str(e)}", - }, - } - ) + await self._emit_notification( + __event_emitter__, f"生成知识卡片时出错: {str(e)}", "error" + ) return body + 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}} + ) + def _remove_existing_html(self, content: str) -> str: """移除内容中已有的插件生成 HTML 代码块 (通过标记识别)。""" pattern = r"```html\s*[\s\S]*?```" diff --git a/plugins/actions/smart-mind-map/smart_mind_map.py b/plugins/actions/smart-mind-map/smart_mind_map.py index 4ad5d25..35810e1 100644 --- a/plugins/actions/smart-mind-map/smart_mind_map.py +++ b/plugins/actions/smart-mind-map/smart_mind_map.py @@ -394,11 +394,11 @@ SCRIPT_TEMPLATE_MINDMAP = """ class Action: class Valves(BaseModel): - show_status: bool = Field( + SHOW_STATUS: bool = Field( default=True, description="Whether to show action status updates in the chat interface.", ) - LLM_MODEL_ID: str = Field( + MODEL_ID: str = Field( default="", description="Built-in LLM model ID for text analysis. If empty, uses the current conversation's model.", ) @@ -434,6 +434,20 @@ class Action: extracted_content = llm_output.strip() return extracted_content.replace("", "<\\/script>") + async def _emit_status(self, emitter, description: str, done: bool = False): + """Emits a status update event.""" + 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"): + """Emits a notification event (info/success/warning/error).""" + if emitter: + await emitter( + {"type": "notification", "data": {"type": ntype, "content": content}} + ) + def _remove_existing_html(self, content: str) -> str: """Removes existing plugin-generated HTML code blocks from the content.""" pattern = r"```html\s*[\s\S]*?```" @@ -523,16 +537,11 @@ class Action: current_year = now.strftime("%Y") current_timezone_str = "Unknown" - if __event_emitter__: - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "info", - "content": "Smart Mind Map is starting, generating mind map for you...", - }, - } - ) + await self._emit_notification( + __event_emitter__, + "Smart Mind Map is starting, generating mind map for you...", + "info", + ) messages = body.get("messages") if ( @@ -541,13 +550,7 @@ class Action: or not messages[-1].get("content") ): error_message = "Unable to retrieve valid user message content." - if __event_emitter__: - await __event_emitter__( - { - "type": "notification", - "data": {"type": "error", "content": error_message}, - } - ) + await self._emit_notification(__event_emitter__, error_message, "error") return { "messages": [{"role": "assistant", "content": f"❌ {error_message}"}] } @@ -565,30 +568,20 @@ class Action: if len(long_text_content) < self.valves.MIN_TEXT_LENGTH: short_text_message = f"Text content is too short ({len(long_text_content)} characters), unable to perform effective analysis. Please provide at least {self.valves.MIN_TEXT_LENGTH} characters of text." - if __event_emitter__: - await __event_emitter__( - { - "type": "notification", - "data": {"type": "warning", "content": short_text_message}, - } - ) + await self._emit_notification( + __event_emitter__, short_text_message, "warning" + ) return { "messages": [ {"role": "assistant", "content": f"⚠️ {short_text_message}"} ] } - if self.valves.show_status and __event_emitter__: - await __event_emitter__( - { - "type": "status", - "data": { - "description": "Smart Mind Map: Analyzing text structure in depth...", - "done": False, - "hidden": False, - }, - } - ) + await self._emit_status( + __event_emitter__, + "Smart Mind Map: Analyzing text structure in depth...", + False, + ) try: unique_id = f"id_{int(time.time() * 1000)}" @@ -603,7 +596,7 @@ class Action: ) # Determine model to use - target_model = self.valves.LLM_MODEL_ID + target_model = self.valves.MODEL_ID if not target_model: target_model = body.get("model") @@ -684,26 +677,14 @@ class Action: 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__: - await __event_emitter__( - { - "type": "status", - "data": { - "description": "Smart Mind Map: Drawing completed!", - "done": True, - "hidden": False, - }, - } - ) - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "success", - "content": f"Mind map has been generated, {user_name}!", - }, - } - ) + await self._emit_status( + __event_emitter__, "Smart Mind Map: Drawing completed!", True + ) + await self._emit_notification( + __event_emitter__, + f"Mind map has been generated, {user_name}!", + "success", + ) logger.info("Action: Smart Mind Map (v0.7.2) completed successfully") except Exception as e: @@ -714,26 +695,13 @@ class Action: "content" ] = f"{long_text_content}\n\n❌ **Error:** {user_facing_error}" - if __event_emitter__: - if self.valves.show_status: - await __event_emitter__( - { - "type": "status", - "data": { - "description": "Smart Mind Map: Processing failed.", - "done": True, - "hidden": False, - }, - } - ) - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "error", - "content": f"Smart Mind Map generation failed, {user_name}!", - }, - } - ) + await self._emit_status( + __event_emitter__, "Smart Mind Map: Processing failed.", True + ) + await self._emit_notification( + __event_emitter__, + f"Smart Mind Map generation failed, {user_name}!", + "error", + ) return body diff --git a/plugins/actions/smart-mind-map/思维导图.py b/plugins/actions/smart-mind-map/思维导图.py index b518e1c..f7ba0ff 100644 --- a/plugins/actions/smart-mind-map/思维导图.py +++ b/plugins/actions/smart-mind-map/思维导图.py @@ -394,10 +394,10 @@ SCRIPT_TEMPLATE_MINDMAP = """ class Action: class Valves(BaseModel): - show_status: bool = Field( + SHOW_STATUS: bool = Field( default=True, description="是否在聊天界面显示操作状态更新。" ) - LLM_MODEL_ID: str = Field( + MODEL_ID: str = Field( default="", description="用于文本分析的内置LLM模型ID。如果为空,则使用当前对话的模型。", ) @@ -433,6 +433,20 @@ class Action: 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}} + ) + def _remove_existing_html(self, content: str) -> str: """移除内容中已有的插件生成 HTML 代码块 (通过标记识别)。""" pattern = r"```html\s*[\s\S]*?```" @@ -522,16 +536,9 @@ class Action: current_year = now.strftime("%Y") current_timezone_str = "未知时区" - if __event_emitter__: - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "info", - "content": "智绘心图已启动,正在为您生成思维导图...", - }, - } - ) + await self._emit_notification( + __event_emitter__, "智绘心图已启动,正在为您生成思维导图...", "info" + ) messages = body.get("messages") if ( @@ -540,13 +547,7 @@ class Action: or not messages[-1].get("content") ): error_message = "无法获取有效的用户消息内容。" - if __event_emitter__: - await __event_emitter__( - { - "type": "notification", - "data": {"type": "error", "content": error_message}, - } - ) + await self._emit_notification(__event_emitter__, error_message, "error") return { "messages": [{"role": "assistant", "content": f"❌ {error_message}"}] } @@ -564,30 +565,18 @@ class Action: if len(long_text_content) < self.valves.MIN_TEXT_LENGTH: short_text_message = f"文本内容过短({len(long_text_content)}字符),无法进行有效分析。请提供至少{self.valves.MIN_TEXT_LENGTH}字符的文本。" - if __event_emitter__: - await __event_emitter__( - { - "type": "notification", - "data": {"type": "warning", "content": short_text_message}, - } - ) + await self._emit_notification( + __event_emitter__, short_text_message, "warning" + ) return { "messages": [ {"role": "assistant", "content": f"⚠️ {short_text_message}"} ] } - if self.valves.show_status and __event_emitter__: - await __event_emitter__( - { - "type": "status", - "data": { - "description": "智绘心图: 深入分析文本结构...", - "done": False, - "hidden": False, - }, - } - ) + await self._emit_status( + __event_emitter__, "智绘心图: 深入分析文本结构...", False + ) try: unique_id = f"id_{int(time.time() * 1000)}" @@ -602,7 +591,7 @@ class Action: ) # 确定使用的模型 - target_model = self.valves.LLM_MODEL_ID + target_model = self.valves.MODEL_ID if not target_model: target_model = body.get("model") @@ -682,26 +671,10 @@ class Action: 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__: - await __event_emitter__( - { - "type": "status", - "data": { - "description": "智绘心图: 绘制完成!", - "done": True, - "hidden": False, - }, - } - ) - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "success", - "content": f"思维导图已生成,{user_name}!", - }, - } - ) + await self._emit_status(__event_emitter__, "智绘心图: 绘制完成!", True) + await self._emit_notification( + __event_emitter__, f"思维导图已生成,{user_name}!", "success" + ) logger.info("Action: 智绘心图 (v12) completed successfully") except Exception as e: @@ -712,26 +685,9 @@ class Action: "content" ] = f"{long_text_content}\n\n❌ **错误:** {user_facing_error}" - if __event_emitter__: - if self.valves.show_status: - await __event_emitter__( - { - "type": "status", - "data": { - "description": "智绘心图: 处理失败。", - "done": True, - "hidden": False, - }, - } - ) - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "error", - "content": f"智绘心图生成失败, {user_name}!", - }, - } - ) + await self._emit_status(__event_emitter__, "智绘心图: 处理失败。", True) + await self._emit_notification( + __event_emitter__, f"智绘心图生成失败, {user_name}!", "error" + ) return body diff --git a/plugins/actions/summary/summary.py b/plugins/actions/summary/summary.py index 2828c84..9b798ea 100644 --- a/plugins/actions/summary/summary.py +++ b/plugins/actions/summary/summary.py @@ -305,11 +305,11 @@ CONTENT_TEMPLATE_SUMMARY = """ class Action: class Valves(BaseModel): - show_status: bool = Field( + SHOW_STATUS: bool = Field( default=True, description="Whether to show operation status updates in the chat interface.", ) - LLM_MODEL_ID: str = Field( + MODEL_ID: str = Field( default="", description="Built-in LLM Model ID used for text analysis. If empty, uses the current conversation's model.", ) @@ -381,6 +381,20 @@ class Action: "actions_html": actions_html, } + async def _emit_status(self, emitter, description: str, done: bool = False): + """Emits a status update event.""" + 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"): + """Emits a notification event (info/success/warning/error).""" + if emitter: + await emitter( + {"type": "notification", "data": {"type": ntype, "content": content}} + ) + def _remove_existing_html(self, content: str) -> str: """Removes existing plugin-generated HTML code blocks from the content.""" pattern = r"```html\s*[\s\S]*?```" @@ -485,13 +499,9 @@ class Action: if len(original_content) < self.valves.MIN_TEXT_LENGTH: short_text_message = f"Text content too short ({len(original_content)} chars), recommended at least {self.valves.MIN_TEXT_LENGTH} chars for effective deep analysis.\n\n💡 Tip: For short texts, consider using '⚡ Flash Card' for quick refinement." - if __event_emitter__: - await __event_emitter__( - { - "type": "notification", - "data": {"type": "warning", "content": short_text_message}, - } - ) + await self._emit_notification( + __event_emitter__, short_text_message, "warning" + ) return { "messages": [ {"role": "assistant", "content": f"⚠️ {short_text_message}"} @@ -500,37 +510,22 @@ class Action: # Recommend for longer texts if len(original_content) < self.valves.RECOMMENDED_MIN_LENGTH: - if __event_emitter__: - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "info", - "content": f"Text length is {len(original_content)} chars. Recommended {self.valves.RECOMMENDED_MIN_LENGTH}+ chars for best analysis results.", - }, - } - ) - - if __event_emitter__: - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "info", - "content": "📖 Deep Reading started, analyzing deeply...", - }, - } + await self._emit_notification( + __event_emitter__, + f"Text length is {len(original_content)} chars. Recommended {self.valves.RECOMMENDED_MIN_LENGTH}+ chars for best analysis results.", + "info", ) - if self.valves.show_status: - await __event_emitter__( - { - "type": "status", - "data": { - "description": "📖 Deep Reading: Analyzing text, extracting essence...", - "done": False, - }, - } - ) + + await self._emit_notification( + __event_emitter__, + "📖 Deep Reading started, analyzing deeply...", + "info", + ) + await self._emit_status( + __event_emitter__, + "📖 Deep Reading: Analyzing text, extracting essence...", + False, + ) formatted_user_prompt = USER_PROMPT_GENERATE_SUMMARY.format( user_name=user_name, @@ -542,7 +537,7 @@ class Action: ) # Determine model to use - target_model = self.valves.LLM_MODEL_ID + target_model = self.valves.MODEL_ID if not target_model: target_model = body.get("model") @@ -611,25 +606,14 @@ class Action: 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__: - await __event_emitter__( - { - "type": "status", - "data": { - "description": "📖 Deep Reading: Analysis complete!", - "done": True, - }, - } - ) - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "success", - "content": f"📖 Deep Reading complete, {user_name}! Deep analysis report generated.", - }, - } - ) + await self._emit_status( + __event_emitter__, "📖 Deep Reading: Analysis complete!", True + ) + await self._emit_notification( + __event_emitter__, + f"📖 Deep Reading complete, {user_name}! Deep analysis report generated.", + "success", + ) except Exception as e: error_message = f"Deep Reading processing failed: {str(e)}" @@ -639,25 +623,13 @@ class Action: "content" ] = f"{original_content}\n\n❌ **Error:** {user_facing_error}" - if __event_emitter__: - if self.valves.show_status: - await __event_emitter__( - { - "type": "status", - "data": { - "description": "Deep Reading: Processing failed.", - "done": True, - }, - } - ) - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "error", - "content": f"Deep Reading processing failed, {user_name}!", - }, - } - ) + await self._emit_status( + __event_emitter__, "Deep Reading: Processing failed.", True + ) + await self._emit_notification( + __event_emitter__, + f"Deep Reading processing failed, {user_name}!", + "error", + ) return body diff --git a/plugins/actions/summary/精读.py b/plugins/actions/summary/精读.py index d23835e..63d3975 100644 --- a/plugins/actions/summary/精读.py +++ b/plugins/actions/summary/精读.py @@ -302,10 +302,10 @@ CONTENT_TEMPLATE_SUMMARY = """ class Action: class Valves(BaseModel): - show_status: bool = Field( + SHOW_STATUS: bool = Field( default=True, description="是否在聊天界面显示操作状态更新。" ) - LLM_MODEL_ID: str = Field( + MODEL_ID: str = Field( default="", description="用于文本分析的内置LLM模型ID。如果为空,则使用当前对话的模型。", ) @@ -379,6 +379,20 @@ class Action: "actions_html": actions_html, } + 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}} + ) + def _remove_existing_html(self, content: str) -> str: """移除内容中已有的插件生成 HTML 代码块 (通过标记识别)。""" pattern = r"```html\s*[\s\S]*?```" @@ -484,13 +498,9 @@ class Action: if len(original_content) < self.valves.MIN_TEXT_LENGTH: short_text_message = f"文本内容过短({len(original_content)}字符),建议至少{self.valves.MIN_TEXT_LENGTH}字符以获得有效的深度分析。\n\n💡 提示:对于短文本,建议使用'⚡ 闪记卡'进行快速提炼。" - if __event_emitter__: - await __event_emitter__( - { - "type": "notification", - "data": {"type": "warning", "content": short_text_message}, - } - ) + await self._emit_notification( + __event_emitter__, short_text_message, "warning" + ) return { "messages": [ {"role": "assistant", "content": f"⚠️ {short_text_message}"} @@ -499,37 +509,18 @@ class Action: # Recommend for longer texts if len(original_content) < self.valves.RECOMMENDED_MIN_LENGTH: - if __event_emitter__: - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "info", - "content": f"文本长度为{len(original_content)}字符。建议{self.valves.RECOMMENDED_MIN_LENGTH}字符以上可获得更好的分析效果。", - }, - } - ) - - if __event_emitter__: - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "info", - "content": "📖 精读已启动,正在进行深度分析...", - }, - } + await self._emit_notification( + __event_emitter__, + f"文本长度为{len(original_content)}字符。建议{self.valves.RECOMMENDED_MIN_LENGTH}字符以上可获得更好的分析效果。", + "info", ) - if self.valves.show_status: - await __event_emitter__( - { - "type": "status", - "data": { - "description": "📖 精读: 深入分析文本,提炼精华...", - "done": False, - }, - } - ) + + await self._emit_notification( + __event_emitter__, "📖 精读已启动,正在进行深度分析...", "info" + ) + await self._emit_status( + __event_emitter__, "📖 精读: 深入分析文本,提炼精华...", False + ) formatted_user_prompt = USER_PROMPT_GENERATE_SUMMARY.format( user_name=user_name, @@ -541,7 +532,7 @@ class Action: ) # 确定使用的模型 - target_model = self.valves.LLM_MODEL_ID + target_model = self.valves.MODEL_ID if not target_model: target_model = body.get("model") @@ -610,22 +601,12 @@ class Action: 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__: - await __event_emitter__( - { - "type": "status", - "data": {"description": "📖 精读: 分析完成!", "done": True}, - } - ) - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "success", - "content": f"📖 精读完成,{user_name}!深度分析报告已生成。", - }, - } - ) + await self._emit_status(__event_emitter__, "📖 精读: 分析完成!", True) + await self._emit_notification( + __event_emitter__, + f"📖 精读完成,{user_name}!深度分析报告已生成。", + "success", + ) except Exception as e: error_message = f"精读处理失败: {str(e)}" @@ -635,25 +616,9 @@ class Action: "content" ] = f"{original_content}\n\n❌ **错误:** {user_facing_error}" - if __event_emitter__: - if self.valves.show_status: - await __event_emitter__( - { - "type": "status", - "data": { - "description": "精读: 处理失败。", - "done": True, - }, - } - ) - await __event_emitter__( - { - "type": "notification", - "data": { - "type": "error", - "content": f"精读处理失败, {user_name}!", - }, - } - ) + await self._emit_status(__event_emitter__, "精读: 处理失败。", True) + await self._emit_notification( + __event_emitter__, f"精读处理失败, {user_name}!", "error" + ) return body