From 88d70a8013f75234e8e364066833f0844b98931b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B0=95=E6=B0=99?= <2014440212@qq.com> Date: Fri, 3 Apr 2026 09:03:29 +0800 Subject: [PATCH 1/7] =?UTF-8?q?docs:=20=E5=9C=A8=20uv=20=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E4=B8=AD=E6=B7=BB=E5=8A=A0=E4=B8=8D=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20WebUI=20=E5=8D=87=E7=BA=A7=E7=9A=84=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=20(#7298)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: 在 uv 部署文档中添加不支持 WebUI 升级的说明 通过 astrbot run(CLI 模式)启动时,会设置 ASTRBOT_CLI 环境变量, updator 会拒绝 WebUI 触发的升级操作以避免版本管理混乱。 用户需要通过命令行执行 uv tool upgrade astrbot 来更新。 Closes #7291 * Update README.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update README_fr.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update README_ja.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update README_ru.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update README_zh-TW.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update README_zh.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: LIghtJUNction Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- README.md | 3 +++ README_fr.md | 3 +++ README_ja.md | 3 +++ README_ru.md | 3 +++ README_zh-TW.md | 3 +++ README_zh.md | 3 +++ docs/en/deploy/astrbot/package.md | 5 +++++ docs/zh/deploy/astrbot/package.md | 5 +++++ 8 files changed, 28 insertions(+) diff --git a/README.md b/README.md index a901c5e13..d3c30ccda 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,9 @@ Update `astrbot`: uv tool upgrade astrbot ``` +> [!WARNING] +> AstrBot deployed via `uv` **does not support upgrading through the WebUI**. To update, please run the command above from the command line. + ### Docker Deployment For users familiar with containers and looking for a more stable, production-ready deployment method, we recommend deploying AstrBot with Docker / Docker Compose. diff --git a/README_fr.md b/README_fr.md index 979ba62f6..8324808e1 100644 --- a/README_fr.md +++ b/README_fr.md @@ -92,6 +92,9 @@ Mettre à jour `astrbot` : uv tool upgrade astrbot ``` +> [!WARNING] +> AstrBot déployé via `uv` **ne prend pas en charge la mise à jour via le WebUI**. Pour mettre à jour, exécutez la commande ci-dessus depuis le terminal. + ### Déploiement Docker Pour les utilisateurs familiers avec les conteneurs et qui souhaitent une méthode plus stable et adaptée à la production, nous recommandons de déployer AstrBot avec Docker / Docker Compose. diff --git a/README_ja.md b/README_ja.md index 0e6d2dba4..621bbbf6e 100644 --- a/README_ja.md +++ b/README_ja.md @@ -92,6 +92,9 @@ astrbot run uv tool upgrade astrbot ``` +> [!WARNING] +> `uv` 経由でデプロイした AstrBot は、**WebUI からのバージョンアップグレードに対応していません**。更新するには、上記のコマンドをコマンドラインで実行してください。 + ### Docker デプロイ コンテナ運用に慣れており、より安定した本番向けのデプロイ方法を求めるユーザーには、Docker / Docker Compose での AstrBot デプロイをおすすめします。 diff --git a/README_ru.md b/README_ru.md index 64d742c5a..742c207bd 100644 --- a/README_ru.md +++ b/README_ru.md @@ -92,6 +92,9 @@ astrbot run uv tool upgrade astrbot ``` +> [!WARNING] +> AstrBot, развёрнутый через `uv`, **не поддерживает обновление через WebUI**. Для обновления выполните указанную выше команду из командной строки. + ### Развёртывание Docker Для пользователей, знакомых с контейнерами и которым нужен более стабильный и подходящий для production способ, мы рекомендуем разворачивать AstrBot через Docker / Docker Compose. diff --git a/README_zh-TW.md b/README_zh-TW.md index 5bfea0d27..478831223 100644 --- a/README_zh-TW.md +++ b/README_zh-TW.md @@ -92,6 +92,9 @@ astrbot run uv tool upgrade astrbot ``` +> [!WARNING] +> 透過 `uv` 部署的 AstrBot **不支援在 WebUI 中進行版本升級**。如需更新,請透過命令列執行上述命令。 + ### Docker 部署 對於熟悉容器、希望獲得更穩定且更適合正式環境部署方式的使用者,我們推薦使用 Docker / Docker Compose 部署 AstrBot。 diff --git a/README_zh.md b/README_zh.md index c85203352..246945658 100644 --- a/README_zh.md +++ b/README_zh.md @@ -92,6 +92,9 @@ astrbot run uv tool upgrade astrbot ``` +> [!WARNING] +> 通过 `uv` 部署的 AstrBot **不支持在 WebUI 中进行版本升级**。如需更新,请通过命令行执行上述命令。 + ### Docker 部署 对于熟悉容器、希望获得更稳定且更适合生产环境部署方式的用户,我们推荐使用 Docker / Docker Compose 部署 AstrBot。 diff --git a/docs/en/deploy/astrbot/package.md b/docs/en/deploy/astrbot/package.md index d72961b95..22207fdfd 100644 --- a/docs/en/deploy/astrbot/package.md +++ b/docs/en/deploy/astrbot/package.md @@ -9,6 +9,11 @@ If `uv` is not installed, install it first by following the official guide: `uv` supports Linux, Windows, and macOS. +## Important Notes + +> [!WARNING] +> AstrBot deployed via `uv` **does not support upgrading through the WebUI**. To update, run `uv tool upgrade astrbot` from the command line. + ## Install and Start ```bash diff --git a/docs/zh/deploy/astrbot/package.md b/docs/zh/deploy/astrbot/package.md index 70969f344..a8345e010 100644 --- a/docs/zh/deploy/astrbot/package.md +++ b/docs/zh/deploy/astrbot/package.md @@ -8,6 +8,11 @@ `uv` 支持 Linux、Windows、macOS。 +## 注意事项 + +> [!WARNING] +> 通过 `uv` 部署的 AstrBot **不支持在 WebUI 中进行版本升级**。如需更新,请在命令行中执行 `uv tool upgrade astrbot`。 + ## 安装并启动 ```bash From 5886c437529910c2085ddaec538c11414cb5ba93 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 3 Apr 2026 13:14:48 +0800 Subject: [PATCH 2/7] fix: add qrcode package for QR code generation support closes: #7327 --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index d092d885e..04ac5265d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -56,3 +56,4 @@ tenacity>=9.1.2 shipyard-python-sdk>=0.2.4 shipyard-neo-sdk>=0.2.0 packaging>=24.2 +qrcode>=8.2 \ No newline at end of file From 6a7b622c480f735a9df573d4f70f8fb80fb7b787 Mon Sep 17 00:00:00 2001 From: Soulter <37870767+Soulter@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:03:23 +0800 Subject: [PATCH 3/7] fix: resolve Discord/Misskey hot reload issue by fixing client_self_id misuse (#7331) * fix: resolve Discord/Misskey hot reload issue by fixing client_self_id misuse fixes: #7187 closes: #7188 * fix: english logging --- .../core/platform/sources/discord/client.py | 10 +- .../discord/discord_platform_adapter.py | 120 ++++++++++-------- .../sources/misskey/misskey_adapter.py | 34 ++--- .../platform/sources/misskey/misskey_utils.py | 16 +-- .../platform/sources/telegram/tg_adapter.py | 3 +- .../platform/sources/wecom/wecom_adapter.py | 1 - .../weixin_offacc_adapter.py | 2 - 7 files changed, 99 insertions(+), 87 deletions(-) diff --git a/astrbot/core/platform/sources/discord/client.py b/astrbot/core/platform/sources/discord/client.py index ebd32c471..7bff9e39e 100644 --- a/astrbot/core/platform/sources/discord/client.py +++ b/astrbot/core/platform/sources/discord/client.py @@ -35,11 +35,11 @@ class DiscordBotClient(discord.Bot): async def on_ready(self) -> None: """当机器人成功连接并准备就绪时触发""" if self.user is None: - logger.error("[Discord] 客户端未正确加载用户信息 (self.user is None)") + logger.error("[Discord] Bot user not loaded correctly (self.user is None)") return - logger.info(f"[Discord] 已作为 {self.user} (ID: {self.user.id}) 登录") - logger.info("[Discord] 客户端已准备就绪。") + logger.info(f"[Discord] Logged in as {self.user} (ID: {self.user.id})") + logger.info("[Discord] Client is ready.") if self.on_ready_once_callback and not self._ready_once_fired: self._ready_once_fired = True @@ -47,7 +47,7 @@ class DiscordBotClient(discord.Bot): await self.on_ready_once_callback() except Exception as e: logger.error( - f"[Discord] on_ready_once_callback 执行失败: {e}", + f"[Discord] Failed to execute on_ready_once_callback: {e}", exc_info=True, ) @@ -99,7 +99,7 @@ class DiscordBotClient(discord.Bot): return logger.debug( - f"[Discord] 收到原始消息 from {message.author.name}: {message.content}", + f"[Discord] Received raw message from {message.author.name}: {message.content}", ) if self.on_message_received: diff --git a/astrbot/core/platform/sources/discord/discord_platform_adapter.py b/astrbot/core/platform/sources/discord/discord_platform_adapter.py index 7657962a1..19b9c81b4 100644 --- a/astrbot/core/platform/sources/discord/discord_platform_adapter.py +++ b/astrbot/core/platform/sources/discord/discord_platform_adapter.py @@ -46,7 +46,7 @@ class DiscordPlatformAdapter(Platform): ) -> None: super().__init__(platform_config, event_queue) self.settings = platform_settings - self.client_self_id: str | None = None + self.bot_self_id: str | None = None self.registered_handlers = [] # 指令注册相关 self.enable_command_register = self.config.get("discord_command_register", True) @@ -64,7 +64,7 @@ class DiscordPlatformAdapter(Platform): """通过会话发送消息""" if self.client.user is None: logger.error( - "[Discord] 客户端未就绪 (self.client.user is None),无法发送消息" + "[Discord] Client is not ready (self.client.user is None); message send skipped" ) return @@ -92,10 +92,10 @@ class DiscordPlatformAdapter(Platform): message_obj.message_str = message_chain.get_plain_text() message_obj.sender = MessageMember( - user_id=str(self.client_self_id), + user_id=str(self.bot_self_id), nickname=self.client.user.display_name, ) - message_obj.self_id = cast(str, self.client_self_id) + message_obj.self_id = cast(str, self.bot_self_id) message_obj.session_id = session.session_id message_obj.message = message_chain.chain @@ -115,7 +115,7 @@ class DiscordPlatformAdapter(Platform): """返回平台元数据""" return PlatformMetadata( "discord", - "Discord 适配器", + "Discord Adapter", id=cast(str, self.config.get("id")), default_config_tmpl=self.config, support_streaming_message=False, @@ -127,16 +127,18 @@ class DiscordPlatformAdapter(Platform): # 初始化回调函数 async def on_received(message_data) -> None: - logger.debug(f"[Discord] 收到消息: {message_data}") - if self.client_self_id is None: - self.client_self_id = message_data.get("bot_id") + logger.debug(f"[Discord] Message received: {message_data}") + if self.bot_self_id is None: + self.bot_self_id = message_data.get("bot_id") abm = await self.convert_message(data=message_data) await self.handle_msg(abm) # 初始化 Discord 客户端 token = str(self.config.get("discord_token")) if not token: - logger.error("[Discord] Bot Token 未配置。请在配置文件中正确设置 token。") + logger.error( + "[Discord] Bot token is not configured. Please set a valid token in the config file." + ) return proxy = self.config.get("discord_proxy") or None @@ -144,12 +146,17 @@ class DiscordPlatformAdapter(Platform): self.client.on_message_received = on_received async def callback() -> None: - if self.enable_command_register: - await self._collect_and_register_commands() - if self.activity_name: - await self.client.change_presence( - status=discord.Status.online, - activity=discord.CustomActivity(name=self.activity_name), + try: + if self.enable_command_register: + await self._collect_and_register_commands() + if self.activity_name: + await self.client.change_presence( + status=discord.Status.online, + activity=discord.CustomActivity(name=self.activity_name), + ) + except Exception as e: + logger.error( + f"[Discord] on_ready_once_callback err: {e}", exc_info=True ) self.client.on_ready_once_callback = callback @@ -158,11 +165,16 @@ class DiscordPlatformAdapter(Platform): self._polling_task = asyncio.create_task(self.client.start_polling()) await self.shutdown_event.wait() except discord.errors.LoginFailure: - logger.error("[Discord] 登录失败。请检查你的 Bot Token 是否正确。") + logger.error( + "[Discord] Login failed. Please check whether the bot token is correct." + ) except discord.errors.ConnectionClosed: - logger.warning("[Discord] 与 Discord 的连接已关闭。") + logger.warning("[Discord] Connection with Discord has been closed.") except Exception as e: - logger.error(f"[Discord] 适配器运行时发生意外错误: {e}", exc_info=True) + logger.error( + f"[Discord] Unexpected error while adapter is running: {e}", + exc_info=True, + ) def _get_message_type( self, @@ -241,7 +253,7 @@ class DiscordPlatformAdapter(Platform): ) abm.message = message_chain abm.raw_message = message - abm.self_id = cast(str, self.client_self_id) + abm.self_id = cast(str, self.bot_self_id) abm.session_id = str(message.channel.id) abm.message_id = str(message.id) return abm @@ -264,7 +276,7 @@ class DiscordPlatformAdapter(Platform): if self.client.user is None: logger.error( - "[Discord] 客户端未就绪 (self.client.user is None),无法处理消息" + "[Discord] Client is not ready (self.client.user is None); message handling skipped" ) return @@ -283,7 +295,7 @@ class DiscordPlatformAdapter(Platform): raw_message = message.raw_message if not isinstance(raw_message, discord.Message): logger.warning( - f"[Discord] 收到非 Message 类型的消息: {type(raw_message)},已忽略。" + f"[Discord] Non-Message type received and ignored: {type(raw_message)}" ) return @@ -324,20 +336,9 @@ class DiscordPlatformAdapter(Platform): @override async def terminate(self) -> None: - """终止适配器""" - logger.info("[Discord] 正在终止适配器... (step 1: cancel polling task)") + logger.info("[Discord] Shutting down adapter...") self.shutdown_event.set() - # 优先 cancel polling_task - if self._polling_task: - self._polling_task.cancel() - try: - await asyncio.wait_for(self._polling_task, timeout=10) - except asyncio.CancelledError: - logger.info("[Discord] polling_task 已取消。") - except Exception as e: - logger.warning(f"[Discord] polling_task 取消异常: {e}") - logger.info("[Discord] 正在清理已注册的斜杠指令... (step 2)") - # 清理指令 + logger.info("[Discord] Cleaning up commands...") if self.enable_command_register and self.client: try: await asyncio.wait_for( @@ -347,16 +348,29 @@ class DiscordPlatformAdapter(Platform): ), timeout=10, ) - logger.info("[Discord] 指令清理完成。") + logger.info("[Discord] Commands cleaned up successfully.") except Exception as e: - logger.error(f"[Discord] 清理指令时发生错误: {e}", exc_info=True) - logger.info("[Discord] 正在关闭 Discord 客户端... (step 3)") + logger.warning( + f"[Discord] Error occurred while cleaning up commands: {e}" + ) + + if self._polling_task: + self._polling_task.cancel() + try: + await asyncio.wait_for(self._polling_task, timeout=10) + except asyncio.CancelledError: + logger.info("[Discord] Polling task cancelled successfully.") + except Exception as e: + logger.warning( + f"[Discord] Error occurred while cancelling polling task: {e}" + ) + logger.info("[Discord] Closing client connection...") if self.client and hasattr(self.client, "close"): try: await asyncio.wait_for(self.client.close(), timeout=10) except Exception as e: - logger.warning(f"[Discord] 客户端关闭异常: {e}") - logger.info("[Discord] 适配器已终止。") + logger.warning(f"[Discord] Error occurred while closing client: {e}") + logger.info("[Discord] Adapter shutdown complete.") def register_handler(self, handler_info) -> None: """注册处理器信息""" @@ -364,7 +378,7 @@ class DiscordPlatformAdapter(Platform): async def _collect_and_register_commands(self) -> None: """收集所有指令并注册到Discord""" - logger.info("[Discord] 开始收集并注册斜杠指令...") + logger.info("[Discord] Collecting and registering slash commands...") registered_commands = [] for handler_md in star_handlers_registry: @@ -405,15 +419,15 @@ class DiscordPlatformAdapter(Platform): if registered_commands: logger.info( - f"[Discord] 准备同步 {len(registered_commands)} 个指令: {', '.join(registered_commands)}", + f"[Discord] Ready to sync {len(registered_commands)} commands: {', '.join(registered_commands)}", ) else: - logger.info("[Discord] 没有发现可注册的指令。") + logger.info("[Discord] No commands found for registration.") # 使用 Pycord 的方法同步指令 # 注意:这可能需要一些时间,并且有频率限制 await self.client.sync_commands() - logger.info("[Discord] 指令同步完成。") + logger.info("[Discord] Command synchronization completed.") def _create_dynamic_callback(self, cmd_name: str): """为每个指令动态创建一个异步回调函数""" @@ -422,17 +436,17 @@ class DiscordPlatformAdapter(Platform): ctx: discord.ApplicationContext, params: str | None = None ) -> None: # 将平台特定的前缀'/'剥离,以适配通用的CommandFilter - logger.debug(f"[Discord] 回调函数触发: {cmd_name}") - logger.debug(f"[Discord] 回调函数参数: {ctx}") - logger.debug(f"[Discord] 回调函数参数: {params}") + logger.debug(f"[Discord] Callback triggered: {cmd_name}") + logger.debug(f"[Discord] Callback context: {ctx}") + logger.debug(f"[Discord] Callback params: {params}") message_str_for_filter = cmd_name if params: message_str_for_filter += f" {params}" logger.debug( - f"[Discord] 斜杠指令 '{cmd_name}' 被触发。 " - f"原始参数: '{params}'. " - f"构建的指令字符串: '{message_str_for_filter}'", + f"[Discord] Slash command '{cmd_name}' triggered. " + f"Raw params: '{params}'. " + f"Built command string: '{message_str_for_filter}'", ) # 尝试立即响应,防止超时 @@ -441,7 +455,7 @@ class DiscordPlatformAdapter(Platform): await ctx.defer() followup_webhook = ctx.followup except Exception as e: - logger.warning(f"[Discord] 指令 '{cmd_name}' defer 失败: {e}") + logger.warning(f"[Discord] Failed to defer command '{cmd_name}': {e}") # 2. 构建 AstrBotMessage channel = ctx.channel @@ -465,7 +479,7 @@ class DiscordPlatformAdapter(Platform): ) abm.message = [Plain(text=message_str_for_filter)] abm.raw_message = ctx.interaction - abm.self_id = cast(str, self.client_self_id) + abm.self_id = cast(str, self.bot_self_id) abm.session_id = str(ctx.channel_id) abm.message_id = str(ctx.interaction.id) @@ -503,10 +517,10 @@ class DiscordPlatformAdapter(Platform): # Discord 斜杠指令名称规范 if not re.match(r"^[a-z0-9_-]{1,32}$", cmd_name): - logger.debug(f"[Discord] 跳过不符合规范的指令: {cmd_name}") + logger.debug(f"[Discord] Skipping invalid slash command format: {cmd_name}") return None - description = handler_metadata.desc or f"指令: {cmd_name}" + description = handler_metadata.desc or f"Command: {cmd_name}" if len(description) > 100: description = f"{description[:97]}..." diff --git a/astrbot/core/platform/sources/misskey/misskey_adapter.py b/astrbot/core/platform/sources/misskey/misskey_adapter.py index fd61c3e50..1692c251c 100644 --- a/astrbot/core/platform/sources/misskey/misskey_adapter.py +++ b/astrbot/core/platform/sources/misskey/misskey_adapter.py @@ -93,7 +93,7 @@ class MisskeyPlatformAdapter(Platform): self.api: MisskeyAPI | None = None self._running = False - self.client_self_id = "" + self.bot_self_id = "" self._bot_username = "" self._user_cache = {} @@ -138,10 +138,10 @@ class MisskeyPlatformAdapter(Platform): try: user_info = await self.api.get_current_user() - self.client_self_id = str(user_info.get("id", "")) + self.bot_self_id = str(user_info.get("id", "")) self._bot_username = user_info.get("username", "") logger.info( - f"[Misskey] 已连接用户: {self._bot_username} (ID: {self.client_self_id})", + f"[Misskey] 已连接用户: {self._bot_username} (ID: {self.bot_self_id})", ) except Exception as e: logger.error(f"[Misskey] 获取用户信息失败: {e}") @@ -312,9 +312,9 @@ class MisskeyPlatformAdapter(Platform): ) room_id = data.get("toRoomId") logger.debug( - f"[Misskey] 收到聊天事件: sender_id={sender_id}, room_id={room_id}, is_self={sender_id == self.client_self_id}", + f"[Misskey] 收到聊天事件: sender_id={sender_id}, room_id={room_id}, is_self={sender_id == self.bot_self_id}", ) - if sender_id == self.client_self_id: + if sender_id == self.bot_self_id: return if room_id: @@ -354,13 +354,13 @@ class MisskeyPlatformAdapter(Platform): mentions = note.get("mentions", []) if self._bot_username and f"@{self._bot_username}" in text: return True - if self.client_self_id in [str(uid) for uid in mentions]: + if self.bot_self_id in [str(uid) for uid in mentions]: return True reply = note.get("reply") if reply and isinstance(reply, dict): reply_user_id = str(reply.get("user", {}).get("id", "")) - if reply_user_id == self.client_self_id: + if reply_user_id == self.bot_self_id: return bool(self._bot_username and f"@{self._bot_username}" in text) return False @@ -598,7 +598,7 @@ class MisskeyPlatformAdapter(Platform): visibility, visible_user_ids = resolve_message_visibility( user_id=user_id_for_cache, user_cache=self._user_cache, - self_id=self.client_self_id, + self_id=self.bot_self_id, default_visibility=self.default_visibility, ) logger.debug( @@ -637,14 +637,14 @@ class MisskeyPlatformAdapter(Platform): message = create_base_message( raw_data, sender_info, - self.client_self_id, + self.bot_self_id, is_chat=False, ) cache_user_info( self._user_cache, sender_info, raw_data, - self.client_self_id, + self.bot_self_id, is_chat=False, ) @@ -656,7 +656,7 @@ class MisskeyPlatformAdapter(Platform): message, raw_text, self._bot_username, - self.client_self_id, + self.bot_self_id, ) message_parts.extend(text_parts) @@ -685,14 +685,14 @@ class MisskeyPlatformAdapter(Platform): message = create_base_message( raw_data, sender_info, - self.client_self_id, + self.bot_self_id, is_chat=True, ) cache_user_info( self._user_cache, sender_info, raw_data, - self.client_self_id, + self.bot_self_id, is_chat=True, ) @@ -713,7 +713,7 @@ class MisskeyPlatformAdapter(Platform): message = create_base_message( raw_data, sender_info, - self.client_self_id, + self.bot_self_id, is_chat=False, room_id=room_id, ) @@ -722,10 +722,10 @@ class MisskeyPlatformAdapter(Platform): self._user_cache, sender_info, raw_data, - self.client_self_id, + self.bot_self_id, is_chat=False, ) - cache_room_info(self._user_cache, raw_data, self.client_self_id) + cache_room_info(self._user_cache, raw_data, self.bot_self_id) raw_text = raw_data.get("text", "") message_parts = [] @@ -736,7 +736,7 @@ class MisskeyPlatformAdapter(Platform): message, raw_text, self._bot_username, - self.client_self_id, + self.bot_self_id, ) message_parts.extend(text_parts) else: diff --git a/astrbot/core/platform/sources/misskey/misskey_utils.py b/astrbot/core/platform/sources/misskey/misskey_utils.py index dd02c13c0..86b76c21f 100644 --- a/astrbot/core/platform/sources/misskey/misskey_utils.py +++ b/astrbot/core/platform/sources/misskey/misskey_utils.py @@ -335,7 +335,7 @@ def extract_sender_info( def create_base_message( raw_data: dict[str, Any], sender_info: dict[str, Any], - client_self_id: str, + bot_self_id: str, is_chat: bool = False, room_id: str | None = None, ) -> AstrBotMessage: @@ -367,7 +367,7 @@ def create_base_message( session_id if sender_info["sender_id"] else f"{session_prefix}%unknown" ) message.message_id = str(raw_data.get("id", "")) - message.self_id = client_self_id + message.self_id = bot_self_id return message @@ -376,7 +376,7 @@ def process_at_mention( message: AstrBotMessage, raw_text: str, bot_username: str, - client_self_id: str, + bot_self_id: str, ) -> tuple[list[str], str]: """处理@提及逻辑,返回消息部分列表和处理后的文本""" message_parts = [] @@ -386,7 +386,7 @@ def process_at_mention( if bot_username and raw_text.startswith(f"@{bot_username}"): at_mention = f"@{bot_username}" - message.message.append(Comp.At(qq=client_self_id)) + message.message.append(Comp.At(qq=bot_self_id)) remaining_text = raw_text[len(at_mention) :].strip() if remaining_text: message.message.append(Comp.Plain(remaining_text)) @@ -401,7 +401,7 @@ def cache_user_info( user_cache: dict[str, Any], sender_info: dict[str, Any], raw_data: dict[str, Any], - client_self_id: str, + bot_self_id: str, is_chat: bool = False, ) -> None: """缓存用户信息""" @@ -410,7 +410,7 @@ def cache_user_info( "username": sender_info["username"], "nickname": sender_info["nickname"], "visibility": "specified", - "visible_user_ids": [client_self_id, sender_info["sender_id"]], + "visible_user_ids": [bot_self_id, sender_info["sender_id"]], } else: user_cache_data = { @@ -428,7 +428,7 @@ def cache_user_info( def cache_room_info( user_cache: dict[str, Any], raw_data: dict[str, Any], - client_self_id: str, + bot_self_id: str, ) -> None: """缓存房间信息""" room_data = raw_data.get("toRoom") @@ -442,7 +442,7 @@ def cache_room_info( "room_description": room_data.get("description", ""), "owner_id": room_data.get("ownerId", ""), "visibility": "specified", - "visible_user_ids": [client_self_id], + "visible_user_ids": [bot_self_id], } diff --git a/astrbot/core/platform/sources/telegram/tg_adapter.py b/astrbot/core/platform/sources/telegram/tg_adapter.py index 337d6f56d..e7d5a7ad4 100644 --- a/astrbot/core/platform/sources/telegram/tg_adapter.py +++ b/astrbot/core/platform/sources/telegram/tg_adapter.py @@ -50,7 +50,6 @@ class TelegramPlatformAdapter(Platform): ) -> None: super().__init__(platform_config, event_queue) self.settings = platform_settings - self.client_self_id = uuid.uuid4().hex[:8] base_url = self.config.get( "telegram_api_base_url", @@ -336,6 +335,8 @@ class TelegramPlatformAdapter(Platform): return None def _apply_caption() -> None: + if not update.message: + return if update.message.caption: message.message_str = update.message.caption message.message.append(Comp.Plain(message.message_str)) diff --git a/astrbot/core/platform/sources/wecom/wecom_adapter.py b/astrbot/core/platform/sources/wecom/wecom_adapter.py index 410b30eea..a30afcd8f 100644 --- a/astrbot/core/platform/sources/wecom/wecom_adapter.py +++ b/astrbot/core/platform/sources/wecom/wecom_adapter.py @@ -148,7 +148,6 @@ class WecomPlatformAdapter(Platform): ) -> None: super().__init__(platform_config, event_queue) self.settingss = platform_settings - self.client_self_id = uuid.uuid4().hex[:8] self.api_base_url = platform_config.get( "api_base_url", "https://qyapi.weixin.qq.com/cgi-bin/", 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 bb7061ca1..8b646e43f 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 @@ -2,7 +2,6 @@ import asyncio import os import sys import time -import uuid from collections.abc import Callable, Coroutine from typing import Any, cast @@ -324,7 +323,6 @@ class WeixinOfficialAccountPlatformAdapter(Platform): ) -> None: super().__init__(platform_config, event_queue) self.settingss = platform_settings - self.client_self_id = uuid.uuid4().hex[:8] self.api_base_url = platform_config.get( "api_base_url", "https://api.weixin.qq.com/cgi-bin/", From 5e78a24d63fdbaa7942d44d0417d01d83a7784b7 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:32:49 +0800 Subject: [PATCH 4/7] fix: satisfy Google Gemini's function_response requirements to avoid 400 Invalid argument errors (#7216) Gemini API requires function_response to be a google.protobuf.Struct (JSON object). When tool results are plain text strings, the API returns 400 Invalid argument. Detect non-JSON tool content for Gemini models and wrap it in {"result": content} before sending. Fixes #7134 --- astrbot/core/provider/sources/openai_source.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index d81185344..9c7c61fae 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -863,6 +863,9 @@ class ProviderOpenAIOfficial(Provider): def _finally_convert_payload(self, payloads: dict) -> None: """Finally convert the payload. Such as think part conversion, tool inject.""" + model = payloads.get("model", "").lower() + is_gemini = "gemini" in model + for message in payloads.get("messages", []): if message.get("role") == "assistant" and isinstance( message.get("content"), list @@ -879,6 +882,18 @@ class ProviderOpenAIOfficial(Provider): if reasoning_content: message["reasoning_content"] = reasoning_content + # Gemini 的 function_response 要求 google.protobuf.Struct(即 JSON 对象), + # 纯文本会触发 400 Invalid argument,需要包一层 JSON。 + if is_gemini and message.get("role") == "tool": + content = message.get("content", "") + if isinstance(content, str): + try: + json.loads(content) + except (json.JSONDecodeError, ValueError): + message["content"] = json.dumps( + {"result": content}, ensure_ascii=False + ) + async def _handle_api_error( self, e: Exception, From 8f95ca9d989187fb43476d588bf0f54cac9d1259 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:35:25 +0800 Subject: [PATCH 5/7] fix: filter Gemini thinking parts from user-facing message chain (#7196) Gemini 3 models return thinking parts (part.thought=True) alongside the actual response text. _process_content_parts was including these thinking parts in the message chain sent to the user, effectively leaking internal reasoning into the output. On platforms that split long messages (e.g. aiocqhttp with realtime segmenting), this caused duplicate or triple replies since the thinking text often mirrors the actual response. The streaming path already handled this correctly via chunk.text which skips thinking parts, but the non-streaming path and the final-chunk processing in streaming both went through _process_content_parts. Also switch the Gemini 3 model name matching from an exhaustive list to prefix matching (gemini-3- / gemini-3.) so new variants like gemini-3.1 get proper thinkingLevel config without code changes. Fixes #7183 --- .../core/provider/sources/gemini_source.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index 388ab5c91..489ca358b 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -241,15 +241,10 @@ class ProviderGoogleGenAI(Provider): thinking_config = types.ThinkingConfig( thinking_budget=thinking_budget, ) - elif model_name in [ - "gemini-3-pro", - "gemini-3-pro-preview", - "gemini-3-flash", - "gemini-3-flash-preview", - "gemini-3-flash-lite", - "gemini-3-flash-lite-preview", - ]: - # The thinkingLevel parameter, recommended for Gemini 3 models and onwards + elif any(model_name.startswith(p) for p in ("gemini-3-", "gemini-3.")): + # The thinkingLevel parameter, recommended for Gemini 3 models and onwards. + # Use prefix match so new variants (3.1, 3-flash-lite-preview, etc.) are + # covered without needing to keep an exhaustive list up to date. # Gemini 2.5 series models don't support thinkingLevel; use thinkingBudget instead. thinking_level = self.provider_config.get("gm_thinking_config", {}).get( "level", "HIGH" @@ -517,7 +512,11 @@ class ProviderGoogleGenAI(Provider): ): chain.append(Comp.Plain("这是图片")) for part in result_parts: - if part.text: + # Skip thinking parts — their text is already captured via + # _extract_reasoning_content above. Including them here would + # leak the model's internal reasoning into the user-facing message, + # which also causes duplicate/triple replies on some platforms. + if part.text and not part.thought: chain.append(Comp.Plain(part.text)) if ( From 1408a8449eca6aab4a3588ff420fc13decf38456 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:38:22 +0800 Subject: [PATCH 6/7] perf: Set content to None when the OpenAI message content list is empty (#6551) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _finally_convert_payload 提取 think 部分后,如果 assistant 消息的 所有 content 都是 think 类型,new_content 会变成空列表 []。 Grok 等 provider 不接受空 content list,直接报 400。 改为 new_content or None,空列表时回退到 None(OpenAI 兼容 API 普遍接受 null content 的 assistant 消息)。 Fixes #6447 Co-authored-by: Yufeng He <40085740+universeplayer@users.noreply.github.com> --- astrbot/core/provider/sources/openai_source.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index 9c7c61fae..72fc176d9 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -877,8 +877,9 @@ class ProviderOpenAIOfficial(Provider): reasoning_content += str(part.get("think")) else: new_content.append(part) - message["content"] = new_content - # reasoning key is "reasoning_content" + # Some providers (Grok, etc.) reject empty content lists. + # When all parts were think blocks, fall back to None. + message["content"] = new_content or None if reasoning_content: message["reasoning_content"] = reasoning_content From 77d5d5cc6a6ae6021697ff04f41b62ae1e879d37 Mon Sep 17 00:00:00 2001 From: SoloLevelingAI <2914633928@QQ.com> Date: Fri, 3 Apr 2026 16:51:20 +0800 Subject: [PATCH 7/7] fix(windows): inherit all system environment variables into MCP runner for Windows (#7054) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Merge environment variables case‑insensitively - Auto‑configure dotnet PATH and suppress console window - Keep PATHEXT inheritance (from old implementation) Replaces the minimal PATHEXT fix with a robust solution that resolves common Windows subprocess issues, especially for .NET servers. * fix(stdio): improve Windows environment setup for subprocesses - Merge environment variables case‑insensitively - Auto‑configure dotnet PATH and suppress console window - Keep PATHEXT inheritance (from old implementation) -Add `show_console` option to control `CREATE_NO_WINDOW` flag.Default remains hidden (backward compatible) but can be overridden. Replaces the minimal PATHEXT fix with a robust solution that resolves common Windows subprocess issues, especially for .NET servers. * fix(stdio): improve Windows environment setup for subprocesses 修复(标准输入输出):改进 Windows 环境设置以支持子进程 - Merge environment variables case‑insensitively - Auto‑configure dotnet PATH and suppress console window - Keep PATHEXT inheritance (from old implementation) -Add `show_console` option to control `CREATE_NO_WINDOW` flag.Default remains hidden (backward compatible) but can be overridden. Replaces the minimal PATHEXT fix with a robust solution that resolves common Windows subprocess issues, especially for .NET servers. * fix(stdio): improve Windows environment setup for subprocesses 修复(标准输入输出):改进 Windows 环境设置以支持子进程 - Merge environment variables case‑insensitively - Auto‑configure dotnet PATH and suppress console window - Keep PATHEXT inheritance (from old implementation) -Add `no_console` option to control `CREATE_NO_WINDOW` flag.Default remains hidden (backward compatible) but can be overridden. Replaces the minimal PATHEXT fix with a robust solution that resolves common Windows subprocess issues, especially for .NET servers. * fix(stdio): improve Windows environment setup for subprocesses 修复(标准输入输出):改进 Windows 环境设置以支持子进程 - Merge environment variables case‑insensitively - Auto‑configure dotnet PATH and suppress console window - Keep PATHEXT inheritance (from old implementation) -Add `no_console` option to control `CREATE_NO_WINDOW` flag.Default remains hidden (backward compatible) but can be overridden. Replaces the minimal PATHEXT fix with a robust solution that resolves common Windows subprocess issues, especially for .NET servers. * fix(windows): inherit all system environment variables for subprocesses - Merge environment variables case‑insensitively - Auto‑configure dotnet PATH and suppress console window - Keep PATHEXT inheritance (from old implementation) - No longer handle as a special case for C#. Replaces the minimal PATHEXT fix with a robust solution that resolves common Windows subprocess issues, especially for .NET servers. * chore: ruff format --------- Co-authored-by: Soulter <905617992@qq.com> --- astrbot/core/agent/mcp_client.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/astrbot/core/agent/mcp_client.py b/astrbot/core/agent/mcp_client.py index af969a3fa..aae1aaa77 100644 --- a/astrbot/core/agent/mcp_client.py +++ b/astrbot/core/agent/mcp_client.py @@ -51,18 +51,29 @@ def _prepare_stdio_env(config: dict) -> dict: """Preserve Windows executable resolution for stdio subprocesses.""" if sys.platform != "win32": return config - - pathext = os.environ.get("PATHEXT") - if not pathext: - return config - prepared = config.copy() env = dict(prepared.get("env") or {}) - env.setdefault("PATHEXT", pathext) + env = _merge_environment_variables(env) prepared["env"] = env return prepared +def _merge_environment_variables(env: dict) -> dict: + """合并环境变量,处理Windows不区分大小写的情况""" + merged = env.copy() + + # 将用户环境变量转换为统一的大小写形式便于比较 + user_keys_lower = {k.lower(): k for k in merged.keys()} + + for sys_key, sys_value in os.environ.items(): + sys_key_lower = sys_key.lower() + if sys_key_lower not in user_keys_lower: + # 使用系统环境变量中的原始大小写 + merged[sys_key] = sys_value + + return merged + + async def _quick_test_mcp_connection(config: dict) -> tuple[bool, str]: """Quick test MCP server connectivity""" import aiohttp