mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-10 15:00:15 +08:00
Compare commits
10 Commits
feat/confi
...
v4.22.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ab750c088 | ||
|
|
70872cd44b | ||
|
|
dc9c17c195 | ||
|
|
77d5d5cc6a | ||
|
|
1408a8449e | ||
|
|
8f95ca9d98 | ||
|
|
5e78a24d63 | ||
|
|
6a7b622c48 | ||
|
|
5886c43752 | ||
|
|
88d70a8013 |
@@ -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。
|
||||
|
||||
@@ -92,6 +92,9 @@ astrbot run
|
||||
uv tool upgrade astrbot
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> 通过 `uv` 部署的 AstrBot **不支持在 WebUI 中进行版本升级**。如需更新,请通过命令行执行上述命令。
|
||||
|
||||
### Docker 部署
|
||||
|
||||
对于熟悉容器、希望获得更稳定且更适合生产环境部署方式的用户,我们推荐使用 Docker / Docker Compose 部署 AstrBot。
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "4.22.2"
|
||||
__version__ = "4.22.3"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Any, TypedDict
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.22.2"
|
||||
VERSION = "4.22.3"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
PERSONAL_WECHAT_CONFIG_METADATA = {
|
||||
"weixin_oc_base_url": {
|
||||
@@ -1761,6 +1761,7 @@ CONFIG_METADATA_2 = {
|
||||
"enable": True,
|
||||
"rerank_api_key": "",
|
||||
"rerank_api_base": "http://127.0.0.1:8000",
|
||||
"rerank_api_suffix": "/v1/rerank",
|
||||
"rerank_model": "BAAI/bge-reranker-base",
|
||||
"timeout": 20,
|
||||
},
|
||||
@@ -1826,7 +1827,12 @@ CONFIG_METADATA_2 = {
|
||||
"rerank_api_base": {
|
||||
"description": "重排序模型 API Base URL",
|
||||
"type": "string",
|
||||
"hint": "AstrBot 会在请求时在末尾加上 /v1/rerank。",
|
||||
"hint": "最终请求路径由 Base URL 和路径后缀拼接而成(默认为 /v1/rerank)。",
|
||||
},
|
||||
"rerank_api_suffix": {
|
||||
"description": "API URL 路径后缀",
|
||||
"type": "string",
|
||||
"hint": "追加到 base_url 后的路径,如 /v1/rerank。留空则不追加。",
|
||||
},
|
||||
"rerank_api_key": {
|
||||
"description": "API Key",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]}..."
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -532,6 +532,7 @@ class ProviderOpenAIOfficial(Provider):
|
||||
**payloads,
|
||||
stream=True,
|
||||
extra_body=extra_body,
|
||||
stream_options={"include_usage": True},
|
||||
)
|
||||
|
||||
llm_response = LLMResponse("assistant", is_chunk=True)
|
||||
@@ -539,12 +540,10 @@ class ProviderOpenAIOfficial(Provider):
|
||||
state = ChatCompletionStreamState()
|
||||
|
||||
async for chunk in stream:
|
||||
if not chunk.choices:
|
||||
continue
|
||||
choice = chunk.choices[0]
|
||||
delta = choice.delta
|
||||
choice = chunk.choices[0] if chunk.choices else None
|
||||
delta = choice.delta if choice else None
|
||||
|
||||
if dtcs := delta.tool_calls:
|
||||
if delta and (dtcs := delta.tool_calls):
|
||||
for idx, tc in enumerate(dtcs):
|
||||
# siliconflow workaround
|
||||
if tc.function and tc.function.arguments:
|
||||
@@ -574,7 +573,7 @@ class ProviderOpenAIOfficial(Provider):
|
||||
_y = True
|
||||
if chunk.usage:
|
||||
llm_response.usage = self._extract_usage(chunk.usage)
|
||||
elif choice_usage := getattr(choice, "usage", None):
|
||||
elif choice and (choice_usage := getattr(choice, "usage", None)):
|
||||
# Workaround for some providers that only return usage in choices[].usage, e.g. MoonshotAI
|
||||
# See https://github.com/AstrBotDevs/AstrBot/issues/6614
|
||||
llm_response.usage = self._extract_usage(choice_usage)
|
||||
@@ -863,6 +862,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
|
||||
@@ -874,11 +876,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,
|
||||
|
||||
@@ -20,6 +20,11 @@ class VLLMRerankProvider(RerankProvider):
|
||||
self.auth_key = provider_config.get("rerank_api_key", "")
|
||||
self.base_url = provider_config.get("rerank_api_base", "http://127.0.0.1:8000")
|
||||
self.base_url = self.base_url.rstrip("/")
|
||||
self.api_suffix = provider_config.get("rerank_api_suffix", "/v1/rerank")
|
||||
if self.api_suffix is None:
|
||||
self.api_suffix = "/v1/rerank"
|
||||
if self.api_suffix and not self.api_suffix.startswith("/"):
|
||||
self.api_suffix = "/" + self.api_suffix
|
||||
self.timeout = provider_config.get("timeout", 20)
|
||||
self.model = provider_config.get("rerank_model", "BAAI/bge-reranker-base")
|
||||
|
||||
@@ -45,8 +50,9 @@ class VLLMRerankProvider(RerankProvider):
|
||||
if top_n is not None:
|
||||
payload["top_n"] = top_n
|
||||
assert self.client is not None
|
||||
rerank_url = f"{self.base_url}{self.api_suffix}"
|
||||
async with self.client.post(
|
||||
f"{self.base_url}/v1/rerank",
|
||||
rerank_url,
|
||||
json=payload,
|
||||
) as response:
|
||||
response_data = await response.json()
|
||||
|
||||
68
changelogs/v4.22.3.md
Normal file
68
changelogs/v4.22.3.md
Normal file
@@ -0,0 +1,68 @@
|
||||
- [更新日志(简体中文)](#chinese)
|
||||
- [Changelog(English)](#english)
|
||||
|
||||
<a id="chinese"></a>
|
||||
|
||||
## What's Changed
|
||||
|
||||
### 新增
|
||||
|
||||
- 新增 `vllm_rerank` 提供商支持可配置的 `rerank_api_suffix`,可灵活适配不同 API 路径。([#7278](https://github.com/AstrBotDevs/AstrBot/pull/7278))
|
||||
- 适配 llama.cpp 的 token 用量提取支持,增强 token 统计与计费分析能力。([#7358](https://github.com/AstrBotDevs/AstrBot/pull/7358))
|
||||
- WebUI 捆绑 Monaco Editor 的 worker 文件,防止从 CDN 下载失败。([#7249](https://github.com/AstrBotDevs/AstrBot/pull/7249))
|
||||
- 全新的模型/平台总览统计面板。([#7152](https://github.com/AstrBotDevs/AstrBot/pull/7152))
|
||||
|
||||
### 优化
|
||||
|
||||
- 优化 OpenAI 消息处理,若 `content` 列表为空时将其置为 `None`,并降低空内容下的异常率。([#6551](https://github.com/AstrBotDevs/AstrBot/pull/6551))
|
||||
- 优化 provider page 布局与样式,提升整体设置页面排版一致性。([#7248](https://github.com/AstrBotDevs/AstrBot/pull/7248))
|
||||
- 用户已登录时将直接跳转欢迎页,减少再次进入登录页的步骤。([commit](https://github.com/AstrBotDevs/AstrBot/commit/9896b48c))
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复 Windows 下 MCP runner 未继承完整系统环境变量导致启动异常的问题。([#7054](https://github.com/AstrBotDevs/AstrBot/pull/7054))
|
||||
- 修复 Gemini 思考片段泄露到用户可见消息链路的问题。([#7196](https://github.com/AstrBotDevs/AstrBot/pull/7196))
|
||||
- 修复 Gemini(OpenAI 兼容 API) 下,`function_response` 格式要求导致的 `400 Invalid argument` 错误。([#7216](https://github.com/AstrBotDevs/AstrBot/pull/7216)
|
||||
- 修复知识库初始化失败时的错误提示与恢复问题。([#7243](https://github.com/AstrBotDevs/AstrBot/pull/7243))
|
||||
- 修复 Bailian Rerank 对新旧响应格式兼容性,避免解析失败。([#7217](https://github.com/AstrBotDevs/AstrBot/pull/7217))
|
||||
- Docker 镜像补充 AMR 编码器依赖,修复部分部署场景下的音频转码缺失问题。([#7247](https://github.com/AstrBotDevs/AstrBot/pull/7247))
|
||||
- 修复 pip 安装缺少 `pysocks` 依赖导致的 SOCKS5 代理不可用问题。([#7221](https://github.com/AstrBotDevs/AstrBot/pull/7221))
|
||||
- 修复 `cached_tokens` 在 `_extract_usage` 中的处理逻辑,避免 token 统计错乱。([#6719](https://github.com/AstrBotDevs/AstrBot/pull/6719))
|
||||
- 增强 SSE 断开保护,压缩传输下主动发送心跳,减少 WebChat 误断连。([#7003](https://github.com/AstrBotDevs/AstrBot/pull/7003))
|
||||
- 修复严格 API 下空 assistant 消息导致的 400 错误。([#7202](https://github.com/AstrBotDevs/AstrBot/pull/7202))
|
||||
- 新增二维码功能缺失的 `qrcode` 依赖修复,恢复二维码生成功能。([commit](https://github.com/AstrBotDevs/AstrBot/commit/5886c437))
|
||||
- 修复配置器数字输入组件的交互体验问题,避免异常输入场景下的处理偏差。([#7153](https://github.com/AstrBotDevs/AstrBot/pull/7153))
|
||||
- 修复 `Record.path` 在 Pydantic v2 下被错误视为必填字段的问题。([#7048](https://github.com/AstrBotDevs/AstrBot/pull/7048))
|
||||
|
||||
<a id="english"></a>
|
||||
## What's Changed (EN)
|
||||
|
||||
### New Features
|
||||
|
||||
- Added configurable `rerank_api_suffix` for the `vllm_rerank` provider to support custom API paths.([#7278](https://github.com/AstrBotDevs/AstrBot/pull/7278))
|
||||
- Added token usage extraction support for llama.cpp to improve token accounting.([#7358](https://github.com/AstrBotDevs/AstrBot/pull/7358))
|
||||
- Bundled Monaco Editor workers in WebUI so code editing does not depend on external CDN downloads.([#7249](https://github.com/AstrBotDevs/AstrBot/pull/7249))
|
||||
- Added a new statistics overview for model and platform usage.([#7152](https://github.com/AstrBotDevs/AstrBot/pull/7152))
|
||||
- Redirected logged-in users to the welcome page instead of the login page.([commit](https://github.com/AstrBotDevs/AstrBot/commit/9896b48c))
|
||||
|
||||
### Improvements
|
||||
|
||||
- Improved OpenAI message handling by setting message `content` to `None` when content lists are empty, reducing empty-payload edge cases.([#6551](https://github.com/AstrBotDevs/AstrBot/pull/6551))
|
||||
- Improved provider page layout and styling to keep settings pages consistent.([#7248](https://github.com/AstrBotDevs/AstrBot/pull/7248))
|
||||
- Refined connection-success handling and messaging when users are already logged in.([commit](https://github.com/AstrBotDevs/AstrBot/commit/2de23184))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Inherited all system environment variables in Windows MCP runner to fix environment-dependent launch issues.([#7054](https://github.com/AstrBotDevs/AstrBot/pull/7054))
|
||||
- Filtered Gemini thinking parts from user-facing message chains.([#7196](https://github.com/AstrBotDevs/AstrBot/pull/7196))
|
||||
- Fixed Gemini `function_response` compatibility to prevent `400 Invalid argument` responses.([#7216](https://github.com/AstrBotDevs/AstrBot/pull/7216))
|
||||
- Improved knowledge base initialization error handling.([#7243](https://github.com/AstrBotDevs/AstrBot/pull/7243))
|
||||
- Fixed Bailian rerank parsing for both old and new response formats.([#7217](https://github.com/AstrBotDevs/AstrBot/pull/7217))
|
||||
- Added AMR encoder support in Docker builds to fix missing audio codec support.([#7247](https://github.com/AstrBotDevs/AstrBot/pull/7247))
|
||||
- Added the `pysocks` dependency so pip installs can use SOCKS5 proxy correctly.([#7221](https://github.com/AstrBotDevs/AstrBot/pull/7221))
|
||||
- Fixed `_extract_usage` cached token handling.([#6719](https://github.com/AstrBotDevs/AstrBot/pull/6719))
|
||||
- Sent SSE heartbeat packets to prevent WebChat disconnection under compression.([#7003](https://github.com/AstrBotDevs/AstrBot/pull/7003))
|
||||
- Fixed `400` errors caused by empty assistant messages on strict APIs.([#7202](https://github.com/AstrBotDevs/AstrBot/pull/7202))
|
||||
- Added missing `qrcode` dependency to restore QR code generation.([commit](https://github.com/AstrBotDevs/AstrBot/commit/5886c437))
|
||||
- Fixed number input UX in config renderer.([#7153](https://github.com/AstrBotDevs/AstrBot/pull/7153))
|
||||
- Fixed `Record.path` being incorrectly treated as required under Pydantic v2.([#7048](https://github.com/AstrBotDevs/AstrBot/pull/7048))
|
||||
@@ -1075,7 +1075,11 @@
|
||||
},
|
||||
"rerank_api_base": {
|
||||
"description": "Rerank Model API Base URL",
|
||||
"hint": "AstrBot appends /v1/rerank to the request URL."
|
||||
"hint": "The full request URL is formed by combining the Base URL and a path suffix (defaults to /v1/rerank)."
|
||||
},
|
||||
"rerank_api_suffix": {
|
||||
"description": "API URL path suffix",
|
||||
"hint": "Path appended to base_url, e.g. /v1/rerank. Leave empty to disable auto-append."
|
||||
},
|
||||
"rerank_api_key": {
|
||||
"description": "API Key",
|
||||
|
||||
@@ -1076,7 +1076,11 @@
|
||||
},
|
||||
"rerank_api_base": {
|
||||
"description": "Base URL API модели Rerank",
|
||||
"hint": "AstrBot добавляет /v1/rerank к URL запроса."
|
||||
"hint": "Полный URL запроса формируется путём добавления суффикса к Base URL (по умолчанию /v1/rerank)."
|
||||
},
|
||||
"rerank_api_suffix": {
|
||||
"description": "Суффикс пути API",
|
||||
"hint": "Суффикс пути, добавляемый к base_url, например /v1/rerank. Оставьте пустым, чтобы не добавлять."
|
||||
},
|
||||
"rerank_api_key": {
|
||||
"description": "API Key",
|
||||
|
||||
@@ -1077,7 +1077,11 @@
|
||||
},
|
||||
"rerank_api_base": {
|
||||
"description": "重排序模型 API Base URL",
|
||||
"hint": "AstrBot 会在请求时在末尾加上 /v1/rerank。"
|
||||
"hint": "最终请求路径由 Base URL 和路径后缀拼接而成(默认为 /v1/rerank)。"
|
||||
},
|
||||
"rerank_api_suffix": {
|
||||
"description": "API URL 路径后缀",
|
||||
"hint": "追加到 base_url 后的路径后缀,如 /v1/rerank。留空则不追加。"
|
||||
},
|
||||
"rerank_api_key": {
|
||||
"description": "API Key",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.22.2"
|
||||
version = "4.22.3"
|
||||
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
||||
readme = "README.md"
|
||||
license = { text = "AGPL-3.0-or-later" }
|
||||
|
||||
@@ -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
|
||||
@@ -2,6 +2,7 @@ from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from openai.types.chat.chat_completion import ChatCompletion
|
||||
from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
|
||||
from PIL import Image as PILImage
|
||||
|
||||
from astrbot.core.exceptions import EmptyModelOutputError
|
||||
@@ -1175,6 +1176,93 @@ async def test_parse_openai_completion_raises_empty_model_output_error():
|
||||
await provider.terminate()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_stream_extracts_usage_from_empty_choices_chunk(monkeypatch):
|
||||
provider = _make_provider()
|
||||
try:
|
||||
chunks = [
|
||||
ChatCompletionChunk.model_validate(
|
||||
{
|
||||
"id": "chatcmpl-stream",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": 0,
|
||||
"model": "gpt-4o-mini",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"role": "assistant",
|
||||
"content": "ok",
|
||||
},
|
||||
"finish_reason": None,
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
ChatCompletionChunk.model_validate(
|
||||
{
|
||||
"id": "chatcmpl-stream",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": 0,
|
||||
"model": "gpt-4o-mini",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
ChatCompletionChunk.model_validate(
|
||||
{
|
||||
"id": "chatcmpl-stream",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": 0,
|
||||
"model": "gpt-4o-mini",
|
||||
"choices": [],
|
||||
"usage": {
|
||||
"prompt_tokens": 2550,
|
||||
"completion_tokens": 125,
|
||||
"total_tokens": 2675,
|
||||
"prompt_tokens_details": {
|
||||
"cached_tokens": 2488,
|
||||
},
|
||||
},
|
||||
}
|
||||
),
|
||||
]
|
||||
|
||||
async def fake_stream():
|
||||
for chunk in chunks:
|
||||
yield chunk
|
||||
|
||||
async def fake_create(**kwargs):
|
||||
return fake_stream()
|
||||
|
||||
monkeypatch.setattr(provider.client.chat.completions, "create", fake_create)
|
||||
|
||||
responses = [
|
||||
response
|
||||
async for response in provider._query_stream(
|
||||
payloads={
|
||||
"model": "gpt-4o-mini",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
},
|
||||
tools=None,
|
||||
)
|
||||
]
|
||||
|
||||
final_response = responses[-1]
|
||||
assert final_response.completion_text == "ok"
|
||||
assert final_response.usage is not None
|
||||
assert final_response.usage.input_other == 62
|
||||
assert final_response.usage.input_cached == 2488
|
||||
assert final_response.usage.output == 125
|
||||
finally:
|
||||
await provider.terminate()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_filters_empty_assistant_message_without_tool_calls(monkeypatch):
|
||||
"""Test that empty assistant messages without tool_calls are filtered out."""
|
||||
|
||||
Reference in New Issue
Block a user