# matrix_service.py from nio import ( AsyncClient, LoginResponse, RoomMessageText, InviteMemberEvent, ClientConfig, MegolmEvent, exceptions, KeyVerificationEvent, KeyVerificationStart, KeyVerificationCancel, KeyVerificationKey, KeyVerificationMac, ToDeviceError ) import config import asyncio import schedule import requests from datetime import datetime, timedelta import os import json from typing import Optional, Tuple import aiofiles from openai import OpenAI STORE_FOLDER = "store/" SESSION_DETAILS_FILE = "credentials.json" MODEL_SELECT_RETRIES = 3 # 模型选择最大重试次数 # 初始化XAI客户端 xai_client = OpenAI( api_key=config.XAI_API_KEY, base_url=config.XAI_BASE_URL, ) class MatrixService: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super(MatrixService, cls).__new__(cls) # 確保存儲目錄存在 if not os.path.exists(STORE_FOLDER): os.makedirs(STORE_FOLDER) # 創建客戶端配置 client_config = ClientConfig( store_sync_tokens=True, encryption_enabled=True ) # 初始化客戶端 cls._instance.client = AsyncClient( config.MATRIX_SERVER, config.USERNAME, device_id=config.DEVICE_ID, store_path=STORE_FOLDER, config=client_config, ) cls._instance.is_logged_in = False cls._instance.start_time = datetime.now() # 用戶AI對話狀態和會話歷史 # (room_id, user_id) -> (last_activity_time, messages, model_key) cls._instance.ai_chat_users = {} # 用户等待AI响应状态 # (room_id, user_id) -> (waiting_since, last_question) cls._instance.ai_waiting_users = {} # 用户模型使用记录 # user_id -> {model_key -> (last_reset_time, count)} cls._instance.model_usage = {} # 模型选择状态 # (room_id, user_id) -> (retry_count) cls._instance.model_select_state = {} # 已發送歡迎消息的房間記錄 cls._instance.welcomed_rooms = set() # 记录所有私聊房间信息 # room_id -> user_id cls._instance.private_chat_rooms = {} # 添加事件回調 cls._instance.client.add_event_callback( cls._instance.message_callback, RoomMessageText ) cls._instance.client.add_event_callback( cls._instance.encrypted_message_callback, MegolmEvent ) cls._instance.client.add_event_callback( cls._instance.invite_callback, InviteMemberEvent ) # 添加设备验证回调 cls._instance.client.add_to_device_callback( cls._instance.to_device_callback, (KeyVerificationEvent,) ) # 啟動AI超時檢查任務 asyncio.create_task(cls._instance.check_ai_chat_timeout()) # 启动AI响应超时检查任务 asyncio.create_task(cls._instance.check_ai_response_timeout()) return cls._instance async def to_device_callback(self, event): """处理设备验证事件,自动接受所有验证请求""" try: if isinstance(event, KeyVerificationStart): print(f"收到验证请求,来自 {event.sender}") # 自动接受所有验证请求,不限制验证方式 resp = await self.client.accept_key_verification(event.transaction_id) if isinstance(resp, ToDeviceError): print(f"接受验证请求失败: {resp}") return sas = self.client.key_verifications[event.transaction_id] todevice_msg = sas.share_key() resp = await self.client.to_device(todevice_msg) if isinstance(resp, ToDeviceError): print(f"发送设备密钥失败: {resp}") elif isinstance(event, KeyVerificationCancel): print(f"验证被 {event.sender} 取消,原因: {event.reason}") elif isinstance(event, KeyVerificationKey): print(f"收到验证密钥,自动接受验证") resp = await self.client.confirm_short_auth_string(event.transaction_id) if isinstance(resp, ToDeviceError): print(f"确认验证失败: {resp}") elif isinstance(event, KeyVerificationMac): sas = self.client.key_verifications[event.transaction_id] try: todevice_msg = sas.get_mac() resp = await self.client.to_device(todevice_msg) if isinstance(resp, ToDeviceError): print(f"发送MAC失败: {resp}") else: print(f"验证完成,设备已验证: {sas.verified_devices}") # 重新信任所有已验证的设备 for device_id in sas.verified_devices: if event.sender in self.client.device_store: if device_id in self.client.device_store[event.sender]: olm_device = ( self.client.device_store[event.sender][device_id] ) self.client.verify_device(olm_device) print( f"已信任设备 {device_id} 来自用户 {event.sender}") except Exception as e: print(f"处理MAC时出错: {e}") except Exception as e: print(f"处理设备验证事件时出错: {e}") def trust_devices(self, user_id: str) -> None: """信任用戶的所有設備,包括自己的设备""" print(f"信任用戶 {user_id} 的所有設備") try: if user_id not in self.client.device_store: print(f"找不到用户 {user_id} 的设备信息") return for device_id, olm_device in self.client.device_store[user_id].items(): # 不跳过任何设备,包括自己的设备 print(f"信任設備: {device_id}") self.client.verify_device(olm_device) print(f"已信任设备 {device_id} 来自用户 {user_id}") except Exception as e: print(f"信任设备时出错: {e}") async def check_ai_response_timeout(self): """检查AI响应超时""" while True: try: current_time = datetime.now() timeout_users = [] for (room_id, user_id), (waiting_since, _) in ( self.ai_waiting_users.items() ): timeout = timedelta( minutes=config.AI_RESPONSE_TIMEOUT_MINUTES ) if current_time - waiting_since > timeout: timeout_users.append((room_id, user_id)) # 处理超时的用户 for room_id, user_id in timeout_users: chat_key = (room_id, user_id) if chat_key in self.ai_waiting_users: del self.ai_waiting_users[chat_key] if chat_key in self.ai_chat_users: del self.ai_chat_users[chat_key] await self.send_message( "AI系统貌似并没有反应了,目前先退出这次AI对话," "可能有其他问题,并反馈给管理员知道并等待修复", room_id=room_id, encrypted=True ) await asyncio.sleep(60) # 每分钟检查一次 except Exception as e: print(f"检查AI响应超时时出错: {e}") await asyncio.sleep(60) def get_model_list_message(self, user_id: str) -> str: """生成模型列表消息""" message = "请问你需要使用下面模型?\n\n" # 首先添加XAI模型作为第一个选项 # 获取XAI使用统计 xai_usage = self.get_model_usage(user_id, "xai") if config.XAI_DAILY_LIMIT == -1: xai_limit_text = "无限制" else: xai_limit_text = f"每天最多{config.XAI_DAILY_LIMIT}条" if xai_usage > 0: xai_limit_text += f"(已使用{xai_usage}条)" message += f"(1){config.XAI_MODEL_DIYNAME} {xai_limit_text}\n" # 然后添加其他OpenRouter模型 for i, (key, model) in enumerate(config.OPENROUTER_MODELS.items(), 2): # 获取使用统计 usage = self.get_model_usage(user_id, key) # 构建显示文本 if model["daily_limit"] == -1: limit_text = "无限制" else: limit_text = f"每天最多{model['daily_limit']}条" if usage > 0: limit_text += f"(已使用{usage}条)" message += f"({i}){model['name']} {limit_text}\n" message += ( "\n如果你想使用第一个默认模型,请回复1或者'好的'就开始AI对话," "如果使用其他模型请输入对应的数字就可以啦" ) return message def get_model_key_by_index(self, index: int) -> Optional[str]: """根据索引获取模型key""" if index == 1: # XAI模型 return "xai" try: return list(config.OPENROUTER_MODELS.keys())[index - 2] except IndexError: return None async def chat_with_xai(self, message: str, history: list) -> tuple[str, Optional[str]]: """与XAI API交互获取回复""" try: if not config.XAI_API_KEY: return "抱歉,XAI服务暂时不可用,请选择其他模型。", None completion = xai_client.chat.completions.create( model="grok-beta", messages=history + [{"role": "user", "content": message}], ) content = completion.choices[0].message.content # 检查是否有富文本格式 formatted_content = None if "```" in content: formatted_content = content.replace( "```", "
", 1
                ).replace("```", "
", 1) return content, formatted_content except Exception as e: print(f"XAI API调用出错: {e}") return "抱歉,XAI服务暂时无法使用,请稍后再试。", None def get_model_usage(self, user_id: str, model_key: str) -> int: """获取用户对特定模型的当天使用次数""" if user_id not in self.model_usage: self.model_usage[user_id] = {} if model_key not in self.model_usage[user_id]: self.model_usage[user_id][model_key] = (datetime.now(), 0) last_reset, count = self.model_usage[user_id][model_key] # 检查是否需要重置计数 if datetime.now().date() > last_reset.date(): self.model_usage[user_id][model_key] = (datetime.now(), 0) return 0 return count def increment_model_usage(self, user_id: str, model_key: str) -> int: """增加用户对特定模型的使用次数,返回新的使用次数""" current_usage = self.get_model_usage(user_id, model_key) self.model_usage[user_id][model_key] = ( self.model_usage[user_id][model_key][0], current_usage + 1 ) return current_usage + 1 def check_model_limit( self, user_id: str, model_key: str ) -> Tuple[bool, Optional[str]]: """检查模型使用限制,返回(是否可用, 提示消息)""" if model_key == "xai": # XAI模型使用XAI_DAILY_LIMIT daily_limit = config.XAI_DAILY_LIMIT else: model_info = config.OPENROUTER_MODELS[model_key] daily_limit = model_info["daily_limit"] if daily_limit == -1: return True, None current_usage = self.get_model_usage(user_id, model_key) if current_usage >= daily_limit: model_name = config.XAI_MODEL_DIYNAME if model_key == "xai" else config.OPENROUTER_MODELS[ model_key]["name"] return False, ( f"感谢你使用{model_name},已经到达当天最大条数," f"欢迎你明天之后继续使用该模型!你也可以使用其他的模型!" ) # 检查是否需要发送提醒 remaining = daily_limit - current_usage if remaining in [6, 2, 1]: return True, f"(温馨提示:今天还剩余{remaining-1}条对话次数)" return True, None def process_model_selection( self, user_id: str, room_id: str, message: str ) -> Optional[str]: """处理模型选择,返回选择的模型key或None""" chat_key = (room_id, user_id) # 检查是否是默认选择 if message.lower() in ['1', '好的']: return "xai" # 返回XAI作为默认模型 # 尝试解析数字选择 try: index = int(message) model_key = self.get_model_key_by_index(index) if model_key: return model_key except ValueError: pass # 更新重试次数 if chat_key not in self.model_select_state: self.model_select_state[chat_key] = 1 else: self.model_select_state[chat_key] += 1 return None async def reestablish_room_session(self, room_id: str, user_id: str): """重新建立房间的加密会话""" try: print(f"正在重新建立与用户 {user_id} 在房间 {room_id} 的加密会话...") # 清除现有的房间密钥 if room_id in self.client.rooms: self.client.rooms[room_id].encrypted = True # 上传新的设备密钥 if self.client.should_upload_keys: await self.client.keys_upload() # 信任所有设备,包括自己的设备 self.trust_devices(self.client.user_id) self.trust_devices(user_id) # 强制同步以获取最新状态 await self.client.sync() # 发送测试消息以触发新的密钥交换 test_content = { "msgtype": "m.text", "body": "正在重新建立加密会话..." } await self.client.room_send( room_id=room_id, message_type="m.room.message", content=test_content ) print(f"已重新建立与用户 {user_id} 的加密会话") return True except Exception as e: print(f"重新建立加密会话失败: {e}") return False def trust_devices(self, user_id: str) -> None: """信任用戶的所有設備""" print(f"信任用戶 {user_id} 的所有設備") try: for device_id, olm_device in self.client.device_store[user_id].items(): if user_id == self.client.user_id and device_id == self.client.device_id: continue print(f"信任設備: {device_id}") self.client.verify_device(olm_device) print(f"已经信任设备:{device_id}") except Exception as e: print(f"信任用户设备时出错:{e}") async def trust_room_devices(self, room_id: str): """信任房間中所有用戶的所有設備""" try: room = self.client.rooms[room_id] for user_id in room.users: self.trust_devices(user_id) except Exception as e: print(f"信任房間設備時出錯: {e}") async def login(self): """登錄並處理默認房間""" if not self.is_logged_in: try: # 嘗試恢復會話 if os.path.exists(SESSION_DETAILS_FILE): async with aiofiles.open(SESSION_DETAILS_FILE) as f: print("密钥文件存在,开始读取") contents = await f.read() session_data = json.loads(contents) self.client.access_token = session_data["access_token"] self.client.user_id = session_data["user_id"] self.client.device_id = session_data["device_id"] print("准备加载STORE") self.client.load_store() print(f"使用已保存的憑證登錄: {self.client.user_id}") self.is_logged_in = True # 上传设备密钥 if self.client.should_upload_keys: await self.client.keys_upload() # 信任自己的设备 self.trust_devices(self.client.user_id) print("正在同步房間狀態...") await self.client.sync() return True # 如果沒有保存的會話,使用密碼登錄 print("准备开始使用账号密码登陆") response = await self.client.login( password=config.PASSWORD, device_name=config.DEVICE_NAME ) print("Login response:", response) if isinstance(response, LoginResponse): self.is_logged_in = True print("登錄成功!") # 保存會話信息 async with aiofiles.open(SESSION_DETAILS_FILE, "w") as f: await f.write(json.dumps({ "access_token": response.access_token, "device_id": response.device_id, "user_id": response.user_id, })) # 上传设备密钥 if self.client.should_upload_keys: await self.client.keys_upload() # 信任自己的设备 self.trust_devices(self.client.user_id) print("正在同步房間狀態...") await self.client.sync() if hasattr(config, 'ROOM_IDS') and config.ROOM_IDS: for room_id in config.ROOM_IDS: if room_id in self.client.rooms: print(f"已在房間 {room_id} 中") else: print(f"嘗試加入房間 {room_id}") await self.ensure_joined(room_id) else: print("未配置默認房間,等待用戶邀請...") return True else: print("登錄失敗!") return False except Exception as e: print(f"登錄過程出錯: {e}") return False return True async def check_ai_chat_timeout(self): """定期檢查AI對話超時""" while True: try: current_time = datetime.now() timeout_sessions = [] for (room_id, user_id), (last_activity, _, _) in ( self.ai_chat_users.items() ): if isinstance(last_activity, datetime): timeout = timedelta( minutes=config.AI_CHAT_TIMEOUT_MINUTES ) if current_time - last_activity > timeout: timeout_sessions.append((room_id, user_id)) # 移除超時會話 for room_id, user_id in timeout_sessions: chat_key = (room_id, user_id) if chat_key in self.ai_chat_users: del self.ai_chat_users[chat_key] if chat_key in self.ai_waiting_users: del self.ai_waiting_users[chat_key] print(f"用戶 {user_id} 在房間 {room_id} 的AI對話已超時") await asyncio.sleep(60) # 每分鐘檢查一次 except Exception as e: print(f"檢查AI對話超時時出錯: {e}") await asyncio.sleep(60) def get_welcome_message(self) -> str: """獲取歡迎消息""" return ( "你好!我是一個自動回覆機器人。\n" "你可以使用以下命令:\n" "- !ping: 測試我是否在線\n" "- 加密货币: 獲取當前加密貨幣價格\n" "- 人工智能: 開始AI對話" ) async def invite_callback(self, room, event): """當 BOT 被邀請到房間時自動加入並處理驗證""" try: print(f"收到來自 {event.sender} 的邀請") # 自動加入房間 join_response = await self.client.join(room.room_id) if join_response: print(f"已加入與 {event.sender} 的房間") # 同步以獲取房間狀態 await self.client.sync() # 信任房間中的設備 await self.trust_room_devices(room.room_id) # 检查是否为私聊房间(只有两个用户) room_obj = self.client.rooms[room.room_id] if len(room_obj.users) == 2: # 记录私聊房间信息 self.private_chat_rooms[room.room_id] = event.sender print(f"记录新的私聊房间: {room.room_id} - 用户: {event.sender}") # 發送歡迎消息 await self.send_message( self.get_welcome_message(), room_id=room.room_id, encrypted=True ) # 記錄已發送歡迎消息 self.welcomed_rooms.add(room.room_id) except Exception as e: print(f"處理邀請時出錯: {e}") def is_room_encrypted(self, room_id: str) -> bool: """檢查房間是否加密""" try: if room_id not in self.client.rooms: return False room = self.client.rooms[room_id] return getattr(room, "encrypted", False) except Exception as e: print(f"檢查房間加密狀態時出錯: {e}") return False async def ensure_joined(self, room_id: str): """確保加入指定房間""" try: if room_id not in self.client.rooms: response = await self.client.join(room_id) if isinstance(response, LoginResponse): print(f"加入房間失敗: {response.message}") return print(f"成功加入房間: {room_id}") await self.client.sync() # 信任房間中的設備 await self.trust_room_devices(room_id) except Exception as e: print(f"確保加入房間時出錯: {e}") async def get_crypto_prices(self): """使用 CoinGecko API 獲取加密貨幣價格和漲跌幅信息""" try: # CoinGecko API 基礎URL base_url = "https://api.coingecko.com/api/v3" # 獲取BTC、ETH和ADA的數據 coins = { "bitcoin": "BTCUSDT", "ethereum": "ETHUSDT" # , # "cardano": "ADAUSDT" } # 獲取當前價格和24小時漲跌幅 current_prices_url = ( f"{base_url}/simple/price" f"?ids={','.join(coins.keys())}" f"&vs_currencies=usd" f"&include_24hr_change=true" ) response = requests.get( current_prices_url, headers={"accept": "application/json"} ) response.raise_for_status() current_data = response.json() # 獲取7天前的日期 seven_days_ago = ( datetime.now() - timedelta(days=7) ).strftime('%d-%m-%Y') # 獲取30天前的日期 thirty_days_ago = ( datetime.now() - timedelta(days=30) ).strftime('%d-%m-%Y') prices = {} for coin_id, symbol in coins.items(): try: # 獲取當前價格和24小時漲跌幅 current_price = current_data[coin_id]["usd"] price_change_24h = current_data[coin_id]["usd_24h_change"] # 獲取7天前的價格 history_url_7d = ( f"{base_url}/coins/{coin_id}/history?date={seven_days_ago}" ) response = requests.get( history_url_7d, headers={"accept": "application/json"} ) response.raise_for_status() history_data_7d = response.json() price_7d_ago = ( history_data_7d["market_data"]["current_price"]["usd"] ) # 獲取30天前的價格 history_url_30d = ( f"{base_url}/coins/{coin_id}/history?date={thirty_days_ago}" ) response = requests.get( history_url_30d, headers={"accept": "application/json"} ) response.raise_for_status() history_data_30d = response.json() price_30d_ago = ( history_data_30d["market_data"]["current_price"]["usd"] ) # 計算7天和30天漲跌幅 price_change_7d = ( (current_price - price_7d_ago) / price_7d_ago) * 100 price_change_30d = ( (current_price - price_30d_ago) / price_30d_ago) * 100 prices[symbol] = { "current_price": current_price, "price_change_percent_24h": price_change_24h, "price_change_percent_7d": price_change_7d, "price_change_percent_30d": price_change_30d } except Exception as e: print(f"處理 {coin_id} 數據時出錯: {e}") continue return prices if prices else None except requests.exceptions.HTTPError as e: print(f"HTTP API請求錯誤: {e}") if e.response.status_code == 429: return "获取加密货币的接口请求过于频繁,请等一段时间之后再获取!" else: return "网络接口出现问题不能连通,请稍后再尝试或者等待修复之后再使用!" except requests.exceptions.RequestException as e: err_msg = (f"API請求錯誤: {e}") return err_msg except KeyError as e: print(f"數據解析錯誤: {e}") return None except Exception as e: print(f"獲取加密貨幣價格時出錯: {e}") return None async def send_message( self, message: str, room_id: str = config.ROOM_ID, formatted_message: Optional[str] = None, encrypted: bool = False ): """發送消息(支持加密和未加密房間)""" if not self.is_logged_in: print("Error: Not logged in!") return content = {"msgtype": "m.text", "body": message} if formatted_message: content["format"] = "org.matrix.custom.html" content["formatted_body"] = formatted_message try: # 如果需要加密或房間是加密的 if encrypted or self.is_room_encrypted(room_id): # 获取房间中的所有用户 room = self.client.rooms[room_id] for user_id in room.users: if user_id != self.client.user_id: # 确保信任所有设备 self.trust_devices(user_id) await self.client.room_send( room_id=room_id, message_type="m.room.message", content=content ) print(f"Message sent successfully to room {room_id}") except exceptions.OlmUnverifiedDeviceError as e: print(f"房間有未驗證的設備: {e}") try: # 获取房间中的所有用户 room = self.client.rooms[room_id] for user_id in room.users: if user_id != self.client.user_id: # 重新信任设备 self.trust_devices(user_id) # 重试发送 await self.client.room_send( room_id=room_id, message_type="m.room.message", content=content ) print("重试发送消息成功") except Exception as retry_e: print(f"重试发送消息失败: {retry_e}") except Exception as e: print(f"Error sending message: {e}") # else: # print("Error: Not logged in!") async def sync(self): print("Start Sync...") if self.is_logged_in: while True: print("Sync task running...") await self.client.sync_forever(timeout=60000, full_state=True) def schedule_daily_message(self, message): schedule.every().day.at("09:00").do( lambda: asyncio.run(self.send_message(message)) ) def schedule_hourly_updates(self): schedule.every().hour.do(self.fetch_and_send_news) async def send_crypto_prices(self, room_id=config.ROOM_ID): """發送加密貨幣價格信息""" crypto_prices = await self.get_crypto_prices() if crypto_prices and not isinstance(crypto_prices, str): # 準備富文本消息 formatted_message = ( "

📊 加密貨幣市場報告

" "" "" "" "" "" "" "" "" ) # 準備純文本消息 plain_message = "📊 加密貨幣市場報告\n\n" # 幣種名稱映射 coin_names = { "BTCUSDT": "比特幣 (BTC)", "ETHUSDT": "以太坊 (ETH)" # , # "ADAUSDT": "卡爾達諾 (ADA)" } for symbol, info in crypto_prices.items(): # 獲取價格變化的顏色 color_24h = ( "green" if info["price_change_percent_24h"] >= 0 else "red" ) color_7d = ( "green" if info["price_change_percent_7d"] >= 0 else "red" ) color_30d = ( "green" if info["price_change_percent_30d"] >= 0 else "red" ) # 添加到富文本消息 formatted_message += ( f"" f"" f"" f"" f"" f"" f"" ) # 添加到純文本消息 plain_message += ( f"{coin_names[symbol]}:\n" f" 當前價格: ${info['current_price']:,.2f}\n" f" 24h漲跌: {info['price_change_percent_24h']:+.2f}%\n" f" 7d漲跌: {info['price_change_percent_7d']:+.2f}%\n" f" 30d漲跌: {info['price_change_percent_30d']:+.2f}%\n\n" ) formatted_message += "
幣種當前價格24h漲跌7d漲跌30d漲跌
{coin_names[symbol]}" f"${info['current_price']:,.2f}" f"{info['price_change_percent_24h']:+.2f}%" f"{info['price_change_percent_7d']:+.2f}%" f"{info['price_change_percent_30d']:+.2f}%
" # 發送消息 await self.send_message( plain_message, room_id=room_id, formatted_message=formatted_message, encrypted=True ) else: if not crypto_prices: error_msg = "抱歉,暫時無法獲取加密貨幣價格信息。" elif crypto_prices and isinstance(crypto_prices, str): error_msg = crypto_prices await self.send_message( error_msg, room_id=room_id, encrypted=True ) async def chat_with_ai( self, message: str, history: list, model_key: str ) -> tuple[str, Optional[str]]: """統一的AI對話接口""" if model_key == "xai": return await self.chat_with_xai(message, history) else: # 原有的OpenRouter API交互逻辑 try: headers = { "Authorization": f"Bearer {config.OPENROUTER_API_KEY}", "Content-Type": "application/json", "HTTP-Referer": "https://github.com", "X-Title": "Bot" } messages = history + [{"role": "user", "content": message}] data = { "model": config.OPENROUTER_MODELS[model_key]["model"], "messages": messages } response = requests.post( config.OPENROUTER_API_URL, headers=headers, json=data ) response.raise_for_status() result = response.json() content = result["choices"][0]["message"]["content"] # 檢查是否有富文本格式 formatted_content = None if "```" in content: # 將markdown代碼塊轉換為HTML格式 formatted_content = content.replace( "```", "
", 1
                    ).replace("```", "
", 1) return content, formatted_content except requests.exceptions.HTTPError as e: if e.response.status_code == 402: return "对不起,由于系统后台余额不足,请等待管理员充值之后再重新进行对话", None print(f"AI API調用出錯: {e}") return "抱歉,AI服務暫時無法使用,請稍後再試。", None except Exception as e: print(f"AI API調用出錯: {e}") return "抱歉,AI服務暫時無法使用,請稍後再試。", None async def encrypted_message_callback(self, room, event): """處理加密消息""" print(f"收到加密消息,房間ID: {room.room_id}") # print(f"输出event{event}") try: # 对于MegolmEvent,我们需要从解密后的内容中获取消息 if hasattr(event, "decrypted_data"): # 从解密数据中获取消息内容 message_content = event.decrypted_data.get("content", {}) # 确保event对象有body属性供process_message使用 event.body = message_content.get("body", "") await self.process_message(room, event) else: # 检查是否是因为缺少会话ID导致的解密失败 error_msg = str(event.decryption_error) if hasattr( event, "decryption_error") else "" if "no session found with session id" in error_msg: print(f"解密失败,尝试重新建立会话: {error_msg}") # 获取发送者ID sender_id = event.sender # 尝试重新建立会话 success = await self.reestablish_room_session(room.room_id, sender_id) if success: # 通知用户重新建立了会话 await self.send_message( "已重新建立加密会话,请重新发送您的消息。", room_id=room.room_id, encrypted=True ) else: print("重新建立会话失败") else: print("无法处理未解密的消息") except Exception as e: print(f"处理加密消息时出错: {e}") async def message_callback(self, room, event): """處理未加密消息""" print(f"收到未加密消息,房間ID: {room.room_id}") await self.process_message(room, event) async def process_message(self, room, event): """統一處理消息的邏輯""" # 忽略自己發送的消息 if event.sender == self.client.user_id: return print( f"處理來自房間 {room.room_id} 的消息: {event.body}" f", 事件ID: {event.event_id}" ) # 將消息時間戳轉換為datetime message_time = datetime.fromtimestamp(event.server_timestamp / 1000) if message_time >= self.start_time: # 只響應程序啟動後的消息 user_id = event.sender room_id = room.room_id chat_key = (room_id, user_id) # 檢查是否為群聊房間 is_group_chat = room_id in config.ROOM_IDS # 如果是私聊房间且尚未记录,则记录下来 if not is_group_chat and room_id not in self.private_chat_rooms: print(f"新的私聊房间:{room.users}") if len(room.users) == 2: # 确认是私聊房间 self.private_chat_rooms[room_id] = user_id print(f"记录新的私聊房间: {room_id} - 用户: {user_id}") # 處理命令 if event.body == "!ping": await self.send_message( "Pong!", room_id=room_id, encrypted=True ) elif event.body == "加密货币": await self.send_crypto_prices(room_id) elif event.body == "人工智能": if is_group_chat: # 在群聊中提示私聊 await self.send_message( "如果想要AI对话,請你點擊我頭像,向我進行私聊," "就重新进行指令就可以进行AI對話", room_id=room_id, encrypted=True ) else: # 显示模型列表 await self.send_message( self.get_model_list_message(user_id), room_id=room_id, encrypted=True ) # 初始化模型选择状态 self.model_select_state[chat_key] = 0 elif chat_key in self.model_select_state: # 处理模型选择 model_key = self.process_model_selection( user_id, room_id, event.body) if model_key: # 检查模型使用限制 can_use, limit_msg = self.check_model_limit( user_id, model_key) if can_use: # 初始化AI对话 self.ai_chat_users[chat_key] = ( datetime.now(), [], # 空的会话历史 model_key # 记录选择的模型 ) # 获取模型名称 model_name = (config.XAI_MODEL_DIYNAME if model_key == "xai" else config.OPENROUTER_MODELS[model_key]["name"]) welcome_msg = ( f"歡迎你使用AI智哥哥功能1.02,請你回覆我你想要知道的內容" f"[如果是更大的模型的回复会更详细,可能需要很多的时间来准备内容," f"所以发送后请耐心等候回复,不要重复发送]," f"閒置{config.AI_CHAT_TIMEOUT_MINUTES}分鐘之後功能會自動關閉," f"或者也可以手动输入'退出'就可以手动退出AI对话功能" f"[目前选择的大模型是{model_name}]" ) # 如果有使用限制提示,添加到欢迎消息 if limit_msg: welcome_msg += f"\n{limit_msg}" await self.send_message( welcome_msg, room_id=room_id, encrypted=True ) else: # 发送限制消息并清除选择状态 await self.send_message( limit_msg, room_id=room_id, encrypted=True ) # 清除模型选择状态 del self.model_select_state[chat_key] else: # 更新重试次数 self.model_select_state[chat_key] += 1 if self.model_select_state[chat_key] >= MODEL_SELECT_RETRIES: # 超过重试次数,取消操作 del self.model_select_state[chat_key] await self.send_message( "已超过重试次数,如需使用AI对话请重新输入'人工智能'", room_id=room_id, encrypted=True ) else: # 重新显示模型列表 await self.send_message( self.get_model_list_message(user_id), room_id=room_id, encrypted=True ) elif chat_key in self.ai_chat_users: if event.body == "退出": # 清除所有相关状态 if chat_key in self.ai_chat_users: del self.ai_chat_users[chat_key] if chat_key in self.ai_waiting_users: del self.ai_waiting_users[chat_key] await self.send_message( "已经退出了AI对话啦!欢迎你的使用!", room_id=room_id, encrypted=True ) else: # 检查用户是否正在等待AI响应 if chat_key in self.ai_waiting_users: await self.send_message( "AI已经接收到你的问题啦,不过还需要时间进行反应,请你耐心等待...", room_id=room_id, encrypted=True ) return # 获取会话状态 last_activity, history, model_key = self.ai_chat_users[chat_key] # 检查模型使用限制 can_use, limit_msg = self.check_model_limit( user_id, model_key) if not can_use: # 达到限制,结束对话 del self.ai_chat_users[chat_key] await self.send_message( limit_msg, room_id=room_id, encrypted=True ) return # 记录用户正在等待AI响应 self.ai_waiting_users[chat_key] = ( datetime.now(), event.body) # 发送等待提示 await self.send_message( "已经收到你的请求,请耐心等待AI智哥哥的思考...", room_id=room_id, encrypted=True ) # 更新用户消息到历史 history.append({"role": "user", "content": event.body}) try: # 获取AI回复 response, formatted_response = await self.chat_with_ai( event.body, history, model_key ) # 更新AI回复到历史 history.append( {"role": "assistant", "content": response}) # 增加使用次数 self.increment_model_usage(user_id, model_key) # 检查是否需要添加剩余次数提示 if limit_msg: response += f"\n{limit_msg}" # 更新会话状态 self.ai_chat_users[chat_key] = ( datetime.now(), history, model_key ) # 发送回复 await self.send_message( response, room_id=room_id, formatted_message=formatted_response, encrypted=True ) except Exception as e: print(f"处理AI回复时出错: {e}") await self.send_message( "抱歉,处理你的请求时出现了错误,请稍后重试。", room_id=room_id, encrypted=True ) finally: # 无论成功还是失败,都清除等待状态 if chat_key in self.ai_waiting_users: del self.ai_waiting_users[chat_key] else: # 如果不是AI對話且不是命令,發送歡迎消息 await self.send_message( self.get_welcome_message(), room_id=room_id, encrypted=True ) else: print( f"忽略消息,發送時間 {message_time}," f"早於程序啟動時間 {self.start_time}" ) async def run_schedule(self): while True: schedule.run_pending() await asyncio.sleep(1) async def send_shutdown_notice(self): """向所有私聊房间发送关闭通知""" shutdown_message = ( "服务器准备进行升级,当服务器升级完毕之后,这个房间可能会无法正常解密消息。" "如果发送内容没有任何反应,请长按头像'离开'这个私聊房间,重新从公共房间中跟AI智哥哥" "再发送私聊信息,这样就可以重新建立连接啦,就可以使用啦!" ) for room_id, user_id in self.private_chat_rooms.items(): try: print(f"正在向房间 {room_id} 发送关闭通知...") await self.send_message( shutdown_message, room_id=room_id, encrypted=True ) # 等待一小段时间确保消息发送成功 await asyncio.sleep(0.5) except Exception as e: print(f"向房间 {room_id} 发送关闭通知失败: {e}") async def close(self): """关闭服务前发送通知并清理资源""" try: if self.private_chat_rooms: # 暂时不发送关闭时候的通知 print("程序正在关闭...") # print("正在发送关闭通知...") # await self.send_shutdown_notice() # 等待一段时间确保所有通知都发送完成 # await asyncio.sleep(2) except Exception as e: print(f"发送关闭通知时出错: {e}") finally: await self.client.close()