This commit is contained in:
墨梓柒
2025-07-15 17:03:21 +08:00
23 changed files with 335 additions and 238 deletions

View File

@@ -20,3 +20,4 @@
- `chat_api.py`中获取流的参数中可以使用一个特殊的枚举类型来获得所有平台的 ChatStream 了。 - `chat_api.py`中获取流的参数中可以使用一个特殊的枚举类型来获得所有平台的 ChatStream 了。
- `config_api.py`中的`get_global_config``get_plugin_config`方法现在支持嵌套访问的配置键名。 - `config_api.py`中的`get_global_config``get_plugin_config`方法现在支持嵌套访问的配置键名。
- `database_api.py`中的`db_query`方法调整了参数顺序以增强参数限制的同时保证了typing正确`db_get`方法增加了`single_result`参数,与`db_query`保持一致。 - `database_api.py`中的`db_query`方法调整了参数顺序以增强参数限制的同时保证了typing正确`db_get`方法增加了`single_result`参数,与`db_query`保持一致。
4. 现在增加了参数类型检查,完善了对应注释

View File

@@ -47,7 +47,7 @@ class MaiEmoji:
self.embedding = [] self.embedding = []
self.hash = "" # 初始为空,在创建实例时会计算 self.hash = "" # 初始为空,在创建实例时会计算
self.description = "" self.description = ""
self.emotion = [] self.emotion: List[str] = []
self.usage_count = 0 self.usage_count = 0
self.last_used_time = time.time() self.last_used_time = time.time()
self.register_time = time.time() self.register_time = time.time()

View File

@@ -243,6 +243,8 @@ class HeartFChatting:
loop_start_time = time.time() loop_start_time = time.time()
await self.relationship_builder.build_relation() await self.relationship_builder.build_relation()
available_actions = {}
# 第一步:动作修改 # 第一步:动作修改
with Timer("动作修改", cycle_timers): with Timer("动作修改", cycle_timers):
try: try:

View File

@@ -38,7 +38,9 @@ class HeartFCSender:
def __init__(self): def __init__(self):
self.storage = MessageStorage() self.storage = MessageStorage()
async def send_message(self, message: MessageSending, typing=False, set_reply=False, storage_message=True, show_log=True): async def send_message(
self, message: MessageSending, typing=False, set_reply=False, storage_message=True, show_log=True
):
""" """
处理、发送并存储一条消息。 处理、发送并存储一条消息。

View File

@@ -79,7 +79,9 @@ class ActionPlanner:
self.last_obs_time_mark = 0.0 self.last_obs_time_mark = 0.0
async def plan(self, mode: ChatMode = ChatMode.FOCUS) -> Dict[str, Dict[str, Any] | str]: # sourcery skip: dict-comprehension async def plan(
self, mode: ChatMode = ChatMode.FOCUS
) -> Dict[str, Dict[str, Any] | str]: # sourcery skip: dict-comprehension
""" """
规划器 (Planner): 使用LLM根据上下文决定做出什么动作。 规划器 (Planner): 使用LLM根据上下文决定做出什么动作。
""" """

View File

@@ -480,18 +480,18 @@ class DefaultReplyer:
def build_s4u_chat_history_prompts(self, message_list_before_now: list, target_user_id: str) -> tuple[str, str]: def build_s4u_chat_history_prompts(self, message_list_before_now: list, target_user_id: str) -> tuple[str, str]:
""" """
构建 s4u 风格的分离对话 prompt 构建 s4u 风格的分离对话 prompt
Args: Args:
message_list_before_now: 历史消息列表 message_list_before_now: 历史消息列表
target_user_id: 目标用户ID当前对话对象 target_user_id: 目标用户ID当前对话对象
Returns: Returns:
tuple: (核心对话prompt, 背景对话prompt) tuple: (核心对话prompt, 背景对话prompt)
""" """
core_dialogue_list = [] core_dialogue_list = []
background_dialogue_list = [] background_dialogue_list = []
bot_id = str(global_config.bot.qq_account) bot_id = str(global_config.bot.qq_account)
# 过滤消息分离bot和目标用户的对话 vs 其他用户的对话 # 过滤消息分离bot和目标用户的对话 vs 其他用户的对话
for msg_dict in message_list_before_now: for msg_dict in message_list_before_now:
try: try:
@@ -504,11 +504,11 @@ class DefaultReplyer:
background_dialogue_list.append(msg_dict) background_dialogue_list.append(msg_dict)
except Exception as e: except Exception as e:
logger.error(f"无法处理历史消息记录: {msg_dict}, 错误: {e}") logger.error(f"无法处理历史消息记录: {msg_dict}, 错误: {e}")
# 构建背景对话 prompt # 构建背景对话 prompt
background_dialogue_prompt = "" background_dialogue_prompt = ""
if background_dialogue_list: if background_dialogue_list:
latest_25_msgs = background_dialogue_list[-int(global_config.chat.max_context_size*0.6):] latest_25_msgs = background_dialogue_list[-int(global_config.chat.max_context_size * 0.6) :]
background_dialogue_prompt_str = build_readable_messages( background_dialogue_prompt_str = build_readable_messages(
latest_25_msgs, latest_25_msgs,
replace_bot_name=True, replace_bot_name=True,
@@ -517,12 +517,12 @@ class DefaultReplyer:
show_pic=False, show_pic=False,
) )
background_dialogue_prompt = f"这是其他用户的发言:\n{background_dialogue_prompt_str}" background_dialogue_prompt = f"这是其他用户的发言:\n{background_dialogue_prompt_str}"
# 构建核心对话 prompt # 构建核心对话 prompt
core_dialogue_prompt = "" core_dialogue_prompt = ""
if core_dialogue_list: if core_dialogue_list:
core_dialogue_list = core_dialogue_list[-int(global_config.chat.max_context_size*2):] # 限制消息数量 core_dialogue_list = core_dialogue_list[-int(global_config.chat.max_context_size * 2) :] # 限制消息数量
core_dialogue_prompt_str = build_readable_messages( core_dialogue_prompt_str = build_readable_messages(
core_dialogue_list, core_dialogue_list,
replace_bot_name=True, replace_bot_name=True,
@@ -533,7 +533,7 @@ class DefaultReplyer:
show_actions=True, show_actions=True,
) )
core_dialogue_prompt = core_dialogue_prompt_str core_dialogue_prompt = core_dialogue_prompt_str
return core_dialogue_prompt, background_dialogue_prompt return core_dialogue_prompt, background_dialogue_prompt
async def build_prompt_reply_context( async def build_prompt_reply_context(
@@ -579,14 +579,13 @@ class DefaultReplyer:
action_description = action_info.description action_description = action_info.description
action_descriptions += f"- {action_name}: {action_description}\n" action_descriptions += f"- {action_name}: {action_description}\n"
action_descriptions += "\n" action_descriptions += "\n"
message_list_before_now_long = get_raw_msg_before_timestamp_with_chat( message_list_before_now_long = get_raw_msg_before_timestamp_with_chat(
chat_id=chat_id, chat_id=chat_id,
timestamp=time.time(), timestamp=time.time(),
limit=global_config.chat.max_context_size * 2, limit=global_config.chat.max_context_size * 2,
) )
message_list_before_now = get_raw_msg_before_timestamp_with_chat( message_list_before_now = get_raw_msg_before_timestamp_with_chat(
chat_id=chat_id, chat_id=chat_id,
timestamp=time.time(), timestamp=time.time(),
@@ -713,8 +712,6 @@ class DefaultReplyer:
# 根据sender通过person_info_manager反向查找person_id再获取user_id # 根据sender通过person_info_manager反向查找person_id再获取user_id
person_id = person_info_manager.get_person_id_by_person_name(sender) person_id = person_info_manager.get_person_id_by_person_name(sender)
# 根据配置选择使用哪种 prompt 构建模式 # 根据配置选择使用哪种 prompt 构建模式
if global_config.chat.use_s4u_prompt_mode and person_id: if global_config.chat.use_s4u_prompt_mode and person_id:
# 使用 s4u 对话构建模式:分离当前对话对象和其他对话 # 使用 s4u 对话构建模式:分离当前对话对象和其他对话
@@ -725,16 +722,15 @@ class DefaultReplyer:
except Exception as e: except Exception as e:
logger.warning(f"无法从person_id {person_id} 获取user_id: {e}") logger.warning(f"无法从person_id {person_id} 获取user_id: {e}")
target_user_id = "" target_user_id = ""
# 构建分离的对话 prompt # 构建分离的对话 prompt
core_dialogue_prompt, background_dialogue_prompt = self.build_s4u_chat_history_prompts( core_dialogue_prompt, background_dialogue_prompt = self.build_s4u_chat_history_prompts(
message_list_before_now_long, target_user_id message_list_before_now_long, target_user_id
) )
# 使用 s4u 风格的模板 # 使用 s4u 风格的模板
template_name = "s4u_style_prompt" template_name = "s4u_style_prompt"
return await global_prompt_manager.format_prompt( return await global_prompt_manager.format_prompt(
template_name, template_name,
expression_habits_block=expression_habits_block, expression_habits_block=expression_habits_block,

View File

@@ -37,7 +37,7 @@ class PersonalityConfig(ConfigBase):
personality_side: str personality_side: str
"""人格侧写""" """人格侧写"""
identity: str = "" identity: str = ""
"""身份特征""" """身份特征"""
@@ -106,7 +106,6 @@ class ChatConfig(ConfigBase):
focus_value: float = 1.0 focus_value: float = 1.0
"""麦麦的专注思考能力越低越容易专注消耗token也越多""" """麦麦的专注思考能力越低越容易专注消耗token也越多"""
def get_current_talk_frequency(self, chat_stream_id: Optional[str] = None) -> float: def get_current_talk_frequency(self, chat_stream_id: Optional[str] = None) -> float:
""" """
根据当前时间和聊天流获取对应的 talk_frequency 根据当前时间和聊天流获取对应的 talk_frequency
@@ -246,6 +245,7 @@ class ChatConfig(ConfigBase):
except (ValueError, IndexError): except (ValueError, IndexError):
return None return None
@dataclass @dataclass
class MessageReceiveConfig(ConfigBase): class MessageReceiveConfig(ConfigBase):
"""消息接收配置类""" """消息接收配置类"""
@@ -274,8 +274,6 @@ class NormalChatConfig(ConfigBase):
"""@bot 必然回复""" """@bot 必然回复"""
@dataclass @dataclass
class ExpressionConfig(ConfigBase): class ExpressionConfig(ConfigBase):
"""表达配置类""" """表达配置类"""

View File

@@ -63,11 +63,11 @@ class Individuality:
personality_side: 人格侧面描述 personality_side: 人格侧面描述
identity: 身份细节描述 identity: 身份细节描述
""" """
bot_nickname=global_config.bot.nickname bot_nickname = global_config.bot.nickname
personality_core=global_config.personality.personality_core personality_core = global_config.personality.personality_core
personality_side=global_config.personality.personality_side personality_side = global_config.personality.personality_side
identity=global_config.personality.identity identity = global_config.personality.identity
logger.info("正在初始化个体特征") logger.info("正在初始化个体特征")
person_info_manager = get_person_info_manager() person_info_manager = get_person_info_manager()
self.bot_person_id = person_info_manager.get_person_id("system", "bot_id") self.bot_person_id = person_info_manager.get_person_id("system", "bot_id")
@@ -168,11 +168,10 @@ class Individuality:
else: else:
logger.error("人设构建失败") logger.error("人设构建失败")
async def get_personality_block(self) -> str: async def get_personality_block(self) -> str:
person_info_manager = get_person_info_manager() person_info_manager = get_person_info_manager()
bot_person_id = person_info_manager.get_person_id("system", "bot_id") bot_person_id = person_info_manager.get_person_id("system", "bot_id")
bot_name = global_config.bot.nickname bot_name = global_config.bot.nickname
if global_config.bot.alias_names: if global_config.bot.alias_names:
bot_nickname = f",也有人叫你{','.join(global_config.bot.alias_names)}" bot_nickname = f",也有人叫你{','.join(global_config.bot.alias_names)}"
@@ -197,9 +196,8 @@ class Individuality:
identity = short_impression[1] identity = short_impression[1]
prompt_personality = f"{personality}{identity}" prompt_personality = f"{personality}{identity}"
identity_block = f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}" identity_block = f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}"
return identity_block
return identity_block
def _get_config_hash( def _get_config_hash(
self, bot_nickname: str, personality_core: str, personality_side: str, identity: list self, bot_nickname: str, personality_core: str, personality_side: str, identity: list
@@ -295,7 +293,6 @@ class Individuality:
except IOError as e: except IOError as e:
logger.error(f"保存meta_info文件失败: {e}") logger.error(f"保存meta_info文件失败: {e}")
async def _create_personality(self, personality_core: str, personality_side: str) -> str: async def _create_personality(self, personality_core: str, personality_side: str) -> str:
# sourcery skip: merge-list-append, move-assign # sourcery skip: merge-list-append, move-assign
"""使用LLM创建压缩版本的impression """使用LLM创建压缩版本的impression

View File

@@ -42,7 +42,15 @@ class Personality:
return cls._instance return cls._instance
@classmethod @classmethod
def initialize(cls, bot_nickname: str, personality_core: str, personality_side: str, identity: List[str] = None, compress_personality: bool = True, compress_identity: bool = True) -> "Personality": def initialize(
cls,
bot_nickname: str,
personality_core: str,
personality_side: str,
identity: List[str] = None,
compress_personality: bool = True,
compress_identity: bool = True,
) -> "Personality":
"""初始化人格特质 """初始化人格特质
Args: Args:

View File

@@ -16,27 +16,27 @@ logger = get_logger("context_web")
class ContextMessage: class ContextMessage:
"""上下文消息类""" """上下文消息类"""
def __init__(self, message: MessageRecv): def __init__(self, message: MessageRecv):
self.user_name = message.message_info.user_info.user_nickname self.user_name = message.message_info.user_info.user_nickname
self.user_id = message.message_info.user_info.user_id self.user_id = message.message_info.user_info.user_id
self.content = message.processed_plain_text self.content = message.processed_plain_text
self.timestamp = datetime.now() self.timestamp = datetime.now()
self.group_name = message.message_info.group_info.group_name if message.message_info.group_info else "私聊" self.group_name = message.message_info.group_info.group_name if message.message_info.group_info else "私聊"
def to_dict(self): def to_dict(self):
return { return {
"user_name": self.user_name, "user_name": self.user_name,
"user_id": self.user_id, "user_id": self.user_id,
"content": self.content, "content": self.content,
"timestamp": self.timestamp.strftime("%m-%d %H:%M:%S"), "timestamp": self.timestamp.strftime("%m-%d %H:%M:%S"),
"group_name": self.group_name "group_name": self.group_name,
} }
class ContextWebManager: class ContextWebManager:
"""上下文网页管理器""" """上下文网页管理器"""
def __init__(self, max_messages: int = 10, port: int = 8765): def __init__(self, max_messages: int = 10, port: int = 8765):
self.max_messages = max_messages self.max_messages = max_messages
self.port = port self.port = port
@@ -46,53 +46,53 @@ class ContextWebManager:
self.runner = None self.runner = None
self.site = None self.site = None
self._server_starting = False # 添加启动标志防止并发 self._server_starting = False # 添加启动标志防止并发
async def start_server(self): async def start_server(self):
"""启动web服务器""" """启动web服务器"""
if self.site is not None: if self.site is not None:
logger.debug("Web服务器已经启动跳过重复启动") logger.debug("Web服务器已经启动跳过重复启动")
return return
if self._server_starting: if self._server_starting:
logger.debug("Web服务器正在启动中等待启动完成...") logger.debug("Web服务器正在启动中等待启动完成...")
# 等待启动完成 # 等待启动完成
while self._server_starting and self.site is None: while self._server_starting and self.site is None:
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
return return
self._server_starting = True self._server_starting = True
try: try:
self.app = web.Application() self.app = web.Application()
# 设置CORS # 设置CORS
cors = aiohttp_cors.setup(self.app, defaults={ cors = aiohttp_cors.setup(
"*": aiohttp_cors.ResourceOptions( self.app,
allow_credentials=True, defaults={
expose_headers="*", "*": aiohttp_cors.ResourceOptions(
allow_headers="*", allow_credentials=True, expose_headers="*", allow_headers="*", allow_methods="*"
allow_methods="*" )
) },
}) )
# 添加路由 # 添加路由
self.app.router.add_get('/', self.index_handler) self.app.router.add_get("/", self.index_handler)
self.app.router.add_get('/ws', self.websocket_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("/api/contexts", self.get_contexts_handler)
self.app.router.add_get('/debug', self.debug_handler) self.app.router.add_get("/debug", self.debug_handler)
# 为所有路由添加CORS # 为所有路由添加CORS
for route in list(self.app.router.routes()): for route in list(self.app.router.routes()):
cors.add(route) cors.add(route)
self.runner = web.AppRunner(self.app) self.runner = web.AppRunner(self.app)
await self.runner.setup() await self.runner.setup()
self.site = web.TCPSite(self.runner, 'localhost', self.port) self.site = web.TCPSite(self.runner, "localhost", self.port)
await self.site.start() await self.site.start()
logger.info(f"🌐 上下文网页服务器启动成功在 http://localhost:{self.port}") logger.info(f"🌐 上下文网页服务器启动成功在 http://localhost:{self.port}")
except Exception as e: except Exception as e:
logger.error(f"❌ 启动Web服务器失败: {e}") logger.error(f"❌ 启动Web服务器失败: {e}")
# 清理部分启动的资源 # 清理部分启动的资源
@@ -104,7 +104,7 @@ class ContextWebManager:
raise raise
finally: finally:
self._server_starting = False self._server_starting = False
async def stop_server(self): async def stop_server(self):
"""停止web服务器""" """停止web服务器"""
if self.site: if self.site:
@@ -115,10 +115,11 @@ class ContextWebManager:
self.runner = None self.runner = None
self.site = None self.site = None
self._server_starting = False self._server_starting = False
async def index_handler(self, request): async def index_handler(self, request):
"""主页处理器""" """主页处理器"""
html_content = ''' html_content = (
"""
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@@ -231,7 +232,9 @@ class ContextWebManager:
function connectWebSocket() { function connectWebSocket() {
console.log('正在连接WebSocket...'); console.log('正在连接WebSocket...');
ws = new WebSocket('ws://localhost:''' + str(self.port) + '''/ws'); ws = new WebSocket('ws://localhost:"""
+ str(self.port)
+ """/ws');
ws.onopen = function() { ws.onopen = function() {
console.log('WebSocket连接已建立'); console.log('WebSocket连接已建立');
@@ -402,47 +405,48 @@ class ContextWebManager:
</script> </script>
</body> </body>
</html> </html>
''' """
return web.Response(text=html_content, content_type='text/html') )
return web.Response(text=html_content, content_type="text/html")
async def websocket_handler(self, request): async def websocket_handler(self, request):
"""WebSocket处理器""" """WebSocket处理器"""
ws = web.WebSocketResponse() ws = web.WebSocketResponse()
await ws.prepare(request) await ws.prepare(request)
self.websockets.append(ws) self.websockets.append(ws)
logger.debug(f"WebSocket连接建立当前连接数: {len(self.websockets)}") logger.debug(f"WebSocket连接建立当前连接数: {len(self.websockets)}")
# 发送初始数据 # 发送初始数据
await self.send_contexts_to_websocket(ws) await self.send_contexts_to_websocket(ws)
async for msg in ws: async for msg in ws:
if msg.type == WSMsgType.ERROR: if msg.type == WSMsgType.ERROR:
logger.error(f'WebSocket错误: {ws.exception()}') logger.error(f"WebSocket错误: {ws.exception()}")
break break
# 清理断开的连接 # 清理断开的连接
if ws in self.websockets: if ws in self.websockets:
self.websockets.remove(ws) self.websockets.remove(ws)
logger.debug(f"WebSocket连接断开当前连接数: {len(self.websockets)}") logger.debug(f"WebSocket连接断开当前连接数: {len(self.websockets)}")
return ws return ws
async def get_contexts_handler(self, request): async def get_contexts_handler(self, request):
"""获取上下文API""" """获取上下文API"""
all_context_msgs = [] all_context_msgs = []
for chat_id, contexts in self.contexts.items(): for chat_id, contexts in self.contexts.items():
all_context_msgs.extend(list(contexts)) all_context_msgs.extend(list(contexts))
# 按时间排序,最新的在最后 # 按时间排序,最新的在最后
all_context_msgs.sort(key=lambda x: x.timestamp) all_context_msgs.sort(key=lambda x: x.timestamp)
# 转换为字典格式 # 转换为字典格式
contexts_data = [msg.to_dict() for msg in all_context_msgs[-self.max_messages:]] contexts_data = [msg.to_dict() for msg in all_context_msgs[-self.max_messages :]]
logger.debug(f"返回上下文数据,共 {len(contexts_data)} 条消息") logger.debug(f"返回上下文数据,共 {len(contexts_data)} 条消息")
return web.json_response({"contexts": contexts_data}) return web.json_response({"contexts": contexts_data})
async def debug_handler(self, request): async def debug_handler(self, request):
"""调试信息处理器""" """调试信息处理器"""
debug_info = { debug_info = {
@@ -451,7 +455,7 @@ class ContextWebManager:
"total_chats": len(self.contexts), "total_chats": len(self.contexts),
"total_messages": sum(len(contexts) for contexts in self.contexts.values()), "total_messages": sum(len(contexts) for contexts in self.contexts.values()),
} }
# 构建聊天详情HTML # 构建聊天详情HTML
chats_html = "" chats_html = ""
for chat_id, contexts in self.contexts.items(): for chat_id, contexts in self.contexts.items():
@@ -460,15 +464,15 @@ class ContextWebManager:
timestamp = msg.timestamp.strftime("%H:%M:%S") timestamp = msg.timestamp.strftime("%H:%M:%S")
content = msg.content[:50] + "..." if len(msg.content) > 50 else msg.content content = msg.content[:50] + "..." if len(msg.content) > 50 else msg.content
messages_html += f'<div class="message">[{timestamp}] {msg.user_name}: {content}</div>' messages_html += f'<div class="message">[{timestamp}] {msg.user_name}: {content}</div>'
chats_html += f''' chats_html += f"""
<div class="chat"> <div class="chat">
<h3>聊天 {chat_id} ({len(contexts)} 条消息)</h3> <h3>聊天 {chat_id} ({len(contexts)} 条消息)</h3>
{messages_html} {messages_html}
</div> </div>
''' """
html_content = f''' html_content = f"""
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@@ -510,74 +514,78 @@ class ContextWebManager:
</script> </script>
</body> </body>
</html> </html>
''' """
return web.Response(text=html_content, content_type='text/html') return web.Response(text=html_content, content_type="text/html")
async def add_message(self, chat_id: str, message: MessageRecv): async def add_message(self, chat_id: str, message: MessageRecv):
"""添加新消息到上下文""" """添加新消息到上下文"""
if chat_id not in self.contexts: if chat_id not in self.contexts:
self.contexts[chat_id] = deque(maxlen=self.max_messages) self.contexts[chat_id] = deque(maxlen=self.max_messages)
logger.debug(f"为聊天 {chat_id} 创建新的上下文队列") logger.debug(f"为聊天 {chat_id} 创建新的上下文队列")
context_msg = ContextMessage(message) context_msg = ContextMessage(message)
self.contexts[chat_id].append(context_msg) self.contexts[chat_id].append(context_msg)
# 统计当前总消息数 # 统计当前总消息数
total_messages = sum(len(contexts) for contexts in self.contexts.values()) 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"✅ 添加消息到上下文 [总数: {total_messages}]: [{context_msg.group_name}] {context_msg.user_name}: {context_msg.content}"
)
# 调试:打印当前所有消息 # 调试:打印当前所有消息
logger.info(f"📝 当前上下文中的所有消息:") logger.info(f"📝 当前上下文中的所有消息:")
for cid, contexts in self.contexts.items(): for cid, contexts in self.contexts.items():
logger.info(f" 聊天 {cid}: {len(contexts)} 条消息") logger.info(f" 聊天 {cid}: {len(contexts)} 条消息")
for i, msg in enumerate(contexts): for i, msg in enumerate(contexts):
logger.info(f" {i+1}. [{msg.timestamp.strftime('%H:%M:%S')}] {msg.user_name}: {msg.content[:30]}...") logger.info(
f" {i + 1}. [{msg.timestamp.strftime('%H:%M:%S')}] {msg.user_name}: {msg.content[:30]}..."
)
# 广播更新给所有WebSocket连接 # 广播更新给所有WebSocket连接
await self.broadcast_contexts() await self.broadcast_contexts()
async def send_contexts_to_websocket(self, ws: web.WebSocketResponse): async def send_contexts_to_websocket(self, ws: web.WebSocketResponse):
"""向单个WebSocket发送上下文数据""" """向单个WebSocket发送上下文数据"""
all_context_msgs = [] all_context_msgs = []
for chat_id, contexts in self.contexts.items(): for chat_id, contexts in self.contexts.items():
all_context_msgs.extend(list(contexts)) all_context_msgs.extend(list(contexts))
# 按时间排序,最新的在最后 # 按时间排序,最新的在最后
all_context_msgs.sort(key=lambda x: x.timestamp) all_context_msgs.sort(key=lambda x: x.timestamp)
# 转换为字典格式 # 转换为字典格式
contexts_data = [msg.to_dict() for msg in all_context_msgs[-self.max_messages:]] contexts_data = [msg.to_dict() for msg in all_context_msgs[-self.max_messages :]]
data = {"contexts": contexts_data} data = {"contexts": contexts_data}
await ws.send_str(json.dumps(data, ensure_ascii=False)) await ws.send_str(json.dumps(data, ensure_ascii=False))
async def broadcast_contexts(self): async def broadcast_contexts(self):
"""向所有WebSocket连接广播上下文更新""" """向所有WebSocket连接广播上下文更新"""
if not self.websockets: if not self.websockets:
logger.debug("没有WebSocket连接跳过广播") logger.debug("没有WebSocket连接跳过广播")
return return
all_context_msgs = [] all_context_msgs = []
for chat_id, contexts in self.contexts.items(): for chat_id, contexts in self.contexts.items():
all_context_msgs.extend(list(contexts)) all_context_msgs.extend(list(contexts))
# 按时间排序,最新的在最后 # 按时间排序,最新的在最后
all_context_msgs.sort(key=lambda x: x.timestamp) all_context_msgs.sort(key=lambda x: x.timestamp)
# 转换为字典格式 # 转换为字典格式
contexts_data = [msg.to_dict() for msg in all_context_msgs[-self.max_messages:]] contexts_data = [msg.to_dict() for msg in all_context_msgs[-self.max_messages :]]
data = {"contexts": contexts_data} data = {"contexts": contexts_data}
message = json.dumps(data, ensure_ascii=False) message = json.dumps(data, ensure_ascii=False)
logger.info(f"广播 {len(contexts_data)} 条消息到 {len(self.websockets)} 个WebSocket连接") logger.info(f"广播 {len(contexts_data)} 条消息到 {len(self.websockets)} 个WebSocket连接")
# 创建WebSocket列表的副本避免在遍历时修改 # 创建WebSocket列表的副本避免在遍历时修改
websockets_copy = self.websockets.copy() websockets_copy = self.websockets.copy()
removed_count = 0 removed_count = 0
for ws in websockets_copy: for ws in websockets_copy:
if ws.closed: if ws.closed:
if ws in self.websockets: if ws in self.websockets:
@@ -592,7 +600,7 @@ class ContextWebManager:
if ws in self.websockets: if ws in self.websockets:
self.websockets.remove(ws) self.websockets.remove(ws)
removed_count += 1 removed_count += 1
if removed_count > 0: if removed_count > 0:
logger.debug(f"清理了 {removed_count} 个断开的WebSocket连接") logger.debug(f"清理了 {removed_count} 个断开的WebSocket连接")
@@ -613,5 +621,4 @@ async def init_context_web_manager():
"""初始化上下文网页管理器""" """初始化上下文网页管理器"""
manager = get_context_web_manager() manager = get_context_web_manager()
await manager.start_server() await manager.start_server()
return manager return manager

View File

@@ -11,6 +11,7 @@ from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
from src.manager.async_task_manager import AsyncTask, async_task_manager from src.manager.async_task_manager import AsyncTask, async_task_manager
from src.plugin_system.apis import send_api from src.plugin_system.apis import send_api
async def send_loading(chat_id: str, content: str): async def send_loading(chat_id: str, content: str):
await send_api.custom_to_stream( await send_api.custom_to_stream(
message_type="loading", message_type="loading",
@@ -19,7 +20,8 @@ async def send_loading(chat_id: str, content: str):
storage_message=False, storage_message=False,
show_log=True, show_log=True,
) )
async def send_unloading(chat_id: str): async def send_unloading(chat_id: str):
await send_api.custom_to_stream( await send_api.custom_to_stream(
message_type="loading", message_type="loading",
@@ -28,4 +30,3 @@ async def send_unloading(chat_id: str):
storage_message=False, storage_message=False,
show_log=True, show_log=True,
) )

View File

@@ -29,7 +29,6 @@ class MessageSenderContainer:
self._task: Optional[asyncio.Task] = None self._task: Optional[asyncio.Task] = None
self._paused_event = asyncio.Event() self._paused_event = asyncio.Event()
self._paused_event.set() # 默认设置为非暂停状态 self._paused_event.set() # 默认设置为非暂停状态
async def add_message(self, chunk: str): async def add_message(self, chunk: str):
"""向队列中添加一个消息块。""" """向队列中添加一个消息块。"""
@@ -201,10 +200,10 @@ class S4UChat:
score = 0.0 score = 0.0
# 如果消息 @ 了机器人,则增加一个很大的分数 # 如果消息 @ 了机器人,则增加一个很大的分数
# if f"@{global_config.bot.nickname}" in message.processed_plain_text or any( # if f"@{global_config.bot.nickname}" in message.processed_plain_text or any(
# f"@{alias}" in message.processed_plain_text for alias in global_config.bot.alias_names # f"@{alias}" in message.processed_plain_text for alias in global_config.bot.alias_names
# ): # ):
# score += self.at_bot_priority_bonus # score += self.at_bot_priority_bonus
# 加上消息自带的优先级 # 加上消息自带的优先级
score += priority_info.get("message_priority", 0.0) score += priority_info.get("message_priority", 0.0)
@@ -214,9 +213,9 @@ class S4UChat:
async def add_message(self, message: MessageRecv) -> None: async def add_message(self, message: MessageRecv) -> None:
"""根据VIP状态和中断逻辑将消息放入相应队列。""" """根据VIP状态和中断逻辑将消息放入相应队列。"""
await self.relationship_builder.build_relation() await self.relationship_builder.build_relation()
priority_info = self._get_priority_info(message) priority_info = self._get_priority_info(message)
is_vip = self._is_vip(priority_info) is_vip = self._is_vip(priority_info)
new_priority_score = self._calculate_base_priority_score(message, priority_info) new_priority_score = self._calculate_base_priority_score(message, priority_info)
@@ -273,36 +272,38 @@ class S4UChat:
"""清理普通队列中不在最近N条消息范围内的消息""" """清理普通队列中不在最近N条消息范围内的消息"""
if self._normal_queue.empty(): if self._normal_queue.empty():
return return
# 计算阈值:保留最近 recent_message_keep_count 条消息 # 计算阈值:保留最近 recent_message_keep_count 条消息
cutoff_counter = max(0, self._entry_counter - self.recent_message_keep_count) cutoff_counter = max(0, self._entry_counter - self.recent_message_keep_count)
# 临时存储需要保留的消息 # 临时存储需要保留的消息
temp_messages = [] temp_messages = []
removed_count = 0 removed_count = 0
# 取出所有普通队列中的消息 # 取出所有普通队列中的消息
while not self._normal_queue.empty(): while not self._normal_queue.empty():
try: try:
item = self._normal_queue.get_nowait() item = self._normal_queue.get_nowait()
neg_priority, entry_count, timestamp, message = item neg_priority, entry_count, timestamp, message = item
# 如果消息在最近N条消息范围内保留它 # 如果消息在最近N条消息范围内保留它
if entry_count >= cutoff_counter: if entry_count >= cutoff_counter:
temp_messages.append(item) temp_messages.append(item)
else: else:
removed_count += 1 removed_count += 1
self._normal_queue.task_done() # 标记被移除的任务为完成 self._normal_queue.task_done() # 标记被移除的任务为完成
except asyncio.QueueEmpty: except asyncio.QueueEmpty:
break break
# 将保留的消息重新放入队列 # 将保留的消息重新放入队列
for item in temp_messages: for item in temp_messages:
self._normal_queue.put_nowait(item) self._normal_queue.put_nowait(item)
if removed_count > 0: if removed_count > 0:
logger.info(f"[{self.stream_name}] Cleaned up {removed_count} old normal messages outside recent {self.recent_message_keep_count} range.") logger.info(
f"[{self.stream_name}] Cleaned up {removed_count} old normal messages outside recent {self.recent_message_keep_count} range."
)
async def _message_processor(self): async def _message_processor(self):
"""调度器优先处理VIP队列然后处理普通队列。""" """调度器优先处理VIP队列然后处理普通队列。"""
@@ -311,7 +312,7 @@ class S4UChat:
# 等待有新消息的信号,避免空转 # 等待有新消息的信号,避免空转
await self._new_message_event.wait() await self._new_message_event.wait()
self._new_message_event.clear() self._new_message_event.clear()
# 清理普通队列中的过旧消息 # 清理普通队列中的过旧消息
self._cleanup_old_normal_messages() self._cleanup_old_normal_messages()
@@ -372,16 +373,16 @@ class S4UChat:
async def _generate_and_send(self, message: MessageRecv): async def _generate_and_send(self, message: MessageRecv):
"""为单个消息生成文本回复。整个过程可以被中断。""" """为单个消息生成文本回复。整个过程可以被中断。"""
self._is_replying = True self._is_replying = True
await send_loading(self.stream_id, "......") await send_loading(self.stream_id, "......")
# 视线管理:开始生成回复时切换视线状态 # 视线管理:开始生成回复时切换视线状态
chat_watching = watching_manager.get_watching_by_chat_id(self.stream_id) chat_watching = watching_manager.get_watching_by_chat_id(self.stream_id)
await chat_watching.on_reply_start() await chat_watching.on_reply_start()
# 回复生成实时展示:开始生成 # 回复生成实时展示:开始生成
user_name = message.message_info.user_info.user_nickname user_name = message.message_info.user_info.user_nickname
sender_container = MessageSenderContainer(self.chat_stream, message) sender_container = MessageSenderContainer(self.chat_stream, message)
sender_container.start() sender_container.start()
@@ -395,13 +396,11 @@ class S4UChat:
# a. 发送文本块 # a. 发送文本块
await sender_container.add_message(chunk) await sender_container.add_message(chunk)
# 等待所有文本消息发送完成 # 等待所有文本消息发送完成
await sender_container.close() await sender_container.close()
await sender_container.join() await sender_container.join()
logger.info(f"[{self.stream_name}] 所有文本块处理完毕。") logger.info(f"[{self.stream_name}] 所有文本块处理完毕。")
except asyncio.CancelledError: except asyncio.CancelledError:
@@ -412,13 +411,13 @@ class S4UChat:
# 回复生成实时展示:清空内容(出错时) # 回复生成实时展示:清空内容(出错时)
finally: finally:
self._is_replying = False self._is_replying = False
await send_unloading(self.stream_id) await send_unloading(self.stream_id)
# 视线管理:回复结束时切换视线状态 # 视线管理:回复结束时切换视线状态
chat_watching = watching_manager.get_watching_by_chat_id(self.stream_id) chat_watching = watching_manager.get_watching_by_chat_id(self.stream_id)
await chat_watching.on_reply_finished() await chat_watching.on_reply_finished()
# 确保发送器被妥善关闭(即使已关闭,再次调用也是安全的) # 确保发送器被妥善关闭(即使已关闭,再次调用也是安全的)
sender_container.resume() sender_container.resume()
if not sender_container._task.done(): if not sender_container._task.done():

View File

@@ -125,7 +125,7 @@ class ChatMood:
) )
self.last_change_time = 0 self.last_change_time = 0
# 发送初始情绪状态到ws端 # 发送初始情绪状态到ws端
asyncio.create_task(self.send_emotion_update(self.mood_values)) asyncio.create_task(self.send_emotion_update(self.mood_values))
@@ -231,10 +231,10 @@ class ChatMood:
if numerical_mood_response: if numerical_mood_response:
_old_mood_values = self.mood_values.copy() _old_mood_values = self.mood_values.copy()
self.mood_values = numerical_mood_response self.mood_values = numerical_mood_response
# 发送情绪更新到ws端 # 发送情绪更新到ws端
await self.send_emotion_update(self.mood_values) await self.send_emotion_update(self.mood_values)
logger.info(f"[{self.chat_id}] 情绪变化: {_old_mood_values} -> {self.mood_values}") logger.info(f"[{self.chat_id}] 情绪变化: {_old_mood_values} -> {self.mood_values}")
self.last_change_time = message_time self.last_change_time = message_time
@@ -308,10 +308,10 @@ class ChatMood:
if numerical_mood_response: if numerical_mood_response:
_old_mood_values = self.mood_values.copy() _old_mood_values = self.mood_values.copy()
self.mood_values = numerical_mood_response self.mood_values = numerical_mood_response
# 发送情绪更新到ws端 # 发送情绪更新到ws端
await self.send_emotion_update(self.mood_values) await self.send_emotion_update(self.mood_values)
logger.info(f"[{self.chat_id}] 情绪回归: {_old_mood_values} -> {self.mood_values}") logger.info(f"[{self.chat_id}] 情绪回归: {_old_mood_values} -> {self.mood_values}")
self.regression_count += 1 self.regression_count += 1
@@ -322,9 +322,9 @@ class ChatMood:
"joy": mood_values.get("joy", 5), "joy": mood_values.get("joy", 5),
"anger": mood_values.get("anger", 1), "anger": mood_values.get("anger", 1),
"sorrow": mood_values.get("sorrow", 1), "sorrow": mood_values.get("sorrow", 1),
"fear": mood_values.get("fear", 1) "fear": mood_values.get("fear", 1),
} }
await send_api.custom_to_stream( await send_api.custom_to_stream(
message_type="emotion", message_type="emotion",
content=emotion_data, content=emotion_data,
@@ -332,7 +332,7 @@ class ChatMood:
storage_message=False, storage_message=False,
show_log=True, show_log=True,
) )
logger.info(f"[{self.chat_id}] 发送情绪更新: {emotion_data}") logger.info(f"[{self.chat_id}] 发送情绪更新: {emotion_data}")
@@ -345,27 +345,27 @@ class MoodRegressionTask(AsyncTask):
async def run(self): async def run(self):
self.run_count += 1 self.run_count += 1
logger.info(f"[回归任务] 第{self.run_count}次检查,当前管理{len(self.mood_manager.mood_list)}个聊天的情绪状态") logger.info(f"[回归任务] 第{self.run_count}次检查,当前管理{len(self.mood_manager.mood_list)}个聊天的情绪状态")
now = time.time() now = time.time()
regression_executed = 0 regression_executed = 0
for mood in self.mood_manager.mood_list: for mood in self.mood_manager.mood_list:
chat_info = f"chat {mood.chat_id}" chat_info = f"chat {mood.chat_id}"
if mood.last_change_time == 0: if mood.last_change_time == 0:
logger.debug(f"[回归任务] {chat_info} 尚未有情绪变化,跳过回归") logger.debug(f"[回归任务] {chat_info} 尚未有情绪变化,跳过回归")
continue continue
time_since_last_change = now - mood.last_change_time time_since_last_change = now - mood.last_change_time
# 检查是否有极端情绪需要快速回归 # 检查是否有极端情绪需要快速回归
high_emotions = {k: v for k, v in mood.mood_values.items() if v >= 8} high_emotions = {k: v for k, v in mood.mood_values.items() if v >= 8}
has_extreme_emotion = len(high_emotions) > 0 has_extreme_emotion = len(high_emotions) > 0
# 回归条件1. 正常时间间隔(120s) 或 2. 有极端情绪且距上次变化>=30s # 回归条件1. 正常时间间隔(120s) 或 2. 有极端情绪且距上次变化>=30s
should_regress = False should_regress = False
regress_reason = "" regress_reason = ""
if time_since_last_change > 120: if time_since_last_change > 120:
should_regress = True should_regress = True
regress_reason = f"常规回归(距上次变化{int(time_since_last_change)}秒)" regress_reason = f"常规回归(距上次变化{int(time_since_last_change)}秒)"
@@ -373,24 +373,28 @@ class MoodRegressionTask(AsyncTask):
should_regress = True should_regress = True
high_emotion_str = ", ".join([f"{k}={v}" for k, v in high_emotions.items()]) 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)}秒)" regress_reason = f"极端情绪快速回归({high_emotion_str}, 距上次变化{int(time_since_last_change)}秒)"
if should_regress: if should_regress:
if mood.regression_count >= 3: if mood.regression_count >= 3:
logger.debug(f"[回归任务] {chat_info} 已达到最大回归次数(3次),停止回归") logger.debug(f"[回归任务] {chat_info} 已达到最大回归次数(3次),停止回归")
continue continue
logger.info(f"[回归任务] {chat_info} 开始情绪回归 ({regress_reason},第{mood.regression_count + 1}次回归)") logger.info(
f"[回归任务] {chat_info} 开始情绪回归 ({regress_reason},第{mood.regression_count + 1}次回归)"
)
await mood.regress_mood() await mood.regress_mood()
regression_executed += 1 regression_executed += 1
else: else:
if has_extreme_emotion: if has_extreme_emotion:
remaining_time = 5 - time_since_last_change remaining_time = 5 - time_since_last_change
high_emotion_str = ", ".join([f"{k}={v}" for k, v in high_emotions.items()]) 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)}") logger.debug(
f"[回归任务] {chat_info} 存在极端情绪({high_emotion_str}),距离快速回归还需等待{int(remaining_time)}"
)
else: else:
remaining_time = 120 - time_since_last_change remaining_time = 120 - time_since_last_change
logger.debug(f"[回归任务] {chat_info} 距离回归还需等待{int(remaining_time)}") logger.debug(f"[回归任务] {chat_info} 距离回归还需等待{int(remaining_time)}")
if regression_executed > 0: if regression_executed > 0:
logger.info(f"[回归任务] 本次执行了{regression_executed}个聊天的情绪回归") logger.info(f"[回归任务] 本次执行了{regression_executed}个聊天的情绪回归")
else: else:
@@ -409,11 +413,11 @@ class MoodManager:
return return
logger.info("启动情绪管理任务...") logger.info("启动情绪管理任务...")
# 启动情绪回归任务 # 启动情绪回归任务
regression_task = MoodRegressionTask(self) regression_task = MoodRegressionTask(self)
await async_task_manager.add_task(regression_task) await async_task_manager.add_task(regression_task)
self.task_started = True self.task_started = True
logger.info("情绪管理任务已启动(情绪回归)") logger.info("情绪管理任务已启动(情绪回归)")
@@ -435,7 +439,7 @@ class MoodManager:
# 发送重置后的情绪状态到ws端 # 发送重置后的情绪状态到ws端
asyncio.create_task(mood.send_emotion_update(mood.mood_values)) asyncio.create_task(mood.send_emotion_update(mood.mood_values))
return return
# 如果没有找到现有的mood创建新的 # 如果没有找到现有的mood创建新的
new_mood = ChatMood(chat_id) new_mood = ChatMood(chat_id)
self.mood_list.append(new_mood) self.mood_list.append(new_mood)

View File

@@ -103,7 +103,7 @@ class S4UMessageProcessor:
await s4u_chat.add_message(message) await s4u_chat.add_message(message)
interested_rate, _ = await _calculate_interest(message) interested_rate, _ = await _calculate_interest(message)
await mood_manager.start() await mood_manager.start()
chat_mood = mood_manager.get_mood_by_chat_id(chat.stream_id) chat_mood = mood_manager.get_mood_by_chat_id(chat.stream_id)
@@ -111,7 +111,7 @@ class S4UMessageProcessor:
chat_action = action_manager.get_action_state_by_chat_id(chat.stream_id) chat_action = action_manager.get_action_state_by_chat_id(chat.stream_id)
asyncio.create_task(chat_action.update_action_by_message(message)) asyncio.create_task(chat_action.update_action_by_message(message))
# asyncio.create_task(chat_action.update_facial_expression_by_message(message, interested_rate)) # asyncio.create_task(chat_action.update_facial_expression_by_message(message, interested_rate))
# 视线管理:收到消息时切换视线状态 # 视线管理:收到消息时切换视线状态
chat_watching = watching_manager.get_watching_by_chat_id(chat.stream_id) chat_watching = watching_manager.get_watching_by_chat_id(chat.stream_id)
asyncio.create_task(chat_watching.on_message_received()) asyncio.create_task(chat_watching.on_message_received())
@@ -124,25 +124,25 @@ class S4UMessageProcessor:
async def _handle_context_web_update(self, chat_id: str, message: MessageRecv): async def _handle_context_web_update(self, chat_id: str, message: MessageRecv):
"""处理上下文网页更新的独立task """处理上下文网页更新的独立task
Args: Args:
chat_id: 聊天ID chat_id: 聊天ID
message: 消息对象 message: 消息对象
""" """
try: try:
logger.debug(f"🔄 开始处理上下文网页更新: {message.message_info.user_info.user_nickname}") logger.debug(f"🔄 开始处理上下文网页更新: {message.message_info.user_info.user_nickname}")
context_manager = get_context_web_manager() context_manager = get_context_web_manager()
# 只在服务器未启动时启动(避免重复启动) # 只在服务器未启动时启动(避免重复启动)
if context_manager.site is None: if context_manager.site is None:
logger.info("🚀 首次启动上下文网页服务器...") logger.info("🚀 首次启动上下文网页服务器...")
await context_manager.start_server() await context_manager.start_server()
# 添加消息到上下文并更新网页 # 添加消息到上下文并更新网页
await context_manager.add_message(chat_id, message) await context_manager.add_message(chat_id, message)
logger.debug(f"✅ 上下文网页更新完成: {message.message_info.user_info.user_nickname}") logger.debug(f"✅ 上下文网页更新完成: {message.message_info.user_info.user_nickname}")
except Exception as e: except Exception as e:
logger.error(f"❌ 处理上下文网页更新失败: {e}", exc_info=True) logger.error(f"❌ 处理上下文网页更新失败: {e}", exc_info=True)

View File

@@ -107,7 +107,6 @@ class S4UStreamGenerator:
model_name: str, model_name: str,
**kwargs, **kwargs,
) -> AsyncGenerator[str, None]: ) -> AsyncGenerator[str, None]:
buffer = "" buffer = ""
delimiters = ",。!?,.!?\n\r" # For final trimming delimiters = ",。!?,.!?\n\r" # For final trimming
punctuation_buffer = "" punctuation_buffer = ""

View File

@@ -43,23 +43,24 @@ logger = get_logger("watching")
class WatchingState(Enum): class WatchingState(Enum):
"""视线状态枚举""" """视线状态枚举"""
WANDERING = "wandering" # 随意看 WANDERING = "wandering" # 随意看
DANMU = "danmu" # 看弹幕 DANMU = "danmu" # 看弹幕
LENS = "lens" # 看镜头 LENS = "lens" # 看镜头
class ChatWatching: class ChatWatching:
def __init__(self, chat_id: str): def __init__(self, chat_id: str):
self.chat_id: str = chat_id self.chat_id: str = chat_id
self.current_state: WatchingState = WatchingState.LENS # 默认看镜头 self.current_state: WatchingState = WatchingState.LENS # 默认看镜头
self.last_sent_state: Optional[WatchingState] = None # 上次发送的状态 self.last_sent_state: Optional[WatchingState] = None # 上次发送的状态
self.state_needs_update: bool = True # 是否需要更新状态 self.state_needs_update: bool = True # 是否需要更新状态
# 状态切换相关 # 状态切换相关
self.is_replying: bool = False # 是否正在生成回复 self.is_replying: bool = False # 是否正在生成回复
self.reply_finished_time: Optional[float] = None # 回复完成时间 self.reply_finished_time: Optional[float] = None # 回复完成时间
self.danmu_viewing_duration: float = 1.0 # 看弹幕持续时间(秒) self.danmu_viewing_duration: float = 1.0 # 看弹幕持续时间(秒)
logger.info(f"[{self.chat_id}] 视线管理器初始化,默认状态: {self.current_state.value}") logger.info(f"[{self.chat_id}] 视线管理器初始化,默认状态: {self.current_state.value}")
async def _change_state(self, new_state: WatchingState, reason: str = ""): async def _change_state(self, new_state: WatchingState, reason: str = ""):
@@ -69,7 +70,7 @@ class ChatWatching:
self.current_state = new_state self.current_state = new_state
self.state_needs_update = True self.state_needs_update = True
logger.info(f"[{self.chat_id}] 视线状态切换: {old_state.value}{new_state.value} ({reason})") logger.info(f"[{self.chat_id}] 视线状态切换: {old_state.value}{new_state.value} ({reason})")
# 立即发送视线状态更新 # 立即发送视线状态更新
await self._send_watching_update() await self._send_watching_update()
else: else:
@@ -86,7 +87,7 @@ class ChatWatching:
"""开始生成回复时调用""" """开始生成回复时调用"""
self.is_replying = True self.is_replying = True
self.reply_finished_time = None self.reply_finished_time = None
if look_at_lens: if look_at_lens:
await self._change_state(WatchingState.LENS, "开始生成回复-看镜头") await self._change_state(WatchingState.LENS, "开始生成回复-看镜头")
else: else:
@@ -96,35 +97,29 @@ class ChatWatching:
"""生成回复完毕时调用""" """生成回复完毕时调用"""
self.is_replying = False self.is_replying = False
self.reply_finished_time = time.time() self.reply_finished_time = time.time()
# 先看弹幕1秒 # 先看弹幕1秒
await self._change_state(WatchingState.DANMU, "回复完毕-看弹幕") await self._change_state(WatchingState.DANMU, "回复完毕-看弹幕")
logger.info(f"[{self.chat_id}] 回复完毕,将看弹幕{self.danmu_viewing_duration}秒后转为看镜头") logger.info(f"[{self.chat_id}] 回复完毕,将看弹幕{self.danmu_viewing_duration}秒后转为看镜头")
# 设置定时器1秒后自动切换到看镜头 # 设置定时器1秒后自动切换到看镜头
asyncio.create_task(self._auto_switch_to_lens()) asyncio.create_task(self._auto_switch_to_lens())
async def _auto_switch_to_lens(self): async def _auto_switch_to_lens(self):
"""自动切换到看镜头(延迟执行)""" """自动切换到看镜头(延迟执行)"""
await asyncio.sleep(self.danmu_viewing_duration) await asyncio.sleep(self.danmu_viewing_duration)
# 检查是否仍需要切换(可能状态已经被其他事件改变) # 检查是否仍需要切换(可能状态已经被其他事件改变)
if (self.reply_finished_time is not None and if self.reply_finished_time is not None and self.current_state == WatchingState.DANMU and not self.is_replying:
self.current_state == WatchingState.DANMU and
not self.is_replying):
await self._change_state(WatchingState.LENS, "看弹幕时间结束") await self._change_state(WatchingState.LENS, "看弹幕时间结束")
self.reply_finished_time = None # 重置完成时间 self.reply_finished_time = None # 重置完成时间
async def _send_watching_update(self): async def _send_watching_update(self):
"""立即发送视线状态更新""" """立即发送视线状态更新"""
await send_api.custom_to_stream( await send_api.custom_to_stream(
message_type="watching", message_type="watching", content=self.current_state.value, stream_id=self.chat_id, storage_message=False
content=self.current_state.value,
stream_id=self.chat_id,
storage_message=False
) )
logger.info(f"[{self.chat_id}] 发送视线状态更新: {self.current_state.value}") logger.info(f"[{self.chat_id}] 发送视线状态更新: {self.current_state.value}")
self.last_sent_state = self.current_state self.last_sent_state = self.current_state
self.state_needs_update = False self.state_needs_update = False
@@ -139,11 +134,10 @@ class ChatWatching:
"current_state": self.current_state.value, "current_state": self.current_state.value,
"is_replying": self.is_replying, "is_replying": self.is_replying,
"reply_finished_time": self.reply_finished_time, "reply_finished_time": self.reply_finished_time,
"state_needs_update": self.state_needs_update "state_needs_update": self.state_needs_update,
} }
class WatchingManager: class WatchingManager:
def __init__(self): def __init__(self):
self.watching_list: list[ChatWatching] = [] self.watching_list: list[ChatWatching] = []
@@ -156,7 +150,7 @@ class WatchingManager:
return return
logger.info("启动视线管理系统...") logger.info("启动视线管理系统...")
self.task_started = True self.task_started = True
logger.info("视线管理系统已启动(状态变化时立即发送)") logger.info("视线管理系统已启动(状态变化时立即发送)")
@@ -169,10 +163,10 @@ class WatchingManager:
new_watching = ChatWatching(chat_id) new_watching = ChatWatching(chat_id)
self.watching_list.append(new_watching) self.watching_list.append(new_watching)
logger.info(f"为chat {chat_id}创建新的视线管理器") logger.info(f"为chat {chat_id}创建新的视线管理器")
# 发送初始状态 # 发送初始状态
asyncio.create_task(new_watching._send_watching_update()) asyncio.create_task(new_watching._send_watching_update())
return new_watching return new_watching
def reset_watching_by_chat_id(self, chat_id: str): def reset_watching_by_chat_id(self, chat_id: str):
@@ -185,27 +179,24 @@ class WatchingManager:
watching.is_replying = False watching.is_replying = False
watching.reply_finished_time = None watching.reply_finished_time = None
logger.info(f"[{chat_id}] 视线状态已重置为默认状态") logger.info(f"[{chat_id}] 视线状态已重置为默认状态")
# 发送重置后的状态 # 发送重置后的状态
asyncio.create_task(watching._send_watching_update()) asyncio.create_task(watching._send_watching_update())
return return
# 如果没有找到现有的watching创建新的 # 如果没有找到现有的watching创建新的
new_watching = ChatWatching(chat_id) new_watching = ChatWatching(chat_id)
self.watching_list.append(new_watching) self.watching_list.append(new_watching)
logger.info(f"为chat {chat_id}创建并重置视线管理器") logger.info(f"为chat {chat_id}创建并重置视线管理器")
# 发送初始状态 # 发送初始状态
asyncio.create_task(new_watching._send_watching_update()) asyncio.create_task(new_watching._send_watching_update())
def get_all_watching_info(self) -> dict: def get_all_watching_info(self) -> dict:
"""获取所有聊天的视线状态信息(用于调试)""" """获取所有聊天的视线状态信息(用于调试)"""
return { return {watching.chat_id: watching.get_state_info() for watching in self.watching_list}
watching.chat_id: watching.get_state_info()
for watching in self.watching_list
}
# 全局视线管理器实例 # 全局视线管理器实例
watching_manager = WatchingManager() watching_manager = WatchingManager()
"""全局视线管理器""" """全局视线管理器"""

View File

@@ -46,10 +46,10 @@ def init_prompt():
class ChatMood: class ChatMood:
def __init__(self, chat_id: str): def __init__(self, chat_id: str):
self.chat_id: str = chat_id self.chat_id: str = chat_id
chat_manager = get_chat_manager() chat_manager = get_chat_manager()
self.chat_stream = chat_manager.get_stream(self.chat_id) self.chat_stream = chat_manager.get_stream(self.chat_id)
self.log_prefix = f"[{self.chat_stream.group_info.group_name if self.chat_stream.group_info else self.chat_stream.user_info.user_nickname}]" self.log_prefix = f"[{self.chat_stream.group_info.group_name if self.chat_stream.group_info else self.chat_stream.user_info.user_nickname}]"
self.mood_state: str = "感觉很平静" self.mood_state: str = "感觉很平静"
@@ -92,7 +92,7 @@ class ChatMood:
chat_id=self.chat_id, chat_id=self.chat_id,
timestamp_start=self.last_change_time, timestamp_start=self.last_change_time,
timestamp_end=message_time, timestamp_end=message_time,
limit=int(global_config.chat.max_context_size/3), limit=int(global_config.chat.max_context_size / 3),
limit_mode="last", limit_mode="last",
) )
chat_talking_prompt = build_readable_messages( chat_talking_prompt = build_readable_messages(
@@ -121,14 +121,12 @@ class ChatMood:
mood_state=self.mood_state, mood_state=self.mood_state,
) )
response, (reasoning_content, model_name) = await self.mood_model.generate_response_async(prompt=prompt) response, (reasoning_content, model_name) = await self.mood_model.generate_response_async(prompt=prompt)
if global_config.debug.show_prompt: if global_config.debug.show_prompt:
logger.info(f"{self.log_prefix} prompt: {prompt}") logger.info(f"{self.log_prefix} prompt: {prompt}")
logger.info(f"{self.log_prefix} response: {response}") logger.info(f"{self.log_prefix} response: {response}")
logger.info(f"{self.log_prefix} reasoning_content: {reasoning_content}") logger.info(f"{self.log_prefix} reasoning_content: {reasoning_content}")
logger.info(f"{self.log_prefix} 情绪状态更新为: {response}") logger.info(f"{self.log_prefix} 情绪状态更新为: {response}")
self.mood_state = response self.mood_state = response
@@ -170,15 +168,14 @@ class ChatMood:
mood_state=self.mood_state, mood_state=self.mood_state,
) )
response, (reasoning_content, model_name) = await self.mood_model.generate_response_async(prompt=prompt) response, (reasoning_content, model_name) = await self.mood_model.generate_response_async(prompt=prompt)
if global_config.debug.show_prompt: if global_config.debug.show_prompt:
logger.info(f"{self.log_prefix} prompt: {prompt}") logger.info(f"{self.log_prefix} prompt: {prompt}")
logger.info(f"{self.log_prefix} response: {response}") logger.info(f"{self.log_prefix} response: {response}")
logger.info(f"{self.log_prefix} reasoning_content: {reasoning_content}") logger.info(f"{self.log_prefix} reasoning_content: {reasoning_content}")
logger.info(f"{self.log_prefix} 情绪状态回归为: {response}") logger.info(f"{self.log_prefix} 情绪状态回归为: {response}")
self.mood_state = response self.mood_state = response

View File

@@ -39,7 +39,12 @@ class ChatManager:
Returns: Returns:
List[ChatStream]: 聊天流列表 List[ChatStream]: 聊天流列表
Raises:
TypeError: 如果 platform 不是字符串或 SpecialTypes 枚举类型
""" """
if not isinstance(platform, (str, SpecialTypes)):
raise TypeError("platform 必须是字符串或是 SpecialTypes 枚举")
streams = [] streams = []
try: try:
for _, stream in get_chat_manager().streams.items(): for _, stream in get_chat_manager().streams.items():
@@ -60,6 +65,8 @@ class ChatManager:
Returns: Returns:
List[ChatStream]: 群聊聊天流列表 List[ChatStream]: 群聊聊天流列表
""" """
if not isinstance(platform, (str, SpecialTypes)):
raise TypeError("platform 必须是字符串或是 SpecialTypes 枚举")
streams = [] streams = []
try: try:
for _, stream in get_chat_manager().streams.items(): for _, stream in get_chat_manager().streams.items():
@@ -79,7 +86,12 @@ class ChatManager:
Returns: Returns:
List[ChatStream]: 私聊聊天流列表 List[ChatStream]: 私聊聊天流列表
Raises:
TypeError: 如果 platform 不是字符串或 SpecialTypes 枚举类型
""" """
if not isinstance(platform, (str, SpecialTypes)):
raise TypeError("platform 必须是字符串或是 SpecialTypes 枚举")
streams = [] streams = []
try: try:
for _, stream in get_chat_manager().streams.items(): for _, stream in get_chat_manager().streams.items():
@@ -102,7 +114,17 @@ class ChatManager:
Returns: Returns:
Optional[ChatStream]: 聊天流对象如果未找到返回None Optional[ChatStream]: 聊天流对象如果未找到返回None
Raises:
ValueError: 如果 group_id 为空字符串
TypeError: 如果 group_id 不是字符串类型或 platform 不是字符串或 SpecialTypes
""" """
if not isinstance(group_id, str):
raise TypeError("group_id 必须是字符串类型")
if not isinstance(platform, (str, SpecialTypes)):
raise TypeError("platform 必须是字符串或是 SpecialTypes 枚举")
if not group_id:
raise ValueError("group_id 不能为空")
try: try:
for _, stream in get_chat_manager().streams.items(): for _, stream in get_chat_manager().streams.items():
if ( if (
@@ -129,7 +151,17 @@ class ChatManager:
Returns: Returns:
Optional[ChatStream]: 聊天流对象如果未找到返回None Optional[ChatStream]: 聊天流对象如果未找到返回None
Raises:
ValueError: 如果 user_id 为空字符串
TypeError: 如果 user_id 不是字符串类型或 platform 不是字符串或 SpecialTypes
""" """
if not isinstance(user_id, str):
raise TypeError("user_id 必须是字符串类型")
if not isinstance(platform, (str, SpecialTypes)):
raise TypeError("platform 必须是字符串或是 SpecialTypes 枚举")
if not user_id:
raise ValueError("user_id 不能为空")
try: try:
for _, stream in get_chat_manager().streams.items(): for _, stream in get_chat_manager().streams.items():
if ( if (
@@ -153,9 +185,15 @@ class ChatManager:
Returns: Returns:
str: 聊天类型 ("group", "private", "unknown") str: 聊天类型 ("group", "private", "unknown")
Raises:
TypeError: 如果 chat_stream 不是 ChatStream 类型
ValueError: 如果 chat_stream 为空
""" """
if not isinstance(chat_stream, ChatStream):
raise TypeError("chat_stream 必须是 ChatStream 类型")
if not chat_stream: if not chat_stream:
raise ValueError("chat_stream cannot be None") raise ValueError("chat_stream 不能为 None")
if hasattr(chat_stream, "group_info"): if hasattr(chat_stream, "group_info"):
return "group" if chat_stream.group_info else "private" return "group" if chat_stream.group_info else "private"
@@ -170,9 +208,15 @@ class ChatManager:
Returns: Returns:
Dict[str, Any]: 聊天流信息字典 Dict[str, Any]: 聊天流信息字典
Raises:
TypeError: 如果 chat_stream 不是 ChatStream 类型
ValueError: 如果 chat_stream 为空
""" """
if not chat_stream: if not chat_stream:
return {} raise ValueError("chat_stream 不能为 None")
if not isinstance(chat_stream, ChatStream):
raise TypeError("chat_stream 必须是 ChatStream 类型")
try: try:
info: Dict[str, Any] = { info: Dict[str, Any] = {

View File

@@ -8,6 +8,8 @@
count = emoji_api.get_count() count = emoji_api.get_count()
""" """
import random
from typing import Optional, Tuple, List from typing import Optional, Tuple, List
from src.common.logger import get_logger from src.common.logger import get_logger
from src.chat.emoji_system.emoji_manager import get_emoji_manager from src.chat.emoji_system.emoji_manager import get_emoji_manager
@@ -29,7 +31,15 @@ async def get_by_description(description: str) -> Optional[Tuple[str, str, str]]
Returns: Returns:
Optional[Tuple[str, str, str]]: (base64编码, 表情包描述, 匹配的情感标签) 或 None Optional[Tuple[str, str, str]]: (base64编码, 表情包描述, 匹配的情感标签) 或 None
Raises:
ValueError: 如果描述为空字符串
TypeError: 如果描述不是字符串类型
""" """
if not description:
raise ValueError("描述不能为空")
if not isinstance(description, str):
raise TypeError("描述必须是字符串类型")
try: try:
logger.debug(f"[EmojiAPI] 根据描述获取表情包: {description}") logger.debug(f"[EmojiAPI] 根据描述获取表情包: {description}")
@@ -55,7 +65,7 @@ async def get_by_description(description: str) -> Optional[Tuple[str, str, str]]
return None return None
async def get_random(count: int = 1) -> Optional[List[Tuple[str, str, str]]]: async def get_random(count: Optional[int] = 1) -> Optional[List[Tuple[str, str, str]]]:
"""随机获取指定数量的表情包 """随机获取指定数量的表情包
Args: Args:
@@ -63,8 +73,17 @@ async def get_random(count: int = 1) -> Optional[List[Tuple[str, str, str]]]:
Returns: Returns:
Optional[List[Tuple[str, str, str]]]: 包含(base64编码, 表情包描述, 随机情感标签)的元组列表如果失败则为None Optional[List[Tuple[str, str, str]]]: 包含(base64编码, 表情包描述, 随机情感标签)的元组列表如果失败则为None
Raises:
TypeError: 如果count不是整数类型
ValueError: 如果count为负数
""" """
if count <= 0: if not isinstance(count, int):
raise TypeError("count 必须是整数类型")
if count < 0:
raise ValueError("count 不能为负数")
if count == 0:
logger.warning("[EmojiAPI] count 为0返回空列表")
return [] return []
try: try:
@@ -90,8 +109,6 @@ async def get_random(count: int = 1) -> Optional[List[Tuple[str, str, str]]]:
count = len(valid_emojis) count = len(valid_emojis)
# 随机选择 # 随机选择
import random
selected_emojis = random.sample(valid_emojis, count) selected_emojis = random.sample(valid_emojis, count)
results = [] results = []
@@ -128,7 +145,15 @@ async def get_by_emotion(emotion: str) -> Optional[Tuple[str, str, str]]:
Returns: Returns:
Optional[Tuple[str, str, str]]: (base64编码, 表情包描述, 匹配的情感标签) 或 None Optional[Tuple[str, str, str]]: (base64编码, 表情包描述, 匹配的情感标签) 或 None
Raises:
ValueError: 如果情感标签为空字符串
TypeError: 如果情感标签不是字符串类型
""" """
if not emotion:
raise ValueError("情感标签不能为空")
if not isinstance(emotion, str):
raise TypeError("情感标签必须是字符串类型")
try: try:
logger.info(f"[EmojiAPI] 根据情感获取表情包: {emotion}") logger.info(f"[EmojiAPI] 根据情感获取表情包: {emotion}")
@@ -146,8 +171,6 @@ async def get_by_emotion(emotion: str) -> Optional[Tuple[str, str, str]]:
return None return None
# 随机选择匹配的表情包 # 随机选择匹配的表情包
import random
selected_emoji = random.choice(matching_emojis) selected_emoji = random.choice(matching_emojis)
emoji_base64 = image_path_to_base64(selected_emoji.full_path) emoji_base64 = image_path_to_base64(selected_emoji.full_path)
@@ -185,11 +208,11 @@ def get_count() -> int:
return 0 return 0
def get_info() -> dict: def get_info():
"""获取表情包系统信息 """获取表情包系统信息
Returns: Returns:
dict: 包含表情包数量、最大数量信息 dict: 包含表情包数量、最大数量、可用数量信息
""" """
try: try:
emoji_manager = get_emoji_manager() emoji_manager = get_emoji_manager()
@@ -203,7 +226,7 @@ def get_info() -> dict:
return {"current_count": 0, "max_count": 0, "available_emojis": 0} return {"current_count": 0, "max_count": 0, "available_emojis": 0}
def get_emotions() -> list: def get_emotions() -> List[str]:
"""获取所有可用的情感标签 """获取所有可用的情感标签
Returns: Returns:
@@ -223,7 +246,7 @@ def get_emotions() -> list:
return [] return []
def get_descriptions() -> list: def get_descriptions() -> List[str]:
"""获取所有表情包描述 """获取所有表情包描述
Returns: Returns:

View File

@@ -5,11 +5,12 @@
使用方式: 使用方式:
from src.plugin_system.apis import generator_api from src.plugin_system.apis import generator_api
replyer = generator_api.get_replyer(chat_stream) replyer = generator_api.get_replyer(chat_stream)
success, reply_set = await generator_api.generate_reply(chat_stream, action_data, reasoning) success, reply_set, _ = await generator_api.generate_reply(chat_stream, action_data, reasoning)
""" """
import traceback import traceback
from typing import Tuple, Any, Dict, List, Optional from typing import Tuple, Any, Dict, List, Optional
from rich.traceback import install
from src.common.logger import get_logger from src.common.logger import get_logger
from src.chat.replyer.default_generator import DefaultReplyer from src.chat.replyer.default_generator import DefaultReplyer
from src.chat.message_receive.chat_stream import ChatStream from src.chat.message_receive.chat_stream import ChatStream
@@ -17,6 +18,8 @@ from src.chat.utils.utils import process_llm_response
from src.chat.replyer.replyer_manager import replyer_manager from src.chat.replyer.replyer_manager import replyer_manager
from src.plugin_system.base.component_types import ActionInfo from src.plugin_system.base.component_types import ActionInfo
install(extra_lines=3)
logger = get_logger("generator_api") logger = get_logger("generator_api")
@@ -44,7 +47,12 @@ def get_replyer(
Returns: Returns:
Optional[DefaultReplyer]: 回复器对象如果获取失败则返回None Optional[DefaultReplyer]: 回复器对象如果获取失败则返回None
Raises:
ValueError: chat_stream 和 chat_id 均为空
""" """
if not chat_id and not chat_stream:
raise ValueError("chat_stream 和 chat_id 不可均为空")
try: try:
logger.debug(f"[GeneratorAPI] 正在获取回复器chat_id: {chat_id}, chat_stream: {'' if chat_stream else ''}") logger.debug(f"[GeneratorAPI] 正在获取回复器chat_id: {chat_id}, chat_stream: {'' if chat_stream else ''}")
return replyer_manager.get_replyer( return replyer_manager.get_replyer(

View File

@@ -14,7 +14,6 @@ from src.config.config import global_config
logger = get_logger("llm_api") logger = get_logger("llm_api")
# ============================================================================= # =============================================================================
# LLM模型API函数 # LLM模型API函数
# ============================================================================= # =============================================================================
@@ -31,8 +30,21 @@ def get_available_models() -> Dict[str, Any]:
logger.error("[LLMAPI] 无法获取模型列表:全局配置中未找到 model 配置") logger.error("[LLMAPI] 无法获取模型列表:全局配置中未找到 model 配置")
return {} return {}
# 自动获取所有属性并转换为字典形式
rets = {}
models = global_config.model models = global_config.model
return models attrs = dir(models)
for attr in attrs:
if not attr.startswith("__"):
try:
value = getattr(models, attr)
if not callable(value): # 排除方法
rets[attr] = value
except Exception as e:
logger.debug(f"[LLMAPI] 获取属性 {attr} 失败: {e}")
continue
return rets
except Exception as e: except Exception as e:
logger.error(f"[LLMAPI] 获取可用模型失败: {e}") logger.error(f"[LLMAPI] 获取可用模型失败: {e}")
return {} return {}

View File

@@ -114,7 +114,11 @@ async def _send_to_target(
# 发送消息 # 发送消息
sent_msg = await heart_fc_sender.send_message( sent_msg = await heart_fc_sender.send_message(
bot_message, typing=typing, set_reply=(anchor_message is not None), storage_message=storage_message, show_log=show_log bot_message,
typing=typing,
set_reply=(anchor_message is not None),
storage_message=storage_message,
show_log=show_log,
) )
if sent_msg: if sent_msg:
@@ -363,7 +367,9 @@ async def custom_to_stream(
Returns: Returns:
bool: 是否发送成功 bool: 是否发送成功
""" """
return await _send_to_target(message_type, content, stream_id, display_message, typing, reply_to, storage_message, show_log) return await _send_to_target(
message_type, content, stream_id, display_message, typing, reply_to, storage_message, show_log
)
async def text_to_group( async def text_to_group(

View File

@@ -75,7 +75,7 @@ class ReplyAction(BaseAction):
reply_to = self.action_data.get("reply_to", "") reply_to = self.action_data.get("reply_to", "")
sender, target = self._parse_reply_target(reply_to) sender, target = self._parse_reply_target(reply_to)
try: try:
prepared_reply = self.action_data.get("prepared_reply", "") prepared_reply = self.action_data.get("prepared_reply", "")
if not prepared_reply: if not prepared_reply: