mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-09-21 14:35:29 +08:00
Merge remote-tracking branch 'origin/master' into dev
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -92,6 +92,9 @@ astrbot run
|
||||
uv tool upgrade astrbot
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> `uv` 経由でデプロイした AstrBot は、**WebUI からのバージョンアップグレードに対応していません**。更新するには、上記のコマンドをコマンドラインで実行してください。
|
||||
|
||||
### Docker デプロイ
|
||||
|
||||
コンテナ運用に慣れており、より安定した本番向けのデプロイ方法を求めるユーザーには、Docker / Docker Compose での AstrBot デプロイをおすすめします。
|
||||
|
||||
@@ -92,6 +92,9 @@ astrbot run
|
||||
uv tool upgrade astrbot
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> AstrBot, развёрнутый через `uv`, **не поддерживает обновление через WebUI**. Для обновления выполните указанную выше команду из командной строки.
|
||||
|
||||
### Развёртывание Docker
|
||||
|
||||
Для пользователей, знакомых с контейнерами и которым нужен более стабильный и подходящий для production способ, мы рекомендуем разворачивать AstrBot через Docker / Docker Compose.
|
||||
|
||||
@@ -92,6 +92,9 @@ astrbot run
|
||||
uv tool upgrade astrbot
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> 透過 `uv` 部署的 AstrBot **不支援在 WebUI 中進行版本升級**。如需更新,請透過命令列執行上述命令。
|
||||
|
||||
### Docker 部署
|
||||
|
||||
對於熟悉容器、希望獲得更穩定且更適合正式環境部署方式的使用者,我們推薦使用 Docker / Docker Compose 部署 AstrBot。
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]}..."
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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/"
|
||||
)
|
||||
|
||||
@@ -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/"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
|
||||
`uv` 支持 Linux、Windows、macOS。
|
||||
|
||||
## 注意事项
|
||||
|
||||
> [!WARNING]
|
||||
> 通过 `uv` 部署的 AstrBot **不支持在 WebUI 中进行版本升级**。如需更新,请在命令行中执行 `uv tool upgrade astrbot`。
|
||||
|
||||
## 安装并启动
|
||||
|
||||
```bash
|
||||
|
||||
Reference in New Issue
Block a user