From 8e34ab885a33d304beb92dcf94ed2eae7f35b133 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Mon, 14 Jul 2025 23:44:01 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E4=B8=BAs4u=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=B8=80=E4=B8=AA=E9=80=8F=E6=98=8E=E5=BA=95=E7=9A=84=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E8=AE=B0=E5=BD=95=E7=BD=91=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 1 + src/mais4u/mais4u_chat/context_web_manager.py | 544 +++++++++++++++++ src/mais4u/mais4u_chat/s4u_chat.py | 4 +- src/mais4u/mais4u_chat/s4u_mood_manager.py | 573 +++--------------- src/mais4u/mais4u_chat/s4u_msg_processor.py | 29 + .../mais4u_chat/s4u_watching_manager.py | 3 +- template/bot_config_template.toml | 4 +- 7 files changed, 651 insertions(+), 507 deletions(-) create mode 100644 src/mais4u/mais4u_chat/context_web_manager.py diff --git a/requirements.txt b/requirements.txt index 32403c966..a09637a91 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ APScheduler Pillow aiohttp +aiohttp-cors colorama customtkinter dotenv diff --git a/src/mais4u/mais4u_chat/context_web_manager.py b/src/mais4u/mais4u_chat/context_web_manager.py new file mode 100644 index 000000000..844782092 --- /dev/null +++ b/src/mais4u/mais4u_chat/context_web_manager.py @@ -0,0 +1,544 @@ +import asyncio +import json +from collections import deque +from datetime import datetime +from typing import Dict, List, Optional +from aiohttp import web, WSMsgType +import aiohttp_cors +from threading import Thread +import weakref + +from src.chat.message_receive.message import MessageRecv +from src.common.logger import get_logger + +logger = get_logger("context_web") + + +class ContextMessage: + """上下文消息类""" + + def __init__(self, message: MessageRecv): + self.user_name = message.message_info.user_info.user_nickname + self.user_id = message.message_info.user_info.user_id + self.content = message.processed_plain_text + self.timestamp = datetime.now() + self.group_name = message.message_info.group_info.group_name if message.message_info.group_info else "私聊" + + def to_dict(self): + return { + "user_name": self.user_name, + "user_id": self.user_id, + "content": self.content, + "timestamp": self.timestamp.strftime("%m-%d %H:%M:%S"), + "group_name": self.group_name + } + + +class ContextWebManager: + """上下文网页管理器""" + + def __init__(self, max_messages: int = 10, port: int = 8765): + self.max_messages = max_messages + self.port = port + self.contexts: Dict[str, deque] = {} # chat_id -> deque of ContextMessage + self.websockets: List[web.WebSocketResponse] = [] + self.app = None + self.runner = None + self.site = None + self._server_starting = False # 添加启动标志防止并发 + + async def start_server(self): + """启动web服务器""" + if self.site is not None: + logger.debug("Web服务器已经启动,跳过重复启动") + return + + if self._server_starting: + logger.debug("Web服务器正在启动中,等待启动完成...") + # 等待启动完成 + while self._server_starting and self.site is None: + await asyncio.sleep(0.1) + return + + self._server_starting = True + + try: + self.app = web.Application() + + # 设置CORS + cors = aiohttp_cors.setup(self.app, defaults={ + "*": aiohttp_cors.ResourceOptions( + allow_credentials=True, + expose_headers="*", + allow_headers="*", + allow_methods="*" + ) + }) + + # 添加路由 + self.app.router.add_get('/', self.index_handler) + self.app.router.add_get('/ws', self.websocket_handler) + self.app.router.add_get('/api/contexts', self.get_contexts_handler) + self.app.router.add_get('/debug', self.debug_handler) + + # 为所有路由添加CORS + for route in list(self.app.router.routes()): + cors.add(route) + + self.runner = web.AppRunner(self.app) + await self.runner.setup() + + self.site = web.TCPSite(self.runner, 'localhost', self.port) + await self.site.start() + + logger.info(f"🌐 上下文网页服务器启动成功在 http://localhost:{self.port}") + + except Exception as e: + logger.error(f"❌ 启动Web服务器失败: {e}") + # 清理部分启动的资源 + if self.runner: + await self.runner.cleanup() + self.app = None + self.runner = None + self.site = None + raise + finally: + self._server_starting = False + + async def stop_server(self): + """停止web服务器""" + if self.site: + await self.site.stop() + if self.runner: + await self.runner.cleanup() + self.app = None + self.runner = None + self.site = None + self._server_starting = False + + async def index_handler(self, request): + """主页处理器""" + html_content = ''' + + + + + 聊天上下文 + + + +
+
正在连接...
+ 🔧 调试 +
+
暂无消息
+
+
+ + + + + ''' + return web.Response(text=html_content, content_type='text/html') + + async def websocket_handler(self, request): + """WebSocket处理器""" + ws = web.WebSocketResponse() + await ws.prepare(request) + + self.websockets.append(ws) + logger.debug(f"WebSocket连接建立,当前连接数: {len(self.websockets)}") + + # 发送初始数据 + await self.send_contexts_to_websocket(ws) + + async for msg in ws: + if msg.type == WSMsgType.ERROR: + logger.error(f'WebSocket错误: {ws.exception()}') + break + + # 清理断开的连接 + if ws in self.websockets: + self.websockets.remove(ws) + logger.debug(f"WebSocket连接断开,当前连接数: {len(self.websockets)}") + + return ws + + async def get_contexts_handler(self, request): + """获取上下文API""" + all_context_msgs = [] + for chat_id, contexts in self.contexts.items(): + all_context_msgs.extend(list(contexts)) + + # 按时间排序,最新的在最后 + all_context_msgs.sort(key=lambda x: x.timestamp) + + # 转换为字典格式 + contexts_data = [msg.to_dict() for msg in all_context_msgs[-self.max_messages:]] + + logger.debug(f"返回上下文数据,共 {len(contexts_data)} 条消息") + return web.json_response({"contexts": contexts_data}) + + async def debug_handler(self, request): + """调试信息处理器""" + debug_info = { + "server_status": "running", + "websocket_connections": len(self.websockets), + "total_chats": len(self.contexts), + "total_messages": sum(len(contexts) for contexts in self.contexts.values()), + } + + # 构建聊天详情HTML + chats_html = "" + for chat_id, contexts in self.contexts.items(): + messages_html = "" + for msg in contexts: + timestamp = msg.timestamp.strftime("%H:%M:%S") + content = msg.content[:50] + "..." if len(msg.content) > 50 else msg.content + messages_html += f'
[{timestamp}] {msg.user_name}: {content}
' + + chats_html += f''' +
+

聊天 {chat_id} ({len(contexts)} 条消息)

+ {messages_html} +
+ ''' + + html_content = f''' + + + + + 调试信息 + + + +

上下文网页管理器调试信息

+ +
+

服务器状态

+

状态: {debug_info["server_status"]}

+

WebSocket连接数: {debug_info["websocket_connections"]}

+

聊天总数: {debug_info["total_chats"]}

+

消息总数: {debug_info["total_messages"]}

+
+ +
+

聊天详情

+ {chats_html} +
+ +
+

操作

+ + + +
+ + + + + ''' + + return web.Response(text=html_content, content_type='text/html') + + async def add_message(self, chat_id: str, message: MessageRecv): + """添加新消息到上下文""" + if chat_id not in self.contexts: + self.contexts[chat_id] = deque(maxlen=self.max_messages) + logger.debug(f"为聊天 {chat_id} 创建新的上下文队列") + + context_msg = ContextMessage(message) + self.contexts[chat_id].append(context_msg) + + # 统计当前总消息数 + total_messages = sum(len(contexts) for contexts in self.contexts.values()) + + logger.info(f"✅ 添加消息到上下文 [总数: {total_messages}]: [{context_msg.group_name}] {context_msg.user_name}: {context_msg.content}") + + # 调试:打印当前所有消息 + logger.info(f"📝 当前上下文中的所有消息:") + for cid, contexts in self.contexts.items(): + logger.info(f" 聊天 {cid}: {len(contexts)} 条消息") + for i, msg in enumerate(contexts): + logger.info(f" {i+1}. [{msg.timestamp.strftime('%H:%M:%S')}] {msg.user_name}: {msg.content[:30]}...") + + # 广播更新给所有WebSocket连接 + await self.broadcast_contexts() + + async def send_contexts_to_websocket(self, ws: web.WebSocketResponse): + """向单个WebSocket发送上下文数据""" + all_context_msgs = [] + for chat_id, contexts in self.contexts.items(): + all_context_msgs.extend(list(contexts)) + + # 按时间排序,最新的在最后 + all_context_msgs.sort(key=lambda x: x.timestamp) + + # 转换为字典格式 + contexts_data = [msg.to_dict() for msg in all_context_msgs[-self.max_messages:]] + + data = {"contexts": contexts_data} + await ws.send_str(json.dumps(data, ensure_ascii=False)) + + async def broadcast_contexts(self): + """向所有WebSocket连接广播上下文更新""" + if not self.websockets: + logger.debug("没有WebSocket连接,跳过广播") + return + + all_context_msgs = [] + for chat_id, contexts in self.contexts.items(): + all_context_msgs.extend(list(contexts)) + + # 按时间排序,最新的在最后 + all_context_msgs.sort(key=lambda x: x.timestamp) + + # 转换为字典格式 + contexts_data = [msg.to_dict() for msg in all_context_msgs[-self.max_messages:]] + + data = {"contexts": contexts_data} + message = json.dumps(data, ensure_ascii=False) + + logger.info(f"广播 {len(contexts_data)} 条消息到 {len(self.websockets)} 个WebSocket连接") + + # 创建WebSocket列表的副本,避免在遍历时修改 + websockets_copy = self.websockets.copy() + removed_count = 0 + + for ws in websockets_copy: + if ws.closed: + if ws in self.websockets: + self.websockets.remove(ws) + removed_count += 1 + else: + try: + await ws.send_str(message) + logger.debug("消息发送成功") + except Exception as e: + logger.error(f"发送WebSocket消息失败: {e}") + if ws in self.websockets: + self.websockets.remove(ws) + removed_count += 1 + + if removed_count > 0: + logger.debug(f"清理了 {removed_count} 个断开的WebSocket连接") + + +# 全局实例 +_context_web_manager: Optional[ContextWebManager] = None + + +def get_context_web_manager() -> ContextWebManager: + """获取上下文网页管理器实例""" + global _context_web_manager + if _context_web_manager is None: + _context_web_manager = ContextWebManager() + return _context_web_manager + + +async def init_context_web_manager(): + """初始化上下文网页管理器""" + manager = get_context_web_manager() + await manager.start_server() + return manager \ No newline at end of file diff --git a/src/mais4u/mais4u_chat/s4u_chat.py b/src/mais4u/mais4u_chat/s4u_chat.py index 8f4d771c6..a1e2efb9e 100644 --- a/src/mais4u/mais4u_chat/s4u_chat.py +++ b/src/mais4u/mais4u_chat/s4u_chat.py @@ -142,7 +142,7 @@ def get_s4u_chat_manager() -> S4UChatManager: class S4UChat: - _MESSAGE_TIMEOUT_SECONDS = 60 # 普通消息存活时间(秒) + _MESSAGE_TIMEOUT_SECONDS = 30 # 普通消息存活时间(秒) def __init__(self, chat_stream: ChatStream): """初始化 S4UChat 实例。""" @@ -167,7 +167,7 @@ class S4UChat: self.gpt = S4UStreamGenerator() self.interest_dict: Dict[str, float] = {} # 用户兴趣分 self.at_bot_priority_bonus = 100.0 # @机器人的优先级加成 - self.normal_queue_max_size = 50 # 普通队列最大容量 + self.normal_queue_max_size = 5 # 普通队列最大容量 logger.info(f"[{self.stream_name}] S4UChat with two-queue system initialized.") def _get_priority_info(self, message: MessageRecv) -> dict: diff --git a/src/mais4u/mais4u_chat/s4u_mood_manager.py b/src/mais4u/mais4u_chat/s4u_mood_manager.py index 22b1400cb..a394e9429 100644 --- a/src/mais4u/mais4u_chat/s4u_mood_manager.py +++ b/src/mais4u/mais4u_chat/s4u_mood_manager.py @@ -1,7 +1,6 @@ import asyncio import json import time -import random from src.chat.message_receive.message import MessageRecv from src.llm_models.utils_model import LLMRequest @@ -13,54 +12,23 @@ from src.manager.async_task_manager import AsyncTask, async_task_manager from src.plugin_system.apis import send_api """ -面部表情系统使用说明: +情绪管理系统使用说明: -1. 预定义的面部表情: - - happy: 高兴表情(眼睛微笑 + 眉毛微笑 + 嘴巴大笑) - - very_happy: 非常高兴(高兴表情 + 脸红) - - sad: 悲伤表情(眼睛哭泣 + 眉毛忧伤 + 嘴巴悲伤) - - angry: 生气表情(眉毛生气 + 嘴巴生气) - - fear: 恐惧表情(眼睛闭上) - - shy: 害羞表情(嘴巴嘟起 + 脸红) - - neutral: 中性表情(无表情) +1. 情绪数值系统: + - 情绪包含四个维度:joy(喜), anger(怒), sorrow(哀), fear(惧) + - 每个维度的取值范围为1-10 + - 当情绪发生变化时,会自动发送到ws端处理 -2. 使用方法: - # 获取面部表情管理器 - facial_expression = mood_manager.get_facial_expression_by_chat_id(chat_id) - - # 发送指定表情 - await facial_expression.send_expression("happy") - - # 根据情绪值自动选择表情 - await facial_expression.send_expression_by_mood(mood_values) - - # 重置为中性表情 - await facial_expression.reset_expression() - - # 执行眨眼动作 - await facial_expression.perform_blink() +2. 情绪更新机制: + - 接收到新消息时会更新情绪状态 + - 定期进行情绪回归(冷静下来) + - 每次情绪变化都会发送到ws端,格式为: + type: "emotion" + data: {"joy": 5, "anger": 1, "sorrow": 1, "fear": 1} -3. 自动表情系统: - - 当情绪值更新时,系统会自动根据mood_values选择合适的面部表情 - - 只有当新表情与当前表情不同时才会发送,避免重复发送 - - 支持joy >= 8时显示very_happy,joy >= 6时显示happy等梯度表情 - -4. amadus表情更新系统: - - 每1秒检查一次表情是否有变化,如有变化则发送到amadus - - 每次mood更新后立即发送表情更新 - - 发送消息类型为"amadus_expression_update",格式为{"action": "表情名", "data": 1.0} - -5. 眨眼系统: - - 每4-6秒随机执行一次眨眼动作 - - 眨眼包含两个阶段:先闭眼(eye_close=1.0),保持0.1-0.15秒,然后睁眼(eye_close=0.0) - - 眨眼使用override_values参数临时覆盖eye_close值,不修改原始表情状态 - - 眨眼时会发送完整的表情状态,包含当前表情的所有动作 - - 当eye部位已经是eye_happy_weak时,跳过眨眼动作 - -6. 表情选择逻辑: - - 系统会找出最强的情绪(joy, anger, sorrow, fear) - - 根据情绪强度选择相应的表情组合 - - 默认情况下返回neutral表情 +3. ws端处理: + - 本地只负责情绪计算和发送情绪数值 + - 表情渲染和动作由ws端根据情绪数值处理 """ logger = get_logger("mood") @@ -137,300 +105,6 @@ def init_prompt(): ) -class FacialExpression: - def __init__(self, chat_id: str): - self.chat_id: str = chat_id - - # 预定义面部表情动作(根据用户定义的表情动作) - self.expressions = { - # 眼睛表情 - "eye_happy_weak": {"action": "eye_happy_weak", "data": 1.0}, - "eye_close": {"action": "eye_close", "data": 1.0}, - "eye_shift_left": {"action": "eye_shift_left", "data": 1.0}, - "eye_shift_right": {"action": "eye_shift_right", "data": 1.0}, - # "eye_smile": {"action": "eye_smile", "data": 1.0}, # 未定义,占位 - # "eye_cry": {"action": "eye_cry", "data": 1.0}, # 未定义,占位 - # "eye_normal": {"action": "eye_normal", "data": 1.0}, # 未定义,占位 - - # 眉毛表情 - "eyebrow_happy_weak": {"action": "eyebrow_happy_weak", "data": 1.0}, - "eyebrow_happy_strong": {"action": "eyebrow_happy_strong", "data": 1.0}, - "eyebrow_angry_weak": {"action": "eyebrow_angry_weak", "data": 1.0}, - "eyebrow_angry_strong": {"action": "eyebrow_angry_strong", "data": 1.0}, - "eyebrow_sad_weak": {"action": "eyebrow_sad_weak", "data": 1.0}, - "eyebrow_sad_strong": {"action": "eyebrow_sad_strong", "data": 1.0}, - # "eyebrow_smile": {"action": "eyebrow_smile", "data": 1.0}, # 未定义,占位 - # "eyebrow_angry": {"action": "eyebrow_angry", "data": 1.0}, # 未定义,占位 - # "eyebrow_sad": {"action": "eyebrow_sad", "data": 1.0}, # 未定义,占位 - # "eyebrow_normal": {"action": "eyebrow_normal", "data": 1.0}, # 未定义,占位 - - # 嘴巴表情(注意:用户定义的是mouth,可能是mouth的拼写错误) - "mouth_default": {"action": "mouth_default", "data": 1.0}, - "mouth_happy_strong": {"action": "mouth_happy_strong", "data": 1.0}, # 保持用户原始拼写 - "mouth_angry_weak": {"action": "mouth_angry_weak", "data": 1.0}, - # "mouth_sad": {"action": "mouth_sad", "data": 1.0}, # 未定义,占位 - # "mouth_angry": {"action": "mouth_angry", "data": 1.0}, # 未定义,占位 - # "mouth_laugh": {"action": "mouth_laugh", "data": 1.0}, # 未定义,占位 - # "mouth_pout": {"action": "mouth_pout", "data": 1.0}, # 未定义,占位 - # "mouth_normal": {"action": "mouth_normal", "data": 1.0}, # 未定义,占位 - - # 脸部表情 - # "face_blush": {"action": "face_blush", "data": 1.0}, # 未定义,占位 - # "face_normal": {"action": "face_normal", "data": 1.0}, # 未定义,占位 - } - - # 表情组合模板(根据新的表情动作调整) - self.expression_combinations = { - "happy": { - "eye": "eye_happy_weak", - "eyebrow": "eyebrow_happy_weak", - "mouth": "mouth_default", - }, - "very_happy": { - "eye": "eye_happy_weak", - "eyebrow": "eyebrow_happy_strong", - "mouth": "mouth_happy_strong", - }, - "sad": { - "eyebrow": "eyebrow_sad_strong", - "mouth": "mouth_default", - }, - "angry": { - "eyebrow": "eyebrow_angry_strong", - "mouth": "mouth_angry_weak", - }, - "fear": { - "eyebrow": "eyebrow_sad_weak", - "mouth": "mouth_default", - }, - "shy": { - "eyebrow": "eyebrow_happy_weak", - "mouth": "mouth_default", - }, - "neutral": { - "eyebrow": "eyebrow_happy_weak", - "mouth": "mouth_default", - } - } - - # 未定义的表情部位(保留备用): - # 眼睛:eye_smile, eye_cry, eye_close, eye_normal - # 眉毛:eyebrow_smile, eyebrow_angry, eyebrow_sad, eyebrow_normal - # 嘴巴:mouth_sad, mouth_angry, mouth_laugh, mouth_pout, mouth_normal - # 脸部:face_blush, face_normal - - # 初始化当前表情状态 - self.last_expression = "neutral" - - def select_expression_by_mood(self, mood_values: dict[str, int]) -> str: - """根据情绪值选择合适的表情组合""" - joy = mood_values.get("joy", 5) - anger = mood_values.get("anger", 1) - sorrow = mood_values.get("sorrow", 1) - fear = mood_values.get("fear", 1) - - # 找出最强的情绪 - emotions = { - "joy": joy, - "anger": anger, - "sorrow": sorrow, - "fear": fear - } - - # 获取最强情绪 - dominant_emotion = max(emotions, key=emotions.get) - _dominant_value = emotions[dominant_emotion] - - # 根据情绪强度和类型选择表情 - if dominant_emotion == "joy": - if joy >= 8: - return "very_happy" - elif joy >= 6: - return "happy" - elif joy >= 4: - return "shy" - else: - return "neutral" - elif dominant_emotion == "anger" and anger >= 6: - return "angry" - elif dominant_emotion == "sorrow" and sorrow >= 6: - return "sad" - elif dominant_emotion == "fear" and fear >= 6: - return "fear" - else: - return "neutral" - - async def _send_expression_actions(self, expression_name: str, log_prefix: str = "发送面部表情", override_values: dict = None): - """统一的表情动作发送函数 - 发送完整的表情状态 - - Args: - expression_name: 表情名称 - log_prefix: 日志前缀 - override_values: 需要覆盖的动作值,格式为 {"action_name": value} - """ - if expression_name not in self.expression_combinations: - logger.warning(f"[{self.chat_id}] 未知表情: {expression_name}") - return - - combination = self.expression_combinations[expression_name] - - # 按部位分组所有已定义的表情动作 - expressions_by_part = { - "eye": {}, - "eyebrow": {}, - "mouth": {} - } - - # 将所有已定义的表情按部位分组 - for expression_key, expression_data in self.expressions.items(): - if expression_key.startswith("eye_"): - expressions_by_part["eye"][expression_key] = expression_data - elif expression_key.startswith("eyebrow_"): - expressions_by_part["eyebrow"][expression_key] = expression_data - elif expression_key.startswith("mouth_"): - expressions_by_part["mouth"][expression_key] = expression_data - - # 构建完整的表情状态 - complete_expression_state = {} - - # 为每个部位构建完整的表情动作状态 - for part in expressions_by_part.keys(): - if expressions_by_part[part]: # 如果该部位有已定义的表情 - part_actions = {} - active_expression = combination.get(part) # 当前激活的表情 - - # 添加该部位所有已定义的表情动作 - for expression_key, expression_data in expressions_by_part[part].items(): - # 复制表情数据并设置激活状态 - action_data = expression_data.copy() - - # 检查是否有覆盖值 - if override_values and expression_key in override_values: - action_data["data"] = override_values[expression_key] - else: - action_data["data"] = 1.0 if expression_key == active_expression else 0.0 - - part_actions[expression_key] = action_data - - complete_expression_state[part] = part_actions - logger.debug(f"[{self.chat_id}] 部位 {part}: 激活 {active_expression}, 总共 {len(part_actions)} 个动作") - - # 发送完整的表情状态 - if complete_expression_state: - package_data = { - "expression_name": expression_name, - "actions": complete_expression_state - } - - await send_api.custom_to_stream( - message_type="face_emotion", - content=package_data, - stream_id=self.chat_id, - storage_message=False, - show_log=False, - ) - - # 统计信息 - total_actions = sum(len(part_actions) for part_actions in complete_expression_state.values()) - active_actions = [f"{part}:{combination.get(part, 'none')}" for part in complete_expression_state.keys()] - logger.info(f"[{self.chat_id}] {log_prefix}: {expression_name} - 发送{total_actions}个动作,激活: {', '.join(active_actions)}") - else: - logger.warning(f"[{self.chat_id}] 表情 {expression_name} 没有有效的动作可发送") - - async def send_expression(self, expression_name: str): - """发送表情组合""" - await self._send_expression_actions(expression_name, "发送面部表情") - - # 通知ChatMood需要更新amadus - # 这里需要从mood_manager获取ChatMood实例并标记 - chat_mood = mood_manager.get_mood_by_chat_id(self.chat_id) - if chat_mood.last_expression != expression_name: - chat_mood.last_expression = expression_name - chat_mood.expression_needs_update = True - - async def send_expression_by_mood(self, mood_values: dict[str, int]): - """根据情绪值发送相应的面部表情""" - expression_name = self.select_expression_by_mood(mood_values) - logger.info(f"[{self.chat_id}] 根据情绪值选择表情: {expression_name}, 情绪值: {mood_values}") - await self.send_expression(expression_name) - - async def reset_expression(self): - """重置为中性表情""" - await self.send_expression("neutral") - - async def perform_blink(self): - """执行眨眼动作""" - # 检查当前表情组合中eye部位是否为eye_happy_weak - current_combination = self.expression_combinations.get(self.last_expression, {}) - current_eye_expression = current_combination.get("eye") - - if current_eye_expression == "eye_happy_weak": - logger.debug(f"[{self.chat_id}] 当前eye表情为{current_eye_expression},跳过眨眼动作") - return - - logger.debug(f"[{self.chat_id}] 执行眨眼动作") - - # 第一阶段:闭眼 - await self._send_expression_actions( - self.last_expression, - "眨眼-闭眼", - override_values={"eye_close": 1.0} - ) - - # 等待0.1-0.15秒 - blink_duration = random.uniform(0.7, 0.12) - await asyncio.sleep(blink_duration) - - # 第二阶段:睁眼 - await self._send_expression_actions( - self.last_expression, - "眨眼-睁眼", - override_values={"eye_close": 0.0} - ) - - - async def perform_shift(self): - """执行眨眼动作""" - # 检查当前表情组合中eye部位是否为eye_happy_weak - current_combination = self.expression_combinations.get(self.last_expression, {}) - current_eye_expression = current_combination.get("eye") - - direction = random.choice(["left", "right"]) - strength = random.randint(6, 9) / 10 - time_duration = random.randint(5, 15) / 10 - - if current_eye_expression == "eye_happy_weak" or current_eye_expression == "eye_close": - logger.debug(f"[{self.chat_id}] 当前eye表情为{current_eye_expression},跳过漂移动作") - return - - logger.debug(f"[{self.chat_id}] 执行漂移动作,方向:{direction},强度:{strength},时间:{time_duration}") - - if direction == "left": - override_values = {"eye_shift_left": strength} - back_values = {"eye_shift_left": 0.0} - else: - override_values = {"eye_shift_right": strength} - back_values = {"eye_shift_right": 0.0} - - # 第一阶段:闭眼 - await self._send_expression_actions( - self.last_expression, - "漂移", - override_values=override_values - ) - - # 等待0.1-0.15秒 - await asyncio.sleep(time_duration) - - # 第二阶段:睁眼 - await self._send_expression_actions( - self.last_expression, - "回归", - override_values=back_values - ) - - - class ChatMood: def __init__(self, chat_id: str): self.chat_id: str = chat_id @@ -452,14 +126,8 @@ class ChatMood: self.last_change_time = 0 - # 添加面部表情系统 - self.facial_expression = FacialExpression(chat_id) - self.last_expression = "neutral" # 记录上一次的表情 - self.expression_needs_update = False # 标记表情是否需要更新 - - # 设置初始中性表情 - asyncio.create_task(self.facial_expression.reset_expression()) - self.expression_needs_update = True # 初始化时也标记需要更新 + # 发送初始情绪状态到ws端 + asyncio.create_task(self.send_emotion_update(self.mood_values)) def _parse_numerical_mood(self, response: str) -> dict[str, int] | None: try: @@ -564,13 +232,10 @@ class ChatMood: _old_mood_values = self.mood_values.copy() self.mood_values = numerical_mood_response - # 发送面部表情 - new_expression = self.facial_expression.select_expression_by_mood(self.mood_values) - if new_expression != self.last_expression: - # 立即发送表情 - asyncio.create_task(self.facial_expression.send_expression(new_expression)) - self.last_expression = new_expression - self.expression_needs_update = True # 标记表情已更新 + # 发送情绪更新到ws端 + await self.send_emotion_update(self.mood_values) + + logger.info(f"[{self.chat_id}] 情绪变化: {_old_mood_values} -> {self.mood_values}") self.last_change_time = message_time @@ -644,33 +309,31 @@ class ChatMood: _old_mood_values = self.mood_values.copy() self.mood_values = numerical_mood_response - # 发送面部表情 - new_expression = self.facial_expression.select_expression_by_mood(self.mood_values) - if new_expression != self.last_expression: - # 立即发送表情 - asyncio.create_task(self.facial_expression.send_expression(new_expression)) - self.last_expression = new_expression - self.expression_needs_update = True # 标记表情已更新 + # 发送情绪更新到ws端 + await self.send_emotion_update(self.mood_values) + + logger.info(f"[{self.chat_id}] 情绪回归: {_old_mood_values} -> {self.mood_values}") self.regression_count += 1 - async def send_expression_update_if_needed(self): - """如果表情有变化,发送更新到amadus""" - if self.expression_needs_update: - # 使用统一的表情发送函数 - await self.facial_expression._send_expression_actions( - self.last_expression, - "发送表情更新到amadus" - ) - self.expression_needs_update = False # 重置标记 - - async def perform_blink(self): - """执行眨眼动作""" - await self.facial_expression.perform_blink() + async def send_emotion_update(self, mood_values: dict[str, int]): + """发送情绪更新到ws端""" + emotion_data = { + "joy": mood_values.get("joy", 5), + "anger": mood_values.get("anger", 1), + "sorrow": mood_values.get("sorrow", 1), + "fear": mood_values.get("fear", 1) + } - async def perform_shift(self): - """执行漂移动作""" - await self.facial_expression.perform_shift() + await send_api.custom_to_stream( + message_type="emotion", + content=emotion_data, + stream_id=self.chat_id, + storage_message=False, + show_log=True, + ) + + logger.info(f"[{self.chat_id}] 发送情绪更新: {emotion_data}") class MoodRegressionTask(AsyncTask): @@ -695,17 +358,38 @@ class MoodRegressionTask(AsyncTask): time_since_last_change = now - mood.last_change_time - if time_since_last_change > 120: # 2分钟 + # 检查是否有极端情绪需要快速回归 + high_emotions = {k: v for k, v in mood.mood_values.items() if v >= 8} + has_extreme_emotion = len(high_emotions) > 0 + + # 回归条件:1. 正常时间间隔(120s) 或 2. 有极端情绪且距上次变化>=30s + should_regress = False + regress_reason = "" + + if time_since_last_change > 120: + should_regress = True + regress_reason = f"常规回归(距上次变化{int(time_since_last_change)}秒)" + elif has_extreme_emotion and time_since_last_change > 30: + should_regress = True + high_emotion_str = ", ".join([f"{k}={v}" for k, v in high_emotions.items()]) + regress_reason = f"极端情绪快速回归({high_emotion_str}, 距上次变化{int(time_since_last_change)}秒)" + + if should_regress: if mood.regression_count >= 3: logger.debug(f"[回归任务] {chat_info} 已达到最大回归次数(3次),停止回归") continue - logger.info(f"[回归任务] {chat_info} 开始情绪回归 (距上次变化{int(time_since_last_change)}秒,第{mood.regression_count + 1}次回归)") + logger.info(f"[回归任务] {chat_info} 开始情绪回归 ({regress_reason},第{mood.regression_count + 1}次回归)") await mood.regress_mood() regression_executed += 1 else: - remaining_time = 120 - time_since_last_change - logger.debug(f"[回归任务] {chat_info} 距离回归还需等待{int(remaining_time)}秒") + if has_extreme_emotion: + remaining_time = 5 - time_since_last_change + high_emotion_str = ", ".join([f"{k}={v}" for k, v in high_emotions.items()]) + logger.debug(f"[回归任务] {chat_info} 存在极端情绪({high_emotion_str}),距离快速回归还需等待{int(remaining_time)}秒") + else: + remaining_time = 120 - time_since_last_change + logger.debug(f"[回归任务] {chat_info} 距离回归还需等待{int(remaining_time)}秒") if regression_executed > 0: logger.info(f"[回归任务] 本次执行了{regression_executed}个聊天的情绪回归") @@ -713,96 +397,6 @@ class MoodRegressionTask(AsyncTask): logger.debug(f"[回归任务] 本次没有符合回归条件的聊天") -class ExpressionUpdateTask(AsyncTask): - def __init__(self, mood_manager: "MoodManager"): - super().__init__(task_name="ExpressionUpdateTask", run_interval=0.3) - self.mood_manager = mood_manager - self.run_count = 0 - self.last_log_time = 0 - - async def run(self): - self.run_count += 1 - now = time.time() - - # 每60秒输出一次状态信息(避免日志太频繁) - if now - self.last_log_time > 60: - logger.info(f"[表情任务] 已运行{self.run_count}次,当前管理{len(self.mood_manager.mood_list)}个聊天的表情状态") - self.last_log_time = now - - updates_sent = 0 - for mood in self.mood_manager.mood_list: - if mood.expression_needs_update: - logger.debug(f"[表情任务] chat {mood.chat_id} 检测到表情变化,发送更新") - await mood.send_expression_update_if_needed() - updates_sent += 1 - - if updates_sent > 0: - logger.info(f"[表情任务] 发送了{updates_sent}个表情更新") - - -class BlinkTask(AsyncTask): - def __init__(self, mood_manager: "MoodManager"): - # 初始随机间隔4-6秒 - super().__init__(task_name="BlinkTask", run_interval=4) - self.mood_manager = mood_manager - self.run_count = 0 - self.last_log_time = 0 - - async def run(self): - self.run_count += 1 - now = time.time() - - # 每60秒输出一次状态信息(避免日志太频繁) - if now - self.last_log_time > 20: - logger.debug(f"[眨眼任务] 已运行{self.run_count}次,当前管理{len(self.mood_manager.mood_list)}个聊天的眨眼状态") - self.last_log_time = now - - interval_add = random.randint(0, 2) - await asyncio.sleep(interval_add) - - blinks_executed = 0 - for mood in self.mood_manager.mood_list: - try: - await mood.perform_blink() - blinks_executed += 1 - except Exception as e: - logger.error(f"[眨眼任务] 处理chat {mood.chat_id}时出错: {e}") - - if blinks_executed > 0: - logger.debug(f"[眨眼任务] 本次执行了{blinks_executed}个聊天的眨眼动作") - -class ShiftTask(AsyncTask): - def __init__(self, mood_manager: "MoodManager"): - # 初始随机间隔4-6秒 - super().__init__(task_name="ShiftTask", run_interval=8) - self.mood_manager = mood_manager - self.run_count = 0 - self.last_log_time = 0 - - async def run(self): - self.run_count += 1 - now = time.time() - - # 每60秒输出一次状态信息(避免日志太频繁) - if now - self.last_log_time > 20: - logger.debug(f"[漂移任务] 已运行{self.run_count}次,当前管理{len(self.mood_manager.mood_list)}个聊天的漂移状态") - self.last_log_time = now - - interval_add = random.randint(0, 3) - await asyncio.sleep(interval_add) - - blinks_executed = 0 - for mood in self.mood_manager.mood_list: - try: - await mood.perform_shift() - blinks_executed += 1 - except Exception as e: - logger.error(f"[漂移任务] 处理chat {mood.chat_id}时出错: {e}") - - if blinks_executed > 0: - logger.debug(f"[漂移任务] 本次执行了{blinks_executed}个聊天的漂移动作") - - class MoodManager: def __init__(self): self.mood_list: list[ChatMood] = [] @@ -820,20 +414,8 @@ class MoodManager: regression_task = MoodRegressionTask(self) await async_task_manager.add_task(regression_task) - # 启动表情更新任务 - expression_task = ExpressionUpdateTask(self) - await async_task_manager.add_task(expression_task) - - # 启动眨眼任务 - blink_task = BlinkTask(self) - await async_task_manager.add_task(blink_task) - - # 启动漂移任务 - shift_task = ShiftTask(self) - await async_task_manager.add_task(shift_task) - self.task_started = True - logger.info("情绪管理任务已启动(包含情绪回归、表情更新和眨眼动作)") + logger.info("情绪管理任务已启动(情绪回归)") def get_mood_by_chat_id(self, chat_id: str) -> ChatMood: for mood in self.mood_list: @@ -850,28 +432,15 @@ class MoodManager: mood.mood_state = "感觉很平静" mood.mood_values = {"joy": 5, "anger": 1, "sorrow": 1, "fear": 1} mood.regression_count = 0 - # 重置面部表情为中性 - asyncio.create_task(mood.facial_expression.reset_expression()) - mood.last_expression = "neutral" - mood.expression_needs_update = True # 标记表情需要更新 + # 发送重置后的情绪状态到ws端 + asyncio.create_task(mood.send_emotion_update(mood.mood_values)) return # 如果没有找到现有的mood,创建新的 new_mood = ChatMood(chat_id) self.mood_list.append(new_mood) - asyncio.create_task(new_mood.facial_expression.reset_expression()) - new_mood.expression_needs_update = True # 标记表情需要更新 - - def get_facial_expression_by_chat_id(self, chat_id: str) -> FacialExpression: - """获取聊天对应的面部表情管理器""" - for mood in self.mood_list: - if mood.chat_id == chat_id: - return mood.facial_expression - - # 如果没有找到,创建新的 - new_mood = ChatMood(chat_id) - self.mood_list.append(new_mood) - return new_mood.facial_expression + # 发送初始情绪状态到ws端 + asyncio.create_task(new_mood.send_emotion_update(new_mood.mood_values)) init_prompt() diff --git a/src/mais4u/mais4u_chat/s4u_msg_processor.py b/src/mais4u/mais4u_chat/s4u_msg_processor.py index 6a6cf25f0..86ea90275 100644 --- a/src/mais4u/mais4u_chat/s4u_msg_processor.py +++ b/src/mais4u/mais4u_chat/s4u_msg_processor.py @@ -13,6 +13,7 @@ from src.config.config import global_config from src.mais4u.mais4u_chat.body_emotion_action_manager import action_manager from src.mais4u.mais4u_chat.s4u_mood_manager import mood_manager from src.mais4u.mais4u_chat.s4u_watching_manager import watching_manager +from src.mais4u.mais4u_chat.context_web_manager import get_context_web_manager from .s4u_chat import get_s4u_chat_manager @@ -115,5 +116,33 @@ class S4UMessageProcessor: chat_watching = watching_manager.get_watching_by_chat_id(chat.stream_id) asyncio.create_task(chat_watching.on_message_received()) + # 上下文网页管理:启动独立task处理消息上下文 + asyncio.create_task(self._handle_context_web_update(chat.stream_id, message)) + # 7. 日志记录 logger.info(f"[S4U]{userinfo.user_nickname}:{message.processed_plain_text}") + + async def _handle_context_web_update(self, chat_id: str, message: MessageRecv): + """处理上下文网页更新的独立task + + Args: + chat_id: 聊天ID + message: 消息对象 + """ + try: + logger.debug(f"🔄 开始处理上下文网页更新: {message.message_info.user_info.user_nickname}") + + context_manager = get_context_web_manager() + + # 只在服务器未启动时启动(避免重复启动) + if context_manager.site is None: + logger.info("🚀 首次启动上下文网页服务器...") + await context_manager.start_server() + + # 添加消息到上下文并更新网页 + await context_manager.add_message(chat_id, message) + + logger.debug(f"✅ 上下文网页更新完成: {message.message_info.user_info.user_nickname}") + + except Exception as e: + logger.error(f"❌ 处理上下文网页更新失败: {e}", exc_info=True) diff --git a/src/mais4u/mais4u_chat/s4u_watching_manager.py b/src/mais4u/mais4u_chat/s4u_watching_manager.py index afa1421f1..897ef7f70 100644 --- a/src/mais4u/mais4u_chat/s4u_watching_manager.py +++ b/src/mais4u/mais4u_chat/s4u_watching_manager.py @@ -121,7 +121,8 @@ class ChatWatching: await send_api.custom_to_stream( message_type="watching", content=self.current_state.value, - stream_id=self.chat_id + stream_id=self.chat_id, + storage_message=False ) logger.info(f"[{self.chat_id}] 发送视线状态更新: {self.current_state.value}") diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index d6d13017c..41fc80d9b 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -20,10 +20,10 @@ alias_names = ["麦叠", "牢麦"] # 麦麦的别名 [personality] # 建议50字以内,描述人格的核心特质 personality_core = "是一个积极向上的女大学生" -# 人格的细节,可以描述人格的一些侧面,条数任意,不能为0,不宜太多 +# 人格的细节,描述人格的一些侧面 personality_side = "用一句话或几句话描述人格的侧面特质" #アイデンティティがない 生まれないらららら -# 可以描述外貌,性别,身高,职业,属性等等描述,条数任意,不能为0 +# 可以描述外貌,性别,身高,职业,属性等等描述 identity = "年龄为19岁,是女孩子,身高为160cm,有黑色的短发" compress_personality = false # 是否压缩人格,压缩后会精简人格信息,节省token消耗并提高回复性能,但是会丢失一些信息,如果人设不长,可以关闭