diff --git a/pyproject.toml b/pyproject.toml index cbf1dd913..0c0246a3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "MoFox-Bot" -version = "0.12.0" +version = "0.13.0" description = "MoFox-Bot 是一个基于大语言模型的可交互智能体" requires-python = ">=3.11,<=3.13" dependencies = [ diff --git a/src/common/database/core/migration.py b/src/common/database/core/migration.py index b86c69e53..5b718224f 100644 --- a/src/common/database/core/migration.py +++ b/src/common/database/core/migration.py @@ -84,11 +84,12 @@ async def check_and_migrate_database(existing_engine=None): try: # 检查并添加缺失的列 - db_columns = await connection.run_sync( + db_columns_info = await connection.run_sync( lambda conn: { - col["name"] for col in inspector.get_columns(table_name) + col["name"]: col for col in inspector.get_columns(table_name) } ) + db_columns = set(db_columns_info.keys()) model_columns = {col.name for col in table.c} missing_columns = model_columns - db_columns @@ -144,7 +145,12 @@ async def check_and_migrate_database(existing_engine=None): # 提交列添加事务 await connection.commit() else: - logger.info(f"表 '{table_name}' 的列结构一致。") + logger.debug(f"表 '{table_name}' 的列结构一致。") + + # 3. 检查并修复列类型不匹配(仅 PostgreSQL) + await _check_and_fix_column_types( + connection, inspector, table_name, table, db_columns_info + ) # 检查并创建缺失的索引 db_indexes = await connection.run_sync( @@ -225,3 +231,126 @@ async def drop_all_tables(existing_engine=None): await connection.run_sync(Base.metadata.drop_all) logger.warning("所有数据库表已删除。") + + +# ============================================================================= +# 列类型修复辅助函数 +# ============================================================================= + +# 已知需要修复的列类型映射 +# 格式: {(表名, 列名): (期望的Python类型类别, PostgreSQL USING 子句)} +# Python类型类别: "boolean", "integer", "float", "string" +_BOOLEAN_USING_CLAUSE = ( + "boolean", + "USING CASE WHEN {column} IS NULL THEN FALSE " + "WHEN {column} = 0 THEN FALSE ELSE TRUE END" +) + +_COLUMN_TYPE_FIXES = { + # messages 表的布尔列 + ("messages", "is_public_notice"): _BOOLEAN_USING_CLAUSE, + ("messages", "should_reply"): _BOOLEAN_USING_CLAUSE, + ("messages", "should_act"): _BOOLEAN_USING_CLAUSE, + ("messages", "is_mentioned"): _BOOLEAN_USING_CLAUSE, + ("messages", "is_emoji"): _BOOLEAN_USING_CLAUSE, + ("messages", "is_picid"): _BOOLEAN_USING_CLAUSE, + ("messages", "is_command"): _BOOLEAN_USING_CLAUSE, + ("messages", "is_notify"): _BOOLEAN_USING_CLAUSE, +} + + +def _get_expected_pg_type(python_type_category: str) -> str: + """获取期望的 PostgreSQL 类型名称""" + mapping = { + "boolean": "boolean", + "integer": "integer", + "float": "double precision", + "string": "text", + } + return mapping.get(python_type_category, "text") + + +def _normalize_pg_type(type_name: str) -> str: + """标准化 PostgreSQL 类型名称用于比较""" + type_name = type_name.lower().strip() + # 处理常见的别名 + aliases = { + "bool": "boolean", + "int": "integer", + "int4": "integer", + "int8": "bigint", + "float8": "double precision", + "float4": "real", + "numeric": "numeric", + "decimal": "numeric", + } + return aliases.get(type_name, type_name) + + +async def _check_and_fix_column_types(connection, inspector, table_name, table, db_columns_info): + """检查并修复列类型不匹配的问题(仅 PostgreSQL) + + Args: + connection: 数据库连接 + inspector: SQLAlchemy inspector + table_name: 表名 + table: SQLAlchemy Table 对象 + db_columns_info: 数据库中列的信息字典 + """ + # 获取数据库方言 + def get_dialect_name(conn): + return conn.dialect.name + + dialect_name = await connection.run_sync(get_dialect_name) + + # 目前只处理 PostgreSQL + if dialect_name != "postgresql": + return + + for (fix_table, fix_column), (expected_type_category, using_clause) in _COLUMN_TYPE_FIXES.items(): + if fix_table != table_name: + continue + + if fix_column not in db_columns_info: + continue + + col_info = db_columns_info[fix_column] + current_type = _normalize_pg_type(str(col_info.get("type", ""))) + expected_type = _get_expected_pg_type(expected_type_category) + + # 如果类型已经正确,跳过 + if current_type == expected_type: + continue + + # 检查是否需要修复:如果当前是 numeric 但期望是 boolean + if current_type == "numeric" and expected_type == "boolean": + logger.warning( + f"发现列类型不匹配: {table_name}.{fix_column} " + f"(当前: {current_type}, 期望: {expected_type})" + ) + + # PostgreSQL 需要先删除默认值,再修改类型,最后重新设置默认值 + using_sql = using_clause.format(column=fix_column) + drop_default_sql = f"ALTER TABLE {table_name} ALTER COLUMN {fix_column} DROP DEFAULT" + alter_type_sql = f"ALTER TABLE {table_name} ALTER COLUMN {fix_column} TYPE BOOLEAN {using_sql}" + set_default_sql = f"ALTER TABLE {table_name} ALTER COLUMN {fix_column} SET DEFAULT FALSE" + + try: + def execute_alter(conn): + # 步骤 1: 删除默认值 + try: + conn.execute(text(drop_default_sql)) + except Exception: + pass # 如果没有默认值,忽略错误 + # 步骤 2: 修改类型 + conn.execute(text(alter_type_sql)) + # 步骤 3: 重新设置默认值 + conn.execute(text(set_default_sql)) + + await connection.run_sync(execute_alter) + await connection.commit() + logger.info(f"成功修复列类型: {table_name}.{fix_column} -> BOOLEAN") + except Exception as e: + logger.error(f"修复列类型失败 {table_name}.{fix_column}: {e}") + await connection.rollback() + diff --git a/src/plugins/built_in/napcat_adapter/plugin.py b/src/plugins/built_in/napcat_adapter/plugin.py index 9b8d14376..57dde90ce 100644 --- a/src/plugins/built_in/napcat_adapter/plugin.py +++ b/src/plugins/built_in/napcat_adapter/plugin.py @@ -99,8 +99,47 @@ class NapcatAdapter(BaseAdapter): self.meta_event_handler.set_plugin_config(self.plugin.config) self.send_handler.set_plugin_config(self.plugin.config) + # 注册 notice 事件到 event manager + await self._register_notice_events() + logger.info("Napcat 适配器已加载") + async def _register_notice_events(self) -> None: + """注册 notice 相关事件到 event manager""" + from src.plugin_system.core.event_manager import event_manager + from .src.event_types import NapcatEvent + + # 定义所有 notice 事件类型 + notice_events = [ + NapcatEvent.ON_RECEIVED.POKE, + NapcatEvent.ON_RECEIVED.EMOJI_LIEK, + NapcatEvent.ON_RECEIVED.GROUP_UPLOAD, + NapcatEvent.ON_RECEIVED.GROUP_BAN, + NapcatEvent.ON_RECEIVED.GROUP_LIFT_BAN, + NapcatEvent.ON_RECEIVED.FRIEND_RECALL, + NapcatEvent.ON_RECEIVED.GROUP_RECALL, + NapcatEvent.ON_RECEIVED.FRIEND_INPUT, + ] + + # 注册所有事件 + registered_count = 0 + for event_type in notice_events: + try: + # 使用同步的 register_event 方法注册事件 + success = event_manager.register_event( + event_name=event_type, + allowed_triggers=["napcat_adapter_plugin"], # 只允许此插件触发 + ) + if success: + registered_count += 1 + logger.debug(f"已注册 notice 事件: {event_type}") + else: + logger.debug(f"notice 事件已存在: {event_type}") + except Exception as e: + logger.warning(f"注册 notice 事件失败: {event_type}, 错误: {e}") + + logger.info(f"已注册 {registered_count} 个新 notice 事件类型(共 {len(notice_events)} 个)") + async def on_adapter_unloaded(self) -> None: """适配器卸载时的清理""" logger.info("Napcat 适配器正在关闭...") @@ -133,22 +172,28 @@ class NapcatAdapter(BaseAdapter): if not future.done(): future.set_result(raw) - # 消息事件 - if post_type == "message": - return await self.message_handler.handle_raw_message(raw) # type: ignore[return-value] + try: + # 消息事件 + if post_type == "message": + return await self.message_handler.handle_raw_message(raw) # type: ignore[return-value] + # 通知事件 + elif post_type == "notice": + return await self.notice_handler.handle_notice(raw) # type: ignore[return-value] - # 通知事件 - elif post_type == "notice": - return await self.notice_handler.handle_notice(raw) # type: ignore[return-value] - - # 元事件 - elif post_type == "meta_event": - return await self.meta_event_handler.handle_meta_event(raw) # type: ignore[return-value] - - # 未知事件类型 - else: - return + # 元事件 + elif post_type == "meta_event": + return await self.meta_event_handler.handle_meta_event(raw) # type: ignore[return-value] + # 未知事件类型 + else: + return None + except ValueError as ve: + logger.warning(f"处理 Napcat 事件时数据无效: {ve}") + return None + except Exception as e: + logger.error(f"处理 Napcat 事件失败: {e}, 原始数据: {raw}") + return None + async def _send_platform_message(self, envelope: MessageEnvelope) -> None: # type: ignore[override] """ 将 MessageEnvelope 转换并发送到 Napcat @@ -156,7 +201,10 @@ class NapcatAdapter(BaseAdapter): 这里不直接通过 WebSocket 发送 envelope, 而是调用 Napcat API(send_group_msg, send_private_msg 等) """ - await self.send_handler.handle_message(envelope) + try: + await self.send_handler.handle_message(envelope) + except Exception as e: + logger.error(f"发送 Napcat 消息失败: {e}") async def send_napcat_api(self, action: str, params: Dict[str, Any], timeout: float = 30.0) -> Dict[str, Any]: """ @@ -265,6 +313,10 @@ class NapcatAdapterPlugin(BasePlugin): "private_list": ConfigField(type=list, default=[], description="私聊名单;根据名单模式过滤"), "ban_user_id": ConfigField(type=list, default=[], description="全局封禁的用户 ID 列表"), "ban_qq_bot": ConfigField(type=bool, default=False, description="是否屏蔽其他 QQ 机器人消息"), + "enable_poke": ConfigField(type=bool, default=True, description="是否启用戳一戳消息处理"), + "ignore_non_self_poke": ConfigField(type=bool, default=False, description="是否忽略不是针对自己的戳一戳消息"), + "poke_debounce_seconds": ConfigField(type=float, default=2.0, description="戳一戳防抖时间(秒)"), + "enable_emoji_like": ConfigField(type=bool, default=True, description="是否启用群聊表情回复处理"), }, } diff --git a/src/plugins/built_in/napcat_adapter/src/event_models.py b/src/plugins/built_in/napcat_adapter/src/event_models.py index ef5e2317e..6917e9716 100644 --- a/src/plugins/built_in/napcat_adapter/src/event_models.py +++ b/src/plugins/built_in/napcat_adapter/src/event_models.py @@ -108,7 +108,7 @@ ACCEPT_FORMAT = [ ] # 插件名称 -PLUGIN_NAME = "NEW_napcat_adapter" +PLUGIN_NAME = "napcat_adapter_plugin" # QQ表情映射表 QQ_FACE = {