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 a2a221a7e..2bd2c7539 100644 --- a/README_zh.md +++ b/README_zh.md @@ -95,6 +95,9 @@ uv tool install git+https://github.com/AstrBotDevs/AstrBot@dev uv tool upgrade astrbot ``` +> [!WARNING] +> 通过 `uv` 部署的 AstrBot **不支持在 WebUI 中进行版本升级**。如需更新,请通过命令行执行上述命令。 + ### Docker 部署 对于熟悉容器、希望获得更稳定且更适合生产环境部署方式的用户,我们推荐使用 Docker / Docker Compose 部署 AstrBot。 diff --git a/astrbot/core/agent/mcp_client.py b/astrbot/core/agent/mcp_client.py index 71135f271..e82e4af40 100644 --- a/astrbot/core/agent/mcp_client.py +++ b/astrbot/core/agent/mcp_client.py @@ -95,18 +95,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 diff --git a/astrbot/core/platform/sources/discord/client.py b/astrbot/core/platform/sources/discord/client.py index d086b154a..385cd95c2 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 081f08ee5..10180d0fd 100644 --- a/astrbot/core/platform/sources/discord/discord_platform_adapter.py +++ b/astrbot/core/platform/sources/discord/discord_platform_adapter.py @@ -50,9 +50,8 @@ class DiscordPlatformAdapter(Platform): ) -> None: super().__init__(platform_config, event_queue) self.settings = platform_settings - self.client_self_id: str | None = None - self.registered_handlers: list[Any] = [] - self.sdk_plugin_bridge = None + self.bot_self_id: str | None = None + self.registered_handlers = [] # 指令注册相关 self.enable_command_register = self.config.get("discord_command_register", True) self.guild_id = self.config.get("discord_guild_id_for_debug", None) @@ -69,7 +68,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 @@ -97,9 +96,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.bot_self_id) message_obj.session_id = session.session_id message_obj.message = message_chain.chain @@ -119,7 +119,7 @@ class DiscordPlatformAdapter(Platform): """返回平台元数据""" return PlatformMetadata( "discord", - "Discord 适配器", + "Discord Adapter", id=str(self.config.get("id")), default_config_tmpl=self.config, support_streaming_message=False, @@ -131,16 +131,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 @@ -148,12 +150,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 @@ -162,11 +169,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, @@ -252,7 +264,7 @@ class DiscordPlatformAdapter(Platform): ) abm.message = message_chain abm.raw_message = message - abm.self_id = 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 @@ -275,7 +287,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 @@ -294,7 +306,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 @@ -335,26 +347,41 @@ 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 + logger.info("[Discord] Cleaning up commands...") + if self.enable_command_register and self.client: + try: + await asyncio.wait_for( + self.client.sync_commands( + commands=[], + guild_ids=[self.guild_id] if self.guild_id else None, + ), + timeout=10, + ) + logger.info("[Discord] Commands cleaned up successfully.") + except Exception as e: + 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 已取消。") + logger.info("[Discord] Polling task cancelled successfully.") except Exception as e: - logger.warning(f"[Discord] polling_task 取消异常: {e}") - logger.info("[Discord] 跳过斜杠指令清理,避免重启时重复创建命令。") - logger.info("[Discord] 正在关闭 Discord 客户端... (step 2)") + 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: """注册处理器信息""" @@ -362,7 +389,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 cmd_name, description in self.collect_commands(): callback = self._create_dynamic_callback(cmd_name) @@ -449,7 +476,41 @@ class DiscordPlatformAdapter(Platform): ) command_dict.setdefault(cmd_name, description) - return sorted(command_dict.items(), key=lambda item: item[0].lower()) + # 创建动态回调 + callback = self._create_dynamic_callback(cmd_name) + + # 创建一个通用的参数选项来接收所有文本输入 + options = [ + discord.Option( + name="params", + description="指令的所有参数", + type=discord.SlashCommandOptionType.string, + required=False, + ), + ] + + # 创建SlashCommand + slash_command = discord.SlashCommand( + name=cmd_name, + description=description, + func=callback, + options=options, + guild_ids=[self.guild_id] if self.guild_id else None, + ) + self.client.add_application_command(slash_command) + registered_commands.append(cmd_name) + + if registered_commands: + logger.info( + f"[Discord] Ready to sync {len(registered_commands)} commands: {', '.join(registered_commands)}", + ) + else: + logger.info("[Discord] No commands found for registration.") + + # 使用 Pycord 的方法同步指令 + # 注意:这可能需要一些时间,并且有频率限制 + await self.client.sync_commands() + logger.info("[Discord] Command synchronization completed.") def _create_dynamic_callback(self, cmd_name: str): """为每个指令动态创建一个异步回调函数""" @@ -457,18 +518,18 @@ class DiscordPlatformAdapter(Platform): async def dynamic_callback( 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}") + # 将平台特定的前缀'/'剥离,以适配通用的CommandFilter + 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}'", ) # 尝试立即响应,防止超时 @@ -477,7 +538,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 @@ -512,7 +573,7 @@ class DiscordPlatformAdapter(Platform): ) abm.message = [Plain(text=message_str_for_filter)] abm.raw_message = ctx.interaction - abm.self_id = 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(getattr(ctx.interaction, "id", ctx.interaction)) @@ -552,10 +613,10 @@ class DiscordPlatformAdapter(Platform): return None 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 d2eec37ca..6f3dcfa2d 100644 --- a/astrbot/core/platform/sources/misskey/misskey_adapter.py +++ b/astrbot/core/platform/sources/misskey/misskey_adapter.py @@ -82,7 +82,7 @@ class MisskeyPlatformAdapter(Platform): self.max_download_bytes = None self.api: MisskeyAPI | None = None self._running = False - self.client_self_id = "" + self.bot_self_id = "" self._bot_username = "" self._user_cache: dict[str, Any] = {} @@ -123,10 +123,10 @@ class MisskeyPlatformAdapter(Platform): self._running = True 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}") @@ -275,9 +275,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: raw_text = data.get("text", "") @@ -313,12 +313,12 @@ 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 @@ -517,7 +517,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( @@ -548,16 +548,26 @@ class MisskeyPlatformAdapter(Platform): """将 Misskey 贴文数据转换为 AstrBotMessage 对象""" sender_info = extract_sender_info(raw_data, is_chat=False) message = create_base_message( - raw_data, sender_info, self.client_self_id, is_chat=False + raw_data, + sender_info, + self.bot_self_id, + is_chat=False, ) cache_user_info( - self._user_cache, sender_info, raw_data, self.client_self_id, is_chat=False + self._user_cache, + sender_info, + raw_data, + self.bot_self_id, + is_chat=False, ) message_parts = [] raw_text = raw_data.get("text", "") if raw_text: - text_parts, _processed_text = process_at_mention( - message, raw_text, self._bot_username, self.client_self_id + text_parts, processed_text = process_at_mention( + message, + raw_text, + self._bot_username, + self.bot_self_id, ) message_parts.extend(text_parts) files = raw_data.get("files", []) @@ -581,10 +591,17 @@ class MisskeyPlatformAdapter(Platform): """将 Misskey 聊天消息数据转换为 AstrBotMessage 对象""" sender_info = extract_sender_info(raw_data, is_chat=True) message = create_base_message( - raw_data, sender_info, self.client_self_id, is_chat=True + raw_data, + sender_info, + self.bot_self_id, + is_chat=True, ) cache_user_info( - self._user_cache, sender_info, raw_data, self.client_self_id, is_chat=True + self._user_cache, + sender_info, + raw_data, + self.bot_self_id, + is_chat=True, ) raw_text = raw_data.get("text", "") if raw_text: @@ -599,18 +616,30 @@ class MisskeyPlatformAdapter(Platform): sender_info = extract_sender_info(raw_data, is_chat=True) room_id = raw_data.get("toRoomId", "") message = create_base_message( - raw_data, sender_info, self.client_self_id, is_chat=False, room_id=room_id + raw_data, + sender_info, + self.bot_self_id, + is_chat=False, + room_id=room_id, ) cache_user_info( - self._user_cache, sender_info, raw_data, self.client_self_id, is_chat=False + self._user_cache, + sender_info, + raw_data, + 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 = [] if raw_text: if self._bot_username and f"@{self._bot_username}" in raw_text: - text_parts, _processed_text = process_at_mention( - message, raw_text, self._bot_username, self.client_self_id + text_parts, processed_text = process_at_mention( + message, + raw_text, + self._bot_username, + 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 b4e9ceb09..00f49f479 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: list[str] = [] @@ -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 320a5d360..ece8d951d 100644 --- a/astrbot/core/platform/sources/telegram/tg_adapter.py +++ b/astrbot/core/platform/sources/telegram/tg_adapter.py @@ -46,8 +46,12 @@ class TelegramPlatformAdapter(Platform): ) -> None: super().__init__(platform_config, event_queue) self.settings = platform_settings +<<<<<<< HEAD self.client_self_id = uuid.uuid4().hex[:8] self.sdk_plugin_bridge = None +======= + +>>>>>>> origin/master base_url = self.config.get( "telegram_api_base_url", "https://api.telegram.org/bot" ) @@ -303,8 +307,15 @@ class TelegramPlatformAdapter(Platform): msg = update.message def _apply_caption() -> None: +<<<<<<< HEAD if msg.caption: message.message_str = msg.caption +======= + if not update.message: + return + if update.message.caption: + message.message_str = update.message.caption +>>>>>>> origin/master message.message.append(Comp.Plain(message.message_str)) if msg.caption and msg.caption_entities: for entity in msg.caption_entities: diff --git a/astrbot/core/platform/sources/wecom/wecom_adapter.py b/astrbot/core/platform/sources/wecom/wecom_adapter.py index 4723ee41d..eb60de730 100644 --- a/astrbot/core/platform/sources/wecom/wecom_adapter.py +++ b/astrbot/core/platform/sources/wecom/wecom_adapter.py @@ -137,7 +137,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 6cbb9bd33..3198fa8d2 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 @@ -1,7 +1,6 @@ import asyncio import os import time -import uuid from collections.abc import Callable, Coroutine from typing import Any, override @@ -282,7 +281,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/" ) diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index a3638bae8..8bc764a0e 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -198,15 +198,14 @@ class ProviderGoogleGenAI(Provider): "budget", 0 ) if thinking_budget is not None: - 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", - ]: + thinking_config = types.ThinkingConfig( + thinking_budget=thinking_budget, + ) + 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" ) @@ -437,7 +436,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 ( part.function_call diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index 314107b87..71bf2b588 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -868,6 +868,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,11 +882,24 @@ 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 + # 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, 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