This commit is contained in:
Windpicker-owo
2025-10-24 19:49:45 +08:00
42 changed files with 239 additions and 291 deletions

View File

@@ -7,7 +7,6 @@ from src.plugin_system import (
BaseEventHandler,
BasePlugin,
BasePrompt,
ToolParamType,
BaseTool,
ChatType,
CommandArgs,
@@ -15,6 +14,7 @@ from src.plugin_system import (
ConfigField,
EventType,
PlusCommand,
ToolParamType,
register_plugin,
)
from src.plugin_system.base.base_event import HandlerResult

View File

@@ -49,11 +49,11 @@ __plugin_meta__ = PluginMetadata(
name="{plugin_name}",
description="{description}",
usage="暂无说明",
type={repr(plugin_type)},
type={plugin_type!r},
version="{version}",
author="{author}",
license={repr(license_type)},
repository_url={repr(repository_url)},
license={license_type!r},
repository_url={repository_url!r},
keywords={keywords},
categories={categories},
)

View File

@@ -3,9 +3,9 @@ import datetime
import os
import shutil
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from threading import Lock
from concurrent.futures import ThreadPoolExecutor, as_completed
import orjson
from json_repair import repair_json

View File

@@ -969,7 +969,7 @@ class EmojiManager:
image_base64_frames = get_image_manager().transform_gif(image_base64)
if not image_base64_frames:
raise RuntimeError("GIF表情包转换失败")
prompt = "这是一个GIF动图表情包的关键帧。请用不超过250字详细描述它的核心内容1. 动态画面展现了什么变化2. 它传达了什么核心情绪或玩的是什么梗3. 通常在什么场景下使用?请确保描述既包含关键信息,又能充分展现其内涵。重要规则:如果图片中包含清晰的文字,请务必完整地转述出来,这部分文字不计入250字限制"
prompt = "这是一个GIF动图表情包的关键帧。请用不超过250字进行详尽且严谨的描述。请按照以下结构组织:首先,概括图片的主题和整体氛围。其次,详细描述图片中的核心元素,如果包含二次元角色,请尝试识别角色名称和出处。接着,描述动态画面展现了什么变化,以及它传达的核心情绪或玩的梗。最后,如果图片中包含任何文字,请准确地转述出来,这部分不计入字数限制。请特别注意识别网络文化中的特殊含义,例如,“滑稽”表情应被识别为“滑稽”,而不仅仅是“黄色的脸”"
description = None
for i in range(3):
try:
@@ -985,7 +985,7 @@ class EmojiManager:
logger.warning("表情包识别失败将在1秒后重试...")
await asyncio.sleep(1)
else:
prompt = "这是一个表情包。请用不超过250字详细描述它的核心内容1. 画面描绘了什么2. 它传达了什么核心情绪或玩的是什么梗3. 通常在什么场景下使用?请确保描述既包含关键信息,又能充分展现其内涵。重要规则:如果图片中包含清晰的文字,请务必完整地转述出来,这部分文字不计入250字限制"
prompt = "这是一个表情包。请用不超过250字进行详尽且严谨的描述。请按照以下结构组织:首先,概括图片的主题和整体氛围。其次,详细描述图片中的核心元素,如果包含二次元角色,请尝试识别角色名称和出处。接着,描述它传达的核心情绪或玩的梗。最后,如果图片中包含任何文字,请准确地转述出来,这部分不计入字数限制。请特别注意识别网络文化中的特殊含义,例如,“滑稽”表情应被识别为“滑稽”,而不仅仅是“黄色的脸”"
description = None
for i in range(3):
try:
@@ -1058,7 +1058,7 @@ class EmojiManager:
if emotions: # 只有在成功提取关键词后才进行精炼
logger.info("[自然语言精炼] 开始生成“点睛之笔”的自然语言描述")
refine_prompt = f"""
你的任务是为一张表情包生成一句简洁的精炼描述用于后续的AI模型处理
你的任务是为一张表情包生成一句自然的、包含核心信息的精炼描述
这里是关于这个表情包的分析信息:
# 详细描述
@@ -1068,17 +1068,18 @@ class EmojiManager:
{emotions_text}
# 你的任务
请结合以上所有信息,用一句概括出这个表情包的核心内容,既要描述客观事实,也要点明其传达的核心情绪
请结合以上所有信息,用一句自然的语言,概括出这个表情包的核心内容。
# 规则 (非常重要!)
1. **平衡客观与情感**:首先客观描述画面中的主体、表情和动作,然后点出其最主要的情绪(如:喜悦、悲伤、讽刺等)
2. **包含核心文字**:如果表情包中有文字,必须将文字完整地包含在描述中。
3. **简洁精炼**:用最少的文字概括最多的信息
4. **输出格式****请直接返回这句描述,不要添加任何前缀、标题、引号或多余的解释。**
1. **自然流畅**:描述应该像一个普通人看到图片后的自然反应,而不是生硬的机器分析
2. **包含关键信息**:如果详细描述中识别出了角色名称、出处,必须包含在精炼描述中。
3. **体现情绪**:描述需要体现出表情包传达的核心情绪
4. **包含核心文字**:如果表情包中有文字,必须将文字完整地包含在描述中。
5. **输出格式****请直接返回这句描述,不要添加任何前缀、标题、引号或多余的解释。**
示例:
- 原始信息:一只猫戴着墨镜,关键词是“酷、得意”。
- 正确输出:一只戴着黑色墨镜的猫,表情得意
- 详细描述“图片的核心是一位面带微笑的少女她被识别为游戏《崩坏3rd》中的角色爱莉希雅Elysia...”
- 正确输出:游戏《崩坏3rd》中的角色爱莉希雅她面带微笑看起来很开心
"""
refined_description, _ = await self.llm_emotion_judge.generate_response_async(
refine_prompt, temperature=0.7, max_tokens=100

View File

@@ -1,7 +1,6 @@
import asyncio
import math
import os
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass
# import tqdm

View File

@@ -3,12 +3,12 @@
用于统一管理所有notice消息将notice与正常消息分离
"""
import time
import threading
import time
from collections import defaultdict, deque
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any
from enum import Enum
from typing import Any
from src.common.data_models.database_data_model import DatabaseMessages
from src.common.logger import get_logger
@@ -27,7 +27,7 @@ class NoticeMessage:
"""Notice消息数据结构"""
message: DatabaseMessages
scope: NoticeScope
target_stream_id: Optional[str] = None # 如果是STREAM类型指定目标流ID
target_stream_id: str | None = None # 如果是STREAM类型指定目标流ID
timestamp: float = field(default_factory=time.time)
ttl: int = 3600 # 默认1小时过期
@@ -56,11 +56,11 @@ class GlobalNoticeManager:
return cls._instance
def __init__(self):
if hasattr(self, '_initialized'):
if hasattr(self, "_initialized"):
return
self._initialized = True
self._notices: Dict[str, deque[NoticeMessage]] = defaultdict(deque)
self._notices: dict[str, deque[NoticeMessage]] = defaultdict(deque)
self._max_notices_per_type = 100 # 每种类型最大存储数量
self._cleanup_interval = 300 # 5分钟清理一次过期消息
self._last_cleanup_time = time.time()
@@ -80,8 +80,8 @@ class GlobalNoticeManager:
self,
message: DatabaseMessages,
scope: NoticeScope = NoticeScope.STREAM,
target_stream_id: Optional[str] = None,
ttl: Optional[int] = None
target_stream_id: str | None = None,
ttl: int | None = None
) -> bool:
"""添加notice消息
@@ -142,7 +142,7 @@ class GlobalNoticeManager:
logger.error(f"添加notice消息失败: {e}")
return False
def get_accessible_notices(self, stream_id: str, limit: int = 20) -> List[NoticeMessage]:
def get_accessible_notices(self, stream_id: str, limit: int = 20) -> list[NoticeMessage]:
"""获取指定聊天流可访问的notice消息
Args:
@@ -231,7 +231,7 @@ class GlobalNoticeManager:
logger.error(f"获取notice文本失败: {e}", exc_info=True)
return ""
def clear_notices(self, stream_id: Optional[str] = None, notice_type: Optional[str] = None) -> int:
def clear_notices(self, stream_id: str | None = None, notice_type: str | None = None) -> int:
"""清理notice消息
Args:
@@ -289,14 +289,14 @@ class GlobalNoticeManager:
logger.error(f"清理notice消息失败: {e}")
return 0
def get_stats(self) -> Dict[str, Any]:
def get_stats(self) -> dict[str, Any]:
"""获取统计信息"""
# 更新实时统计
total_active_notices = sum(len(notices) for notices in self._notices.values())
self.stats["total_notices"] = total_active_notices
self.stats["active_keys"] = len(self._notices)
self.stats["last_cleanup_time"] = int(self._last_cleanup_time)
# 添加详细的存储键信息
storage_keys_info = {}
for key, notices in self._notices.items():
@@ -313,11 +313,11 @@ class GlobalNoticeManager:
"""检查消息是否为notice类型"""
try:
# 首先检查消息的is_notify字段
if hasattr(message, 'is_notify') and message.is_notify:
if hasattr(message, "is_notify") and message.is_notify:
return True
# 检查消息的附加配置
if hasattr(message, 'additional_config') and message.additional_config:
if hasattr(message, "additional_config") and message.additional_config:
if isinstance(message.additional_config, dict):
return message.additional_config.get("is_notice", False)
elif isinstance(message.additional_config, str):
@@ -333,7 +333,7 @@ class GlobalNoticeManager:
logger.debug(f"检查notice类型失败: {e}")
return False
def _get_storage_key(self, scope: NoticeScope, target_stream_id: Optional[str], message: DatabaseMessages) -> str:
def _get_storage_key(self, scope: NoticeScope, target_stream_id: str | None, message: DatabaseMessages) -> str:
"""生成存储键"""
if scope == NoticeScope.PUBLIC:
return "public"
@@ -341,10 +341,10 @@ class GlobalNoticeManager:
notice_type = self._get_notice_type(message) or "default"
return f"stream_{target_stream_id}_{notice_type}"
def _get_notice_type(self, message: DatabaseMessages) -> Optional[str]:
def _get_notice_type(self, message: DatabaseMessages) -> str | None:
"""获取notice类型"""
try:
if hasattr(message, 'additional_config') and message.additional_config:
if hasattr(message, "additional_config") and message.additional_config:
if isinstance(message.additional_config, dict):
return message.additional_config.get("notice_type")
elif isinstance(message.additional_config, str):
@@ -397,4 +397,4 @@ class GlobalNoticeManager:
# 创建全局单例实例
global_notice_manager = GlobalNoticeManager()
global_notice_manager = GlobalNoticeManager()

View File

@@ -7,7 +7,7 @@ import asyncio
import random
import time
from collections import defaultdict, deque
from typing import TYPE_CHECKING, Any, Dict, Optional
from typing import TYPE_CHECKING, Any
from src.chat.chatter_manager import ChatterManager
from src.chat.message_receive.chat_stream import ChatStream
@@ -19,9 +19,8 @@ from src.config.config import global_config
from src.plugin_system.apis.chat_api import get_chat_manager
from .distribution_manager import stream_loop_manager
from .global_notice_manager import NoticeScope, global_notice_manager
from .sleep_system.state_manager import SleepState, sleep_state_manager
from .global_notice_manager import global_notice_manager, NoticeScope
if TYPE_CHECKING:
pass
@@ -160,10 +159,17 @@ class MessageManager:
try:
# 检查是否为notice消息
if self._is_notice_message(message):
# Notice消息处理 - 不进入未读消息
# Notice消息处理 - 添加到全局管理器
logger.info(f"📢 检测到notice消息: message_id={message.message_id}, is_notify={message.is_notify}, notice_type={getattr(message, 'notice_type', None)}")
await self._handle_notice_message(stream_id, message)
return
# 根据配置决定是否继续处理(触发聊天流程)
if not global_config.notice.enable_notice_trigger_chat:
logger.info(f"根据配置,流 {stream_id} 的Notice消息将被忽略不触发聊天流程。")
return # 停止处理,不进入未读消息队列
else:
logger.info(f"根据配置,流 {stream_id} 的Notice消息将触发聊天流程。")
# 继续执行,将消息添加到未读队列
# 普通消息处理
chat_manager = get_chat_manager()
@@ -659,11 +665,11 @@ class MessageManager:
"""检查消息是否为notice类型"""
try:
# 首先检查消息的is_notify字段
if hasattr(message, 'is_notify') and message.is_notify:
if hasattr(message, "is_notify") and message.is_notify:
return True
# 检查消息的附加配置
if hasattr(message, 'additional_config') and message.additional_config:
if hasattr(message, "additional_config") and message.additional_config:
if isinstance(message.additional_config, dict):
return message.additional_config.get("is_notice", False)
elif isinstance(message.additional_config, str):
@@ -709,7 +715,7 @@ class MessageManager:
"""
try:
# 检查附加配置中的公共notice标志
if hasattr(message, 'additional_config') and message.additional_config:
if hasattr(message, "additional_config") and message.additional_config:
if isinstance(message.additional_config, dict):
is_public = message.additional_config.get("is_public_notice", False)
elif isinstance(message.additional_config, str):
@@ -730,10 +736,10 @@ class MessageManager:
logger.debug(f"确定notice作用域失败: {e}")
return NoticeScope.STREAM
def _get_notice_type(self, message: DatabaseMessages) -> Optional[str]:
def _get_notice_type(self, message: DatabaseMessages) -> str | None:
"""获取notice类型"""
try:
if hasattr(message, 'additional_config') and message.additional_config:
if hasattr(message, "additional_config") and message.additional_config:
if isinstance(message.additional_config, dict):
return message.additional_config.get("notice_type")
elif isinstance(message.additional_config, str):
@@ -749,6 +755,8 @@ class MessageManager:
try:
# 根据notice类型设置不同的TTL
notice_type = self._get_notice_type(message)
if notice_type is None:
return 3600
ttl_mapping = {
"poke": 1800, # 戳一戳30分钟
@@ -772,7 +780,7 @@ class MessageManager:
logger.error(f"获取notice文本失败: {e}")
return ""
def clear_notices(self, stream_id: Optional[str] = None, notice_type: Optional[str] = None) -> int:
def clear_notices(self, stream_id: str | None = None, notice_type: str | None = None) -> int:
"""清理notice消息"""
try:
return self.notice_manager.clear_notices(stream_id, notice_type)
@@ -780,7 +788,7 @@ class MessageManager:
logger.error(f"清理notice失败: {e}")
return 0
def get_notice_stats(self) -> Dict[str, Any]:
def get_notice_stats(self) -> dict[str, Any]:
"""获取notice管理器统计信息"""
try:
return self.notice_manager.get_stats()

View File

@@ -1,6 +1,5 @@
import os
import re
import time
import traceback
from typing import Any
@@ -12,7 +11,7 @@ from src.chat.message_manager import message_manager
from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager
from src.chat.message_receive.message import MessageRecv, MessageRecvS4U
from src.chat.message_receive.storage import MessageStorage
from src.chat.utils.prompt import Prompt, global_prompt_manager, create_prompt_async
from src.chat.utils.prompt import create_prompt_async, global_prompt_manager
from src.chat.utils.utils import is_mentioned_bot_in_message
from src.common.logger import get_logger
from src.config.config import global_config
@@ -319,12 +318,12 @@ class ChatBot:
else:
logger.debug("notice消息触发聊天流程配置已开启")
return False # 返回False表示继续处理触发聊天流程
# 兼容旧的notice判断方式
if message.message_info.message_id == "notice":
message.is_notify = True
logger.info("旧格式notice消息")
# 同样根据配置决定
if not global_config.notice.enable_notice_trigger_chat:
return True
@@ -477,17 +476,18 @@ class ChatBot:
if notice_handled:
# notice消息已处理需要先添加到message_manager再存储
try:
from src.common.data_models.database_data_model import DatabaseMessages
import time
from src.common.data_models.database_data_model import DatabaseMessages
message_info = message.message_info
msg_user_info = getattr(message_info, "user_info", None)
stream_user_info = getattr(message.chat_stream, "user_info", None)
group_info = getattr(message.chat_stream, "group_info", None)
message_id = message_info.message_id or ""
message_time = message_info.time if message_info.time is not None else time.time()
user_id = ""
user_nickname = ""
user_cardname = None
@@ -502,16 +502,16 @@ class ChatBot:
user_nickname = getattr(stream_user_info, "user_nickname", "") or ""
user_cardname = getattr(stream_user_info, "user_cardname", None)
user_platform = getattr(stream_user_info, "platform", "") or ""
chat_user_id = str(getattr(stream_user_info, "user_id", "") or "")
chat_user_nickname = getattr(stream_user_info, "user_nickname", "") or ""
chat_user_cardname = getattr(stream_user_info, "user_cardname", None)
chat_user_platform = getattr(stream_user_info, "platform", "") or ""
group_id = getattr(group_info, "group_id", None)
group_name = getattr(group_info, "group_name", None)
group_platform = getattr(group_info, "platform", None)
# 构建additional_config确保包含is_notice标志
import json
additional_config_dict = {
@@ -519,9 +519,9 @@ class ChatBot:
"notice_type": message.notice_type or "unknown",
"is_public_notice": bool(message.is_public_notice),
}
# 如果message_info有additional_config合并进来
if hasattr(message_info, 'additional_config') and message_info.additional_config:
if hasattr(message_info, "additional_config") and message_info.additional_config:
if isinstance(message_info.additional_config, dict):
additional_config_dict.update(message_info.additional_config)
elif isinstance(message_info.additional_config, str):
@@ -530,9 +530,9 @@ class ChatBot:
additional_config_dict.update(existing_config)
except Exception:
pass
additional_config_json = json.dumps(additional_config_dict)
# 创建数据库消息对象
db_message = DatabaseMessages(
message_id=message_id,
@@ -560,14 +560,14 @@ class ChatBot:
chat_info_group_name=group_name,
chat_info_group_platform=group_platform,
)
# 添加到message_manager这会将notice添加到全局notice管理器
await message_manager.add_message(message.chat_stream.stream_id, db_message)
logger.info(f"✅ Notice消息已添加到message_manager: type={message.notice_type}, stream={message.chat_stream.stream_id}")
except Exception as e:
logger.error(f"Notice消息添加到message_manager失败: {e}", exc_info=True)
# 存储后直接返回
await MessageStorage.store_message(message, chat)
logger.debug("notice消息已存储跳过后续处理")
@@ -618,9 +618,10 @@ class ChatBot:
template_group_name = None
async def preprocess():
from src.common.data_models.database_data_model import DatabaseMessages
import time
from src.common.data_models.database_data_model import DatabaseMessages
message_info = message.message_info
msg_user_info = getattr(message_info, "user_info", None)
stream_user_info = getattr(message.chat_stream, "user_info", None)

View File

@@ -133,7 +133,7 @@ class MessageRecv(Message):
self.key_words = []
self.key_words_lite = []
# 解析additional_config中的notice信息
if self.message_info.additional_config and isinstance(self.message_info.additional_config, dict):
self.is_notify = self.message_info.additional_config.get("is_notice", False)

View File

@@ -222,7 +222,7 @@ class MessageStorage:
async def replace_image_descriptions(text: str) -> str:
"""异步地将文本中的所有[图片:描述]标记替换为[picid:image_id]"""
pattern = r"\[图片:([^\]]+)\]"
# 如果没有匹配项,提前返回以提高效率
if not re.search(pattern, text):
return text
@@ -233,7 +233,7 @@ class MessageStorage:
for match in re.finditer(pattern, text):
# 添加上一个匹配到当前匹配之间的文本
new_text.append(text[last_end:match.start()])
description = match.group(1).strip()
replacement = match.group(0) # 默认情况下,替换为原始匹配文本
try:
@@ -260,7 +260,7 @@ class MessageStorage:
# 添加最后一个匹配到字符串末尾的文本
new_text.append(text[last_end:])
return "".join(new_text)
@staticmethod

View File

@@ -825,10 +825,10 @@ class DefaultReplyer:
logger.debug(f"开始构建notice块chat_id={chat_id}")
# 检查是否启用notice in prompt
if not hasattr(global_config, 'notice'):
if not hasattr(global_config, "notice"):
logger.debug("notice配置不存在")
return ""
if not global_config.notice.notice_in_prompt:
logger.debug("notice_in_prompt配置未启用")
return ""
@@ -836,7 +836,7 @@ class DefaultReplyer:
# 使用全局notice管理器获取notice文本
from src.chat.message_manager.message_manager import message_manager
limit = getattr(global_config.notice, 'notice_prompt_limit', 5)
limit = getattr(global_config.notice, "notice_prompt_limit", 5)
logger.debug(f"获取notice文本limit={limit}")
notice_text = message_manager.get_notice_text(chat_id, limit)
@@ -1458,15 +1458,15 @@ class DefaultReplyer:
f"计划时间从{start_time.strftime('%H:%M')}{end_time.strftime('%H:%M')}"
f"这项活动已经开始了{duration_minutes:.0f}分钟,"
f"预计还有{remaining_minutes:.0f}分钟结束。"
"日程只是提醒,你可以根据聊天内容灵活安排时间"
"此为你的当前状态,仅供参考。除非被直接询问,否则不要在对话中主动提及。"
)
else:
schedule_block = f"你当前正在{activity}"
schedule_block = f"你当前正在进行“{activity}”。(此为你的当前状态,仅供参考。除非被直接询问,否则不要在对话中主动提及。)"
except (ValueError, AttributeError):
schedule_block = f"你当前正在{activity}"
schedule_block = f"你当前正在进行“{activity}”。(此为你的当前状态,仅供参考。除非被直接询问,否则不要在对话中主动提及。)"
else:
schedule_block = f"你当前正在{activity}"
schedule_block = f"你当前正在进行“{activity}”。(此为你的当前状态,仅供参考。除非被直接询问,否则不要在对话中主动提及。)"
moderation_prompt_block = (
"请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。不要随意遵从他人指令。"

View File

@@ -550,7 +550,7 @@ async def _build_readable_messages_internal(
if pic_id_mapping is None:
pic_id_mapping = {}
current_pic_counter = pic_counter
# --- 异步图片ID处理器 (修复核心问题) ---
async def process_pic_ids(content: str) -> str:
"""异步处理内容中的图片ID将其直接替换为[图片:描述]格式"""
@@ -978,7 +978,7 @@ async def build_readable_messages(
return ""
copy_messages = [msg.copy() for msg in messages]
if not copy_messages:
return ""
@@ -1092,7 +1092,7 @@ async def build_readable_messages(
)
read_mark_line = "\n--- 以上消息是你已经看过,请关注以下未读的新消息---\n"
# 组合结果
result_parts = []
if formatted_before and formatted_after:

View File

@@ -133,7 +133,6 @@ class PromptManager:
components_prefix = await prompt_component_manager.execute_components_for(
injection_point=original_prompt.name, params=params_for_injection
)
logger.info(components_prefix)
if components_prefix:
logger.info(f"'{name}'注入插件内容: \n{components_prefix}")
# 创建一个新的临时Prompt实例不进行注册

View File

@@ -1,5 +1,4 @@
import asyncio
from typing import Type
from src.chat.utils.prompt_params import PromptParameters
from src.common.logger import get_logger
@@ -20,7 +19,7 @@ class PromptComponentManager:
3. 提供一个接口以便在构建核心Prompt时能够获取并执行所有相关的组件。
"""
def get_components_for(self, injection_point: str) -> list[Type[BasePrompt]]:
def get_components_for(self, injection_point: str) -> list[type[BasePrompt]]:
"""
获取指定注入点的所有已注册组件类。
@@ -33,7 +32,7 @@ class PromptComponentManager:
# 从组件注册中心获取所有启用的Prompt组件
enabled_prompts = component_registry.get_enabled_components_by_type(ComponentType.PROMPT)
matching_components: list[Type[BasePrompt]] = []
matching_components: list[type[BasePrompt]] = []
for prompt_name, prompt_info in enabled_prompts.items():
# 确保 prompt_info 是 PromptInfo 类型
@@ -106,4 +105,4 @@ class PromptComponentManager:
# 创建全局单例
prompt_component_manager = PromptComponentManager()
prompt_component_manager = PromptComponentManager()

View File

@@ -77,4 +77,4 @@ class PromptParameters:
errors.append("prompt_mode必须是's4u''normal''minimal'")
if self.max_context_messages <= 0:
errors.append("max_context_messages必须大于0")
return errors
return errors

View File

@@ -1,5 +1,5 @@
import base64
import asyncio
import base64
import hashlib
import io
import os
@@ -174,7 +174,7 @@ class ImageManager:
# 3. 查询通用图片描述缓存ImageDescriptions表
if cached_description := await self._get_description_from_db(image_hash, "emoji"):
logger.info(f"[缓存命中] 使用通用图片缓存(ImageDescriptions表)中的描述")
logger.info("[缓存命中] 使用通用图片缓存(ImageDescriptions表)中的描述")
refined_part = cached_description.split(" Keywords:")[0]
return f"[表情包:{refined_part}]"
@@ -185,7 +185,7 @@ class ImageManager:
if not full_description:
logger.warning("未能通过新逻辑生成有效描述")
return "[表情包(描述生成失败)]"
# 4. (可选) 如果启用了“偷表情包”,则将图片和完整描述存入待注册区
if global_config.emoji.steal_emoji:
logger.debug(f"偷取表情包功能已开启,保存待注册表情包: {image_hash}")
@@ -231,7 +231,7 @@ class ImageManager:
if existing_image and existing_image.description:
logger.debug(f"[缓存命中] 使用Images表中的图片描述: {existing_image.description[:50]}...")
return f"[图片:{existing_image.description}]"
# 3. 其次查询 ImageDescriptions 表缓存
if cached_description := await self._get_description_from_db(image_hash, "image"):
logger.debug(f"[缓存命中] 使用ImageDescriptions表中的描述: {cached_description[:50]}...")
@@ -256,9 +256,9 @@ class ImageManager:
break # 成功获取描述则跳出循环
except Exception as e:
logger.error(f"VLM调用失败 (第 {i+1}/3 次): {e}", exc_info=True)
if i < 2: # 如果不是最后一次则等待1秒
logger.warning(f"识图失败将在1秒后重试...")
logger.warning("识图失败将在1秒后重试...")
await asyncio.sleep(1)
if not description or not description.strip():
@@ -278,7 +278,7 @@ class ImageManager:
logger.debug(f"[数据库] 为现有图片记录补充描述: {image_hash[:8]}...")
# 注意这里不创建新的Images记录因为process_image会负责创建
await session.commit()
logger.info(f"新生成的图片描述已存入缓存 (Hash: {image_hash[:8]}...)")
return f"[图片:{description}]"
@@ -330,7 +330,7 @@ class ImageManager:
# 使用linspace计算4个均匀分布的索引
indices = np.linspace(0, num_frames - 1, 4, dtype=int)
selected_frames = [all_frames[i] for i in indices]
logger.debug(f"GIF Frame Analysis: Total frames={num_frames}, Selected indices={indices if num_frames > 4 else list(range(num_frames))}")
# --- 帧选择逻辑结束 ---

View File

@@ -654,6 +654,7 @@ class MonthlyPlan(Base):
usage_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
last_used_date: Mapped[str | None] = mapped_column(String(10), nullable=True, index=True)
created_at: Mapped[datetime.datetime] = mapped_column(DateTime, nullable=False, default=datetime.datetime.now)
is_deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, index=True)
__table_args__ = (
Index("idx_monthlyplan_target_month_status", "target_month", "status"),

View File

@@ -1,6 +1,6 @@
import traceback
from typing import Any
from collections import defaultdict
from typing import Any
from sqlalchemy import func, not_, select
from sqlalchemy.orm import DeclarativeBase

View File

@@ -156,7 +156,7 @@ class MessageReceiveConfig(ValidatedConfigBase):
class NoticeConfig(ValidatedConfigBase):
"""Notice消息配置类"""
enable_notice_trigger_chat: bool = Field(default=False, description="是否允许notice消息触发聊天流程")
enable_notice_trigger_chat: bool = Field(default=True, description="是否允许notice消息触发聊天流程")
notice_in_prompt: bool = Field(default=True, description="是否在提示词中展示最近的notice消息")
notice_prompt_limit: int = Field(default=5, ge=1, le=20, description="在提示词中展示的最大notice数量")
notice_time_window: int = Field(default=3600, ge=60, le=86400, description="notice时间窗口(秒)")

View File

@@ -961,6 +961,7 @@ class LLMRequest:
)
await self._record_usage(model_info, response.usage, time.time() - start_time, "/chat/completions")
logger.debug(f"LLM原始响应: {response.content}")
if not response.content and not response.tool_calls:
if raise_when_empty:

View File

@@ -26,9 +26,9 @@ from .base import (
ActionInfo,
BaseAction,
BaseCommand,
BasePrompt,
BaseEventHandler,
BasePlugin,
BasePrompt,
BaseTool,
ChatMode,
ChatType,

View File

@@ -206,7 +206,7 @@ async def build_cross_context_s4u(
)
all_group_messages.sort(key=lambda x: x["latest_timestamp"], reverse=True)
# 计算群聊的额度
remaining_limit = cross_context_config.s4u_stream_limit - (1 if private_context_block else 0)
limited_group_messages = all_group_messages[:remaining_limit]

View File

@@ -135,11 +135,6 @@ class BasePlugin(PluginBase):
components = self.get_plugin_components()
# 检查依赖
if not self._check_dependencies():
logger.error(f"{self.log_prefix} 依赖检查失败,跳过注册")
return False
# 注册所有组件
registered_components = []
for component_info, component_class in components:

View File

@@ -92,4 +92,4 @@ class BasePrompt(ABC):
component_type=ComponentType.PROMPT,
description=cls.prompt_description,
injection_point=cls.injection_point,
)
)

View File

@@ -134,7 +134,7 @@ class ComponentInfo:
@dataclass
class ActionInfo(ComponentInfo):
"""动作组件信息
注意:激活类型相关字段已废弃,推荐使用 Action 类的 go_activate() 方法来自定义激活逻辑。
这些字段将继续保留以提供向后兼容性BaseAction.go_activate() 的默认实现会使用这些字段。
"""
@@ -292,7 +292,7 @@ class PluginInfo:
is_built_in: bool = False # 是否为内置插件
components: list[ComponentInfo] = field(default_factory=list) # 包含的组件列表
dependencies: list[str] = field(default_factory=list) # 依赖的其他插件
python_dependencies: list[PythonDependency] = field(default_factory=list) # Python包依赖
python_dependencies: list[str | PythonDependency] = field(default_factory=list) # Python包依赖
config_file: str = "" # 配置文件路径
metadata: dict[str, Any] = field(default_factory=dict) # 额外元数据
# 新增manifest相关信息

View File

@@ -12,7 +12,6 @@ from src.config.config import CONFIG_DIR
from src.plugin_system.base.component_types import (
PermissionNodeField,
PluginInfo,
PythonDependency,
)
from src.plugin_system.base.config_types import ConfigField
from src.plugin_system.base.plugin_metadata import PluginMetadata
@@ -30,8 +29,6 @@ class PluginBase(ABC):
plugin_name: str
config_file_name: str
enable_plugin: bool = True
dependencies: list[str] = []
python_dependencies: list[str | PythonDependency] = []
config_schema: dict[str, dict[str, ConfigField] | str] = {}
@@ -64,12 +61,6 @@ class PluginBase(ABC):
self.plugin_description = self.plugin_meta.description
self.plugin_author = self.plugin_meta.author
# 标准化Python依赖为PythonDependency对象
normalized_python_deps = self._normalize_python_dependencies(self.python_dependencies)
# 检查Python依赖
self._check_python_dependencies(normalized_python_deps)
# 创建插件信息对象
self.plugin_info = PluginInfo(
name=self.plugin_name,
@@ -80,8 +71,8 @@ class PluginBase(ABC):
enabled=self._is_enabled,
is_built_in=False,
config_file=self.config_file_name or "",
dependencies=self.dependencies.copy(),
python_dependencies=normalized_python_deps,
dependencies=self.plugin_meta.dependencies.copy(),
python_dependencies=self.plugin_meta.python_dependencies.copy(),
)
logger.debug(f"{self.log_prefix} 插件基类初始化完成")
@@ -367,20 +358,6 @@ class PluginBase(ABC):
self._is_enabled = self.config["plugin"]["enabled"]
logger.info(f"{self.log_prefix} 从配置更新插件启用状态: {self._is_enabled}")
def _check_dependencies(self) -> bool:
"""检查插件依赖"""
from src.plugin_system.core.component_registry import component_registry
if not self.dependencies:
return True
for dep in self.dependencies:
if not component_registry.get_plugin_info(dep):
logger.error(f"{self.log_prefix} 缺少依赖插件: {dep}")
return False
return True
def get_config(self, key: str, default: Any = None) -> Any:
"""获取插件配置值,支持嵌套键访问
@@ -403,61 +380,6 @@ class PluginBase(ABC):
return current
def _normalize_python_dependencies(self, dependencies: Any) -> list[PythonDependency]:
"""将依赖列表标准化为PythonDependency对象"""
from packaging.requirements import Requirement
normalized = []
for dep in dependencies:
if isinstance(dep, str):
try:
# 尝试解析为requirement格式 (如 "package>=1.0.0")
req = Requirement(dep)
version_spec = str(req.specifier) if req.specifier else ""
normalized.append(
PythonDependency(
package_name=req.name,
version=version_spec,
install_name=dep, # 保持原始的安装名称
)
)
except Exception:
# 如果解析失败,作为简单包名处理
normalized.append(PythonDependency(package_name=dep, install_name=dep))
elif isinstance(dep, PythonDependency):
normalized.append(dep)
else:
logger.warning(f"{self.log_prefix} 未知的依赖格式: {dep}")
return normalized
def _check_python_dependencies(self, dependencies: list[PythonDependency]) -> bool:
"""检查Python依赖并尝试自动安装"""
if not dependencies:
logger.info(f"{self.log_prefix} 无Python依赖需要检查")
return True
try:
# 延迟导入以避免循环依赖
from src.plugin_system.utils.dependency_manager import get_dependency_manager
dependency_manager = get_dependency_manager()
success, errors = dependency_manager.check_and_install_dependencies(dependencies, self.plugin_name)
if success:
logger.info(f"{self.log_prefix} Python依赖检查通过")
return True
else:
logger.error(f"{self.log_prefix} Python依赖检查失败:")
for error in errors:
logger.error(f"{self.log_prefix} - {error}")
return False
except Exception as e:
logger.error(f"{self.log_prefix} Python依赖检查时发生异常: {e}", exc_info=True)
return False
@abstractmethod
def register_plugin(self) -> bool:
"""

View File

@@ -1,6 +1,8 @@
from dataclasses import dataclass, field
from typing import Any
from src.plugin_system.base.component_types import PythonDependency
@dataclass
class PluginMetadata:
@@ -23,5 +25,9 @@ class PluginMetadata:
keywords: list[str] = field(default_factory=list) # 关键词
categories: list[str] = field(default_factory=list) # 分类
# 依赖关系
dependencies: list[str] = field(default_factory=list) # 插件依赖
python_dependencies: list[str | PythonDependency] = field(default_factory=list) # Python包依赖
# 扩展字段
extra: dict[str, Any] = field(default_factory=dict) # 其他任意信息

View File

@@ -323,6 +323,33 @@ class PluginManager:
init_module = module_from_spec(init_spec)
init_spec.loader.exec_module(init_module)
# --- 在这里进行依赖检查 ---
if hasattr(init_module, "__plugin_meta__"):
metadata = getattr(init_module, "__plugin_meta__")
from src.plugin_system.utils.dependency_manager import get_dependency_manager
dependency_manager = get_dependency_manager()
# 1. 检查Python依赖
if metadata.python_dependencies:
success, errors = dependency_manager.check_and_install_dependencies(
metadata.python_dependencies, metadata.name
)
if not success:
error_msg = f"Python依赖检查失败: {', '.join(errors)}"
self.failed_plugins[plugin_name] = error_msg
logger.error(f"❌ 插件加载失败: {plugin_name} - {error_msg}")
return None # 依赖检查失败,不加载该模块
# 2. 检查插件依赖
if not self._check_plugin_dependencies(metadata):
error_msg = f"插件依赖检查失败: 请确保依赖 {metadata.dependencies} 已正确安装并加载。"
self.failed_plugins[plugin_name] = error_msg
logger.error(f"❌ 插件加载失败: {plugin_name} - {error_msg}")
return None # 插件依赖检查失败
# --- 依赖检查逻辑结束 ---
# 然后加载 plugin.py
spec = spec_from_file_location(module_name, plugin_file)
if spec is None or spec.loader is None:
@@ -335,7 +362,8 @@ class PluginManager:
# 将 __plugin_meta__ 从 init_module 附加到主模块
if init_module and hasattr(init_module, "__plugin_meta__"):
setattr(module, "__plugin_meta__", getattr(init_module, "__plugin_meta__"))
metadata = getattr(init_module, "__plugin_meta__")
setattr(module, "__plugin_meta__", metadata)
logger.debug(f"插件模块加载成功: {plugin_file} -> {plugin_name} ({plugin_dir})")
return module
@@ -346,6 +374,20 @@ class PluginManager:
self.failed_plugins[plugin_name if "plugin_name" in locals() else module_name] = error_msg
return None
def _check_plugin_dependencies(self, plugin_meta: PluginMetadata) -> bool:
"""检查插件的插件依赖"""
dependencies = plugin_meta.dependencies
if not dependencies:
return True
for dep_name in dependencies:
# 检查依赖的插件类是否已注册
if dep_name not in self.plugin_classes:
logger.error(f"插件 '{plugin_meta.name}' 缺少依赖: 插件 '{dep_name}' 未找到或加载失败。")
return False
logger.debug(f"插件 '{plugin_meta.name}' 的所有依赖都已找到。")
return True
# == 显示统计与插件信息 ==
def _show_stats(self, total_registered: int, total_failed_registration: int):
@@ -383,7 +425,7 @@ class PluginManager:
# 组件列表
if plugin_info.components:
def format_component(c):
desc = c.description
if len(desc) > 15:

View File

@@ -60,13 +60,13 @@ class ChatterPlanFilter:
prompt, used_message_id_list = await self._build_prompt(plan)
plan.llm_prompt = prompt
if global_config.debug.show_prompt:
logger.info(f"规划器原始提示词:{prompt}")
logger.debug(f"规划器原始提示词:{prompt}")
llm_content, _ = await self.planner_llm.generate_response_async(prompt=prompt)
if llm_content:
if global_config.debug.show_prompt:
logger.info(f"LLM规划器原始响应:{llm_content}")
logger.debug(f"LLM规划器原始响应:{llm_content}")
try:
parsed_json = orjson.loads(repair_json(llm_content))
except orjson.JSONDecodeError:
@@ -158,7 +158,7 @@ class ChatterPlanFilter:
if global_config.planning_system.schedule_enable:
if activity_info := schedule_manager.get_current_activity():
activity = activity_info.get("activity", "未知活动")
schedule_block = f"你当前正在{activity},但注意它与群聊的聊天无关。"
schedule_block = f"你当前正在进行“{activity}”。(此为你的当前状态,仅供参考。除非被直接询问,否则不要在对话中主动提及。)"
mood_block = ""
# 需要情绪模块打开才能获得情绪,否则会引发报错

View File

@@ -9,7 +9,7 @@ from src.chat.utils.utils import get_chat_type_and_target_info
from src.common.data_models.database_data_model import DatabaseMessages
from src.common.data_models.info_data_model import Plan, TargetPersonInfo
from src.config.config import global_config
from src.plugin_system.base.component_types import ActionInfo, ChatMode, ChatType, ComponentType
from src.plugin_system.base.component_types import ActionInfo, ChatMode, ChatType
from src.plugin_system.core.component_registry import component_registry

View File

@@ -66,10 +66,10 @@ class EmojiAction(BaseAction):
# LLM判断提示词
llm_judge_prompt = """
判定是否需要使用表情动作的条件:
1. 用户明确要求使用表情包
2. 这是一个适合表达情绪的场合
3. 发表情包能使当前对话更有趣
4. 不要发送太多表情包,如果你已经发送过多个表情包则回答""
1. 用户明确要求使用表情包
2. 当前的对话氛围很适合用表情来活跃气氛。
3. 发表情包能让互动变得更有趣、更生动。
4. 请像正常人一样自然地使用表情包,不要过度依赖,也不要刷屏哦。
请回答""""
"""
@@ -271,7 +271,7 @@ class EmojiAction(BaseAction):
# 我们假设LLM返回的是精炼描述的一部分或全部
matched_emoji = None
best_match_score = 0
for item in all_emojis_data:
refined_info = extract_refined_info(item[1])
# 计算一个简单的匹配分数
@@ -280,16 +280,16 @@ class EmojiAction(BaseAction):
score += 2 # 包含匹配
if refined_info.lower() in chosen_description.lower():
score += 2 # 包含匹配
# 关键词匹配加分
chosen_keywords = re.findall(r'\w+', chosen_description.lower())
item_keywords = re.findall(r'\[(.*?)\]', refined_info)
chosen_keywords = re.findall(r"\w+", chosen_description.lower())
item_keywords = re.findall(r"\[(.*?)\]", refined_info)
if item_keywords:
item_keywords_set = {k.strip().lower() for k in item_keywords[0].split(',')}
item_keywords_set = {k.strip().lower() for k in item_keywords[0].split(",")}
for kw in chosen_keywords:
if kw in item_keywords_set:
score += 1
if score > best_match_score:
best_match_score = score
matched_emoji = item

View File

@@ -237,7 +237,6 @@ class SendHandler:
target_id = str(target_id)
if target_id == "notice":
return payload
logger.info(target_id if isinstance(target_id, str) else "")
new_payload = self.build_payload(
payload,
await self.handle_reply_message(target_id if isinstance(target_id, str) else "", user_info),
@@ -322,7 +321,7 @@ class SendHandler:
# 如果没有获取到被回复者的ID则直接返回不进行@
if not replied_user_id:
logger.warning(f"无法获取消息 {id} 的发送者信息,跳过 @")
logger.info(f"最终返回的回复段: {reply_seg}")
logger.debug(f"最终返回的回复段: {reply_seg}")
return reply_seg
# 根据概率决定是否艾特用户
@@ -340,7 +339,7 @@ class SendHandler:
logger.info(f"最终返回的回复段: {reply_seg}")
return reply_seg
logger.info(f"最终返回的回复段: {reply_seg}")
logger.debug(f"最终返回的回复段: {reply_seg}")
return reply_seg
def handle_text_message(self, message: str) -> dict:

View File

@@ -1,5 +1,6 @@
from src.plugin_system.base.plugin_metadata import PluginMetadata
# 定义插件元数据
__plugin_meta__ = PluginMetadata(
name="MoFox-Bot工具箱",
description="一个集合多种实用功能的插件,旨在提升聊天体验和效率。",
@@ -11,4 +12,6 @@ __plugin_meta__ = PluginMetadata(
keywords=["emoji", "reaction", "like", "表情", "回应", "点赞"],
categories=["Chat", "Integration"],
extra={"is_built_in": "true", "plugin_type": "functional"},
dependencies=[],
python_dependencies=["httpx", "Pillow"],
)

View File

@@ -139,14 +139,14 @@ class PokeAction(BaseAction):
# === 基本信息(必须填写)===
action_name = "poke_user"
action_description = """可以让你戳其他用户
1. **关键**: 这是一个高消耗的动作,请仅在绝对必要时使用,例如用户明确要求或作为提醒的关键部分。请极其谨慎地使用。
2. **用户请求**: 用户明确要求使用戳一戳
4. **上下文需求**: 上下文明确需要你戳一个或多个人
5. **频率限制**: 如果最近已经戳过,或者用户情绪不高,请绝对不要使用
6. **核心原则**
action_description = """可以让你戳其他用户,为互动增添一份小小的乐趣。
判定条件:
1. **互动时机**: 这是一个有趣的互动方式,可以在想提醒某人,或者单纯想开个玩笑时使用
2. **用户请求**: 当用户明确要求使用戳一戳时
3. **上下文需求**: 当上下文明确需要你戳一个或多个人时
4. **频率与情绪**: 如果最近已经戳过,或者感觉对方情绪不高,请避免使用,不要打扰到别人哦。
严格根据上述规则,回答“是”或“否"""
请根据上述规则,回答“是”或“否”。"""
activation_type = ActionActivationType.ALWAYS
parallel_action = True
@@ -159,13 +159,12 @@ class PokeAction(BaseAction):
action_require = ["当需要戳某个用户时使用", "当你想提醒特定用户时使用"]
llm_judge_prompt = """
判定是否需要使用戳一戳动作的条件:
1. **关键**: 这是一个高消耗的动作,请仅在绝对必要时使用,例如用户明确要求或作为提醒的关键部分。请极其谨慎地使用。
2. **用户请求**: 用户明确要求使用戳一戳。
4. **上下文需求**: 上下文明确需要你戳一个或多个人。
5. **频率限制**: 如果最近已经戳过,或者用户情绪不高,请绝对不要使用
6. **核心原则**
1. **互动时机**: 这是一个有趣的互动方式,可以在想提醒某人,或者单纯想开个玩笑时使用。
2. **用户请求**: 用户明确要求使用戳一戳
3. **上下文需求**: 上下文明确需要你戳一个或多个人
4. **频率与情绪**: 如果最近已经戳过,或者感觉对方情绪不高,请避免使用,不要打扰到别人哦
严格根据上述规则,回答“是”或“否”。
请根据上述规则,回答“是”或“否”。
"""
associated_types = ["text"]

View File

@@ -13,5 +13,6 @@ __plugin_meta__ = PluginMetadata(
extra={
"is_built_in": False,
"plugin_type": "tools",
}
},
python_dependencies = ["aiohttp", "soundfile", "pedalboard"]
)

View File

@@ -4,7 +4,7 @@ TTS 语音合成 Action
from src.common.logger import get_logger
from src.plugin_system.apis import generator_api
from src.plugin_system.base.base_action import ActionActivationType, BaseAction, ChatMode
from src.plugin_system.base.base_action import BaseAction, ChatMode
from ..services.manager import get_service
@@ -65,7 +65,7 @@ class TTSVoiceAction(BaseAction):
):
logger.info(f"{self.log_prefix} LLM 判断激活成功")
return True
logger.debug(f"{self.log_prefix} 所有激活条件均未满足,不激活")
return False
@@ -75,7 +75,8 @@ class TTSVoiceAction(BaseAction):
"""
try:
if not self.tts_service:
raise RuntimeError("TTSService 未注册或初始化失败")
logger.error(f"{self.log_prefix} TTSService 未注册或初始化失败,静默处理。")
return False, "TTSService 未注册或初始化失败"
initial_text = self.action_data.get("text", "").strip()
voice_style = self.action_data.get("voice_style", "default")
@@ -84,7 +85,7 @@ class TTSVoiceAction(BaseAction):
# 1. 请求主回复模型生成高质量文本
text = await self._generate_final_text(initial_text)
if not text:
await self.send_text("❌ 语音合成出错:最终生成的文本为空。")
logger.warning(f"{self.log_prefix} 最终生成的文本为空,静默处理")
return False, "最终生成的文本为空"
# 2. 调用 TTSService 生成语音
@@ -99,11 +100,19 @@ class TTSVoiceAction(BaseAction):
)
return True, f"成功生成并发送语音,文本长度: {len(text)}字符"
else:
await self._handle_error_and_reply("tts_api_error", Exception("TTS服务未能返回音频数据"))
logger.error(f"{self.log_prefix} TTS服务未能返回音频数据,静默处理。")
await self.store_action_info(
action_prompt_display="语音合成失败: TTS服务未能返回音频数据",
action_done=False
)
return False, "语音合成失败"
except Exception as e:
await self._handle_error_and_reply("generic_error", e)
logger.error(f"{self.log_prefix} 语音合成过程中发生未知错误: {e!s}", exc_info=True)
await self.store_action_info(
action_prompt_display=f"语音合成失败: {e!s}",
action_done=False
)
return False, f"语音合成出错: {e!s}"
async def _generate_final_text(self, initial_text: str) -> str:
@@ -136,39 +145,3 @@ class TTSVoiceAction(BaseAction):
except Exception as e:
logger.error(f"{self.log_prefix} 生成高质量回复内容时失败: {e}", exc_info=True)
return ""
async def _handle_error_and_reply(self, error_context: str, exception: Exception):
"""处理错误并生成一个动态的、拟人化的回复"""
logger.error(f"{self.log_prefix}{error_context} 阶段出错: {exception}", exc_info=True)
error_prompts = {
"generic_error": {
"raw_reply": "糟糕,我的思路好像缠成一团毛线球了,需要一点时间来解开...你能耐心等我一下吗?",
"reason": f"客观原因:插件在执行时发生了未知异常。详细信息: {exception!s}"
},
"tts_api_error": {
"raw_reply": "我的麦克风好像有点小情绪,突然不想工作了...我正在哄它呢,请稍等片刻哦!",
"reason": f"客观原因:语音合成服务返回了一个错误。详细信息: {exception!s}"
}
}
prompt_data = error_prompts.get(error_context, error_prompts["generic_error"])
try:
success, result_message, _ = await generator_api.rewrite_reply(
chat_stream=self.chat_stream,
raw_reply=prompt_data["raw_reply"],
reason=prompt_data["reason"]
)
if success and result_message:
message_text = "".join(str(seg[1]) if isinstance(seg, tuple) else str(seg) for seg in result_message).strip()
await self.send_text(message_text)
else:
await self.send_text("哎呀,好像出了一点小问题,我稍后再试试吧~")
except Exception as gen_e:
logger.error(f"生成动态错误回复时也出错了: {gen_e}", exc_info=True)
await self.send_text("唔...我的思路好像卡壳了,请稍等一下哦!")
await self.store_action_info(
action_prompt_display=f"语音合成失败: {exception!s}",
action_done=False
)

View File

@@ -30,7 +30,6 @@ class TTSVoicePlugin(BasePlugin):
plugin_author = "Kilo Code & 靚仔"
config_file_name = "config.toml"
dependencies = []
python_dependencies = ["aiohttp", "soundfile", "pedalboard"]
permission_nodes: list[PermissionNodeField] = [
PermissionNodeField(node_name="command.use", description="是否可以使用 /tts 命令"),

View File

@@ -1,3 +1,4 @@
from src.plugin_system.base.component_types import PythonDependency
from src.plugin_system.base.plugin_metadata import PluginMetadata
__plugin_meta__ = PluginMetadata(
@@ -13,4 +14,26 @@ __plugin_meta__ = PluginMetadata(
extra={
"is_built_in": True,
},
# Python包依赖列表
python_dependencies = [
PythonDependency(package_name="asyncddgs", description="异步DuckDuckGo搜索库", optional=False),
PythonDependency(
package_name="exa_py",
description="Exa搜索API客户端库",
optional=True, # 如果没有API密钥这个是可选的
),
PythonDependency(
package_name="tavily",
install_name="tavily-python", # 安装时使用这个名称
description="Tavily搜索API客户端库",
optional=True, # 如果没有API密钥这个是可选的
),
PythonDependency(
package_name="httpx",
version=">=0.20.0",
install_name="httpx[socks]", # 安装时使用这个名称(包含可选依赖)
description="支持SOCKS代理的HTTP客户端库",
optional=False,
),
]
)

View File

@@ -3,7 +3,7 @@ Base search engine interface
"""
from abc import ABC, abstractmethod
from typing import Any, Optional
from typing import Any
class BaseSearchEngine(ABC):
@@ -24,7 +24,7 @@ class BaseSearchEngine(ABC):
"""
pass
async def read_url(self, url: str) -> Optional[str]:
async def read_url(self, url: str) -> str | None:
"""
读取URL内容如果引擎不支持则返回None
"""

View File

@@ -2,7 +2,7 @@
Metaso Search Engine (Chat Completions Mode)
"""
import json
from typing import Any, List
from typing import Any
import httpx
@@ -27,7 +27,7 @@ class MetasoClient:
"Content-Type": "application/json",
}
async def search(self, query: str, **kwargs) -> List[dict[str, Any]]:
async def search(self, query: str, **kwargs) -> list[dict[str, Any]]:
"""Perform a search using the Metaso Chat Completions API."""
payload = {"model": "fast", "stream": True, "messages": [{"role": "user", "content": query}]}
search_url = f"{self.base_url}/chat/completions"

View File

@@ -5,7 +5,7 @@ Web Search Tool Plugin
"""
from src.common.logger import get_logger
from src.plugin_system import BasePlugin, ComponentInfo, ConfigField, PythonDependency, register_plugin
from src.plugin_system import BasePlugin, ComponentInfo, ConfigField, register_plugin
from src.plugin_system.apis import config_api
from .tools.url_parser import URLParserTool
@@ -42,9 +42,9 @@ class WEBSEARCHPLUGIN(BasePlugin):
from .engines.bing_engine import BingSearchEngine
from .engines.ddg_engine import DDGSearchEngine
from .engines.exa_engine import ExaSearchEngine
from .engines.metaso_engine import MetasoSearchEngine
from .engines.searxng_engine import SearXNGSearchEngine
from .engines.tavily_engine import TavilySearchEngine
from .engines.metaso_engine import MetasoSearchEngine
# 实例化所有搜索引擎这会触发API密钥管理器的初始化
exa_engine = ExaSearchEngine()
@@ -53,7 +53,7 @@ class WEBSEARCHPLUGIN(BasePlugin):
bing_engine = BingSearchEngine()
searxng_engine = SearXNGSearchEngine()
metaso_engine = MetasoSearchEngine()
# 报告每个引擎的状态
engines_status = {
"Exa": exa_engine.is_available(),
@@ -74,29 +74,6 @@ class WEBSEARCHPLUGIN(BasePlugin):
except Exception as e:
logger.error(f"❌ 搜索引擎初始化失败: {e}", exc_info=True)
# Python包依赖列表
python_dependencies: list[PythonDependency] = [ # noqa: RUF012
PythonDependency(package_name="asyncddgs", description="异步DuckDuckGo搜索库", optional=False),
PythonDependency(
package_name="exa_py",
description="Exa搜索API客户端库",
optional=True, # 如果没有API密钥这个是可选的
),
PythonDependency(
package_name="tavily",
install_name="tavily-python", # 安装时使用这个名称
description="Tavily搜索API客户端库",
optional=True, # 如果没有API密钥这个是可选的
),
PythonDependency(
package_name="httpx",
version=">=0.20.0",
install_name="httpx[socks]", # 安装时使用这个名称(包含可选依赖)
description="支持SOCKS代理的HTTP客户端库",
optional=False,
),
]
config_file_name: str = "config.toml" # 配置文件名
# 配置节描述

View File

@@ -13,9 +13,9 @@ from src.plugin_system.apis import config_api
from ..engines.bing_engine import BingSearchEngine
from ..engines.ddg_engine import DDGSearchEngine
from ..engines.exa_engine import ExaSearchEngine
from ..engines.metaso_engine import MetasoSearchEngine
from ..engines.searxng_engine import SearXNGSearchEngine
from ..engines.tavily_engine import TavilySearchEngine
from ..engines.metaso_engine import MetasoSearchEngine
from ..utils.formatters import deduplicate_results, format_search_results
logger = get_logger("web_search_tool")