feat:为s4u添加一个透明底的聊天记录网页
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
APScheduler
|
||||
Pillow
|
||||
aiohttp
|
||||
aiohttp-cors
|
||||
colorama
|
||||
customtkinter
|
||||
dotenv
|
||||
|
||||
544
src/mais4u/mais4u_chat/context_web_manager.py
Normal file
544
src/mais4u/mais4u_chat/context_web_manager.py
Normal file
@@ -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 = '''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>聊天上下文</title>
|
||||
<style>
|
||||
html, body {
|
||||
background: transparent !important;
|
||||
background-color: transparent !important;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||
color: #ffffff;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: transparent !important;
|
||||
}
|
||||
.message {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
margin: 10px 0;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
border-left: 4px solid #00ff88;
|
||||
backdrop-filter: blur(5px);
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
.message:hover {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
transform: translateX(5px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.user-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.username {
|
||||
font-weight: bold;
|
||||
color: #00ff88;
|
||||
font-size: 18px;
|
||||
}
|
||||
.timestamp {
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
}
|
||||
.group-name {
|
||||
color: #60a5fa;
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
}
|
||||
.content {
|
||||
font-size: 20px;
|
||||
line-height: 1.4;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.status {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
z-index: 1000;
|
||||
}
|
||||
.debug-btn {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: #00ff88;
|
||||
font-size: 12px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
z-index: 1000;
|
||||
text-decoration: none;
|
||||
border: 1px solid #00ff88;
|
||||
}
|
||||
.debug-btn:hover {
|
||||
background: rgba(0, 255, 136, 0.2);
|
||||
}
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.no-messages {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
margin-top: 50px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="status" id="status">正在连接...</div>
|
||||
<a href="/debug" class="debug-btn">🔧 调试</a>
|
||||
<div id="messages">
|
||||
<div class="no-messages">暂无消息</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let ws;
|
||||
let reconnectInterval;
|
||||
|
||||
function connectWebSocket() {
|
||||
console.log('正在连接WebSocket...');
|
||||
ws = new WebSocket('ws://localhost:''' + str(self.port) + '''/ws');
|
||||
|
||||
ws.onopen = function() {
|
||||
console.log('WebSocket连接已建立');
|
||||
document.getElementById('status').textContent = '✅ 已连接';
|
||||
document.getElementById('status').style.color = '#00ff88';
|
||||
if (reconnectInterval) {
|
||||
clearInterval(reconnectInterval);
|
||||
reconnectInterval = null;
|
||||
}
|
||||
};
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
console.log('收到WebSocket消息:', event.data);
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
updateMessages(data.contexts);
|
||||
} catch (e) {
|
||||
console.error('解析消息失败:', e, event.data);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = function(event) {
|
||||
console.log('WebSocket连接关闭:', event.code, event.reason);
|
||||
document.getElementById('status').textContent = '❌ 连接断开,正在重连...';
|
||||
document.getElementById('status').style.color = '#ff6b6b';
|
||||
|
||||
if (!reconnectInterval) {
|
||||
reconnectInterval = setInterval(connectWebSocket, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = function(error) {
|
||||
console.error('WebSocket错误:', error);
|
||||
document.getElementById('status').textContent = '❌ 连接错误';
|
||||
document.getElementById('status').style.color = '#ff6b6b';
|
||||
};
|
||||
}
|
||||
|
||||
function updateMessages(contexts) {
|
||||
const messagesDiv = document.getElementById('messages');
|
||||
|
||||
if (!contexts || contexts.length === 0) {
|
||||
messagesDiv.innerHTML = '<div class="no-messages">暂无消息</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('收到新消息,数量:', contexts.length);
|
||||
messagesDiv.innerHTML = '';
|
||||
|
||||
contexts.forEach(function(msg) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'message';
|
||||
messageDiv.innerHTML = `
|
||||
<div class="user-info">
|
||||
<div>
|
||||
<span class="username">${escapeHtml(msg.user_name)}</span>
|
||||
<span class="group-name">[${escapeHtml(msg.group_name)}]</span>
|
||||
</div>
|
||||
<span class="timestamp">${msg.timestamp}</span>
|
||||
</div>
|
||||
<div class="content">${escapeHtml(msg.content)}</div>
|
||||
`;
|
||||
messagesDiv.appendChild(messageDiv);
|
||||
});
|
||||
|
||||
// 滚动到最底部显示最新消息
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// 初始加载数据
|
||||
fetch('/api/contexts')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('初始数据加载成功:', data);
|
||||
updateMessages(data.contexts);
|
||||
})
|
||||
.catch(err => console.error('加载初始数据失败:', err));
|
||||
|
||||
// 连接WebSocket
|
||||
connectWebSocket();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
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'<div class="message">[{timestamp}] {msg.user_name}: {content}</div>'
|
||||
|
||||
chats_html += f'''
|
||||
<div class="chat">
|
||||
<h3>聊天 {chat_id} ({len(contexts)} 条消息)</h3>
|
||||
{messages_html}
|
||||
</div>
|
||||
'''
|
||||
|
||||
html_content = f'''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>调试信息</title>
|
||||
<style>
|
||||
body {{ font-family: monospace; margin: 20px; }}
|
||||
.section {{ margin: 20px 0; padding: 10px; border: 1px solid #ccc; }}
|
||||
.chat {{ margin: 10px 0; padding: 10px; background: #f5f5f5; }}
|
||||
.message {{ margin: 5px 0; padding: 5px; background: white; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>上下文网页管理器调试信息</h1>
|
||||
|
||||
<div class="section">
|
||||
<h2>服务器状态</h2>
|
||||
<p>状态: {debug_info["server_status"]}</p>
|
||||
<p>WebSocket连接数: {debug_info["websocket_connections"]}</p>
|
||||
<p>聊天总数: {debug_info["total_chats"]}</p>
|
||||
<p>消息总数: {debug_info["total_messages"]}</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>聊天详情</h2>
|
||||
{chats_html}
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>操作</h2>
|
||||
<button onclick="location.reload()">刷新页面</button>
|
||||
<button onclick="window.location.href='/'">返回主页</button>
|
||||
<button onclick="window.location.href='/api/contexts'">查看API数据</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
console.log('调试信息:', {json.dumps(debug_info, ensure_ascii=False, indent=2)});
|
||||
setTimeout(() => location.reload(), 5000); // 5秒自动刷新
|
||||
</script>
|
||||
</body>
|
||||
</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
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -20,10 +20,10 @@ alias_names = ["麦叠", "牢麦"] # 麦麦的别名
|
||||
[personality]
|
||||
# 建议50字以内,描述人格的核心特质
|
||||
personality_core = "是一个积极向上的女大学生"
|
||||
# 人格的细节,可以描述人格的一些侧面,条数任意,不能为0,不宜太多
|
||||
# 人格的细节,描述人格的一些侧面
|
||||
personality_side = "用一句话或几句话描述人格的侧面特质"
|
||||
#アイデンティティがない 生まれないらららら
|
||||
# 可以描述外貌,性别,身高,职业,属性等等描述,条数任意,不能为0
|
||||
# 可以描述外貌,性别,身高,职业,属性等等描述
|
||||
identity = "年龄为19岁,是女孩子,身高为160cm,有黑色的短发"
|
||||
|
||||
compress_personality = false # 是否压缩人格,压缩后会精简人格信息,节省token消耗并提高回复性能,但是会丢失一些信息,如果人设不长,可以关闭
|
||||
|
||||
Reference in New Issue
Block a user