diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index f05e17f43..b12ff53f4 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -5,7 +5,7 @@ import os from astrbot.core.utils.astrbot_path import get_astrbot_data_path -VERSION = "3.5.10" +VERSION = "3.5.11" DB_PATH = os.path.join(get_astrbot_data_path(), "data_v3.db") # 默认配置 @@ -176,6 +176,7 @@ CONFIG_METADATA_2 = { "api_base_url": "https://api.weixin.qq.com/cgi-bin/", "callback_server_host": "0.0.0.0", "port": 6194, + "active_send_mode": False }, "wecom(企业微信)": { "id": "wecom", @@ -220,6 +221,11 @@ CONFIG_METADATA_2 = { }, }, "items": { + "active_send_mode": { + "description": "是否换用主动发送接口", + "type": "bool", + "desc": "只有企业认证的公众号才能主动发送。主动发送接口的限制会少一些。" + }, "wpp_active_message_poll": { "description": "是否启用主动消息轮询", "type": "bool", @@ -841,8 +847,41 @@ CONFIG_METADATA_2 = { "minimax-voice-english-normalization": False, "timeout": 20, }, + "火山引擎_TTS(API)": { + "id": "volcengine_tts", + "type": "volcengine_tts", + "provider_type": "text_to_speech", + "enable": False, + "api_key": "", + "appid": "", + "volcengine_cluster": "volcano_tts", + "volcengine_voice_type": "", + "volcengine_speed_ratio": 1.0, + "api_base": "https://openspeech.bytedance.com/api/v1/tts", + "timeout": 20, + }, }, "items": { + "volcengine_cluster": { + "type": "string", + "description": "火山引擎集群", + "hint": "若使用语音复刻大模型,可选volcano_icl或volcano_icl_concurr,默认使用volcano_tts" + }, + "volcengine_voice_type": { + "type": "string", + "description": "火山引擎音色", + "hint": "输入声音id(Voice_type)" + }, + "volcengine_speed_ratio": { + "type": "float", + "description": "语速设置", + "hint": "语速设置,范围为 0.2 到 3.0,默认值为 1.0" + }, + "volcengine_volume_ratio": { + "type": "float", + "description": "音量设置", + "hint": "音量设置,范围为 0.0 到 2.0,默认值为 1.0" + }, "azure_tts_voice": { "type": "string", "description": "音色设置", diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index 47d4fa9df..2e2ab3878 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -69,39 +69,42 @@ class WeChatPadProAdapter(Platform): self.auth_key = loaded_credentials.get("auth_key") self.wxid = loaded_credentials.get("wxid") + isLoginIn = await self.check_online_status() + # 检查在线状态 - if self.auth_key and await self.check_online_status(): - logger.info("WeChatPadPro 设备已在线,跳过扫码登录。") + if self.auth_key and isLoginIn: + logger.info("WeChatPadPro 设备已在线,凭据存在,跳过扫码登录。") # 如果在线,连接 WebSocket 接收消息 self.ws_handle_task = asyncio.create_task(self.connect_websocket()) else: - logger.info("WeChatPadPro 设备不在线或无可用凭据,开始扫码登录流程。") # 1. 生成授权码 - await self.generate_auth_key() - if not self.auth_key: - logger.error("无法获取授权码,WeChatPadPro 适配器启动失败。") - return + logger.info("WeChatPadPro 无可用凭据,将生成新的授权码。") + await self.generate_auth_key() # 2. 获取登录二维码 - qr_code_url = await self.get_login_qr_code() + if not isLoginIn: + logger.info("WeChatPadPro 设备已离线,开始扫码登录。") + qr_code_url = await self.get_login_qr_code() - if qr_code_url: - logger.info(f"请扫描以下二维码登录: {qr_code_url}") - else: - logger.error("无法获取登录二维码。") - return + if qr_code_url: + logger.info(f"请扫描以下二维码登录: {qr_code_url}") + else: + logger.error("无法获取登录二维码。") + return - # 3. 检测扫码状态 - login_successful = await self.check_login_status() + # 3. 检测扫码状态 + login_successful = await self.check_login_status() - if login_successful: - # 登录成功后,连接 WebSocket 接收消息 - self.ws_handle_task = asyncio.create_task(self.connect_websocket()) - else: - logger.warning("登录失败或超时,WeChatPadPro 适配器将关闭。") - await self.terminate() - return + if login_successful: + logger.info("登录成功,WeChatPadPro适配器已连接。") + else: + logger.warning("登录失败或超时,WeChatPadPro 适配器将关闭。") + await self.terminate() + return + + # 登录成功后,连接 WebSocket 接收消息 + self.ws_handle_task = asyncio.create_task(self.connect_websocket()) self._shutdown_event = asyncio.Event() await self._shutdown_event.wait() @@ -156,16 +159,29 @@ class WeChatPadProAdapter(Platform): if login_state == 1: logger.info("WeChatPadPro 设备当前在线。") return True - else: + # login_state == 3 为离线状态 + elif login_state == 3: logger.info( - f"WeChatPadPro 设备不在线,登录状态: {login_state}" + "WeChatPadPro 设备不在线。" ) return False + else: + logger.error( + f"未知的在线状态: {login_state:}" + ) + return False + # Code == 300 为微信退出状态。 + elif response.status == 200 and response_data.get("Code") == 300: + logger.info( + "WeChatPadPro 设备已退出。" + ) + return False else: logger.error( f"检查在线状态失败: {response.status}, {response_data}" ) return False + except aiohttp.ClientConnectorError as e: logger.error(f"连接到 WeChatPadPro 服务失败: {e}") return False @@ -179,7 +195,7 @@ class WeChatPadProAdapter(Platform): """ url = f"{self.base_url}/admin/GenAuthKey1" params = {"key": self.admin_key} - payload = {"Count": 1, "Days": 30} # 生成一个有效期30天的授权码 + payload = {"Count": 1, "Days": 365} # 生成一个有效期365天的授权码 async with aiohttp.ClientSession() as session: try: @@ -336,7 +352,7 @@ class WeChatPadProAdapter(Platform): message = await asyncio.wait_for( websocket.recv(), timeout=wait_time ) - logger.info(message) + # logger.debug(message) # 不显示原始消息内容 asyncio.create_task(self.handle_websocket_message(message)) except asyncio.TimeoutError: logger.warning( @@ -350,7 +366,7 @@ class WeChatPadProAdapter(Platform): logger.error(f"处理 WebSocket 消息时发生错误: {e}") break except Exception as e: - logger.error(f"WebSocket 连接失败: {e}") + logger.error(f"WebSocket 连接失败: {e}, 请检查WeChatPadPro服务状态,或尝试重启WeChatPadPro适配器。") await asyncio.sleep(5) async def handle_websocket_message(self, message: str): @@ -425,7 +441,7 @@ class WeChatPadProAdapter(Platform): ): # 再根据消息类型处理消息内容 await self._process_message_content(abm, raw_message, msg_type, content) - + return abm return None diff --git a/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py b/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py index 5ed589516..04186ff9d 100644 --- a/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +++ b/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py @@ -20,7 +20,7 @@ from requests import Response from wechatpy.utils import check_signature from wechatpy.crypto import WeChatCrypto from wechatpy import WeChatClient -from wechatpy.messages import TextMessage, ImageMessage, VoiceMessage +from wechatpy.messages import TextMessage, ImageMessage, VoiceMessage, BaseMessage from wechatpy.exceptions import InvalidSignatureException from wechatpy import parse_message from .weixin_offacc_event import WeixinOfficialAccountPlatformEvent @@ -87,7 +87,11 @@ class WecomServer: logger.info(f"解析成功: {msg}") if self.callback: - await self.callback(msg) + result_xml = await self.callback(msg) + if not result_xml: + return "success" + if isinstance(result_xml, str): + return result_xml return "success" @@ -117,6 +121,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform): self.api_base_url = platform_config.get( "api_base_url", "https://api.weixin.qq.com/cgi-bin/" ) + self.active_send_mode = self.config.get("active_send_mode", False) if not self.api_base_url: self.api_base_url = "https://api.weixin.qq.com/cgi-bin/" @@ -138,9 +143,29 @@ class WeixinOfficialAccountPlatformAdapter(Platform): self.client.API_BASE_URL = self.api_base_url - async def callback(msg): + # 微信公众号必须 5 秒内进行回复,否则会重试 3 次,我们需要对其进行消息排重 + # msgid -> Future + self.wexin_event_workers: dict[str, asyncio.Future] = {} + + async def callback(msg: BaseMessage): try: - await self.convert_message(msg) + if self.active_send_mode: + await self.convert_message(msg, None) + else: + if msg.id in self.wexin_event_workers: + future = self.wexin_event_workers[msg.id] + logger.debug(f"duplicate message id checked: {msg.id}") + else: + future = asyncio.get_event_loop().create_future() + self.wexin_event_workers[msg.id] = future + await self.convert_message(msg, future) + # I love shield so much! + result = await asyncio.wait_for(asyncio.shield(future), 60) # wait for 60s + logger.debug(f"Got future result: {result}") + self.wexin_event_workers.pop(msg.id, None) + return result # xml. see weixin_offacc_event.py + except asyncio.TimeoutError: + pass except Exception as e: logger.error(f"转换消息时出现异常: {e}") @@ -163,7 +188,9 @@ class WeixinOfficialAccountPlatformAdapter(Platform): async def run(self): await self.server.start_polling() - async def convert_message(self, msg) -> AstrBotMessage | None: + async def convert_message( + self, msg, future: asyncio.Future = None + ) -> AstrBotMessage | None: abm = AstrBotMessage() if isinstance(msg, TextMessage): abm.message_str = msg.content @@ -177,7 +204,6 @@ class WeixinOfficialAccountPlatformAdapter(Platform): abm.message_id = msg.id abm.timestamp = msg.time abm.session_id = abm.sender.user_id - abm.raw_message = msg elif msg.type == "image": assert isinstance(msg, ImageMessage) abm.message_str = "[图片]" @@ -191,7 +217,6 @@ class WeixinOfficialAccountPlatformAdapter(Platform): abm.message_id = msg.id abm.timestamp = msg.time abm.session_id = abm.sender.user_id - abm.raw_message = msg elif msg.type == "voice": assert isinstance(msg, VoiceMessage) @@ -209,7 +234,9 @@ class WeixinOfficialAccountPlatformAdapter(Platform): audio = AudioSegment.from_file(path) audio.export(path_wav, format="wav") except Exception as e: - logger.error(f"转换音频失败: {e}。如果没有安装 pydub 和 ffmpeg 请先安装。") + logger.error( + f"转换音频失败: {e}。如果没有安装 pydub 和 ffmpeg 请先安装。" + ) path_wav = path return @@ -224,11 +251,16 @@ class WeixinOfficialAccountPlatformAdapter(Platform): abm.message_id = msg.id abm.timestamp = msg.time abm.session_id = abm.sender.user_id - abm.raw_message = msg else: logger.warning(f"暂未实现的事件: {msg.type}") + future.set_result(None) return - + # 很不优雅 :( + abm.raw_message = { + "message": msg, + "future": future, + "active_send_mode": self.active_send_mode, + } logger.info(f"abm: {abm}") await self.handle_msg(abm) diff --git a/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py b/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py index 9519cd497..102812705 100644 --- a/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +++ b/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py @@ -4,6 +4,8 @@ from astrbot.api.event import AstrMessageEvent, MessageChain from astrbot.api.platform import AstrBotMessage, PlatformMetadata from astrbot.api.message_components import Plain, Image, Record from wechatpy import WeChatClient +from wechatpy.replies import TextReply, ImageReply, VoiceReply + from astrbot.api import logger @@ -82,12 +84,23 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent): async def send(self, message: MessageChain): message_obj = self.message_obj + active_send_mode = message_obj.raw_message.get("active_send_mode", False) for comp in message.chain: if isinstance(comp, Plain): # Split long text messages if needed plain_chunks = await self.split_plain(comp.text) for chunk in plain_chunks: - self.client.message.send_text(message_obj.sender.user_id, chunk) + if active_send_mode: + self.client.message.send_text(message_obj.sender.user_id, chunk) + else: + reply = TextReply( + content=chunk, + message=self.message_obj.raw_message["message"], + ) + xml = reply.render() + future = self.message_obj.raw_message["future"] + assert isinstance(future, asyncio.Future) + future.set_result(xml) await asyncio.sleep(0.5) # Avoid sending too fast elif isinstance(comp, Image): img_path = await comp.convert_to_file_path() @@ -102,10 +115,22 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent): ) return logger.debug(f"微信公众平台上传图片返回: {response}") - self.client.message.send_image( - message_obj.sender.user_id, - response["media_id"], - ) + + if active_send_mode: + self.client.message.send_image( + message_obj.sender.user_id, + response["media_id"], + ) + else: + reply = ImageReply( + media_id=response["media_id"], + message=self.message_obj.raw_message["message"], + ) + xml = reply.render() + future = self.message_obj.raw_message["future"] + assert isinstance(future, asyncio.Future) + future.set_result(xml) + elif isinstance(comp, Record): record_path = await comp.convert_to_file_path() # 转成amr @@ -124,10 +149,23 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent): ) return logger.info(f"微信公众平台上传语音返回: {response}") - self.client.message.send_voice( - message_obj.sender.user_id, - response["media_id"], - ) + + + if active_send_mode: + self.client.message.send_voice( + message_obj.sender.user_id, + response["media_id"], + ) + else: + reply = VoiceReply( + media_id=response["media_id"], + message=self.message_obj.raw_message["message"], + ) + xml = reply.render() + future = self.message_obj.raw_message["future"] + assert isinstance(future, asyncio.Future) + future.set_result(xml) + else: logger.warning(f"还没实现这个消息类型的发送逻辑: {comp.type}。") diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index 596293ac2..68aa98e89 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -210,6 +210,10 @@ class ProviderManager: from .sources.minimax_tts_api_source import ( ProviderMiniMaxTTSAPI as ProviderMiniMaxTTSAPI, ) + case "volcengine_tts": + from .sources.volcengine_tts import ( + ProviderVolcengineTTS as ProviderVolcengineTTS, + ) except (ImportError, ModuleNotFoundError) as e: logger.critical( f"加载 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}。可能是因为有未安装的依赖。" diff --git a/astrbot/core/provider/sources/azure_tts_source.py b/astrbot/core/provider/sources/azure_tts_source.py index 18d9bfbac..c35c7ec6c 100644 --- a/astrbot/core/provider/sources/azure_tts_source.py +++ b/astrbot/core/provider/sources/azure_tts_source.py @@ -53,8 +53,8 @@ class OTTSProvider: async def _generate_signature(self) -> str: await self._sync_time() timestamp = int(time.time()) + self.time_offset - nonce = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=10)) - path = re.sub(r'^https?://[^/]+', '', self.api_url) or '/' + nonce = "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=10)) + path = re.sub(r"^https?://[^/]+", "", self.api_url) or "/" return f"{timestamp}-{nonce}-0-{hashlib.md5(f'{path}-{timestamp}-{nonce}-0-{self.skey}'.encode()).hexdigest()}" async def get_audio(self, text: str, voice_params: Dict) -> str: @@ -92,7 +92,7 @@ class AzureNativeProvider(TTSProvider): def __init__(self, provider_config: dict, provider_settings: dict): super().__init__(provider_config, provider_settings) self.subscription_key = provider_config.get("azure_tts_subscription_key", "").strip() - if not re.fullmatch(r'^[a-zA-Z0-9]{32}$', self.subscription_key): + if not re.fullmatch(r"^[a-zA-Z0-9]{32}$", self.subscription_key): raise ValueError("无效的Azure订阅密钥") self.region = provider_config.get("azure_tts_region", "eastus").strip() self.endpoint = f"https://{self.region}.tts.speech.microsoft.com/cognitiveservices/v1" @@ -188,7 +188,7 @@ class AzureTTSProvider(TTSProvider): raise ValueError(error_msg) from e except KeyError as e: raise ValueError(f"配置错误: 缺少必要参数 {e}") from e - if re.fullmatch(r'^[a-zA-Z0-9]{32}$', key_value): + if re.fullmatch(r"^[a-zA-Z0-9]{32}$", key_value): return AzureNativeProvider(config, self.provider_settings) raise ValueError("订阅密钥格式无效,应为32位字母数字或other[...]格式") diff --git a/astrbot/core/provider/sources/volcengine_tts.py b/astrbot/core/provider/sources/volcengine_tts.py new file mode 100644 index 000000000..dca0196b1 --- /dev/null +++ b/astrbot/core/provider/sources/volcengine_tts.py @@ -0,0 +1,107 @@ +import uuid +import base64 +import json +import os +import traceback +import asyncio +import aiohttp +import requests +from ..provider import TTSProvider +from ..entities import ProviderType +from ..register import register_provider_adapter +from astrbot import logger + +@register_provider_adapter( + "volcengine_tts", "火山引擎 TTS", provider_type=ProviderType.TEXT_TO_SPEECH +) +class ProviderVolcengineTTS(TTSProvider): + def __init__(self, provider_config: dict, provider_settings: dict) -> None: + super().__init__(provider_config, provider_settings) + self.api_key = provider_config.get("api_key", "") + self.appid = provider_config.get("appid", "") + self.cluster = provider_config.get("volcengine_cluster", "") + self.voice_type = provider_config.get("volcengine_voice_type", "") + self.speed_ratio = provider_config.get("volcengine_speed_ratio", 1.0) + self.api_base = provider_config.get("api_base", f"https://openspeech.bytedance.com/api/v1/tts") + self.timeout = provider_config.get("timeout", 20) + + def _build_request_payload(self, text: str) -> dict: + return { + "app": { + "appid": self.appid, + "token": self.api_key, + "cluster": self.cluster + }, + "user": { + "uid": str(uuid.uuid4()) + }, + "audio": { + "voice_type": self.voice_type, + "encoding": "mp3", + "speed_ratio": self.speed_ratio, + "volume_ratio": 1.0, + "pitch_ratio": 1.0, + }, + "request": { + "reqid": str(uuid.uuid4()), + "text": text, + "text_type": "plain", + "operation": "query", + "with_frontend": 1, + "frontend_type": "unitTson" + } + } + + async def get_audio(self, text: str) -> str: + """异步方法获取语音文件路径""" + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer; {self.api_key}" + } + + payload = self._build_request_payload(text) + + logger.debug(f"请求头: {headers}") + logger.debug(f"请求 URL: {self.api_base}") + logger.debug(f"请求体: {json.dumps(payload, ensure_ascii=False)[:100]}...") + + try: + async with aiohttp.ClientSession() as session: + async with session.post( + self.api_base, + data=json.dumps(payload), + headers=headers, + timeout=self.timeout + ) as response: + logger.debug(f"响应状态码: {response.status}") + + response_text = await response.text() + logger.debug(f"响应内容: {response_text[:200]}...") + + if response.status == 200: + resp_data = json.loads(response_text) + + if "data" in resp_data: + audio_data = base64.b64decode(resp_data["data"]) + + os.makedirs("data/temp", exist_ok=True) + + file_path = f"data/temp/volcengine_tts_{uuid.uuid4()}.mp3" + + loop = asyncio.get_running_loop() + await loop.run_in_executor( + None, + lambda: open(file_path, "wb").write(audio_data) + ) + + return file_path + else: + error_msg = resp_data.get("message", "未知错误") + raise Exception(f"火山引擎 TTS API 返回错误: {error_msg}") + else: + raise Exception(f"火山引擎 TTS API 请求失败: {response.status}, {response_text}") + + except Exception as e: + error_details = traceback.format_exc() + logger.debug(f"火山引擎 TTS 异常详情: {error_details}") + raise Exception(f"火山引擎 TTS 异常: {str(e)}") \ No newline at end of file diff --git a/changelogs/v3.5.11.md b/changelogs/v3.5.11.md new file mode 100644 index 000000000..9550e26c5 --- /dev/null +++ b/changelogs/v3.5.11.md @@ -0,0 +1,7 @@ +# What's Changed + +1. 新增:火山引擎 TTS +2. 修复:修复了 WeChatPadPro 在重新登录时为新设备的问题 +2. ‼️修复:微信公众号(个人认证或者未认证)的情况下能接收但无法回复消息的问题 +3. 修复:Minimax TTS 相关问题 +4. 优化:登录界面侧边栏、关于页面样式,修复如果此前已经登录但未自行跳转的问题 \ No newline at end of file diff --git a/dashboard/src/assets/images/astrbot_logo_mini.webp b/dashboard/src/assets/images/astrbot_logo_mini.webp new file mode 100644 index 000000000..f6a868c54 Binary files /dev/null and b/dashboard/src/assets/images/astrbot_logo_mini.webp differ diff --git a/dashboard/src/components/shared/Logo.vue b/dashboard/src/components/shared/Logo.vue new file mode 100644 index 000000000..294fae094 --- /dev/null +++ b/dashboard/src/components/shared/Logo.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue index 2a871485e..3ed90cf77 100644 --- a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue +++ b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue @@ -383,7 +383,7 @@ if (localStorage.getItem('change_pwd_hint') != null && localStorage.getItem('cha -
为了安全,请尽快修改默认密码。
+
为了安全,请务必修改默认密码。
- + 关闭 diff --git a/dashboard/src/router/index.ts b/dashboard/src/router/index.ts index a56154f18..c44e0249d 100644 --- a/dashboard/src/router/index.ts +++ b/dashboard/src/router/index.ts @@ -24,6 +24,11 @@ router.beforeEach(async (to, from, next) => { const authRequired = !publicPages.includes(to.path); const auth: AuthStore = useAuthStore(); + // 如果用户已登录且试图访问登录页面,则重定向到首页或之前尝试访问的页面 + if (to.path === '/auth/login' && auth.has_token()) { + return next(auth.returnUrl || '/'); + } + if (to.matched.some((record) => record.meta.requiresAuth)) { if (authRequired && !auth.has_token()) { auth.returnUrl = to.fullPath; diff --git a/dashboard/src/views/authentication/auth/LoginPage.vue b/dashboard/src/views/authentication/auth/LoginPage.vue index 63a1f809c..4bcefe07c 100644 --- a/dashboard/src/views/authentication/auth/LoginPage.vue +++ b/dashboard/src/views/authentication/auth/LoginPage.vue @@ -1,23 +1,111 @@ + diff --git a/packages/astrbot/main.py b/packages/astrbot/main.py index 04d4cadb7..92ef6bfea 100644 --- a/packages/astrbot/main.py +++ b/packages/astrbot/main.py @@ -1462,3 +1462,9 @@ UID: {user_id} 此 ID 可用于设置管理员。 plugin_cfg["reset"] = reset_cfg alter_cmd_cfg["astrbot"] = plugin_cfg sp.put("alter_cmd", alter_cmd_cfg) + + @filter.command("test") + async def test_to(self, event: AstrMessageEvent): + import asyncio + await asyncio.sleep(10) + yield event.plain_result("OK") diff --git a/packages/vpet/main.py b/packages/vpet/main.py new file mode 100644 index 000000000..d345a5ac8 --- /dev/null +++ b/packages/vpet/main.py @@ -0,0 +1,18 @@ +from astrbot.api.event import filter, AstrMessageEvent +from astrbot.api.star import Context, Star, register + +@register("vpet", "AstrBot Team", "虚拟桌宠", "0.0.1") +class VPet(Star): + def __init__(self, context: Context): + super().__init__(context) + + async def initialize(self): + """可选择实现异步的插件初始化方法,当实例化该插件类之后会自动调用该方法。""" + + @filter.llm_tool("screenshot") + async def screenshot(self, event: AstrMessageEvent): + """Capture the screen and return the image.""" + + + async def terminate(self): + """可选择实现异步的插件销毁方法,当插件被卸载/停用时会调用。""" diff --git a/pyproject.toml b/pyproject.toml index b6419061d..a09c59c9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "AstrBot" -version = "3.5.10" +version = "3.5.11" description = "易上手的多平台 LLM 聊天机器人及开发框架" readme = "README.md" requires-python = ">=3.10"