diff --git a/.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.yml b/.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.yml index e5aaaaf78..7eb5ae15c 100644 --- a/.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.yml +++ b/.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.yml @@ -6,7 +6,7 @@ body: - type: markdown attributes: value: | - 欢迎发布插件到插件市场! + 欢迎发布插件到插件市场!请确保您的插件经过**完整的**测试。 - type: textarea attributes: @@ -22,9 +22,10 @@ body: 插件名: 插件作者: 插件简介: - 标签: (可选) - 社交链接: (可选, 将会在插件市场作者名称上作为可点击的链接) - description: 必填。请以列表的字段按顺序将插件名、插件作者、插件简介放在这里。 + 支持的消息平台:(必填,如 QQ、微信、飞书) + 标签:(可选) + 社交链接:(可选, 将会在插件市场作者名称上作为可点击的链接) + description: 必填。请以列表的字段按顺序将插件名、插件作者、插件简介放在这里。如果您不知道支持哪些消息平台,请填写测试过的消息平台。 - type: checkboxes attributes: diff --git a/.gitignore b/.gitignore index 865b0596d..a3b2aad90 100644 --- a/.gitignore +++ b/.gitignore @@ -26,5 +26,5 @@ venv/* packages/python_interpreter/workplace .venv/* .conda/ -.idea/ +.idea pytest.ini diff --git a/README.md b/README.md index b4d97fbbb..9d9589384 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,13 @@ _✨ 易上手的多平台 LLM 聊天机器人及开发框架 ✨_ Soulter%2FAstrBot | Trendshift -[![GitHub release (latest by date)](https://img.shields.io/github/v/release/Soulter/AstrBot)](https://github.com/Soulter/AstrBot/releases/latest) -python -Docker pull -Static Badge -[![wakatime](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018e705a-a1a7-409a-a849-3013485e6c8e.svg)](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018e705a-a1a7-409a-a849-3013485e6c8e) -![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fstats&query=v&label=7%E6%97%A5%E6%B6%88%E6%81%AF%E4%B8%8A%E8%A1%8C%E9%87%8F&cacheSeconds=60) -[![codecov](https://codecov.io/gh/Soulter/AstrBot/graph/badge.svg?token=FF3P5967B8)](https://codecov.io/gh/Soulter/AstrBot) -[![star](https://gitcode.com/Soulter/AstrBot/star/badge.svg)](https://gitcode.com/Soulter/AstrBot) +[![GitHub release (latest by date)](https://img.shields.io/github/v/release/Soulter/AstrBot?style=for-the-badge&color=76bad9)](https://github.com/Soulter/AstrBot/releases/latest) +python +Docker pull +Static Badge +[![wakatime](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018e705a-a1a7-409a-a849-3013485e6c8e.svg?style=for-the-badge&color=76bad9)](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018e705a-a1a7-409a-a849-3013485e6c8e) +![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fstats&query=v&label=7%E6%97%A5%E6%B4%BB%E8%B7%83%E9%87%8F&cacheSeconds=60&style=for-the-badge&color=3b618e) +[![codecov](https://img.shields.io/codecov/c/github/soulter/astrbot?style=for-the-badge)](https://codecov.io/gh/Soulter/AstrBot) English日本語 | @@ -27,6 +26,8 @@ _✨ 易上手的多平台 LLM 聊天机器人及开发框架 ✨_ AstrBot 是一个松耦合、异步、支持多消息平台部署、具有易用的插件系统和完善的大语言模型(LLM)接入功能的聊天机器人及开发框架。 +[![star](https://gitcode.com/Soulter/AstrBot/star/badge.svg?style=for-the-badge)](https://gitcode.com/Soulter/AstrBot) + ## ✨ 主要功能 1. **大语言模型对话**。支持各种大语言模型,包括 OpenAI API、Google Gemini、Llama、Deepseek、ChatGLM 等,支持接入本地部署的大模型,通过 Ollama、LLMTuner。具有多轮对话、人格情境、多模态能力,支持图片理解、语音转文字(Whisper)。 @@ -51,15 +52,19 @@ AstrBot 是一个松耦合、异步、支持多消息平台部署、具有易用 需要电脑上安装有 Python(>3.10)。请参阅官方文档 [使用 Windows 一键安装器部署 AstrBot](https://astrbot.app/deploy/astrbot/windows.html) 。 -#### Replit 部署 +#### 宝塔面板部署 -[![Run on Repl.it](https://repl.it/badge/github/Soulter/AstrBot)](https://repl.it/github/Soulter/AstrBot) +请参阅官方文档 [宝塔面板部署](https://astrbot.app/deploy/astrbot/btpanel.html) 。 #### CasaOS 部署 社区贡献的部署方式。 -请参阅官方文档 [通过源码部署 AstrBot](https://astrbot.app/deploy/astrbot/casaos.html) 。 +请参阅官方文档 [CasaOS 部署](https://astrbot.app/deploy/astrbot/casaos.html) 。 + +#### Replit 部署 + +[![Run on Repl.it](https://repl.it/badge/github/Soulter/AstrBot)](https://repl.it/github/Soulter/AstrBot) #### 手动部署 @@ -106,6 +111,7 @@ AstrBot 是一个松耦合、异步、支持多消息平台部署、具有易用 | Whisper | ✔ | 语音转文本 | 支持 API、本地部署 | | SenseVoice | ✔ | 语音转文本 | 本地部署 | | OpenAI TTS API | ✔ | 文本转语音 | | +| GSVI | ✔ | 文本转语音 | GPT-Sovits-Inference | | Fishaudio | ✔ | 文本转语音 | GPT-Sovits 作者参与的项目 | | Edge-TTS | ✔ | 文本转语音 | Edge 浏览器的免费 TTS | diff --git a/astrbot/api/platform/__init__.py b/astrbot/api/platform/__init__.py index dcc02bb49..5a98c5903 100644 --- a/astrbot/api/platform/__init__.py +++ b/astrbot/api/platform/__init__.py @@ -5,6 +5,7 @@ from astrbot.core.platform import ( MessageMember, MessageType, PlatformMetadata, + Group, ) from astrbot.core.platform.register import register_platform_adapter @@ -18,4 +19,5 @@ __all__ = [ "MessageType", "PlatformMetadata", "register_platform_adapter", + "Group", ] diff --git a/astrbot/core/platform/__init__.py b/astrbot/core/platform/__init__.py index 48ea57b8a..4007b2d90 100644 --- a/astrbot/core/platform/__init__.py +++ b/astrbot/core/platform/__init__.py @@ -1,7 +1,7 @@ from .platform import Platform from .astr_message_event import AstrMessageEvent from .platform_metadata import PlatformMetadata -from .astrbot_message import AstrBotMessage, MessageMember, MessageType +from .astrbot_message import AstrBotMessage, MessageMember, MessageType, Group __all__ = [ "Platform", @@ -10,4 +10,5 @@ __all__ = [ "AstrBotMessage", "MessageMember", "MessageType", + "Group", ] diff --git a/astrbot/core/platform/astr_message_event.py b/astrbot/core/platform/astr_message_event.py index fceb63ce7..3e1b14ee6 100644 --- a/astrbot/core/platform/astr_message_event.py +++ b/astrbot/core/platform/astr_message_event.py @@ -1,11 +1,9 @@ import abc import asyncio from dataclasses import dataclass -from .astrbot_message import AstrBotMessage -from .platform_metadata import PlatformMetadata -from astrbot.core.message.message_event_result import MessageEventResult, MessageChain -from astrbot.core.platform.message_type import MessageType -from typing import List, Union +from typing import List, Union, Optional + +from astrbot.core.db.po import Conversation from astrbot.core.message.components import ( Plain, Image, @@ -16,9 +14,12 @@ from astrbot.core.message.components import ( Forward, Reply, ) -from astrbot.core.utils.metrics import Metric +from astrbot.core.message.message_event_result import MessageEventResult, MessageChain +from astrbot.core.platform.message_type import MessageType from astrbot.core.provider.entites import ProviderRequest -from astrbot.core.db.po import Conversation +from astrbot.core.utils.metrics import Metric +from .astrbot_message import AstrBotMessage, Group +from .platform_metadata import PlatformMetadata @dataclass @@ -201,15 +202,6 @@ class AstrMessageEvent(abc.ABC): """ return self.role == "admin" - async def send(self, message: MessageChain): - """ - 发送消息到消息平台。 - """ - asyncio.create_task( - Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name) - ) - self._has_send_oper = True - async def _pre_send(self): """调度器会在执行 send() 前调用该方法""" @@ -371,3 +363,26 @@ class AstrMessageEvent(abc.ABC): system_prompt=system_prompt, conversation=conversation, ) + + """平台适配器""" + + async def send(self, message: MessageChain): + """发送消息到消息平台。 + + Args: + message (MessageChain): 消息链,具体使用方式请参考文档。 + """ + asyncio.create_task( + Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name) + ) + self._has_send_oper = True + + async def get_group(self, group_id: str = None, **kwargs) -> Optional[Group]: + """获取一个群聊的数据, 如果不填写 group_id: 如果是私聊消息,返回 None。如果是群聊消息,返回当前群聊的数据。 + + 适配情况: + + - gewechat + - aiocqhttp(OneBotv11) + """ + ... diff --git a/astrbot/core/platform/astrbot_message.py b/astrbot/core/platform/astrbot_message.py index ea55eaf4b..e7bd4bd9c 100644 --- a/astrbot/core/platform/astrbot_message.py +++ b/astrbot/core/platform/astrbot_message.py @@ -10,6 +10,41 @@ class MessageMember: user_id: str # 发送者id nickname: str = None + def __str__(self): + # 使用 f-string 来构建返回的字符串表示形式 + return ( + f"User ID: {self.user_id}," + f"Nickname: {self.nickname if self.nickname else 'N/A'}" + ) + + +@dataclass +class Group: + group_id: str + """群号""" + group_name: str = None + """群名称""" + group_avatar: str = None + """群头像""" + group_owner: str = None + """群主 id""" + group_admins: List[str] = None + """群管理员 id""" + members: List[MessageMember] = None + """所有群成员""" + + def __str__(self): + # 使用 f-string 来构建返回的字符串表示形式 + return ( + f"Group ID: {self.group_id}\n" + f"Name: {self.group_name if self.group_name else 'N/A'}\n" + f"Avatar: {self.group_avatar if self.group_avatar else 'N/A'}\n" + f"Owner ID: {self.group_owner if self.group_owner else 'N/A'}\n" + f"Admin IDs: {self.group_admins if self.group_admins else 'N/A'}\n" + f"Members Len: {len(self.members) if self.members else 0}\n" + f"First Member: {self.members[0] if self.members else 'N/A'}\n" + ) + class AstrBotMessage: """ diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py index 0dfe41a4e..c7aede7d1 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py @@ -1,6 +1,7 @@ import asyncio - +import typing from astrbot.api.event import AstrMessageEvent, MessageChain +from astrbot.api.platform import Group, MessageMember from astrbot.api.message_components import Plain, Image, Record, At, Node, Nodes from aiocqhttp import CQHttp @@ -74,3 +75,46 @@ class AiocqhttpMessageEvent(AstrMessageEvent): await self.bot.send(self.message_obj.raw_message, ret) await super().send(message) + + async def get_group(self, group_id=None, **kwargs): + if isinstance(group_id, str) and group_id.isdigit(): + group_id = int(group_id) + elif self.get_group_id(): + group_id = int(self.get_group_id()) + else: + return None + + info: dict = await self.bot.call_action( + "get_group_info", + group_id=group_id, + ) + + members: typing.List[typing.Dict] = await self.bot.call_action( + "get_group_member_list", + group_id=group_id, + ) + + owner_id = None + admin_ids = [] + for member in members: + if member["role"] == "owner": + owner_id = member["user_id"] + if member["role"] == "admin": + admin_ids.append(member["user_id"]) + + group = Group( + group_id=str(group_id), + group_name=info.get("group_name"), + group_avatar="", + group_admins=admin_ids, + group_owner=str(owner_id), + members=[ + MessageMember( + user_id=member["user_id"], + nickname=member.get("nickname") or member.get("card"), + ) + for member in members + ], + ) + + return group diff --git a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py index 507b3bb50..b38c7d5c8 100644 --- a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +++ b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py @@ -196,7 +196,10 @@ class DingtalkPlatformAdapter(Platform): self._event_queue.put_nowait(event) async def run(self): - await self.client_.start() + # await self.client_.start() + loop = asyncio.get_event_loop() + # 钉钉的 SDK 并没有实现真正的异步,start() 里面有堵塞方法。 + await loop.run_in_executor(None, lambda: asyncio.run(self.client_.start())) def get_client(self): return self.client diff --git a/astrbot/core/platform/sources/gewechat/client.py b/astrbot/core/platform/sources/gewechat/client.py index 53b4fe276..d2f28f09d 100644 --- a/astrbot/core/platform/sources/gewechat/client.py +++ b/astrbot/core/platform/sources/gewechat/client.py @@ -1,17 +1,19 @@ -import threading import asyncio -import aiohttp -import quart import base64 import datetime -import re import os +import re +import threading + +import aiohttp import anyio -from astrbot.api.platform import AstrBotMessage, MessageMember, MessageType -from astrbot.api.message_components import Plain, Image, At, Record +import quart + from astrbot.api import logger, sp -from .downloader import GeweDownloader +from astrbot.api.message_components import Plain, Image, At, Record +from astrbot.api.platform import AstrBotMessage, MessageMember, MessageType from astrbot.core.utils.io import download_image_by_url +from .downloader import GeweDownloader class SimpleGewechatClient: @@ -51,11 +53,11 @@ class SimpleGewechatClient: self.server = quart.Quart(__name__) self.server.add_url_rule( - "/astrbot-gewechat/callback", view_func=self.callback, methods=["POST"] + "/astrbot-gewechat/callback", view_func=self._callback, methods=["POST"] ) self.server.add_url_rule( "/astrbot-gewechat/file/", - view_func=self.handle_file, + view_func=self._handle_file, methods=["GET"], ) @@ -73,6 +75,7 @@ class SimpleGewechatClient: self.stop = False async def get_token_id(self): + """获取 Gewechat Token。""" async with aiohttp.ClientSession() as session: async with session.post(f"{self.base_url}/tools/getTokenId") as resp: json_blob = await resp.json() @@ -258,7 +261,7 @@ class SimpleGewechatClient: logger.debug(f"abm: {abm}") return abm - async def callback(self): + async def _callback(self): data = await quart.request.json logger.debug(f"收到 gewechat 回调: {data}") @@ -280,7 +283,7 @@ class SimpleGewechatClient: return quart.jsonify({"r": "AstrBot ACK"}) - async def handle_file(self, file_id): + async def _handle_file(self, file_id): file_path = f"data/temp/{file_id}" return await quart.send_file(file_path) @@ -306,17 +309,17 @@ class SimpleGewechatClient: await self.server.run_task( host="0.0.0.0", port=self.port, - shutdown_trigger=self.shutdown_trigger_placeholder, + shutdown_trigger=self._shutdown_trigger_placeholder, ) - async def shutdown_trigger_placeholder(self): + async def _shutdown_trigger_placeholder(self): # TODO: use asyncio.Event while not self.event_queue.closed and not self.stop: # noqa: ASYNC110 await asyncio.sleep(1) logger.info("gewechat 适配器已关闭。") async def check_online(self, appid: str): - # /login/checkOnline + """检查 APPID 对应的设备是否在线。""" async with aiohttp.ClientSession() as session: async with session.post( f"{self.base_url}/login/checkOnline", @@ -327,6 +330,7 @@ class SimpleGewechatClient: return json_blob["data"] async def logout(self): + """登出 gewechat。""" if self.appid: online = await self.check_online(self.appid) if online: @@ -340,6 +344,7 @@ class SimpleGewechatClient: logger.info(f"登出结果: {json_blob}") async def login(self): + """登录 gewechat。一般来说插件用不到这个方法。""" if self.token is None: await self.get_token_id() @@ -451,9 +456,18 @@ class SimpleGewechatClient: self.appid = appid logger.info(f"已保存 APPID: {appid}") - """API""" + """API 部分。Gewechat 的 API 文档请参考: https://apifox.com/apidoc/shared/69ba62ca-cb7d-437e-85e4-6f3d3df271b1 + """ - async def get_chatroom_member_list(self, chatroom_wxid: str): + async def get_chatroom_member_list(self, chatroom_wxid: str) -> dict: + """获取群成员列表。 + + Args: + chatroom_wxid (str): 微信群聊的id。可以通过 event.get_group_id() 获取。 + + Returns: + dict: 返回群成员列表字典。其中键为 memberList 的值为群成员列表。 + """ payload = {"appId": self.appid, "chatroomId": chatroom_wxid} async with aiohttp.ClientSession() as session: @@ -466,6 +480,7 @@ class SimpleGewechatClient: return json_blob["data"] async def post_text(self, to_wxid, content: str, ats: str = ""): + """发送纯文本消息""" payload = { "appId": self.appid, "toWxid": to_wxid, @@ -482,6 +497,7 @@ class SimpleGewechatClient: logger.debug(f"发送消息结果: {json_blob}") async def post_image(self, to_wxid, image_url: str): + """发送图片消息""" payload = { "appId": self.appid, "toWxid": to_wxid, @@ -496,6 +512,12 @@ class SimpleGewechatClient: logger.debug(f"发送图片结果: {json_blob}") async def post_voice(self, to_wxid, voice_url: str, voice_duration: int): + """发送语音信息 + + Args: + voice_url (str): 语音文件的网络链接 + voice_duration (int): 语音时长,毫秒 + """ payload = { "appId": self.appid, "toWxid": to_wxid, @@ -513,6 +535,13 @@ class SimpleGewechatClient: logger.debug(f"发送语音结果: {json_blob}") async def post_file(self, to_wxid, file_url: str, file_name: str): + """发送文件 + + Args: + to_wxid (string): 微信ID + file_url (str): 文件的网络链接 + file_name (str): 文件名 + """ payload = { "appId": self.appid, "toWxid": to_wxid, @@ -526,3 +555,114 @@ class SimpleGewechatClient: ) as resp: json_blob = await resp.json() logger.debug(f"发送文件结果: {json_blob}") + + async def add_friend(self, v3: str, v4: str, content: str): + """申请添加好友""" + payload = { + "appId": self.appid, + "scene": 3, + "content": content, + "v4": v4, + "v3": v3, + "option": 2, + } + + async with aiohttp.ClientSession() as session: + async with session.post( + f"{self.base_url}/contacts/addContacts", + headers=self.headers, + json=payload, + ) as resp: + json_blob = await resp.json() + logger.debug(f"申请添加好友结果: {json_blob}") + return json_blob + + async def get_group(self, group_id: str): + payload = { + "appId": self.appid, + "chatroomId": group_id, + } + + async with aiohttp.ClientSession() as session: + async with session.post( + f"{self.base_url}/group/getChatroomInfo", + headers=self.headers, + json=payload, + ) as resp: + json_blob = await resp.json() + logger.debug(f"获取群信息结果: {json_blob}") + return json_blob + + async def get_group_member(self, group_id: str): + payload = { + "appId": self.appid, + "chatroomId": group_id, + } + + async with aiohttp.ClientSession() as session: + async with session.post( + f"{self.base_url}/group/getChatroomMemberList", + headers=self.headers, + json=payload, + ) as resp: + json_blob = await resp.json() + logger.debug(f"获取群信息结果: {json_blob}") + return json_blob + + async def accept_group_invite(self, url: str): + """同意进群""" + payload = {"appId": self.appid, "url": url} + + async with aiohttp.ClientSession() as session: + async with session.post( + f"{self.base_url}/group/agreeJoinRoom", + headers=self.headers, + json=payload, + ) as resp: + json_blob = await resp.json() + logger.debug(f"获取群信息结果: {json_blob}") + return json_blob + + async def add_group_member_to_friend( + self, group_id: str, to_wxid: str, content: str + ): + payload = { + "appId": self.appid, + "chatroomId": group_id, + "content": content, + "memberWxid": to_wxid, + } + + async with aiohttp.ClientSession() as session: + async with session.post( + f"{self.base_url}/group/addGroupMemberAsFriend", + headers=self.headers, + json=payload, + ) as resp: + json_blob = await resp.json() + logger.debug(f"获取群信息结果: {json_blob}") + return json_blob + + async def get_user_or_group_info(self, *ids): + """ + 获取用户或群组信息。 + + :param ids: 可变数量的 wxid 参数 + """ + + wxids_str = list(ids) + + payload = { + "appId": self.appid, + "wxids": wxids_str, # 使用逗号分隔的字符串 + } + + async with aiohttp.ClientSession() as session: + async with session.post( + f"{self.base_url}/contacts/getDetailInfo", + headers=self.headers, + json=payload, + ) as resp: + json_blob = await resp.json() + logger.debug(f"获取群信息结果: {json_blob}") + return json_blob diff --git a/astrbot/core/platform/sources/gewechat/gewechat_event.py b/astrbot/core/platform/sources/gewechat/gewechat_event.py index 15f0badd7..3aca64bab 100644 --- a/astrbot/core/platform/sources/gewechat/gewechat_event.py +++ b/astrbot/core/platform/sources/gewechat/gewechat_event.py @@ -6,7 +6,7 @@ from astrbot.core.utils.io import save_temp_img, download_file from astrbot.core.utils.tencent_record_helper import wav_to_tencent_silk from astrbot.api import logger from astrbot.api.event import AstrMessageEvent, MessageChain -from astrbot.api.platform import AstrBotMessage, PlatformMetadata +from astrbot.api.platform import AstrBotMessage, PlatformMetadata, Group, MessageMember from astrbot.api.message_components import Plain, Image, Record, At, File from .client import SimpleGewechatClient @@ -123,3 +123,30 @@ class GewechatPlatformEvent(AstrMessageEvent): to_wxid = self.message_obj.raw_message.get("to_wxid", None) await GewechatPlatformEvent.send_with_client(message, to_wxid, self.client) await super().send(message) + + async def get_group(self, group_id=None, **kwargs): + # 确定有效的 group_id + if group_id is None: + group_id = self.get_group_id() + + if not group_id: + return None + + res = await self.client.get_group(group_id) + data: dict = res["data"] + + if not data["chatroomId"]: + return None + + members = [ + MessageMember(user_id=member["wxid"], nickname=member["nickName"]) + for member in data.get("memberList", []) + ] + + return Group( + group_id=data["chatroomId"], + group_name=data.get("nickName"), + group_avatar=data.get("smallHeadImgUrl"), + group_owner=data.get("chatRoomOwner"), + members=members, + ) diff --git a/compose.yml b/compose.yml index 805d30c11..3bab93fc3 100644 --- a/compose.yml +++ b/compose.yml @@ -1,16 +1,21 @@ version: '3.8' +# 当接入 QQ NapCat 时,请使用这个 compose 文件一件部署: https://github.com/NapNeko/NapCat-Docker/blob/main/compose/astrbot.yml + services: astrbot: image: soulter/astrbot:latest container_name: astrbot + restart: always ports: # mappings description: https://github.com/Soulter/AstrBot/issues/497 - - "6185:6185" - - "6195:6195" # optional, wecom default port - - "6199:6199" # optional, aiocqhttp default port - - "6196:6196" # optional, qq official webhook default port - - "11451:11451" # optional, gewechat default port + - "6185:6185" # 必选,AstrBot WebUI 端口 + - "6195:6195" # 可选, 企业微信 Webhook 端口 + - "6199:6199" # 可选, QQ 个人号 WebSocket 端口 + - "6196:6196" # 可选, QQ 官方接口 Webhook 端口 + - "11451:11451" # 可选, 微信个人号 Webhook 端口 + environment: + - TZ=Asia/Shanghai volumes: - ./data:/AstrBot/data - - /etc/timezone:/etc/timezone:ro - - /etc/localtime:/etc/localtime:ro + # - /etc/timezone:/etc/timezone:ro + # - /etc/localtime:/etc/localtime:ro