diff --git a/references/matrix-bot-chat-reference b/references/matrix-bot-chat-reference deleted file mode 160000 index 972a538..0000000 --- a/references/matrix-bot-chat-reference +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 972a538356431b58e86be929d5de4c86eb420f57 diff --git a/references/matrix-bot-chat-reference/.gitignore b/references/matrix-bot-chat-reference/.gitignore new file mode 100644 index 0000000..6059439 --- /dev/null +++ b/references/matrix-bot-chat-reference/.gitignore @@ -0,0 +1,3 @@ + +__pycache__/config.cpython-311.pyc +services/__pycache__/matrix_service.cpython-311.pyc diff --git a/references/matrix-bot-chat-reference/Dockerfile b/references/matrix-bot-chat-reference/Dockerfile new file mode 100644 index 0000000..a40b9d8 --- /dev/null +++ b/references/matrix-bot-chat-reference/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.11-slim +WORKDIR /app +COPY . /app +RUN pip install -r requirements.txt + +# 设定环境变量 +# ENV PYTHONUNBUFFERED=1 + +CMD ["python", "main.py"] \ No newline at end of file diff --git a/references/matrix-bot-chat-reference/config.py b/references/matrix-bot-chat-reference/config.py new file mode 100644 index 0000000..ed8d9a5 --- /dev/null +++ b/references/matrix-bot-chat-reference/config.py @@ -0,0 +1,59 @@ +# config.py + +MATRIX_SERVER = "https://person.bnicetome.top" +USERNAME = "bots1" +PASSWORD = "bots1@bots1" + +# 房间配置 +ROOM_IDS = [ + "!jTyjRTQNpAQaHGEjer:person.bnicetome.top", # 调试房间(未加密) + "!QDamBNNgTvbmFNXEFo:person.bnicetome.top", # 用户使用房间(未加密) +] + +# 默认房间ID(向后兼容) +ROOM_ID = ROOM_IDS[0] + +# E2EE配置 +STORE_PATH = "store" # 存储密钥和会话信息的目录 +DEVICE_ID = "BOTDEVICE2" # 设备ID +DEVICE_NAME = "MatrixBot" # 设备名称 + +# xAI API配置 +XAI_API_KEY = ( + "xai-Irz5BaEdnraGMHeFmIuTWx85srNRX5rrmXiRofaroFHrXh4dWGynC0B6hhwhUeU70Is5rSow1lHiSSVu" +) +XAI_MODEL_DIYNAME = "马斯克的X平台的AI(GROK,新起之秀,有免费额度,推荐使用)" +XAI_BASE_URL = "https://api.x.ai/v1" +XAI_DAILY_LIMIT = 25 # 每天多少条 + +# OpenRouter API配置 +OPENROUTER_API_KEY = ( + "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" +) +OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions" + +# AI模型配置 +OPENROUTER_MODELS = { + # "qwen-7b": { + # "model": "qwen/qwen-2-7b-instruct:free", + # "name": "通义千问2-7B(阿里巴巴基础免费版,适合问日常简单问题)", + # "daily_limit": -1 # 无限制 + # }, + "qwen-72b": { + "model": "qwen/qwen-2.5-72b-instruct", + "name": "通义千问2.5-72B(阿里巴巴升级版付费模型,适合一些复杂问题,写作,文案)", + "daily_limit": 50 # 每天多少条 + }, + "chatgpt-4o": { + "model": "openai/chatgpt-4o-latest", + "name": "OPENAI ChatGPT-4o最新版(世界公认最强大AI,知识最丰富,目前最强的超大模型,专业性问题和更正确的回答)", + "daily_limit": 20 # 每天多少条 + } +} + +# 默认模型(用于向后兼容) +OPENROUTER_MODEL = OPENROUTER_MODELS["qwen-72b"]["model"] + +# AI对话配置 +AI_CHAT_TIMEOUT_MINUTES = 15 # AI对话闲置超时时间(分钟) +AI_RESPONSE_TIMEOUT_MINUTES = 5 # AI响应超时时间(分钟) diff --git a/references/matrix-bot-chat-reference/credentials.json b/references/matrix-bot-chat-reference/credentials.json new file mode 100644 index 0000000..fd4e678 --- /dev/null +++ b/references/matrix-bot-chat-reference/credentials.json @@ -0,0 +1 @@ +{"access_token": "syt_Ym90czE_gbQAFJKVPLGyukPyWBZh_26VocT", "device_id": "BOTDEVICE", "user_id": "@bots1:person.bnicetome.top"} \ No newline at end of file diff --git a/references/matrix-bot-chat-reference/docker-compose.yml b/references/matrix-bot-chat-reference/docker-compose.yml new file mode 100644 index 0000000..b4486c6 --- /dev/null +++ b/references/matrix-bot-chat-reference/docker-compose.yml @@ -0,0 +1,7 @@ +version: '3.8' +services: + myapp: + build: . + restart: always + volumes: + - .:/app # 将当前目录挂载到容器的 /app 目录 \ No newline at end of file diff --git a/references/matrix-bot-chat-reference/main.py b/references/matrix-bot-chat-reference/main.py new file mode 100644 index 0000000..5396c4f --- /dev/null +++ b/references/matrix-bot-chat-reference/main.py @@ -0,0 +1,53 @@ +# main.py + +import asyncio +from services.matrix_service import MatrixService + + +async def command_listener(matrix_service): + while True: + print("Command Menu:") + print("1. Send Message") + print("q. Quit") + user_input = input("Enter command: ") + + if user_input == "1": + message = input("Enter message to send: ") + await matrix_service.send_message(message) + print("Message sent!") + elif user_input == "q": + print("Exiting command listener...") + break + else: + print("Invalid command. Please try again.") + + +async def main(): + matrix_service = MatrixService() + + try: + # 登录(会自动加入配置的所有房间) + await matrix_service.login() + + # 先进行一次同步以确保房间状态正确 + print("正在同步房间状态...") + await matrix_service.client.sync() + + # 启动同步任务 + sync_task = asyncio.create_task(matrix_service.sync()) + + # 等待同步任务完成(实际上会一直运行) + await sync_task + + except KeyboardInterrupt: + print("\n正在关闭服务...") + await matrix_service.close() + except Exception as e: + print(f"发生错误: {e}") + await matrix_service.close() + finally: + await matrix_service.close() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/references/matrix-bot-chat-reference/requirements.txt b/references/matrix-bot-chat-reference/requirements.txt new file mode 100644 index 0000000..a0b6299 --- /dev/null +++ b/references/matrix-bot-chat-reference/requirements.txt @@ -0,0 +1,5 @@ +Flask +matrix-nio[e2e] +requests +schedule +openai \ No newline at end of file diff --git a/references/matrix-bot-chat-reference/sample/basic_client.py b/references/matrix-bot-chat-reference/sample/basic_client.py new file mode 100644 index 0000000..7e54a9f --- /dev/null +++ b/references/matrix-bot-chat-reference/sample/basic_client.py @@ -0,0 +1,32 @@ +import asyncio + +from nio import AsyncClient, MatrixRoom, RoomMessageText + + +async def message_callback(room: MatrixRoom, event: RoomMessageText) -> None: + print( + f"Message received in room {room.display_name}\n" + f"{room.user_name(event.sender)} | {event.body}" + ) + + +async def main() -> None: + client = AsyncClient("https://matrix.example.org", "@alice:example.org") + client.add_event_callback(message_callback, RoomMessageText) + + print(await client.login("my-secret-password")) + # "Logged in as @alice:example.org device id: RANDOMDID" + + # If you made a new room and haven't joined as that user, you can use + # await client.join("your-room-id") + + await client.room_send( + # Watch out! If you join an old room you'll see lots of old messages + room_id="!my-fave-room:example.org", + message_type="m.room.message", + content={"msgtype": "m.text", "body": "Hello world!"}, + ) + await client.sync_forever(timeout=30000) # milliseconds + + +asyncio.run(main()) diff --git a/references/matrix-bot-chat-reference/sample/manual_encrypted_verify.py b/references/matrix-bot-chat-reference/sample/manual_encrypted_verify.py new file mode 100644 index 0000000..587376c --- /dev/null +++ b/references/matrix-bot-chat-reference/sample/manual_encrypted_verify.py @@ -0,0 +1,332 @@ +import asyncio +import json +import os +import sys +from typing import Optional + +import aiofiles + +from nio import ( + AsyncClient, + ClientConfig, + InviteEvent, + LoginResponse, + MatrixRoom, + RoomMessageText, + crypto, + exceptions, +) + +# This is a fully-documented example of how to do manual verification with nio, +# for when you already know the device IDs of the users you want to trust. If +# you want live verification using emojis, the process is more complicated and +# will be covered in another example. + +# We're building on the restore_login example here to preserve device IDs and +# therefore preserve trust; if @bob trusts @alice's device ID ABC and @alice +# restarts this program, loading the same keys, @bob will preserve trust. If +# @alice logged in again @alice would have new keys and a device ID XYZ, and +# @bob wouldn't trust it. + +# The store is where we want to place encryption details like our keys, trusted +# devices and blacklisted devices. Here we place it in the working directory, +# but if you deploy your program you might consider /var or /opt for storage +STORE_FOLDER = "nio_store/" + +# This file is for restoring login details after closing the program, so you +# can preserve your device ID. If @alice logged in every time instead, @bob +# would have to re-verify. See the restoring login example for more into. +SESSION_DETAILS_FILE = "credentials.json" + +# Only needed for this example, this is who @alice will securely +# communicate with. We need all the device IDs of this user so we can consider +# them "trusted". If an unknown device shows up (like @bob signs into their +# account on another device), this program will refuse to send a message in the +# room. Try it! +BOB_ID = "@bob:example.org" +BOB_DEVICE_IDS = [ + # You can find these in Riot under Settings > Security & Privacy. + # They may also be called "session IDs". You'll want to add ALL of them here + # for the one other user in your encrypted room + "URDEVICEID", +] + +# the ID of the room you want your bot to join and send commands in. +# This can be a direct message or room; Matrix treats them the same +ROOM_ID = "!myfavouriteroom:example.org" + +ALICE_USER_ID = "@alice:example.org" +ALICE_HOMESERVER = "https://matrix.example.org" +ALICE_PASSWORD = "hunter2" + + +class CustomEncryptedClient(AsyncClient): + def __init__( + self, + homeserver, + user="", + device_id="", + store_path="", + config=None, + ssl=None, + proxy=None, + ): + # Calling super.__init__ means we're running the __init__ method + # defined in AsyncClient, which this class derives from. That does a + # bunch of setup for us automatically + super().__init__( + homeserver, + user=user, + device_id=device_id, + store_path=store_path, + config=config, + ssl=ssl, + proxy=proxy, + ) + + # if the store location doesn't exist, we'll make it + if store_path and not os.path.isdir(store_path): + os.mkdir(store_path) + + # auto-join room invites + self.add_event_callback(self.cb_autojoin_room, InviteEvent) + + # print all the messages we receive + self.add_event_callback(self.cb_print_messages, RoomMessageText) + + async def login(self) -> None: + """Log in either using the global variables or (if possible) using the + session details file. + + NOTE: This method kinda sucks. Don't use these kinds of global + variables in your program; it would be much better to pass them + around instead. They are only used here to minimise the size of the + example. + """ + # Restore the previous session if we can + # See the "restore_login.py" example if you're not sure how this works + if os.path.exists(SESSION_DETAILS_FILE) and os.path.isfile( + SESSION_DETAILS_FILE + ): + try: + async with aiofiles.open(SESSION_DETAILS_FILE) as f: + contents = await f.read() + config = json.loads(contents) + self.access_token = config["access_token"] + self.user_id = config["user_id"] + self.device_id = config["device_id"] + + # This loads our verified/blacklisted devices and our keys + self.load_store() + print( + f"Logged in using stored credentials: {self.user_id} on {self.device_id}" + ) + + except OSError as err: + print(f"Couldn't load session from file. Logging in. Error: {err}") + except json.JSONDecodeError: + print("Couldn't read JSON file; overwriting") + + # We didn't restore a previous session, so we'll log in with a password + if not self.user_id or not self.access_token or not self.device_id: + # this calls the login method defined in AsyncClient from nio + resp = await super().login(ALICE_PASSWORD) + + if isinstance(resp, LoginResponse): + print("Logged in using a password; saving details to disk") + self.__write_details_to_disk(resp) + else: + print(f"Failed to log in: {resp}") + sys.exit(1) + + def trust_devices(self, user_id: str, device_list: Optional[str] = None) -> None: + """Trusts the devices of a user. + + If no device_list is provided, all of the users devices are trusted. If + one is provided, only the devices with IDs in that list are trusted. + + Arguments: + user_id {str} -- the user ID whose devices should be trusted. + + Keyword Arguments: + device_list {Optional[str]} -- The full list of device IDs to trust + from that user (default: {None}) + """ + + print(f"{user_id}'s device store: {self.device_store[user_id]}") + + # The device store contains a dictionary of device IDs and known + # OlmDevices for all users that share a room with us, including us. + + # We can only run this after a first sync. We have to populate our + # device store and that requires syncing with the server. + for device_id, olm_device in self.device_store[user_id].items(): + if device_list and device_id not in device_list: + # a list of trusted devices was provided, but this ID is not in + # that list. That's an issue. + print( + f"Not trusting {device_id} as it's not in {user_id}'s pre-approved list." + ) + continue + + if user_id == self.user_id and device_id == self.device_id: + # We cannot explicitly trust the device @alice is using + continue + + self.verify_device(olm_device) + print(f"Trusting {device_id} from user {user_id}") + + def cb_autojoin_room(self, room: MatrixRoom, event: InviteEvent): + """Callback to automatically joins a Matrix room on invite. + + Arguments: + room {MatrixRoom} -- Provided by nio + event {InviteEvent} -- Provided by nio + """ + self.join(room.room_id) + room = self.rooms[ROOM_ID] + print(f"Room {room.name} is encrypted: {room.encrypted}") + + async def cb_print_messages(self, room: MatrixRoom, event: RoomMessageText): + """Callback to print all received messages to stdout. + + Arguments: + room {MatrixRoom} -- Provided by nio + event {RoomMessageText} -- Provided by nio + """ + if event.decrypted: + encrypted_symbol = "🛡 " + else: + encrypted_symbol = "⚠️ " + print( + f"{room.display_name} |{encrypted_symbol}| {room.user_name(event.sender)}: {event.body}" + ) + + async def send_hello_world(self): + # Now we send an encrypted message that @bob can read, although it will + # appear to be "unverified" when they see it, because @bob has not verified + # the device @alice is sending from. + # We'll leave that as an exercise for the reader. + try: + await self.room_send( + room_id=ROOM_ID, + message_type="m.room.message", + content={ + "msgtype": "m.text", + "body": "Hello, this message is encrypted", + }, + ) + except exceptions.OlmUnverifiedDeviceError: + print("These are all known devices:") + device_store: crypto.DeviceStore = device_store # noqa: F821 + [ + print( + f"\t{device.user_id}\t {device.device_id}\t {device.trust_state}\t {device.display_name}" + ) + for device in device_store + ] + sys.exit(1) + + @staticmethod + def __write_details_to_disk(resp: LoginResponse) -> None: + """Writes login details to disk so that we can restore our session later + without logging in again and creating a new device ID. + + Arguments: + resp {LoginResponse} -- the successful client login response. + """ + with open(SESSION_DETAILS_FILE, "w") as f: + json.dump( + { + "access_token": resp.access_token, + "device_id": resp.device_id, + "user_id": resp.user_id, + }, + f, + ) + + +async def run_client(client: CustomEncryptedClient) -> None: + """A basic encrypted chat application using nio.""" + + # This is our own custom login function that looks for a pre-existing config + # file and, if it exists, logs in using those details. Otherwise it will log + # in using a password. + await client.login() + + # Here we create a coroutine that we can call in asyncio.gather later, + # along with sync_forever and any other API-related coroutines you'd like + # to do. + async def after_first_sync(): + # We'll wait for the first firing of 'synced' before trusting devices. + # client.synced is an asyncio event that fires any time nio syncs. This + # code doesn't run in a loop, so it only fires once + print("Awaiting sync") + await client.synced.wait() + + # In practice, you want to have a list of previously-known device IDs + # for each user you want to trust. Here, we require that list as a + # global variable + client.trust_devices(BOB_ID, BOB_DEVICE_IDS) + + # In this case, we'll trust _all_ of @alice's devices. NOTE that this + # is a SUPER BAD IDEA in practice, but for the purpose of this example + # it'll be easier, since you may end up creating lots of sessions for + # @alice as you play with the script + client.trust_devices(ALICE_USER_ID) + + await client.send_hello_world() + + # We're creating Tasks here so that you could potentially write other + # Python coroutines to do other work, like checking an API or using another + # library. All of these Tasks will be run concurrently. + # For more details, check out https://docs.python.org/3/library/asyncio-task.html + + # ensure_future() is for Python 3.5 and 3.6 compatibility. For 3.7+, use + # asyncio.create_task() + after_first_sync_task = asyncio.ensure_future(after_first_sync()) + + # We use full_state=True here to pull any room invites that occurred or + # messages sent in rooms _before_ this program connected to the + # Matrix server + sync_forever_task = asyncio.ensure_future( + client.sync_forever(30000, full_state=True) + ) + + await asyncio.gather( + # The order here IS significant! You have to register the task to trust + # devices FIRST since it awaits the first sync + after_first_sync_task, + sync_forever_task, + ) + + +async def main(): + # By setting `store_sync_tokens` to true, we'll save sync tokens to our + # store every time we sync, thereby preventing reading old, previously read + # events on each new sync. + # For more info, check out https://matrix-nio.readthedocs.io/en/latest/nio.html#asyncclient + config = ClientConfig(store_sync_tokens=True) + client = CustomEncryptedClient( + ALICE_HOMESERVER, + ALICE_USER_ID, + store_path=STORE_FOLDER, + config=config, + ssl=False, + proxy="http://localhost:8080", + ) + + try: + await run_client(client) + except (asyncio.CancelledError, KeyboardInterrupt): + await client.close() + + +# Run the main coroutine, which instantiates our custom subclass, trusts all the +# devices, and syncs forever (or until your press Ctrl+C) + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + pass diff --git a/references/matrix-bot-chat-reference/sample/matrix_store.txt b/references/matrix-bot-chat-reference/sample/matrix_store.txt new file mode 100644 index 0000000..447f087 --- /dev/null +++ b/references/matrix-bot-chat-reference/sample/matrix_store.txt @@ -0,0 +1,63 @@ +要实现你的需求,我们可以利用 matrix-nio 的持久化功能,通过将加密会话信息保存到本地存储,确保 BOT 重启后能够恢复与私聊房间的加密连接并继续通信。下面是基于 matrix-nio 最新文档的实现步骤: +1. 配置持久化存储 + +matrix-nio 提供 CryptoStore 接口来管理加密会话的存储。可以使用 SqliteStore 作为持久化存储方式,确保所有会话状态在 BOT 重启后可以恢复。 + +python + +from nio import AsyncClient, AsyncClientConfig, SqliteStore + +# 配置持久化存储的路径 +store_path = "store/" # 存储密钥信息的目录 +db_path = store_path + "nio_store.db" + +# 配置客户端 +client = AsyncClient( + homeserver, + username, + device_id=device_id, + store_path=store_path, + config=AsyncClientConfig(encryption_enabled=True, store=SqliteStore(db_path)) +) + +2. 确保加密功能开启 + +启用端到端加密(E2EE)功能,并确认私聊房间的密钥交换成功。AsyncClientConfig 中的 encryption_enabled 参数设置为 True。 +3. 使用同步方法以保存加密状态 + +配置好客户端后,执行初次同步以加载房间状态和成员数据。这一步非常关键,因为 matrix-nio 的加密依赖初次同步的数据。 + +python + +await client.sync_forever(timeout=30000, full_state=True) + +4. 处理加密消息 + +为私聊房间建立加密通信,并处理重新连接后的消息解密。 + +python + +@client.add_event_callback(RoomEncryptedEvent, RoomEncryptedEvent) +async def encrypted_message_handler(room, event): + # 确保消息解密后再做处理 + decrypted_event = await client.decrypt_event(event) + if decrypted_event: + print("Decrypted message: ", decrypted_event.body) + else: + print("Failed to decrypt message.") + +5. 关闭并保存会话状态 + +在 BOT 关闭时,确保会话和加密状态都保存至 STORE 目录中的数据库。 + +python + +async def close_client(): + await client.close() + print("Client closed and session saved.") + +6. 重启时加载存储的加密状态 + +当 BOT 重启时,matrix-nio 会自动从指定的 STORE 路径中加载加密状态,无需额外的恢复步骤。 + +这样,你的程序在与用户私聊房间建立加密连接后,会将关键的加密信息存入 STORE 目录,确保即使 BOT 重启也能够从存储中恢复这些状态,继续进行加密的私聊通信。 \ No newline at end of file diff --git a/references/matrix-bot-chat-reference/sample/matrix_store2.txt b/references/matrix-bot-chat-reference/sample/matrix_store2.txt new file mode 100644 index 0000000..24725ec --- /dev/null +++ b/references/matrix-bot-chat-reference/sample/matrix_store2.txt @@ -0,0 +1,41 @@ +It appears that SqliteStore is no longer available directly in matrix-nio's recent versions, which means the documentation might not reflect current capabilities accurately. Here’s an alternative approach to set up a persistence layer using DefaultCryptoStore in matrix-nio for handling encryption without relying on SqliteStore. + +Here’s how to set it up: + + Install the Required E2E Encryption Dependencies: Make sure the encryption dependencies are installed: + + bash + +pip install 'matrix-nio[e2e]' + +Configure DefaultCryptoStore: The DefaultCryptoStore can be used to persist encryption keys, allowing your bot to retain its encryption state across restarts. Here’s a code snippet to configure it: + +python + + from nio import AsyncClient, DefaultCryptoStore + import os + + # Set up your bot's storage directory + store_path = "store" # Ensure this directory exists + + # Initialize the AsyncClient with the DefaultCryptoStore for E2E encryption + client = AsyncClient( + "https://your.matrix.server", + "your_bot_username", + store_path=store_path + ) + + # Configure the client to use the default crypto store + client.crypto_store = DefaultCryptoStore( + client.user_id, + client.device_id, + store_path + ) + + # Now your client will store encryption keys under `store_path` + + Sync and Persist Keys: After setting up, the bot should automatically save keys during communication. Make sure your store_path directory is persistent across reboots, as the stored data within it allows the bot to pick up encrypted chats seamlessly on restart. + + Run the Client and Sync: To maintain encrypted communication, call client.sync_forever() or client.sync() in your main loop as usual. The DefaultCryptoStore will manage loading and saving encryption keys without needing SqliteStore. + +This setup should persist encryption keys across sessions. Let me know if you encounter further issues, and I can guide you through additional configuration steps if necessary. \ No newline at end of file diff --git a/references/matrix-bot-chat-reference/sample/openrouter example.txt b/references/matrix-bot-chat-reference/sample/openrouter example.txt new file mode 100644 index 0000000..18d3b85 --- /dev/null +++ b/references/matrix-bot-chat-reference/sample/openrouter example.txt @@ -0,0 +1,53 @@ +To implement a Python function using OpenRouter’s API to send content to an AI model, receive results, and track the costs, you can follow this approach: + +1. **API Request for Chat Completion**: + - You’ll need to send a `POST` request to `https://openrouter.ai/api/v1/chat/completions`, including the model identifier, your message content, and your API key in the headers. + - The request body should include the AI model you want to use (e.g., `"openai/gpt-3.5-turbo"`) and the `messages` array. + +2. **Tracking Costs**: + - The response will contain `usage` data, which includes `prompt_tokens`, `completion_tokens`, and `total_tokens`. This data allows you to calculate the cost based on token usage directly from the response. + - Alternatively, to get detailed cost data, OpenRouter provides the `/api/v1/generation` endpoint. You can use the `id` from your initial response to query for specific stats, such as exact token counts and the calculated cost. + +3. **Checking Account Balance**: + - Currently, OpenRouter doesn’t appear to provide a direct API endpoint for account balance checks within their documented API endpoints, so this may need to be monitored from their platform dashboard directly. + +Here’s a basic Python function demonstrating the API call, token tracking, and cost retrieval: + +```python +import requests + +def query_openrouter(content, model="openai/gpt-3.5-turbo"): + url = "https://openrouter.ai/api/v1/chat/completions" + headers = { + "Authorization": "Bearer YOUR_API_KEY", + "Content-Type": "application/json" + } + data = { + "model": model, + "messages": [{"role": "user", "content": content}] + } + + response = requests.post(url, headers=headers, json=data) + result = response.json() + + if "usage" in result: + usage_data = result["usage"] + cost_query_url = f"https://openrouter.ai/api/v1/generation?id={result['id']}" + cost_response = requests.get(cost_query_url, headers=headers) + cost_data = cost_response.json() + + return { + "reply": result["choices"][0]["message"]["content"], + "prompt_tokens": usage_data["prompt_tokens"], + "completion_tokens": usage_data["completion_tokens"], + "total_cost": cost_data["data"]["total_cost"] + } + else: + return {"error": "No usage data available in the response"} + +# Example usage +result = query_openrouter("What is the meaning of life?") +print(result) +``` + +For further details, OpenRouter’s official documentation outlines more on usage and token tracking options. \ No newline at end of file diff --git a/references/matrix-bot-chat-reference/sample/restore_login.py b/references/matrix-bot-chat-reference/sample/restore_login.py new file mode 100644 index 0000000..6df4525 --- /dev/null +++ b/references/matrix-bot-chat-reference/sample/restore_login.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 + +import asyncio +import getpass +import json +import os +import sys + +import aiofiles + +from nio import AsyncClient, LoginResponse + +CONFIG_FILE = "credentials.json" + +# Check out main() below to see how it's done. + + +def write_details_to_disk(resp: LoginResponse, homeserver) -> None: + """Writes the required login details to disk so we can log in later without + using a password. + + Arguments: + resp {LoginResponse} -- the successful client login response. + homeserver -- URL of homeserver, e.g. "https://matrix.example.org" + """ + # open the config file in write-mode + with open(CONFIG_FILE, "w") as f: + # write the login details to disk + json.dump( + { + "homeserver": homeserver, # e.g. "https://matrix.example.org" + "user_id": resp.user_id, # e.g. "@user:example.org" + "device_id": resp.device_id, # device ID, 10 uppercase letters + "access_token": resp.access_token, # cryptogr. access token + }, + f, + ) + + +async def main() -> None: + # If there are no previously-saved credentials, we'll use the password + if not os.path.exists(CONFIG_FILE): + print( + "First time use. Did not find credential file. Asking for " + "homeserver, user, and password to create credential file." + ) + homeserver = "https://matrix.example.org" + homeserver = input(f"Enter your homeserver URL: [{homeserver}] ") + + if not (homeserver.startswith("https://") or homeserver.startswith("http://")): + homeserver = "https://" + homeserver + + user_id = "@user:example.org" + user_id = input(f"Enter your full user ID: [{user_id}] ") + + device_name = "matrix-nio" + device_name = input(f"Choose a name for this device: [{device_name}] ") + + client = AsyncClient(homeserver, user_id) + pw = getpass.getpass() + + resp = await client.login(pw, device_name=device_name) + + # check that we logged in successfully + if isinstance(resp, LoginResponse): + write_details_to_disk(resp, homeserver) + else: + print(f'homeserver = "{homeserver}"; user = "{user_id}"') + print(f"Failed to log in: {resp}") + sys.exit(1) + + print( + "Logged in using a password. Credentials were stored.", + "Try running the script again to login with credentials.", + ) + + # Otherwise the config file exists, so we'll use the stored credentials + else: + # open the file in read-only mode + async with aiofiles.open(CONFIG_FILE) as f: + contents = await f.read() + config = json.loads(contents) + client = AsyncClient(config["homeserver"]) + + client.access_token = config["access_token"] + client.user_id = config["user_id"] + client.device_id = config["device_id"] + + # Now we can send messages as the user + room_id = "!myfavouriteroomid:example.org" + room_id = input(f"Enter room id for test message: [{room_id}] ") + + await client.room_send( + room_id, + message_type="m.room.message", + content={"msgtype": "m.text", "body": "Hello world!"}, + ) + print("Logged in using stored credentials. Sent a test message.") + + # Either way we're logged in here, too + await client.close() + + +asyncio.run(main()) diff --git a/references/matrix-bot-chat-reference/sample/verify_with_emoji.py b/references/matrix-bot-chat-reference/sample/verify_with_emoji.py new file mode 100644 index 0000000..afa6d67 --- /dev/null +++ b/references/matrix-bot-chat-reference/sample/verify_with_emoji.py @@ -0,0 +1,400 @@ +#!/usr/bin/env python3 + +"""verify_with_emoji.py A sample program to demo Emoji verification. + +# Objectives: +- Showcase the emoji verification using matrix-nio SDK +- This sample program tries to show the key steps involved in performing + an emoji verification. +- It does so only for incoming request, outgoing emoji verification request + are similar but not shown in this sample program + +# Prerequisites: +- You must have matrix-nio and components for end-to-end encryption installed + See: https://github.com/poljar/matrix-nio +- You must have created a Matrix account already, + and have username and password ready +- You must have already joined a Matrix room with someone, e.g. yourself +- This other party initiates an emoji verification with you +- You are using this sample program to accept this incoming emoji verification + and follow the protocol to successfully verify the other party's device + +# Use Cases: +- Apply similar code in your Matrix bot +- Apply similar code in your Matrix client +- Just to learn about Matrix and the matrix-nio SDK + +# Running the Program: +- Change permissions to allow execution + `chmod 755 ./verify_with_emoji.py` +- Optionally create a store directory, if not it will be done for you + `mkdir ./store/` +- Run the program as-is, no changes needed + `./verify_with_emoji.py` +- Run it as often as you like + +# Sample Screen Output when Running Program: +$ ./verify_with_emoji.py +First time use. Did not find credential file. Asking for +homeserver, user, and password to create credential file. +Enter your homeserver URL: [https://matrix.example.org] matrix.example.org +Enter your full user ID: [@user:example.org] @user:example.org +Choose a name for this device: [matrix-nio] verify_with_emoji +Password: +Logged in using a password. Credentials were stored. +On next execution the stored login credentials will be used. +This program is ready and waiting for the other party to initiate an emoji +verification with us by selecting "Verify by Emoji" in their Matrix client. +[('⚓', 'Anchor'), ('☎️', 'Telephone'), ('😀', 'Smiley'), ('😀', 'Smiley'), + ('☂️', 'Umbrella'), ('⚓', 'Anchor'), ('☎️', 'Telephone')] +Do the emojis match? (Y/N) y +Match! Device will be verified by accepting verification. +sas.we_started_it = False +sas.sas_accepted = True +sas.canceled = False +sas.timed_out = False +sas.verified = True +sas.verified_devices = ['DEVICEIDXY'] +Emoji verification was successful. +Hit Control-C to stop the program or initiate another Emoji verification +from another device or room. + +""" + +import asyncio +import getpass +import json +import os +import sys +import traceback + +import aiofiles + +from nio import ( + AsyncClient, + AsyncClientConfig, + KeyVerificationCancel, + KeyVerificationEvent, + KeyVerificationKey, + KeyVerificationMac, + KeyVerificationStart, + LocalProtocolError, + LoginResponse, + ToDeviceError, +) + +# file to store credentials in case you want to run program multiple times +CONFIG_FILE = "credentials.json" # login credentials JSON file +# directory to store persistent data for end-to-end encryption +STORE_PATH = "./store/" # local directory + + +class Callbacks: + """Class to pass client to callback methods.""" + + def __init__(self, client): + """Store AsyncClient.""" + self.client = client + + async def to_device_callback(self, event): # noqa + """Handle events sent to device.""" + try: + client = self.client + + if isinstance(event, KeyVerificationStart): # first step + """first step: receive KeyVerificationStart + KeyVerificationStart( + source={'content': + {'method': 'm.sas.v1', + 'from_device': 'DEVICEIDXY', + 'key_agreement_protocols': + ['curve25519-hkdf-sha256', 'curve25519'], + 'hashes': ['sha256'], + 'message_authentication_codes': + ['hkdf-hmac-sha256', 'hmac-sha256'], + 'short_authentication_string': + ['decimal', 'emoji'], + 'transaction_id': 'SomeTxId' + }, + 'type': 'm.key.verification.start', + 'sender': '@user2:example.org' + }, + sender='@user2:example.org', + transaction_id='SomeTxId', + from_device='DEVICEIDXY', + method='m.sas.v1', + key_agreement_protocols=[ + 'curve25519-hkdf-sha256', 'curve25519'], + hashes=['sha256'], + message_authentication_codes=[ + 'hkdf-hmac-sha256', 'hmac-sha256'], + short_authentication_string=['decimal', 'emoji']) + """ + + if "emoji" not in event.short_authentication_string: + print( + "Other device does not support emoji verification " + f"{event.short_authentication_string}." + ) + return + resp = await client.accept_key_verification(event.transaction_id) + if isinstance(resp, ToDeviceError): + print(f"accept_key_verification failed with {resp}") + + sas = client.key_verifications[event.transaction_id] + + todevice_msg = sas.share_key() + resp = await client.to_device(todevice_msg) + if isinstance(resp, ToDeviceError): + print(f"to_device failed with {resp}") + + elif isinstance(event, KeyVerificationCancel): # anytime + """at any time: receive KeyVerificationCancel + KeyVerificationCancel(source={ + 'content': {'code': 'm.mismatched_sas', + 'reason': 'Mismatched authentication string', + 'transaction_id': 'SomeTxId'}, + 'type': 'm.key.verification.cancel', + 'sender': '@user2:example.org'}, + sender='@user2:example.org', + transaction_id='SomeTxId', + code='m.mismatched_sas', + reason='Mismatched short authentication string') + """ + + # There is no need to issue a + # client.cancel_key_verification(tx_id, reject=False) + # here. The SAS flow is already cancelled. + # We only need to inform the user. + print( + f"Verification has been cancelled by {event.sender} " + f'for reason "{event.reason}".' + ) + + elif isinstance(event, KeyVerificationKey): # second step + """Second step is to receive KeyVerificationKey + KeyVerificationKey( + source={'content': { + 'key': 'SomeCryptoKey', + 'transaction_id': 'SomeTxId'}, + 'type': 'm.key.verification.key', + 'sender': '@user2:example.org' + }, + sender='@user2:example.org', + transaction_id='SomeTxId', + key='SomeCryptoKey') + """ + sas = client.key_verifications[event.transaction_id] + + print(f"{sas.get_emoji()}") + + yn = input("Do the emojis match? (Y/N) (C for Cancel) ") + if yn.lower() == "y": + print( + "Match! The verification for this " "device will be accepted." + ) + resp = await client.confirm_short_auth_string(event.transaction_id) + if isinstance(resp, ToDeviceError): + print(f"confirm_short_auth_string failed with {resp}") + elif yn.lower() == "n": # no, don't match, reject + print( + "No match! Device will NOT be verified " + "by rejecting verification." + ) + resp = await client.cancel_key_verification( + event.transaction_id, reject=True + ) + if isinstance(resp, ToDeviceError): + print(f"cancel_key_verification failed with {resp}") + else: # C or anything for cancel + print("Cancelled by user! Verification will be " "cancelled.") + resp = await client.cancel_key_verification( + event.transaction_id, reject=False + ) + if isinstance(resp, ToDeviceError): + print(f"cancel_key_verification failed with {resp}") + + elif isinstance(event, KeyVerificationMac): # third step + """Third step is to receive KeyVerificationMac + KeyVerificationMac( + source={'content': { + 'mac': {'ed25519:DEVICEIDXY': 'SomeKey1', + 'ed25519:SomeKey2': 'SomeKey3'}, + 'keys': 'SomeCryptoKey4', + 'transaction_id': 'SomeTxId'}, + 'type': 'm.key.verification.mac', + 'sender': '@user2:example.org'}, + sender='@user2:example.org', + transaction_id='SomeTxId', + mac={'ed25519:DEVICEIDXY': 'SomeKey1', + 'ed25519:SomeKey2': 'SomeKey3'}, + keys='SomeCryptoKey4') + """ + sas = client.key_verifications[event.transaction_id] + try: + todevice_msg = sas.get_mac() + except LocalProtocolError as e: + # e.g. it might have been cancelled by ourselves + print( + f"Cancelled or protocol error: Reason: {e}.\n" + f"Verification with {event.sender} not concluded. " + "Try again?" + ) + else: + resp = await client.to_device(todevice_msg) + if isinstance(resp, ToDeviceError): + print(f"to_device failed with {resp}") + print( + f"sas.we_started_it = {sas.we_started_it}\n" + f"sas.sas_accepted = {sas.sas_accepted}\n" + f"sas.canceled = {sas.canceled}\n" + f"sas.timed_out = {sas.timed_out}\n" + f"sas.verified = {sas.verified}\n" + f"sas.verified_devices = {sas.verified_devices}\n" + ) + print( + "Emoji verification was successful!\n" + "Hit Control-C to stop the program or " + "initiate another Emoji verification from " + "another device or room." + ) + else: + print( + f"Received unexpected event type {type(event)}. " + f"Event is {event}. Event will be ignored." + ) + except BaseException: + print(traceback.format_exc()) + + +def write_details_to_disk(resp: LoginResponse, homeserver) -> None: + """Write the required login details to disk. + + It will allow following logins to be made without password. + + Arguments: + --------- + resp : LoginResponse - successful client login response + homeserver : str - URL of homeserver, e.g. "https://matrix.example.org" + + """ + # open the config file in write-mode + with open(CONFIG_FILE, "w") as f: + # write the login details to disk + json.dump( + { + "homeserver": homeserver, # e.g. "https://matrix.example.org" + "user_id": resp.user_id, # e.g. "@user:example.org" + "device_id": resp.device_id, # device ID, 10 uppercase letters + "access_token": resp.access_token, # cryptogr. access token + }, + f, + ) + + +async def login() -> AsyncClient: + """Handle login with or without stored credentials.""" + # Configuration options for the AsyncClient + client_config = AsyncClientConfig( + max_limit_exceeded=0, + max_timeouts=0, + store_sync_tokens=True, + encryption_enabled=True, + ) + + # If there are no previously-saved credentials, we'll use the password + if not os.path.exists(CONFIG_FILE): + print( + "First time use. Did not find credential file. Asking for " + "homeserver, user, and password to create credential file." + ) + homeserver = "https://matrix.example.org" + homeserver = input(f"Enter your homeserver URL: [{homeserver}] ") + + if not (homeserver.startswith("https://") or homeserver.startswith("http://")): + homeserver = "https://" + homeserver + + user_id = "@user:example.org" + user_id = input(f"Enter your full user ID: [{user_id}] ") + + device_name = "matrix-nio" + device_name = input(f"Choose a name for this device: [{device_name}] ") + + if not os.path.exists(STORE_PATH): + os.makedirs(STORE_PATH) + + # Initialize the matrix client + client = AsyncClient( + homeserver, + user_id, + store_path=STORE_PATH, + config=client_config, + ) + pw = getpass.getpass() + + resp = await client.login(password=pw, device_name=device_name) + + # check that we logged in successfully + if isinstance(resp, LoginResponse): + write_details_to_disk(resp, homeserver) + else: + print(f'homeserver = "{homeserver}"; user = "{user_id}"') + print(f"Failed to log in: {resp}") + sys.exit(1) + + print( + "Logged in using a password. Credentials were stored. " + "On next execution the stored login credentials will be used." + ) + + # Otherwise the config file exists, so we'll use the stored credentials + else: + # open the file in read-only mode + async with aiofiles.open(CONFIG_FILE) as f: + contents = await f.read() + config = json.loads(contents) + # Initialize the matrix client based on credentials from file + client = AsyncClient( + config["homeserver"], + config["user_id"], + device_id=config["device_id"], + store_path=STORE_PATH, + config=client_config, + ) + + client.restore_login( + user_id=config["user_id"], + device_id=config["device_id"], + access_token=config["access_token"], + ) + print("Logged in using stored credentials.") + + return client + + +async def main() -> None: + """Login and wait for and perform emoji verify.""" + client = await login() + # Set up event callbacks + callbacks = Callbacks(client) + client.add_to_device_callback(callbacks.to_device_callback, (KeyVerificationEvent,)) + # Sync encryption keys with the server + # Required for participating in encrypted rooms + if client.should_upload_keys: + await client.keys_upload() + print( + "This program is ready and waiting for the other party to initiate " + 'an emoji verification with us by selecting "Verify by Emoji" ' + "in their Matrix client." + ) + await client.sync_forever(timeout=30000, full_state=True) + + +try: + asyncio.run(main()) +except Exception: + print(traceback.format_exc()) + sys.exit(1) +except KeyboardInterrupt: + print("Received keyboard interrupt.") + sys.exit(0) diff --git a/references/matrix-bot-chat-reference/sample/xAi的API交互方法.txt b/references/matrix-bot-chat-reference/sample/xAi的API交互方法.txt new file mode 100644 index 0000000..16b25ca --- /dev/null +++ b/references/matrix-bot-chat-reference/sample/xAi的API交互方法.txt @@ -0,0 +1,15 @@ +curl https://api.x.ai/v1/chat/completions -H "Content-Type: application/json" -H "Authorization: Bearer xai-Irz5BaEdnraGMHeFmIuTWx85srNRX5rrmXiRofaroFHrXh4dWGynC0B6hhwhUeU70Is5rSow1lHiSSVu" -d '{ + "messages": [ + { + "role": "system", + "content": "You are a test assistant." + }, + { + "role": "user", + "content": "Testing. Just say hi and hello world and nothing else." + } + ], + "model": "grok-beta", + "stream": false, + "temperature": 0 +}' \ No newline at end of file diff --git a/references/matrix-bot-chat-reference/sample/xAi的python的openai插件的交互方法.txt b/references/matrix-bot-chat-reference/sample/xAi的python的openai插件的交互方法.txt new file mode 100644 index 0000000..cb1b3dd --- /dev/null +++ b/references/matrix-bot-chat-reference/sample/xAi的python的openai插件的交互方法.txt @@ -0,0 +1,18 @@ +import os +from openai import OpenAI + +XAI_API_KEY = os.getenv("XAI_API_KEY") +client = OpenAI( + api_key=XAI_API_KEY, + base_url="https://api.x.ai/v1", +) + +completion = client.chat.completions.create( + model="grok-beta", + messages=[ + {"role": "system", "content": "You are Grok, a chatbot inspired by the Hitchhikers Guide to the Galaxy."}, + {"role": "user", "content": "What is the meaning of life, the universe, and everything?"}, + ], +) + +print(completion.choices[0].message) diff --git a/references/matrix-bot-chat-reference/services/matrix_service.py b/references/matrix-bot-chat-reference/services/matrix_service.py new file mode 100644 index 0000000..cae178a --- /dev/null +++ b/references/matrix-bot-chat-reference/services/matrix_service.py @@ -0,0 +1,1242 @@ +# 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()