fix: 更新版本号至 0.13.0,增强数据库迁移功能,注册通知事件处理

This commit is contained in:
Windpicker-owo
2025-11-27 22:37:50 +08:00
parent 73aaedaca6
commit 3538716515
4 changed files with 201 additions and 20 deletions

View File

@@ -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 = [

View File

@@ -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()

View File

@@ -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 APIsend_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="是否启用群聊表情回复处理"),
},
}

View File

@@ -108,7 +108,7 @@ ACCEPT_FORMAT = [
]
# 插件名称
PLUGIN_NAME = "NEW_napcat_adapter"
PLUGIN_NAME = "napcat_adapter_plugin"
# QQ表情映射表
QQ_FACE = {