diff --git a/src/mais4u/config/s4u_config.toml b/src/mais4u/config/s4u_config.toml index 482bdc255..26fdef449 100644 --- a/src/mais4u/config/s4u_config.toml +++ b/src/mais4u/config/s4u_config.toml @@ -1,5 +1,5 @@ [inner] -version = "1.0.1" +version = "1.1.0" #----以下是S4U聊天系统配置文件---- # S4U (Smart 4 U) 聊天系统是MaiBot的核心对话模块 @@ -13,8 +13,8 @@ version = "1.0.1" [s4u] # 消息管理配置 -message_timeout_seconds = 120 # 普通消息存活时间(秒),超过此时间的消息将被丢弃 -recent_message_keep_count = 6 # 保留最近N条消息,超出范围的普通消息将被移除 +message_timeout_seconds = 80 # 普通消息存活时间(秒),超过此时间的消息将被丢弃 +recent_message_keep_count = 8 # 保留最近N条消息,超出范围的普通消息将被移除 # 优先级系统配置 at_bot_priority_bonus = 100.0 # @机器人时的优先级加成分数 @@ -34,7 +34,99 @@ max_typing_delay = 2.0 # 最大打字延迟(秒) enable_old_message_cleanup = true # 是否自动清理过旧的普通消息 enable_loading_indicator = true # 是否显示加载提示 -enable_streaming_output = true # 是否启用流式输出,false时全部生成后一次性发送 +enable_streaming_output = false # 是否启用流式输出,false时全部生成后一次性发送 -max_context_message_length = 20 -max_core_message_length = 30 \ No newline at end of file +max_context_message_length = 30 +max_core_message_length = 20 + +# 模型配置 +[models] +# 主要对话模型配置 +[models.chat] +name = "qwen3-8b" +provider = "BAILIAN" +pri_in = 0.5 +pri_out = 2 +temp = 0.7 +enable_thinking = false + +# 规划模型配置 +[models.motion] +name = "qwen3-8b" +provider = "BAILIAN" +pri_in = 0.5 +pri_out = 2 +temp = 0.7 +enable_thinking = false + +# 情感分析模型配置 +[models.emotion] +name = "qwen3-8b" +provider = "BAILIAN" +pri_in = 0.5 +pri_out = 2 +temp = 0.7 + +# 记忆模型配置 +[models.memory] +name = "qwen3-8b" +provider = "BAILIAN" +pri_in = 0.5 +pri_out = 2 +temp = 0.7 + +# 工具使用模型配置 +[models.tool_use] +name = "qwen3-8b" +provider = "BAILIAN" +pri_in = 0.5 +pri_out = 2 +temp = 0.7 + +# 嵌入模型配置 +[models.embedding] +name = "text-embedding-v1" +provider = "OPENAI" +dimension = 1024 + +# 视觉语言模型配置 +[models.vlm] +name = "qwen-vl-plus" +provider = "BAILIAN" +pri_in = 0.5 +pri_out = 2 +temp = 0.7 + +# 知识库模型配置 +[models.knowledge] +name = "qwen3-8b" +provider = "BAILIAN" +pri_in = 0.5 +pri_out = 2 +temp = 0.7 + +# 实体提取模型配置 +[models.entity_extract] +name = "qwen3-8b" +provider = "BAILIAN" +pri_in = 0.5 +pri_out = 2 +temp = 0.7 + +# 问答模型配置 +[models.qa] +name = "qwen3-8b" +provider = "BAILIAN" +pri_in = 0.5 +pri_out = 2 +temp = 0.7 + +# 兼容性配置(已废弃,请使用models.motion) +[model_motion] # 在麦麦的一些组件中使用的小模型,消耗量较大,建议使用速度较快的小模型 +# 强烈建议使用免费的小模型 +name = "qwen3-8b" +provider = "BAILIAN" +pri_in = 0.5 +pri_out = 2 +temp = 0.7 +enable_thinking = false # 是否启用思考 \ No newline at end of file diff --git a/src/mais4u/config/s4u_config_template.toml b/src/mais4u/config/s4u_config_template.toml index 482bdc255..40adb1f63 100644 --- a/src/mais4u/config/s4u_config_template.toml +++ b/src/mais4u/config/s4u_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "1.0.1" +version = "1.1.0" #----以下是S4U聊天系统配置文件---- # S4U (Smart 4 U) 聊天系统是MaiBot的核心对话模块 @@ -32,9 +32,36 @@ max_typing_delay = 2.0 # 最大打字延迟(秒) # 系统功能开关 enable_old_message_cleanup = true # 是否自动清理过旧的普通消息 -enable_loading_indicator = true # 是否显示加载提示 enable_streaming_output = true # 是否启用流式输出,false时全部生成后一次性发送 max_context_message_length = 20 -max_core_message_length = 30 \ No newline at end of file +max_core_message_length = 30 + +# 模型配置 +[models] +# 主要对话模型配置 +[models.chat] +name = "qwen3-8b" +provider = "BAILIAN" +pri_in = 0.5 +pri_out = 2 +temp = 0.7 +enable_thinking = false + +# 规划模型配置 +[models.motion] +name = "qwen3-32b" +provider = "BAILIAN" +pri_in = 0.5 +pri_out = 2 +temp = 0.7 +enable_thinking = false + +# 情感分析模型配置 +[models.emotion] +name = "qwen3-8b" +provider = "BAILIAN" +pri_in = 0.5 +pri_out = 2 +temp = 0.7 diff --git a/src/mais4u/mais4u_chat/SUPERCHAT_MANAGER_README.md b/src/mais4u/mais4u_chat/SUPERCHAT_MANAGER_README.md deleted file mode 100644 index 359a00ef6..000000000 --- a/src/mais4u/mais4u_chat/SUPERCHAT_MANAGER_README.md +++ /dev/null @@ -1,134 +0,0 @@ -# SuperChat管理器使用说明 - -## 概述 - -SuperChat管理器是用于管理和跟踪超级弹幕消息的核心组件。它能够根据SuperChat的金额自动设置不同的存活时间,并提供多种格式的字符串构建功能。 - -## 主要功能 - -### 1. 自动记录SuperChat -当收到SuperChat消息时,管理器会自动记录以下信息: -- 用户ID和昵称 -- 平台信息 -- 聊天ID -- SuperChat金额和消息内容 -- 时间戳和过期时间 -- 群组名称(如果适用) - -### 2. 基于金额的存活时间 - -SuperChat的存活时间根据金额阶梯设置: - -| 金额范围 | 存活时间 | -|---------|---------| -| ≥500元 | 4小时 | -| 200-499元 | 2小时 | -| 100-199元 | 1小时 | -| 50-99元 | 30分钟 | -| 20-49元 | 15分钟 | -| 10-19元 | 10分钟 | -| <10元 | 5分钟 | - -### 3. 自动清理 -管理器每30秒自动检查并清理过期的SuperChat记录,保持内存使用的高效性。 - -## 使用方法 - -### 基本用法 - -```python -from src.mais4u.mais4u_chat.super_chat_manager import get_super_chat_manager - -# 获取全局管理器实例 -super_chat_manager = get_super_chat_manager() - -# 添加SuperChat(通常在消息处理时自动调用) -await super_chat_manager.add_superchat(message) - -# 获取指定聊天的SuperChat显示字符串 -display_string = super_chat_manager.build_superchat_display_string(chat_id, max_count=10) - -# 获取摘要信息 -summary = super_chat_manager.build_superchat_summary_string(chat_id) - -# 获取统计信息 -stats = super_chat_manager.get_superchat_statistics(chat_id) -``` - -### 结合S4UChat使用 - -```python -from src.mais4u.mais4u_chat.s4u_chat import get_s4u_chat_manager - -# 获取S4UChat实例 -s4u_manager = get_s4u_chat_manager() -s4u_chat = s4u_manager.get_or_create_chat(chat_stream) - -# 便捷方法获取SuperChat信息 -display_string = s4u_chat.get_superchat_display_string(max_count=10) -summary = s4u_chat.get_superchat_summary_string() -stats = s4u_chat.get_superchat_statistics() -``` - -## API 参考 - -### SuperChatManager类 - -#### 主要方法 - -- `add_superchat(message: MessageRecvS4U)`: 添加SuperChat记录 -- `get_superchats_by_chat(chat_id: str)`: 获取指定聊天的有效SuperChat列表 -- `build_superchat_display_string(chat_id: str, max_count: int = 10)`: 构建显示字符串 -- `build_superchat_summary_string(chat_id: str)`: 构建摘要字符串 -- `get_superchat_statistics(chat_id: str)`: 获取统计信息 - -#### 输出格式示例 - -**显示字符串格式:** -``` -📢 当前有效超级弹幕: -1. 【100元】用户名: 消息内容 (剩余25分30秒) -2. 【50元】用户名: 消息内容 (剩余10分15秒) -... 还有3条SuperChat -``` - -**摘要字符串格式:** -``` -当前有5条超级弹幕,总金额350元,最高单笔100元 -``` - -**统计信息格式:** -```python -{ - "count": 5, - "total_amount": 350.0, - "average_amount": 70.0, - "highest_amount": 100.0, - "lowest_amount": 20.0 -} -``` - -### S4UChat扩展方法 - -- `get_superchat_display_string(max_count: int = 10)`: 获取当前聊天的SuperChat显示字符串 -- `get_superchat_summary_string()`: 获取当前聊天的SuperChat摘要字符串 -- `get_superchat_statistics()`: 获取当前聊天的SuperChat统计信息 - -## 集成说明 - -SuperChat管理器已经集成到S4U聊天系统中: - -1. **自动处理**: 当S4UChat收到SuperChat消息时,会自动调用管理器记录 -2. **内存管理**: 管理器会自动清理过期的SuperChat,无需手动管理 -3. **全局单例**: 使用全局单例模式,确保所有聊天共享同一个管理器实例 - -## 注意事项 - -1. SuperChat管理器是全局单例,在应用程序整个生命周期中保持运行 -2. 过期时间基于消息金额自动计算,无需手动设置 -3. 管理器会自动处理异常情况,如无效的价格格式等 -4. 清理任务在后台异步运行,不会阻塞主要功能 - -## 示例文件 - -参考 `superchat_example.py` 文件查看完整的使用示例。 \ No newline at end of file diff --git a/src/mais4u/mais4u_chat/body_emotion_action_manager.py b/src/mais4u/mais4u_chat/body_emotion_action_manager.py index 13d84cdb3..9c45c0a4b 100644 --- a/src/mais4u/mais4u_chat/body_emotion_action_manager.py +++ b/src/mais4u/mais4u_chat/body_emotion_action_manager.py @@ -1,6 +1,6 @@ import json import time - +import random from src.chat.message_receive.message import MessageRecv from src.llm_models.utils_model import LLMRequest from src.common.logger import get_logger @@ -8,10 +8,35 @@ from src.chat.utils.chat_message_builder import build_readable_messages, get_raw from src.config.config import global_config from src.chat.utils.prompt_builder import Prompt, global_prompt_manager from src.manager.async_task_manager import AsyncTask, async_task_manager +from src.plugin_system.apis import send_api from json_repair import repair_json +from src.mais4u.s4u_config import s4u_config logger = get_logger("action") +HEAD_CODE = { + "看向上方": "(0,0.5,0)", + "看向下方": "(0,-0.5,0)", + "看向左边": "(-1,0,0)", + "看向右边": "(1,0,0)", + "随意朝向": "random", + "看向摄像机": "camera", + "注视对方": "(0,0,0)", + "看向正前方": "(0,0,0)", +} + +BODY_CODE = { + "双手背后向前弯腰": "010_0070", + "歪头双手合十": "010_0100", + "标准文静站立": "010_0101", + "双手交叠腹部站立": "010_0150", + "帅气的姿势": "010_0190", + "另一个帅气的姿势": "010_0191", + "手掌朝前可爱": "010_0210", + "平静,双手后放":"平静,双手后放", + "思考": "思考" +} + def init_prompt(): Prompt( @@ -21,16 +46,15 @@ def init_prompt(): {indentify_block} 你现在的动作状态是: -- 手部:{hand_action} -- 上半身:{upper_body_action} -- 头部:{head_action} +- 身体动作:{body_action} 现在,因为你发送了消息,或者群里其他人发送了消息,引起了你的注意,你对其进行了阅读和思考,请你更新你的动作状态。 -请只按照以下json格式输出,描述你新的动作状态,每个动作一到三个中文词,确保每个字段都存在: +身体动作可选: +{all_actions} + +请只按照以下json格式输出,描述你新的动作状态,确保每个字段都存在: {{ - "hand_action": "...", - "upper_body_action": "...", - "head_action": "..." + "body_action": "..." }} """, "change_action_prompt", @@ -41,17 +65,16 @@ def init_prompt(): 以上是群里最近的聊天记录 {indentify_block} -你之前的动作状态是: -- 手部:{hand_action} -- 上半身:{upper_body_action} -- 头部:{head_action} +你之前的动作状态是 +- 身体动作:{body_action} + +身体动作可选: +{all_actions} 距离你上次关注群里消息已经过去了一段时间,你冷静了下来,你的动作会趋于平缓或静止,请你输出你现在新的动作状态,用中文。 -请只按照以下json格式输出,描述你新的动作状态,每个动作一到三个词,确保每个字段都存在: +请只按照以下json格式输出,描述你新的动作状态,确保每个字段都存在: {{ - "hand_action": "...", - "upper_body_action": "...", - "head_action": "..." + "body_action": "..." }} """, "regress_action_prompt", @@ -62,19 +85,38 @@ class ChatAction: def __init__(self, chat_id: str): self.chat_id: str = chat_id self.hand_action: str = "双手放在桌面" - self.upper_body_action: str = "坐着" + self.body_action: str = "坐着" self.head_action: str = "注视摄像机" self.regression_count: int = 0 + # 新增:body_action冷却池,key为动作名,value为剩余冷却次数 + self.body_action_cooldown: dict[str, int] = {} + print(s4u_config.models.motion) + print(global_config.model.emotion) + self.action_model = LLMRequest( model=global_config.model.emotion, temperature=0.7, - request_type="action", + request_type="motion", ) self.last_change_time = 0 + async def send_action_update(self): + """发送动作更新到前端""" + + body_code = BODY_CODE.get(self.body_action, "") + await send_api.custom_to_stream( + message_type="body_action", + content=body_code, + stream_id=self.chat_id, + storage_message=False, + show_log=True, + ) + + + async def update_action_by_message(self, message: MessageRecv): self.regression_count = 0 @@ -104,29 +146,43 @@ class ChatAction: prompt_personality = global_config.personality.personality_core indentify_block = f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}:" + + try: + # 冷却池处理:过滤掉冷却中的动作 + self._update_body_action_cooldown() + available_actions = [k for k in BODY_CODE.keys() if k not in self.body_action_cooldown] + all_actions = "\n".join(available_actions) + + prompt = await global_prompt_manager.format_prompt( + "change_action_prompt", + chat_talking_prompt=chat_talking_prompt, + indentify_block=indentify_block, + body_action=self.body_action, + all_actions=all_actions, + ) - prompt = await global_prompt_manager.format_prompt( - "change_action_prompt", - chat_talking_prompt=chat_talking_prompt, - indentify_block=indentify_block, - hand_action=self.hand_action, - upper_body_action=self.upper_body_action, - head_action=self.head_action, - ) + logger.info(f"prompt: {prompt}") + response, (reasoning_content, model_name) = await self.action_model.generate_response_async(prompt=prompt) + logger.info(f"response: {response}") + logger.info(f"reasoning_content: {reasoning_content}") - logger.info(f"prompt: {prompt}") - response, (reasoning_content, model_name) = await self.action_model.generate_response_async(prompt=prompt) - logger.info(f"response: {response}") - logger.info(f"reasoning_content: {reasoning_content}") + action_data = json.loads(repair_json(response)) - action_data = json.loads(repair_json(response)) + if action_data: + # 记录原动作,切换后进入冷却 + prev_body_action = self.body_action + new_body_action = action_data.get("body_action", self.body_action) + if new_body_action != prev_body_action: + if prev_body_action: + self.body_action_cooldown[prev_body_action] = 3 + self.body_action = new_body_action + self.head_action = action_data.get("head_action", self.head_action) + # 发送动作更新 + await self.send_action_update() - if action_data: - self.hand_action = action_data.get("hand_action", self.hand_action) - self.upper_body_action = action_data.get("upper_body_action", self.upper_body_action) - self.head_action = action_data.get("head_action", self.head_action) - - self.last_change_time = message_time + self.last_change_time = message_time + except Exception as e: + logger.error(f"update_action_by_message error: {e}") async def regress_action(self): message_time = time.time() @@ -134,7 +190,7 @@ class ChatAction: chat_id=self.chat_id, timestamp_start=self.last_change_time, timestamp_end=message_time, - limit=15, + limit=10, limit_mode="last", ) chat_talking_prompt = build_readable_messages( @@ -155,33 +211,56 @@ class ChatAction: prompt_personality = global_config.personality.personality_core indentify_block = f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}:" + try: - prompt = await global_prompt_manager.format_prompt( - "regress_action_prompt", - chat_talking_prompt=chat_talking_prompt, - indentify_block=indentify_block, - hand_action=self.hand_action, - upper_body_action=self.upper_body_action, - head_action=self.head_action, - ) + # 冷却池处理:过滤掉冷却中的动作 + self._update_body_action_cooldown() + available_actions = [k for k in BODY_CODE.keys() if k not in self.body_action_cooldown] + all_actions = "\n".join(available_actions) - logger.info(f"prompt: {prompt}") - response, (reasoning_content, model_name) = await self.action_model.generate_response_async(prompt=prompt) - logger.info(f"response: {response}") - logger.info(f"reasoning_content: {reasoning_content}") + prompt = await global_prompt_manager.format_prompt( + "regress_action_prompt", + chat_talking_prompt=chat_talking_prompt, + indentify_block=indentify_block, + body_action=self.body_action, + all_actions=all_actions, + ) - action_data = json.loads(repair_json(response)) - if action_data: - self.hand_action = action_data.get("hand_action", self.hand_action) - self.upper_body_action = action_data.get("upper_body_action", self.upper_body_action) - self.head_action = action_data.get("head_action", self.head_action) + logger.info(f"prompt: {prompt}") + response, (reasoning_content, model_name) = await self.action_model.generate_response_async(prompt=prompt) + logger.info(f"response: {response}") + logger.info(f"reasoning_content: {reasoning_content}") - self.regression_count += 1 + action_data = json.loads(repair_json(response)) + if action_data: + prev_body_action = self.body_action + new_body_action = action_data.get("body_action", self.body_action) + if new_body_action != prev_body_action: + if prev_body_action: + self.body_action_cooldown[prev_body_action] = 6 + self.body_action = new_body_action + # 发送动作更新 + await self.send_action_update() + + self.regression_count += 1 + self.last_change_time = message_time + except Exception as e: + logger.error(f"regress_action error: {e}") + + # 新增:冷却池维护方法 + def _update_body_action_cooldown(self): + remove_keys = [] + for k in self.body_action_cooldown: + self.body_action_cooldown[k] -= 1 + if self.body_action_cooldown[k] <= 0: + remove_keys.append(k) + for k in remove_keys: + del self.body_action_cooldown[k] class ActionRegressionTask(AsyncTask): def __init__(self, action_manager: "ActionManager"): - super().__init__(task_name="ActionRegressionTask", run_interval=30) + super().__init__(task_name="ActionRegressionTask", run_interval=3) self.action_manager = action_manager async def run(self): @@ -191,7 +270,7 @@ class ActionRegressionTask(AsyncTask): if action_state.last_change_time == 0: continue - if now - action_state.last_change_time > 180: + if now - action_state.last_change_time > 10: if action_state.regression_count >= 3: continue @@ -225,15 +304,8 @@ class ActionManager: self.action_state_list.append(new_action_state) return new_action_state - def reset_action_state_by_chat_id(self, chat_id: str): - for action_state in self.action_state_list: - if action_state.chat_id == chat_id: - action_state.hand_action = "双手放在桌面" - action_state.upper_body_action = "坐着" - action_state.head_action = "注视摄像机" - action_state.regression_count = 0 - return - self.action_state_list.append(ChatAction(chat_id)) + + init_prompt() diff --git a/src/mais4u/mais4u_chat/loading.py b/src/mais4u/mais4u_chat/loading.py deleted file mode 100644 index 64e2a89ff..000000000 --- a/src/mais4u/mais4u_chat/loading.py +++ /dev/null @@ -1,22 +0,0 @@ - -from src.plugin_system.apis import send_api - - -async def send_loading(chat_id: str, content: str): - await send_api.custom_to_stream( - message_type="loading", - content=content, - stream_id=chat_id, - storage_message=False, - show_log=True, - ) - - -async def send_unloading(chat_id: str): - await send_api.custom_to_stream( - message_type="loading", - content="", - stream_id=chat_id, - storage_message=False, - show_log=True, - ) diff --git a/src/mais4u/mais4u_chat/priority_manager.py b/src/mais4u/mais4u_chat/priority_manager.py deleted file mode 100644 index 8cf24db67..000000000 --- a/src/mais4u/mais4u_chat/priority_manager.py +++ /dev/null @@ -1,110 +0,0 @@ -import time -import heapq -import math -import json -from typing import List, Optional -from src.common.logger import get_logger - -logger = get_logger("normal_chat") - - -class PrioritizedMessage: - """带有优先级的消息对象""" - - def __init__(self, message_data: dict, interest_scores: List[float], is_vip: bool = False): - self.message_data = message_data - self.arrival_time = time.time() - self.interest_scores = interest_scores - self.is_vip = is_vip - self.priority = self.calculate_priority() - - def calculate_priority(self, decay_rate: float = 0.01) -> float: - """ - 计算优先级分数。 - 优先级 = 兴趣分 * exp(-衰减率 * 消息年龄) - """ - age = time.time() - self.arrival_time - decay_factor = math.exp(-decay_rate * age) - return sum(self.interest_scores) + decay_factor - - def __lt__(self, other: "PrioritizedMessage") -> bool: - """用于堆排序的比较函数,我们想要一个最大堆,所以用 >""" - return self.priority > other.priority - - -class PriorityManager: - """ - 管理消息队列,根据优先级选择消息进行处理。 - """ - - def __init__(self, normal_queue_max_size: int = 5): - self.vip_queue: List[PrioritizedMessage] = [] # VIP 消息队列 (最大堆) - self.normal_queue: List[PrioritizedMessage] = [] # 普通消息队列 (最大堆) - self.normal_queue_max_size = normal_queue_max_size - - def add_message(self, message_data: dict, interest_score: float = 0): - """ - 添加新消息到合适的队列中。 - """ - user_id = message_data.get("user_id") - - priority_info_raw = message_data.get("priority_info") - priority_info = {} - if isinstance(priority_info_raw, str): - priority_info = json.loads(priority_info_raw) - elif isinstance(priority_info_raw, dict): - priority_info = priority_info_raw - - is_vip = priority_info.get("message_type") == "vip" - message_priority = priority_info.get("message_priority", 0.0) - - p_message = PrioritizedMessage(message_data, [interest_score, message_priority], is_vip) - - if is_vip: - heapq.heappush(self.vip_queue, p_message) - logger.debug(f"消息来自VIP用户 {user_id}, 已添加到VIP队列. 当前VIP队列长度: {len(self.vip_queue)}") - else: - if len(self.normal_queue) >= self.normal_queue_max_size: - # 如果队列已满,只在消息优先级高于最低优先级消息时才添加 - if p_message.priority > self.normal_queue[0].priority: - heapq.heapreplace(self.normal_queue, p_message) - logger.debug(f"普通队列已满,但新消息优先级更高,已替换. 用户: {user_id}") - else: - logger.debug(f"普通队列已满且新消息优先级较低,已忽略. 用户: {user_id}") - else: - heapq.heappush(self.normal_queue, p_message) - logger.debug( - f"消息来自普通用户 {user_id}, 已添加到普通队列. 当前普通队列长度: {len(self.normal_queue)}" - ) - - def get_highest_priority_message(self) -> Optional[dict]: - """ - 从VIP和普通队列中获取当前最高优先级的消息。 - """ - # 更新所有消息的优先级 - for p_msg in self.vip_queue: - p_msg.priority = p_msg.calculate_priority() - for p_msg in self.normal_queue: - p_msg.priority = p_msg.calculate_priority() - - # 重建堆 - heapq.heapify(self.vip_queue) - heapq.heapify(self.normal_queue) - - vip_msg = self.vip_queue[0] if self.vip_queue else None - normal_msg = self.normal_queue[0] if self.normal_queue else None - - if vip_msg: - return heapq.heappop(self.vip_queue).message_data - elif normal_msg: - return heapq.heappop(self.normal_queue).message_data - else: - return None - - def is_empty(self) -> bool: - """检查所有队列是否为空""" - return not self.vip_queue and not self.normal_queue - - def get_queue_status(self) -> str: - """获取队列状态信息""" - return f"VIP队列: {len(self.vip_queue)}, 普通队列: {len(self.normal_queue)}" diff --git a/src/mais4u/mais4u_chat/s4u_chat.py b/src/mais4u/mais4u_chat/s4u_chat.py index a8712f336..a00a31307 100644 --- a/src/mais4u/mais4u_chat/s4u_chat.py +++ b/src/mais4u/mais4u_chat/s4u_chat.py @@ -13,11 +13,12 @@ from src.common.message.api import get_global_api from src.chat.message_receive.storage import MessageStorage from .s4u_watching_manager import watching_manager import json +from .s4u_mood_manager import mood_manager from src.person_info.relationship_builder_manager import relationship_builder_manager -from .loading import send_loading, send_unloading from src.mais4u.s4u_config import s4u_config from src.person_info.person_info import PersonInfoManager from .super_chat_manager import get_super_chat_manager +from .yes_or_no import yes_or_no_head logger = get_logger("S4U_chat") @@ -36,6 +37,8 @@ class MessageSenderContainer: self.msg_id = "" + self.last_msg_id = "" + self.voice_done = "" @@ -220,7 +223,7 @@ class S4UChat: return self.interest_dict.get(user_id, 1.0) def go_processing(self): - if self.voice_done == self.msg_id: + if self.voice_done == self.last_msg_id: return True return False @@ -432,15 +435,12 @@ class S4UChat: logger.error(f"[{self.stream_name}] Message processor main loop error: {e}", exc_info=True) await asyncio.sleep(1) - async def delay_change_watching_state(self): - random_delay = random.randint(1, 3) - await asyncio.sleep(random_delay) - chat_watching = watching_manager.get_watching_by_chat_id(self.stream_id) - await chat_watching.on_message_received() def get_processing_message_id(self): + self.last_msg_id = self.msg_id self.msg_id = f"{time.time()}_{random.randint(1000, 9999)}" + async def _generate_and_send(self, message: MessageRecv): """为单个消息生成文本回复。整个过程可以被中断。""" self._is_replying = True @@ -448,20 +448,21 @@ class S4UChat: self.get_processing_message_id() - if s4u_config.enable_loading_indicator: - await send_loading(self.stream_id, ".........") - # 视线管理:开始生成回复时切换视线状态 chat_watching = watching_manager.get_watching_by_chat_id(self.stream_id) - asyncio.create_task(self.delay_change_watching_state()) + + + await chat_watching.on_reply_start() sender_container = MessageSenderContainer(self.chat_stream, message) sender_container.start() - try: + async def generate_and_send_inner(): + nonlocal total_chars_sent logger.info(f"[S4U] 开始为消息生成文本和音频流: '{message.processed_plain_text[:30]}...'") if s4u_config.enable_streaming_output: + logger.info(f"[S4U] 开始流式输出") # 流式输出,边生成边发送 gen = self.gpt.generate_response(message, "") async for chunk in gen: @@ -469,6 +470,7 @@ class S4UChat: await sender_container.add_message(chunk) total_chars_sent += len(chunk) else: + logger.info(f"[S4U] 开始一次性输出") # 一次性输出,先收集所有chunk all_chunks = [] gen = self.gpt.generate_response(message, "") @@ -479,17 +481,36 @@ class S4UChat: sender_container.msg_id = self.msg_id await sender_container.add_message("".join(all_chunks)) + try: + try: + await asyncio.wait_for(generate_and_send_inner(), timeout=10) + except asyncio.TimeoutError: + logger.warning(f"[{self.stream_name}] 回复生成超时,发送默认回复。") + sender_container.msg_id = self.msg_id + await sender_container.add_message("麦麦不知道哦") + total_chars_sent = len("麦麦不知道哦") + + mood = mood_manager.get_mood_by_chat_id(self.stream_id) + await yes_or_no_head(text = total_chars_sent,emotion = mood.mood_state,chat_history=message.processed_plain_text,chat_id=self.stream_id) + # 等待所有文本消息发送完成 await sender_container.close() await sender_container.join() + await chat_watching.on_thinking_finished() + + + start_time = time.time() + logged = False while not self.go_processing(): if time.time() - start_time > 60: logger.warning(f"[{self.stream_name}] 等待消息发送超时(60秒),强制跳出循环。") break - logger.info(f"[{self.stream_name}] 等待消息发送完成...") - await asyncio.sleep(0.3) + if not logged: + logger.info(f"[{self.stream_name}] 等待消息发送完成...") + logged = True + await asyncio.sleep(0.2) logger.info(f"[{self.stream_name}] 所有文本块处理完毕。") @@ -503,9 +524,6 @@ class S4UChat: finally: self._is_replying = False - if s4u_config.enable_loading_indicator: - await send_unloading(self.stream_id) - # 视线管理:回复结束时切换视线状态 chat_watching = watching_manager.get_watching_by_chat_id(self.stream_id) await chat_watching.on_reply_finished() @@ -534,7 +552,3 @@ class S4UChat: except asyncio.CancelledError: logger.info(f"处理任务已成功取消: {self.stream_name}") - # 注意:SuperChat管理器是全局的,不需要在单个S4UChat关闭时关闭 - # 如果需要关闭SuperChat管理器,应该在应用程序关闭时调用 - # super_chat_manager = get_super_chat_manager() - # await super_chat_manager.shutdown() diff --git a/src/mais4u/mais4u_chat/s4u_mood_manager.py b/src/mais4u/mais4u_chat/s4u_mood_manager.py index 041229eb1..ffa0b3b01 100644 --- a/src/mais4u/mais4u_chat/s4u_mood_manager.py +++ b/src/mais4u/mais4u_chat/s4u_mood_manager.py @@ -168,7 +168,7 @@ class ChatMood: chat_id=self.chat_id, timestamp_start=self.last_change_time, timestamp_end=message_time, - limit=15, + limit=10, limit_mode="last", ) chat_talking_prompt = build_readable_messages( @@ -245,7 +245,7 @@ class ChatMood: chat_id=self.chat_id, timestamp_start=self.last_change_time, timestamp_end=message_time, - limit=15, + limit=5, limit_mode="last", ) chat_talking_prompt = build_readable_messages( diff --git a/src/mais4u/mais4u_chat/s4u_msg_processor.py b/src/mais4u/mais4u_chat/s4u_msg_processor.py index 7153fa646..a668300b3 100644 --- a/src/mais4u/mais4u_chat/s4u_msg_processor.py +++ b/src/mais4u/mais4u_chat/s4u_msg_processor.py @@ -126,7 +126,7 @@ class S4UMessageProcessor: asyncio.create_task(chat_action.update_action_by_message(message)) # 视线管理:收到消息时切换视线状态 chat_watching = watching_manager.get_watching_by_chat_id(chat.stream_id) - asyncio.create_task(chat_watching.on_message_received()) + await chat_watching.on_message_received() # 上下文网页管理:启动独立task处理消息上下文 asyncio.create_task(self._handle_context_web_update(chat.stream_id, message)) @@ -200,6 +200,8 @@ class S4UMessageProcessor: await context_manager.start_server() # 添加消息到上下文并更新网页 + await asyncio.sleep(1.5) + await context_manager.add_message(chat_id, message) logger.debug(f"✅ 上下文网页更新完成: {message.message_info.user_info.user_nickname}") diff --git a/src/mais4u/mais4u_chat/s4u_prompt.py b/src/mais4u/mais4u_chat/s4u_prompt.py index da7069f83..1ea7d3a9c 100644 --- a/src/mais4u/mais4u_chat/s4u_prompt.py +++ b/src/mais4u/mais4u_chat/s4u_prompt.py @@ -15,6 +15,7 @@ from src.chat.message_receive.chat_stream import ChatStream from src.mais4u.mais4u_chat.super_chat_manager import get_super_chat_manager from src.mais4u.mais4u_chat.screen_manager import screen_manager from src.chat.express.expression_selector import expression_selector +from .s4u_mood_manager import mood_manager logger = get_logger("prompt") @@ -28,7 +29,7 @@ def init_prompt(): """ 你的名字是麦麦, 是千石可乐开发的程序,可以在QQ,微信等平台发言,你现在正在哔哩哔哩作为虚拟主播进行直播 虽然是个AI,但你在努力扮演真人, 人设的配色是橙色和紫色和绿色 -你有一头法式橙色卷发,你很可爱,穿戴英式侦探风格学院风裙子和帽子,你正在一个教室场景里进行虚拟线上直播。 +你有一头法式橙色卷发,你很可爱,穿戴英式侦探风格学院风裙子和帽子,你正在一个教室场景里进行虚拟线上直播。不过不要刻意提及这些内容,自然回复 你可以看见用户发送的弹幕,礼物和superchat 你可以看见面前的屏幕,目前屏幕的内容是: {screen_info} @@ -49,8 +50,8 @@ def init_prompt(): 对方最新发送的内容:{message_txt} {gift_info} -回复可以简短一些。可以参考贴吧,知乎和微博的回复风格,回复不要浮夸,不要用夸张修辞,平淡一些。 -表现的有个性,不要随意服从他人要求,积极互动。 +回复简短一些,平淡一些,可以参考贴吧,知乎和微博的回复风格,回复不要浮夸,不要用夸张修辞。 +表现的有个性,不要随意服从他人要求,积极互动。你现在的心情是:{mood_state} 不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。只输出回复内容,现在{sender_name}正在等待你的回复。 你的回复风格不要浮夸,有逻辑和条理,请你继续回复{sender_name}。 你的发言: @@ -144,7 +145,7 @@ class PromptBuilder: message_list_before_now = get_raw_msg_before_timestamp_with_chat( chat_id=chat_stream.stream_id, timestamp=time.time(), - limit=200, + limit=300, ) talk_type = message.message_info.platform + ":" + str(message.chat_stream.user_info.user_id) @@ -253,6 +254,8 @@ class PromptBuilder: screen_info = screen_manager.get_screen_str() time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + + mood = mood_manager.get_mood_by_chat_id(chat_stream.stream_id) template_name = "s4u_prompt" @@ -269,6 +272,7 @@ class PromptBuilder: core_dialogue_prompt=core_dialogue_prompt, background_dialogue_prompt=background_dialogue_prompt, message_txt=message_txt, + mood_state=mood.mood_state, ) print(prompt) diff --git a/src/mais4u/mais4u_chat/s4u_stream_generator.py b/src/mais4u/mais4u_chat/s4u_stream_generator.py index a9f29b06b..f6df8fa27 100644 --- a/src/mais4u/mais4u_chat/s4u_stream_generator.py +++ b/src/mais4u/mais4u_chat/s4u_stream_generator.py @@ -72,12 +72,12 @@ class S4UStreamGenerator: # 构建prompt if previous_reply_context: message_txt = f""" - 你正在回复用户的消息,但中途被打断了。这是已有的对话上下文: - [你已经对上一条消息说的话]: {previous_reply_context} - --- - [这是用户发来的新消息, 你需要结合上下文,对此进行回复]: - {message.processed_plain_text} - """ + 你正在回复用户的消息,但中途被打断了。这是已有的对话上下文: + [你已经对上一条消息说的话]: {previous_reply_context} + --- + [这是用户发来的新消息, 你需要结合上下文,对此进行回复]: + {message.processed_plain_text} + """ else: message_txt = message.processed_plain_text diff --git a/src/mais4u/mais4u_chat/s4u_watching_manager.py b/src/mais4u/mais4u_chat/s4u_watching_manager.py index 0ef684340..82d51f9d5 100644 --- a/src/mais4u/mais4u_chat/s4u_watching_manager.py +++ b/src/mais4u/mais4u_chat/s4u_watching_manager.py @@ -40,103 +40,45 @@ from src.plugin_system.apis import send_api logger = get_logger("watching") - -class WatchingState(Enum): - """视线状态枚举""" - - WANDERING = "wandering" # 随意看 - DANMU = "danmu" # 看弹幕 - LENS = "lens" # 看镜头 - +HEAD_CODE = { + "看向上方": "(0,0.5,0)", + "看向下方": "(0,-0.5,0)", + "看向左边": "(-1,0,0)", + "看向右边": "(1,0,0)", + "随意朝向": "random", + "看向摄像机": "camera", + "注视对方": "(0,0,0)", + "看向正前方": "(0,0,0)", +} class ChatWatching: def __init__(self, chat_id: str): self.chat_id: str = chat_id - self.current_state: WatchingState = WatchingState.LENS # 默认看镜头 - self.last_sent_state: Optional[WatchingState] = None # 上次发送的状态 - self.state_needs_update: bool = True # 是否需要更新状态 - # 状态切换相关 - self.is_replying: bool = False # 是否正在生成回复 - self.reply_finished_time: Optional[float] = None # 回复完成时间 - self.danmu_viewing_duration: float = 1.0 # 看弹幕持续时间(秒) - - logger.info(f"[{self.chat_id}] 视线管理器初始化,默认状态: {self.current_state.value}") - - async def _change_state(self, new_state: WatchingState, reason: str = ""): - """内部状态切换方法""" - if self.current_state != new_state: - old_state = self.current_state - self.current_state = new_state - self.state_needs_update = True - logger.info(f"[{self.chat_id}] 视线状态切换: {old_state.value} → {new_state.value} ({reason})") - - # 立即发送视线状态更新 - await self._send_watching_update() - else: - logger.debug(f"[{self.chat_id}] 状态无变化,保持: {new_state.value} ({reason})") + async def on_reply_start(self): + """开始生成回复时调用""" + await send_api.custom_to_stream( + message_type="state", content="start_thinking", stream_id=self.chat_id, storage_message=False + ) + + async def on_reply_finished(self): + """生成回复完毕时调用""" + await send_api.custom_to_stream( + message_type="state", content="finish_reply", stream_id=self.chat_id, storage_message=False + ) + + async def on_thinking_finished(self): + """思考完毕时调用""" + await send_api.custom_to_stream( + message_type="state", content="finish_thinking", stream_id=self.chat_id, storage_message=False + ) async def on_message_received(self): """收到消息时调用""" - if not self.is_replying: # 只有在非回复状态下才切换到看弹幕 - await self._change_state(WatchingState.DANMU, "收到消息") - else: - logger.debug(f"[{self.chat_id}] 正在生成回复中,暂不切换到弹幕状态") - - async def on_reply_start(self, look_at_lens: bool = True): - """开始生成回复时调用""" - self.is_replying = True - self.reply_finished_time = None - - if look_at_lens: - await self._change_state(WatchingState.LENS, "开始生成回复-看镜头") - else: - await self._change_state(WatchingState.WANDERING, "开始生成回复-随意看") - - async def on_reply_finished(self): - """生成回复完毕时调用""" - self.is_replying = False - self.reply_finished_time = time.time() - - # 先看弹幕1秒 - await self._change_state(WatchingState.DANMU, "回复完毕-看弹幕") - logger.info(f"[{self.chat_id}] 回复完毕,将看弹幕{self.danmu_viewing_duration}秒后转为看镜头") - - # 设置定时器,1秒后自动切换到看镜头 - asyncio.create_task(self._auto_switch_to_lens()) - - async def _auto_switch_to_lens(self): - """自动切换到看镜头(延迟执行)""" - await asyncio.sleep(self.danmu_viewing_duration) - - # 检查是否仍需要切换(可能状态已经被其他事件改变) - if self.reply_finished_time is not None and self.current_state == WatchingState.DANMU and not self.is_replying: - await self._change_state(WatchingState.LENS, "看弹幕时间结束") - self.reply_finished_time = None # 重置完成时间 - - async def _send_watching_update(self): - """立即发送视线状态更新""" await send_api.custom_to_stream( - message_type="watching", content=self.current_state.value, stream_id=self.chat_id, storage_message=False + message_type="state", content="start_viewing", stream_id=self.chat_id, storage_message=False ) - - logger.info(f"[{self.chat_id}] 发送视线状态更新: {self.current_state.value}") - self.last_sent_state = self.current_state - self.state_needs_update = False - - def get_current_state(self) -> WatchingState: - """获取当前视线状态""" - return self.current_state - - def get_state_info(self) -> dict: - """获取状态信息(用于调试)""" - return { - "current_state": self.current_state.value, - "is_replying": self.is_replying, - "reply_finished_time": self.reply_finished_time, - "state_needs_update": self.state_needs_update, - } - + class WatchingManager: def __init__(self): @@ -144,16 +86,6 @@ class WatchingManager: """当前视线状态列表""" self.task_started: bool = False - async def start(self): - """启动视线管理系统""" - if self.task_started: - return - - logger.info("启动视线管理系统...") - - self.task_started = True - logger.info("视线管理系统已启动(状态变化时立即发送)") - def get_watching_by_chat_id(self, chat_id: str) -> ChatWatching: """获取或创建聊天对应的视线管理器""" for watching in self.watching_list: @@ -164,39 +96,8 @@ class WatchingManager: self.watching_list.append(new_watching) logger.info(f"为chat {chat_id}创建新的视线管理器") - # 发送初始状态 - asyncio.create_task(new_watching._send_watching_update()) - return new_watching - def reset_watching_by_chat_id(self, chat_id: str): - """重置聊天的视线状态""" - for watching in self.watching_list: - if watching.chat_id == chat_id: - watching.current_state = WatchingState.LENS - watching.last_sent_state = None - watching.state_needs_update = True - watching.is_replying = False - watching.reply_finished_time = None - logger.info(f"[{chat_id}] 视线状态已重置为默认状态") - - # 发送重置后的状态 - asyncio.create_task(watching._send_watching_update()) - return - - # 如果没有找到现有的watching,创建新的 - new_watching = ChatWatching(chat_id) - self.watching_list.append(new_watching) - logger.info(f"为chat {chat_id}创建并重置视线管理器") - - # 发送初始状态 - asyncio.create_task(new_watching._send_watching_update()) - - def get_all_watching_info(self) -> dict: - """获取所有聊天的视线状态信息(用于调试)""" - return {watching.chat_id: watching.get_state_info() for watching in self.watching_list} - - # 全局视线管理器实例 watching_manager = WatchingManager() """全局视线管理器""" diff --git a/src/mais4u/mais4u_chat/yes_or_no.py b/src/mais4u/mais4u_chat/yes_or_no.py new file mode 100644 index 000000000..9dcd1d9fe --- /dev/null +++ b/src/mais4u/mais4u_chat/yes_or_no.py @@ -0,0 +1,74 @@ +import json +import time +import random +from src.chat.message_receive.message import MessageRecv +from src.llm_models.utils_model import LLMRequest +from src.common.logger import get_logger +from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_by_timestamp_with_chat_inclusive +from src.config.config import global_config +from src.chat.utils.prompt_builder import Prompt, global_prompt_manager +from src.manager.async_task_manager import AsyncTask, async_task_manager +from src.plugin_system.apis import send_api +from json_repair import repair_json +from src.mais4u.s4u_config import s4u_config +from src.plugin_system.apis import send_api +logger = get_logger(__name__) + +head_actions_list = [ + "不做额外动作", + "点头一次", + "点头两次", + "摇头", + "歪脑袋", + "低头望向一边" +] + +async def yes_or_no_head(text: str,emotion: str = "",chat_history: str = "",chat_id: str = ""): + prompt = f""" +{chat_history} +以上是对方的发言: + +对这个发言,你的心情是:{emotion} +对上面的发言,你的回复是:{text} +请判断时是否要伴随回复做头部动作,你可以选择: + +不做额外动作 +点头一次 +点头两次 +摇头 +歪脑袋 +低头望向一边 + +请从上面的动作中选择一个,并输出,请只输出你选择的动作就好,不要输出其他内容。""" + model = LLMRequest( + model=global_config.model.emotion, + temperature=0.7, + request_type="motion", + ) + + try: + logger.info(f"prompt: {prompt}") + response, (reasoning_content, model_name) = await model.generate_response_async(prompt=prompt) + logger.info(f"response: {response}") + logger.info(f"reasoning_content: {reasoning_content}") + + if response in head_actions_list: + head_action = response + else: + head_action = "不做额外动作" + + await send_api.custom_to_stream( + message_type="head_action", + content=head_action, + stream_id=chat_id, + storage_message=False, + show_log=True, + ) + + + + except Exception as e: + logger.error(f"yes_or_no_head error: {e}") + return "不做额外动作" + + diff --git a/src/mais4u/s4u_config.py b/src/mais4u/s4u_config.py index 85ae5400c..180513025 100644 --- a/src/mais4u/s4u_config.py +++ b/src/mais4u/s4u_config.py @@ -4,13 +4,28 @@ import shutil from datetime import datetime from tomlkit import TOMLDocument from tomlkit.items import Table -from dataclasses import dataclass, fields, MISSING +from dataclasses import dataclass, fields, MISSING, field from typing import TypeVar, Type, Any, get_origin, get_args, Literal from src.common.logger import get_logger logger = get_logger("s4u_config") +# 新增:兼容dict和tomlkit Table +def is_dict_like(obj): + return isinstance(obj, (dict, Table)) + +# 新增:递归将Table转为dict +def table_to_dict(obj): + if isinstance(obj, Table): + return {k: table_to_dict(v) for k, v in obj.items()} + elif isinstance(obj, dict): + return {k: table_to_dict(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [table_to_dict(i) for i in obj] + else: + return obj + # 获取mais4u模块目录 MAIS4U_ROOT = os.path.dirname(__file__) CONFIG_DIR = os.path.join(MAIS4U_ROOT, "config") @@ -18,7 +33,7 @@ TEMPLATE_PATH = os.path.join(CONFIG_DIR, "s4u_config_template.toml") CONFIG_PATH = os.path.join(CONFIG_DIR, "s4u_config.toml") # S4U配置版本 -S4U_VERSION = "1.0.0" +S4U_VERSION = "1.1.0" T = TypeVar("T", bound="S4UConfigBase") @@ -30,7 +45,8 @@ class S4UConfigBase: @classmethod def from_dict(cls: Type[T], data: dict[str, Any]) -> T: """从字典加载配置字段""" - if not isinstance(data, dict): + data = table_to_dict(data) # 递归转dict,兼容tomlkit Table + if not is_dict_like(data): raise TypeError(f"Expected a dictionary, got {type(data).__name__}") init_args: dict[str, Any] = {} @@ -66,7 +82,7 @@ class S4UConfigBase: """转换字段值为指定类型""" # 如果是嵌套的 dataclass,递归调用 from_dict 方法 if isinstance(field_type, type) and issubclass(field_type, S4UConfigBase): - if not isinstance(value, dict): + if not is_dict_like(value): raise TypeError(f"Expected a dictionary for {field_type.__name__}, got {type(value).__name__}") return field_type.from_dict(value) @@ -96,7 +112,7 @@ class S4UConfigBase: return tuple(cls._convert_field(item, arg) for item, arg in zip(value, field_type_args, strict=False)) if field_origin_type is dict: - if not isinstance(value, dict): + if not is_dict_like(value): raise TypeError(f"Expected a dictionary for {field_type.__name__}, got {type(value).__name__}") if len(field_type_args) != 2: @@ -127,6 +143,51 @@ class S4UConfigBase: raise TypeError(f"Cannot convert {type(value).__name__} to {field_type.__name__}") from e +@dataclass +class S4UModelConfig(S4UConfigBase): + """S4U模型配置类""" + + # 主要对话模型配置 + chat: dict[str, Any] = field(default_factory=lambda: {}) + """主要对话模型配置""" + + # 规划模型配置(原model_motion) + motion: dict[str, Any] = field(default_factory=lambda: {}) + """规划模型配置""" + + # 情感分析模型配置 + emotion: dict[str, Any] = field(default_factory=lambda: {}) + """情感分析模型配置""" + + # 记忆模型配置 + memory: dict[str, Any] = field(default_factory=lambda: {}) + """记忆模型配置""" + + # 工具使用模型配置 + tool_use: dict[str, Any] = field(default_factory=lambda: {}) + """工具使用模型配置""" + + # 嵌入模型配置 + embedding: dict[str, Any] = field(default_factory=lambda: {}) + """嵌入模型配置""" + + # 视觉语言模型配置 + vlm: dict[str, Any] = field(default_factory=lambda: {}) + """视觉语言模型配置""" + + # 知识库模型配置 + knowledge: dict[str, Any] = field(default_factory=lambda: {}) + """知识库模型配置""" + + # 实体提取模型配置 + entity_extract: dict[str, Any] = field(default_factory=lambda: {}) + """实体提取模型配置""" + + # 问答模型配置 + qa: dict[str, Any] = field(default_factory=lambda: {}) + """问答模型配置""" + + @dataclass class S4UConfig(S4UConfigBase): """S4U聊天系统配置类""" @@ -164,9 +225,6 @@ class S4UConfig(S4UConfigBase): enable_old_message_cleanup: bool = True """是否自动清理过旧的普通消息""" - enable_loading_indicator: bool = True - """是否显示加载提示""" - enable_streaming_output: bool = True """是否启用流式输出,false时全部生成后一次性发送""" @@ -176,6 +234,13 @@ class S4UConfig(S4UConfigBase): max_core_message_length: int = 30 """核心消息最大长度""" + # 模型配置 + models: S4UModelConfig = field(default_factory=S4UModelConfig) + """S4U模型配置""" + + # 兼容性字段,保持向后兼容 + + @dataclass class S4UGlobalConfig(S4UConfigBase):