From 67aa936013770102c6db535769bb69f5ddd39ff8 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Wed, 1 Oct 2025 06:04:07 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix(db):=20=E5=A2=9E=E5=BC=BA=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E4=BC=9A=E8=AF=9D=E7=AE=A1=E7=90=86=E7=9A=84?= =?UTF-8?q?=E5=AE=B9=E9=94=99=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 调整了 `get_db_session` 的行为,当数据库未能成功初始化时,它现在会返回 `None` 并记录错误,而不是抛出异常。这提高了应用在数据库连接不可用时的健壮性,避免了程序因无法获取会话而崩溃。 - `VideoAnalyzer` 已更新,增加了对会话为 `None` 的检查,以安全地跳过数据库读写操作。 - 附带对 `VideoAnalyzer` 和 `LegacyVideoAnalyzer` 进行了重构,将模型选择和API请求执行的逻辑抽象到独立的 `_model_selector` 和 `_executor` 组件中,提升了代码的清晰度和可维护性。 --- src/chat/utils/utils_video.py | 13 ++++++++-- src/chat/utils/utils_video_legacy.py | 7 ++++-- src/common/database/sqlalchemy_models.py | 32 +++++++++++++++--------- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/chat/utils/utils_video.py b/src/chat/utils/utils_video.py index 6ecd599af..bbe489336 100644 --- a/src/chat/utils/utils_video.py +++ b/src/chat/utils/utils_video.py @@ -207,6 +207,9 @@ class VideoAnalyzer: """检查视频是否已经分析过""" try: async with get_db_session() as session: + if not session: + logger.warning("无法获取数据库会话,跳过视频存在性检查。") + return None # 明确刷新会话以确保看到其他事务的最新提交 await session.expire_all() stmt = select(Videos).where(Videos.video_hash == video_hash) @@ -227,6 +230,9 @@ class VideoAnalyzer: try: async with get_db_session() as session: + if not session: + logger.warning("无法获取数据库会话,跳过视频结果存储。") + return None # 只根据video_hash查找 stmt = select(Videos).where(Videos.video_hash == video_hash) result = await session.execute(stmt) @@ -540,11 +546,14 @@ class VideoAnalyzer: # logger.info(f"✅ 多帧消息构建完成,包含{len(frames)}张图片") # 获取模型信息和客户端 - model_info, api_provider, client = self.video_llm._select_model() + selection_result = self.video_llm._model_selector.select_best_available_model(set(), "response") + if not selection_result: + raise RuntimeError("无法为视频分析选择可用模型。") + model_info, api_provider, client = selection_result # logger.info(f"使用模型: {model_info.name} 进行多帧分析") # 直接执行多图片请求 - api_response = await self.video_llm._execute_request( + api_response = await self.video_llm._executor.execute_request( api_provider=api_provider, client=client, request_type=RequestType.RESPONSE, diff --git a/src/chat/utils/utils_video_legacy.py b/src/chat/utils/utils_video_legacy.py index 4d8e06681..77ca88142 100644 --- a/src/chat/utils/utils_video_legacy.py +++ b/src/chat/utils/utils_video_legacy.py @@ -461,11 +461,14 @@ class LegacyVideoAnalyzer: # logger.info(f"✅ 多帧消息构建完成,包含{len(frames)}张图片") # 获取模型信息和客户端 - model_info, api_provider, client = self.video_llm._select_model() + selection_result = self.video_llm._model_selector.select_best_available_model(set(), "response") + if not selection_result: + raise RuntimeError("无法为视频分析选择可用模型 (legacy)。") + model_info, api_provider, client = selection_result # logger.info(f"使用模型: {model_info.name} 进行多帧分析") # 直接执行多图片请求 - api_response = await self.video_llm._execute_request( + api_response = await self.video_llm._executor.execute_request( api_provider=api_provider, client=client, request_type=RequestType.RESPONSE, diff --git a/src/common/database/sqlalchemy_models.py b/src/common/database/sqlalchemy_models.py index cf74eedb5..64c1fd66a 100644 --- a/src/common/database/sqlalchemy_models.py +++ b/src/common/database/sqlalchemy_models.py @@ -759,30 +759,38 @@ async def initialize_database(): @asynccontextmanager -async def get_db_session() -> AsyncGenerator[AsyncSession, None]: - """异步数据库会话上下文管理器""" +async def get_db_session() -> AsyncGenerator[Optional[AsyncSession], None]: + """ + 异步数据库会话上下文管理器。 + 在初始化失败时会yield None,调用方需要检查会话是否为None。 + """ session: Optional[AsyncSession] = None + SessionLocal = None try: - engine, SessionLocal = await initialize_database() + _, SessionLocal = await initialize_database() if not SessionLocal: - raise RuntimeError("Database session not initialized") - session = SessionLocal() + logger.error("数据库会话工厂 (_SessionLocal) 未初始化。") + yield None + return + except Exception as e: + logger.error(f"数据库初始化失败,无法创建会话: {e}") + yield None + return + try: + session = SessionLocal() # 对于 SQLite,在会话开始时设置 PRAGMA from src.config.config import global_config if global_config.database.database_type == "sqlite": - try: - await session.execute(text("PRAGMA busy_timeout = 60000")) - await session.execute(text("PRAGMA foreign_keys = ON")) - except Exception as e: - logger.warning(f"[SQLite] 设置会话 PRAGMA 失败: {e}") + await session.execute(text("PRAGMA busy_timeout = 60000")) + await session.execute(text("PRAGMA foreign_keys = ON")) yield session except Exception as e: - logger.error(f"数据库会话错误: {e}") + logger.error(f"数据库会话期间发生错误: {e}") if session: await session.rollback() - raise + raise # 将会话期间的错误重新抛出给调用者 finally: if session: await session.close() From 3b8e971bd24c28d112885ade71fd2c29c42b82f8 Mon Sep 17 00:00:00 2001 From: xiaoCZX Date: Wed, 1 Oct 2025 11:00:32 +0800 Subject: [PATCH 2/4] =?UTF-8?q?model=5Fconfig=5Ftemplate=E4=BF=AE=E8=AE=A2?= =?UTF-8?q?=E5=8F=B7=E5=8A=A0=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/model_config_template.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/model_config_template.toml b/template/model_config_template.toml index c9774e329..9ee2d5e98 100644 --- a/template/model_config_template.toml +++ b/template/model_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "1.3.5" +version = "1.3.6" # 配置文件版本号迭代规则同bot_config.toml From 254fc2974263f57794b816909b4ab3472a7905c3 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:33:12 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix(chatter):=20=E4=BF=AE=E5=A4=8D=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E8=AE=A1=E5=88=92=E6=89=A7=E8=A1=8C=E5=99=A8=E5=8F=AF?= =?UTF-8?q?=E8=83=BD=E5=AF=B9=E5=90=8C=E4=B8=80=E6=B6=88=E6=81=AF=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E5=9B=9E=E5=A4=8D=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在复杂的对话场景中,行动规划器可能会针对同一条用户消息生成多个回复动作。这会导致机器人对用户的同一句话进行多次回复,影响用户体验。 本次提交通过在执行回复动作前检查 `message_id`,对回复列表进行去重,确保每条消息在单次计划中只被回复一次。同时增加了相应的日志记录,方便追踪过滤行为。 --- .../affinity_flow_chatter/plan_executor.py | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src/plugins/built_in/affinity_flow_chatter/plan_executor.py b/src/plugins/built_in/affinity_flow_chatter/plan_executor.py index 821d52bd6..cf14c221e 100644 --- a/src/plugins/built_in/affinity_flow_chatter/plan_executor.py +++ b/src/plugins/built_in/affinity_flow_chatter/plan_executor.py @@ -111,17 +111,47 @@ class ChatterPlanExecutor: } async def _execute_reply_actions(self, reply_actions: List[ActionPlannerInfo], plan: Plan) -> Dict[str, any]: - """串行执行所有回复动作""" + """串行执行所有回复动作,增加去重逻辑,避免对同一消息多次回复""" results = [] - total_actions = len(reply_actions) - if total_actions > 1: + + # --- 新增去重逻辑 --- + unique_actions = [] + replied_message_ids = set() + for action_info in reply_actions: + target_message = action_info.action_message + message_id = None + if target_message: + # 兼容 Pydantic 对象和字典两种情况 + if hasattr(target_message, "message_id"): + message_id = getattr(target_message, "message_id", None) + elif isinstance(target_message, dict): + message_id = target_message.get("message_id") + + if message_id: + if message_id not in replied_message_ids: + unique_actions.append(action_info) + replied_message_ids.add(message_id) + else: + logger.warning( + f"[多重回复] 检测到对消息ID '{message_id}' 的重复回复,已过滤。" + f" (动作: {action_info.action_type}, 原因: {action_info.reasoning})" + ) + else: + # 如果没有message_id,无法去重,直接添加 + unique_actions.append(action_info) + # --- 去重逻辑结束 --- + + total_actions = len(unique_actions) + if len(reply_actions) > total_actions: + logger.info(f"[多重回复] 原始回复任务 {len(reply_actions)} 个,去重后剩余 {total_actions} 个。") + elif total_actions > 1: logger.info(f"[多重回复] 开始执行 {total_actions} 个回复任务。") - for i, action_info in enumerate(reply_actions): + for i, action_info in enumerate(unique_actions): is_last_action = i == total_actions - 1 if total_actions > 1: logger.info(f"[多重回复] 正在执行第 {i+1}/{total_actions} 个回复...") - + # 传递 clear_unread 参数 result = await self._execute_single_reply_action(action_info, plan, clear_unread=is_last_action) results.append(result) From 9a945075b4531432c0298b56b46fcfa2f7f271f9 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Wed, 1 Oct 2025 12:39:23 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix(maizone):=20=E5=A2=9E=E5=BC=BA=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E7=A9=BA=E9=97=B4=E5=8A=A8=E6=80=81=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E7=9A=84=E5=81=A5=E5=A3=AE=E6=80=A7=E4=BB=A5=E9=98=B2=E6=AD=A2?= =?UTF-8?q?=E5=B4=A9=E6=BA=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QQ空间API在某些情况下可能返回非预期的数据格式,例如 `pictotal` 或 `commentlist` 字段为 `None` 而不是空列表。 之前的代码直接对这些字段进行迭代,当遇到非列表类型时会导致 `TypeError` 异常,从而中断动态的获取流程。 本次修改通过在处理图片和评论列表前添加 `isinstance` 类型检查,确保了只在数据结构符合预期时才进行操作,从而避免了因API返回数据格式异常而导致的程序崩溃。 --- .../services/qzone_service.py | 44 ++++++++++++------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/src/plugins/built_in/maizone_refactored/services/qzone_service.py b/src/plugins/built_in/maizone_refactored/services/qzone_service.py index 40c0d424a..2022461f7 100644 --- a/src/plugins/built_in/maizone_refactored/services/qzone_service.py +++ b/src/plugins/built_in/maizone_refactored/services/qzone_service.py @@ -740,28 +740,42 @@ class QZoneService: feeds_list = [] my_name = json_data.get("logininfo", {}).get("name", "") for msg in json_data.get("msglist", []): + # 只有在处理好友说说时,才检查是否已评论并跳过 + commentlist = msg.get("commentlist") + # 只有在处理好友说说时,才检查是否已评论并跳过 if not is_monitoring_own_feeds: - is_commented = any( - c.get("name") == my_name for c in msg.get("commentlist", []) if isinstance(c, dict) - ) + is_commented = False + if isinstance(commentlist, list): + is_commented = any( + c.get("name") == my_name for c in commentlist if isinstance(c, dict) + ) if is_commented: continue - images = [pic["url1"] for pic in msg.get("pictotal", []) if "url1" in pic] + # --- 安全地处理图片列表 --- + images = [] + pictotal = msg.get("pictotal") + if isinstance(pictotal, list): + images = [ + pic["url1"] for pic in pictotal if isinstance(pic, dict) and "url1" in pic + ] + # --- 安全地处理评论列表 --- comments = [] - if "commentlist" in msg: - for c in msg["commentlist"]: - comments.append( - { - "qq_account": c.get("uin"), - "nickname": c.get("name"), - "content": c.get("content"), - "comment_tid": c.get("tid"), - "parent_tid": c.get("parent_tid"), # API直接返回了父ID - } - ) + if isinstance(commentlist, list): + for c in commentlist: + # 确保评论条目也是字典 + if isinstance(c, dict): + comments.append( + { + "qq_account": c.get("uin"), + "nickname": c.get("name"), + "content": c.get("content"), + "comment_tid": c.get("tid"), + "parent_tid": c.get("parent_tid"), + } + ) feeds_list.append( {