Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
- name: checkout
uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
uses: pnpm/action-setup@v6.0.3
with:
version: 10.28.2
- name: Setup Node.js
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/dashboard_ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
uses: actions/checkout@v6

- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
uses: pnpm/action-setup@v6.0.3
with:
version: 10.28.2

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
echo "tag=$tag" >> "$GITHUB_OUTPUT"

- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
uses: pnpm/action-setup@v6.0.3
with:
version: 10.28.2

Expand Down
1 change: 0 additions & 1 deletion astrbot/core/agent/tool_image_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ def __init__(self) -> None:
self._initialized = True
self._cache_dir = os.path.join(get_astrbot_temp_path(), self.CACHE_DIR_NAME)
os.makedirs(self._cache_dir, exist_ok=True)
logger.debug(f"ToolImageCache initialized, cache dir: {self._cache_dir}")

def _get_file_extension(self, mime_type: str) -> str:
"""Get file extension from MIME type."""
Expand Down
11 changes: 8 additions & 3 deletions astrbot/core/config/astrbot_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ def check_config_integrity(self, refer_conf: dict, conf: dict, path=""):
for key, value in refer_conf.items():
if key not in conf:
# 配置项不存在,插入默认值
logger.info("检查到配置项不存在,已插入默认值")
path_ = path + "." + key if path else key
logger.info("Config key missing; added default.")
new_conf[key] = value
has_new = True
elif conf[key] is None:
Expand Down Expand Up @@ -132,12 +133,16 @@ def check_config_integrity(self, refer_conf: dict, conf: dict, path=""):
# 检查是否存在参考配置中没有的配置项
for key in list(conf.keys()):
if key not in refer_conf:
logger.info("检查到未知配置项,将从当前配置中删除")
path_ = path + "." + key if path else key
logger.info("Config key removed: %s", path_)
has_new = True

# 顺序不一致也算作变更
if list(conf.keys()) != list(new_conf.keys()):
logger.info("检查到配置项顺序不一致,已重新排序")
if path:
logger.info("Config key order fixed: %s", path)
else:
logger.info("Config key order fixed")
has_new = True

# 更新原始配置
Expand Down
2 changes: 1 addition & 1 deletion astrbot/core/core_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ async def start(self) -> None:
用load加载事件总线和任务并初始化, 执行启动完成事件钩子
"""
self._load()
logger.info("AstrBot 启动完成。")
logger.info("AstrBot started.")

# 执行启动完成事件钩子
handlers = star_handlers_registry.get_handlers_by_event_type(
Expand Down
2 changes: 1 addition & 1 deletion astrbot/core/persona_mgr.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def __init__(self, db_helper: BaseDatabase, acm: AstrBotConfigManager) -> None:
async def initialize(self) -> None:
self.personas = await self.get_all_personas()
self.get_v3_persona_data()
logger.info(f"已加载 {len(self.personas)} 个人格。")
logger.info("Loaded %s personas.", len(self.personas))

async def get_persona(self, persona_id: str):
"""获取指定 persona 的信息"""
Expand Down
6 changes: 4 additions & 2 deletions astrbot/core/platform/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,9 @@ async def load_platform(self, platform_config: dict) -> None:
return

logger.info(
f"载入 {platform_config['type']}({platform_config['id']}) 平台适配器 ...",
"Loading IM platform adapter %s(%s) ...",
platform_config["type"],
platform_config["id"],
)
match platform_config["type"]:
case "aiocqhttp":
Expand Down Expand Up @@ -201,7 +203,7 @@ async def load_platform(self, platform_config: dict) -> None:

if platform_config["type"] not in platform_cls_map:
logger.error(
f"未找到适用于 {platform_config['type']}({platform_config['id']}) 平台适配器,请检查是否已经安装或者名称填写错误",
f"Platform adapter not found: {platform_config['type']}({platform_config['id']}).",
)
return
cls_type = platform_cls_map[platform_config["type"]]
Expand Down
2 changes: 1 addition & 1 deletion astrbot/core/platform/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def decorator(cls):
)
platform_registry.append(pm)
platform_cls_map[adapter_name] = cls
logger.debug(f"平台适配器 {adapter_name} 已注册")
logger.debug("Platform adapter registered: %s", adapter_name)
return cls

return decorator
Expand Down
12 changes: 7 additions & 5 deletions astrbot/core/provider/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,9 @@ async def load_provider(self, provider_config: dict) -> None:
return

logger.info(
f"载入 {provider_config['type']}({provider_config['id']}) 服务提供商 ...",
"Loading model %s(%s) ...",
provider_config["type"],
provider_config["id"],
)

# 动态导入
Expand All @@ -591,7 +593,7 @@ async def load_provider(self, provider_config: dict) -> None:

if provider_config["type"] not in provider_cls_map:
logger.error(
f"未找到适用于 {provider_config['type']}({provider_config['id']}) 的提供商适配器,请检查是否已经安装或者名称填写错误。已跳过。",
f"Provider adapter not found: {provider_config['type']}({provider_config['id']}). Skipped.",
exc_info=True,
)
return
Expand Down Expand Up @@ -625,7 +627,7 @@ async def load_provider(self, provider_config: dict) -> None:
):
self.curr_stt_provider_inst = inst
logger.info(
f"已选择 {provider_config['type']}({provider_config['id']}) 作为当前语音转文本提供商适配器。",
f"Selected {provider_config['type']}({provider_config['id']}) as default STT provider",
)
if not self.curr_stt_provider_inst:
self.curr_stt_provider_inst = inst
Expand All @@ -648,7 +650,7 @@ async def load_provider(self, provider_config: dict) -> None:
):
self.curr_tts_provider_inst = inst
logger.info(
f"已选择 {provider_config['type']}({provider_config['id']}) 作为当前文本转语音提供商适配器。",
f"Selected {provider_config['type']}({provider_config['id']}) as default TTS provider",
)
if not self.curr_tts_provider_inst:
self.curr_tts_provider_inst = inst
Expand All @@ -674,7 +676,7 @@ async def load_provider(self, provider_config: dict) -> None:
):
self.curr_provider_inst = inst
logger.info(
f"已选择 {provider_config['type']}({provider_config['id']}) 作为当前提供商适配器。",
f"Selected {provider_config['type']}({provider_config['id']}) as default chat model provider",
)
if not self.curr_provider_inst:
self.curr_provider_inst = inst
Expand Down
2 changes: 1 addition & 1 deletion astrbot/core/provider/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def decorator(cls):
)
provider_registry.append(pm)
provider_cls_map[provider_type_name] = pm
logger.debug(f"服务提供商 Provider {provider_type_name} 已注册")
logger.debug("Model provider registered: %s", provider_type_name)
return cls

return decorator
10 changes: 5 additions & 5 deletions astrbot/core/star/star_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,21 +570,21 @@ def _validate_astrbot_version_specifier(
except InvalidSpecifier:
return (
False,
"astrbot_version 格式无效,请使用 PEP 440 版本范围格式,例如 >=4.16,<5",
"Invalid astrbot_version. Use a PEP 440 range, e.g. >=4.16,<5.",
)

try:
current_version = Version(VERSION)
except InvalidVersion:
return (
False,
f"AstrBot 当前版本 {VERSION} 无法被解析,无法校验插件版本范围。",
f"Invalid current AstrBot version: {VERSION}. Cannot check plugin version range.",
)

if not specifier.contains(current_version, prereleases=True):
return (
False,
f"当前 AstrBot 版本为 {VERSION},不满足插件要求的 astrbot_version: {normalized_spec}",
f"AstrBot {VERSION} does not satisfy plugin astrbot_version: {normalized_spec}",
)
return True, None

Expand Down Expand Up @@ -874,7 +874,7 @@ async def load(
if specified_dir_name and root_dir_name != specified_dir_name:
continue

logger.info(f"正在载入插件 {root_dir_name} ...")
logger.info("Loading plugin %s ...", root_dir_name)

# 尝试导入模块
try:
Expand Down Expand Up @@ -993,7 +993,7 @@ async def load(
setattr(metadata.star_cls, "author", p_author)
setattr(metadata.star_cls, "plugin_id", plugin_id)
else:
logger.info(f"插件 {metadata.name} 已被禁用。")
logger.info("Plugin %s is disabled.", metadata.name)

metadata.module = module
metadata.root_dir_name = root_dir_name
Expand Down
19 changes: 11 additions & 8 deletions astrbot/core/utils/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,14 @@ async def download_file(url: str, path: str, show_progress: bool = False) -> Non
) as session:
async with session.get(url, timeout=1800) as resp:
if resp.status != 200:
raise Exception(f"下载文件失败: {resp.status}")
logger.error(
f"Failed to download file from {url}. HTTP status code: {resp.status}"
)
total_size = int(resp.headers.get("content-length", 0))
downloaded_size = 0
start_time = time.time()
if show_progress:
print(f"文件大小: {total_size / 1024:.2f} KB | 文件地址: {url}")
print(f"Downloading: {url} | Size: {total_size / 1024:.2f} KB")
with open(path, "wb") as f:
while True:
chunk = await resp.content.read(8192)
Expand All @@ -157,13 +159,14 @@ async def download_file(url: str, path: str, show_progress: bool = False) -> Non
)
speed = downloaded_size / 1024 / elapsed_time # KB/s
print(
f"\r下载进度: {downloaded_size / total_size:.2%} 速度: {speed:.2f} KB/s",
f"\rProgress: {downloaded_size / total_size:.2%} Speed: {speed:.2f} KB/s",
end="",
)
except (aiohttp.ClientConnectorSSLError, aiohttp.ClientConnectorCertificateError):
# 关闭SSL验证(仅在证书验证失败时作为fallback)
logger.warning(
"SSL 证书验证失败,已关闭 SSL 验证(不安全,仅用于临时下载)。请检查目标服务器的证书配置。"
f"SSL certificate verification failed for {url}. "
"Falling back to unverified connection (CERT_NONE). "
)
logger.warning(
f"SSL certificate verification failed for {url}. "
Expand All @@ -180,7 +183,7 @@ async def download_file(url: str, path: str, show_progress: bool = False) -> Non
downloaded_size = 0
start_time = time.time()
if show_progress:
print(f"文件大小: {total_size / 1024:.2f} KB | 文件地址: {url}")
print(f"Size: {total_size / 1024:.2f} KB | URL: {url}")
with open(path, "wb") as f:
while True:
chunk = await resp.content.read(8192)
Expand All @@ -192,7 +195,7 @@ async def download_file(url: str, path: str, show_progress: bool = False) -> Non
elapsed_time = time.time() - start_time
speed = downloaded_size / 1024 / elapsed_time # KB/s
print(
f"\r下载进度: {downloaded_size / total_size:.2%} 速度: {speed:.2f} KB/s",
f"\rProgress: {downloaded_size / total_size:.2%} Speed: {speed:.2f} KB/s",
end="",
)
if show_progress:
Expand Down Expand Up @@ -252,7 +255,7 @@ async def download_dashboard(
ver_name = "latest" if latest else version
dashboard_release_url = f"https://astrbot-registry.soulter.top/download/astrbot-dashboard/{ver_name}/dist.zip"
logger.info(
f"准备下载指定发行版本的 AstrBot WebUI 文件: {dashboard_release_url}",
f"Downloading AstrBot WebUI from {dashboard_release_url}",
)
try:
await download_file(
Expand All @@ -274,7 +277,7 @@ async def download_dashboard(
)
else:
url = f"https://github.com/AstrBotDevs/astrbot-release-harbour/releases/download/release-{version}/dist.zip"
logger.info(f"准备下载指定版本的 AstrBot WebUI: {url}")
logger.info(f"Downloading AstrBot WebUI from {url}")
if proxy:
url = f"{proxy}/{url}"
await download_file(url, str(zip_path), show_progress=True)
Expand Down
29 changes: 29 additions & 0 deletions astrbot/dashboard/routes/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,19 @@ async def flush_pending_bot_message():
refs = {}
return saved_record

def build_attachment_saved_event(part: dict | None) -> str | None:
if not part or not part.get("attachment_id") or not part.get("type"):
return None

payload = {
"type": "attachment_saved",
"data": {
"id": part["attachment_id"],
"type": part["type"],
},
}
return f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"

try:
# Emit session_id first so clients can bind the stream immediately.
session_info = {
Expand Down Expand Up @@ -908,25 +921,41 @@ async def flush_pending_bot_message():
filename, "image"
)
message_accumulator.add_attachment(part)
if attachment_saved_event := build_attachment_saved_event(
part
):
yield attachment_saved_event
elif msg_type == "record":
filename = result_text.replace("[RECORD]", "")
part = await self._create_attachment_from_file(
filename, "record"
)
message_accumulator.add_attachment(part)
if attachment_saved_event := build_attachment_saved_event(
part
):
yield attachment_saved_event
elif msg_type == "file":
# 格式: [FILE]filename
filename = result_text.replace("[FILE]", "")
part = await self._create_attachment_from_file(
filename, "file"
)
message_accumulator.add_attachment(part)
if attachment_saved_event := build_attachment_saved_event(
part
):
yield attachment_saved_event
elif msg_type == "video":
filename = result_text.replace("[VIDEO]", "")
part = await self._create_attachment_from_file(
filename, "video"
)
message_accumulator.add_attachment(part)
if attachment_saved_event := build_attachment_saved_event(
part
):
yield attachment_saved_event

should_save = False
if msg_type == "end":
Expand Down
20 changes: 20 additions & 0 deletions astrbot/dashboard/routes/live_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,22 @@ async def flush_pending_bot_message():

pending_bot_message_flusher = flush_pending_bot_message

async def send_attachment_saved_event(part: dict | None) -> None:
if not part or not part.get("attachment_id") or not part.get("type"):
return

await self._send_chat_payload(
session,
{
"ct": "chat",
"type": "attachment_saved",
"data": {
"id": part["attachment_id"],
"type": part["type"],
},
},
)

while True:
if session.should_interrupt:
session.should_interrupt = False
Expand Down Expand Up @@ -586,18 +602,22 @@ async def flush_pending_bot_message():
filename = str(result_text).replace("[IMAGE]", "")
part = await self._create_attachment_from_file(filename, "image")
message_accumulator.add_attachment(part)
await send_attachment_saved_event(part)
elif msg_type == "record":
filename = str(result_text).replace("[RECORD]", "")
part = await self._create_attachment_from_file(filename, "record")
message_accumulator.add_attachment(part)
await send_attachment_saved_event(part)
elif msg_type == "file":
filename = str(result_text).replace("[FILE]", "").split("|", 1)[0]
part = await self._create_attachment_from_file(filename, "file")
message_accumulator.add_attachment(part)
await send_attachment_saved_event(part)
elif msg_type == "video":
filename = str(result_text).replace("[VIDEO]", "").split("|", 1)[0]
part = await self._create_attachment_from_file(filename, "video")
message_accumulator.add_attachment(part)
await send_attachment_saved_event(part)

should_save = False
if msg_type == "end":
Expand Down
Loading
Loading