mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-10-12 23:47:22 +08:00
fix(dashboard): handle missing static assets gracefully and fix async/ruff issues
- Dashboard: Catch missing index.html error, log warning, and disable WebUI instead of crashing. - Dashboard: Use anyio.Path for async file existence checks in SSL config. - CLI/Backup: Replace blocking file operations with anyio async operations. - Core: Replace blocking file operations with anyio in Coze/Dify clients and TTS simulation. - Provider: Rename 'timeout' param to 'init_timeout'/'request_timeout' to fix ASYNC109 warnings. - Ruff: Fix various ASYNC230/ASYNC240 errors across multiple files.
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import mimetypes
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import anyio
|
||||
|
||||
import runtime_bootstrap
|
||||
from astrbot.core import LogBroker, LogManager, db_helper, logger
|
||||
from astrbot.core.config.default import VERSION
|
||||
from astrbot.core.initial_loader import InitialLoader
|
||||
from astrbot.core.utils.astrbot_path import (
|
||||
get_astrbot_config_path,
|
||||
get_astrbot_data_path,
|
||||
get_astrbot_knowledge_base_path,
|
||||
get_astrbot_plugin_path,
|
||||
get_astrbot_root,
|
||||
get_astrbot_site_packages_path,
|
||||
get_astrbot_temp_path,
|
||||
)
|
||||
from astrbot.core.utils.io import (
|
||||
download_dashboard,
|
||||
get_dashboard_version,
|
||||
)
|
||||
|
||||
runtime_bootstrap.initialize_runtime_bootstrap()
|
||||
|
||||
|
||||
# 将父目录添加到 sys.path
|
||||
sys.path.append(Path(__file__).parent.as_posix())
|
||||
|
||||
logo_tmpl = r"""
|
||||
___ _______.___________..______ .______ ______ .___________.
|
||||
/ \ / | || _ \ | _ \ / __ \ | |
|
||||
/ ^ \ | (----`---| |----`| |_) | | |_) | | | | | `---| |----`
|
||||
/ /_\ \ \ \ | | | / | _ < | | | | | |
|
||||
/ _____ \ .----) | | | | |\ \----.| |_) | | `--' | | |
|
||||
/__/ \__\ |_______/ |__| | _| `._____||______/ \______/ |__|
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def check_env() -> None:
|
||||
if not (sys.version_info.major == 3 and sys.version_info.minor >= 10):
|
||||
logger.error("请使用 Python3.10+ 运行本项目。")
|
||||
exit()
|
||||
|
||||
astrbot_root = get_astrbot_root()
|
||||
if astrbot_root not in sys.path:
|
||||
sys.path.insert(0, astrbot_root)
|
||||
|
||||
site_packages_path = get_astrbot_site_packages_path()
|
||||
if site_packages_path not in sys.path:
|
||||
sys.path.insert(0, site_packages_path)
|
||||
|
||||
os.makedirs(get_astrbot_config_path(), exist_ok=True)
|
||||
os.makedirs(get_astrbot_plugin_path(), exist_ok=True)
|
||||
os.makedirs(get_astrbot_temp_path(), exist_ok=True)
|
||||
os.makedirs(get_astrbot_knowledge_base_path(), exist_ok=True)
|
||||
os.makedirs(site_packages_path, exist_ok=True)
|
||||
|
||||
# 针对问题 #181 的临时解决方案
|
||||
mimetypes.add_type("text/javascript", ".js")
|
||||
mimetypes.add_type("text/javascript", ".mjs")
|
||||
mimetypes.add_type("application/json", ".json")
|
||||
|
||||
|
||||
async def check_dashboard_files(webui_dir: str | None = None):
|
||||
"""下载管理面板文件"""
|
||||
# 指定webui目录
|
||||
if webui_dir:
|
||||
if await anyio.Path(webui_dir).exists():
|
||||
logger.info(f"使用指定的 WebUI 目录: {webui_dir}")
|
||||
return webui_dir
|
||||
logger.warning(f"指定的 WebUI 目录 {webui_dir} 不存在,将使用默认逻辑。")
|
||||
|
||||
data_dist_path = os.path.join(get_astrbot_data_path(), "dist")
|
||||
if await anyio.Path(data_dist_path).exists():
|
||||
v = await get_dashboard_version()
|
||||
if v is not None:
|
||||
# 存在文件
|
||||
if v == f"v{VERSION}":
|
||||
logger.info("WebUI 版本已是最新。")
|
||||
else:
|
||||
logger.warning(
|
||||
f"检测到 WebUI 版本 ({v}) 与当前 AstrBot 版本 (v{VERSION}) 不符。",
|
||||
)
|
||||
return data_dist_path
|
||||
|
||||
logger.info(
|
||||
"开始下载管理面板文件...高峰期(晚上)可能导致较慢的速度。如多次下载失败,请前往 https://github.com/AstrBotDevs/AstrBot/releases/latest 下载 dist.zip,并将其中的 dist 文件夹解压至 data 目录下。",
|
||||
)
|
||||
|
||||
try:
|
||||
await download_dashboard(version=f"v{VERSION}", latest=False)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"下载指定版本(v{VERSION})的管理面板文件失败: {e},尝试下载最新版本。"
|
||||
)
|
||||
try:
|
||||
await download_dashboard(latest=True)
|
||||
except Exception as e:
|
||||
logger.critical(f"下载管理面板文件失败: {e}。")
|
||||
return None
|
||||
|
||||
logger.info("管理面板下载完成。")
|
||||
return data_dist_path
|
||||
|
||||
|
||||
async def main_async(webui_dir_arg: str | None) -> None:
|
||||
"""主异步入口"""
|
||||
# 检查仪表板文件
|
||||
webui_dir = await check_dashboard_files(webui_dir_arg)
|
||||
if webui_dir is None:
|
||||
logger.warning(
|
||||
"管理面板文件检查失败,WebUI 功能将不可用。"
|
||||
"请检查网络连接或手动指定 --webui-dir 参数。"
|
||||
)
|
||||
|
||||
db = db_helper
|
||||
|
||||
# 打印 logo
|
||||
logger.info(logo_tmpl)
|
||||
|
||||
core_lifecycle = InitialLoader(db, log_broker)
|
||||
core_lifecycle.webui_dir = webui_dir
|
||||
await core_lifecycle.start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="AstrBot")
|
||||
parser.add_argument(
|
||||
"--webui-dir",
|
||||
type=str,
|
||||
help="指定 WebUI 静态文件目录路径",
|
||||
default=None,
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
check_env()
|
||||
|
||||
# 启动日志代理
|
||||
log_broker = LogBroker()
|
||||
LogManager.set_queue_handler(logger, log_broker)
|
||||
|
||||
# 只使用一次 asyncio.run()
|
||||
asyncio.run(main_async(args.webui_dir))
|
||||
@@ -4,6 +4,7 @@ import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import anyio
|
||||
import click
|
||||
|
||||
from astrbot.core import astrbot_config, db_helper
|
||||
@@ -188,7 +189,7 @@ def export_data(
|
||||
raise subprocess.CalledProcessError(process.returncode or 1, cmd)
|
||||
|
||||
# Clean up original file
|
||||
final_path.unlink()
|
||||
await anyio.Path(final_path).unlink()
|
||||
final_path = gpg_output
|
||||
click.echo(
|
||||
click.style(f"Processed backup created: {final_path}", fg="green")
|
||||
@@ -199,13 +200,13 @@ def export_data(
|
||||
click.echo(f"Calculating {digest} digest...")
|
||||
hash_func = getattr(hashlib, digest)()
|
||||
# Read file in chunks
|
||||
with open(final_path, "rb") as f:
|
||||
while chunk := f.read(8192):
|
||||
async with await anyio.open_file(final_path, "rb") as f:
|
||||
while chunk := await f.read(8192):
|
||||
hash_func.update(chunk)
|
||||
|
||||
digest_val = hash_func.hexdigest()
|
||||
digest_file = final_path.with_name(final_path.name + f".{digest}")
|
||||
digest_file.write_text(
|
||||
await anyio.Path(digest_file).write_text(
|
||||
f"{digest_val} *{final_path.name}\n", encoding="utf-8"
|
||||
)
|
||||
click.echo(click.style(f"Digest generated: {digest_file}", fg="green"))
|
||||
@@ -369,8 +370,8 @@ def import_data_command(backup_file: str, yes: bool):
|
||||
click.echo(f" - {warn}")
|
||||
|
||||
finally:
|
||||
if is_temp_file and zip_path.exists():
|
||||
zip_path.unlink()
|
||||
if is_temp_file and await anyio.Path(zip_path).exists():
|
||||
await anyio.Path(zip_path).unlink()
|
||||
click.echo(f"Cleaned up temporary file: {zip_path}")
|
||||
|
||||
asyncio.run(_run())
|
||||
|
||||
@@ -288,14 +288,16 @@ if __name__ == "__main__":
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
import anyio
|
||||
|
||||
async def test_coze_api_client() -> None:
|
||||
api_key = os.getenv("COZE_API_KEY", "")
|
||||
bot_id = os.getenv("COZE_BOT_ID", "")
|
||||
client = CozeAPIClient(api_key=api_key)
|
||||
|
||||
try:
|
||||
with open("README.md", "rb") as f:
|
||||
file_data = f.read()
|
||||
async with await anyio.open_file("README.md", "rb") as f:
|
||||
file_data = await f.read()
|
||||
file_id = await client.upload_file(file_data)
|
||||
print(f"Uploaded file_id: {file_id}")
|
||||
async for event in client.chat_messages(
|
||||
|
||||
@@ -176,7 +176,7 @@ class DifyAgentRunner(BaseAgentRunner[TContext]):
|
||||
user=session_id,
|
||||
conversation_id=conversation_id,
|
||||
files=files_payload,
|
||||
timeout=self.timeout,
|
||||
request_timeout=self.timeout,
|
||||
):
|
||||
logger.debug(f"dify resp chunk: {chunk}")
|
||||
if chunk["event"] == "message" or chunk["event"] == "agent_message":
|
||||
@@ -216,7 +216,7 @@ class DifyAgentRunner(BaseAgentRunner[TContext]):
|
||||
},
|
||||
user=session_id,
|
||||
files=files_payload,
|
||||
timeout=self.timeout,
|
||||
request_timeout=self.timeout,
|
||||
):
|
||||
logger.debug(f"dify workflow resp chunk: {chunk}")
|
||||
match chunk["event"]:
|
||||
|
||||
@@ -3,6 +3,7 @@ import json
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
import anyio
|
||||
from aiohttp import ClientResponse, ClientSession, FormData
|
||||
|
||||
from astrbot.core import logger
|
||||
@@ -47,20 +48,20 @@ class DifyAPIClient:
|
||||
response_mode: str = "streaming",
|
||||
conversation_id: str = "",
|
||||
files: list[dict[str, Any]] | None = None,
|
||||
timeout: float = 60,
|
||||
request_timeout: float = 60,
|
||||
) -> AsyncGenerator[dict[str, Any], None]:
|
||||
if files is None:
|
||||
files = []
|
||||
url = f"{self.api_base}/chat-messages"
|
||||
payload = locals()
|
||||
payload.pop("self")
|
||||
payload.pop("timeout")
|
||||
payload.pop("request_timeout")
|
||||
logger.info(f"chat_messages payload: {payload}")
|
||||
async with self.session.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers=self.headers,
|
||||
timeout=timeout,
|
||||
timeout=request_timeout,
|
||||
) as resp:
|
||||
if resp.status != 200:
|
||||
text = await resp.text()
|
||||
@@ -76,20 +77,20 @@ class DifyAPIClient:
|
||||
user: str,
|
||||
response_mode: str = "streaming",
|
||||
files: list[dict[str, Any]] | None = None,
|
||||
timeout: float = 60,
|
||||
request_timeout: float = 60,
|
||||
):
|
||||
if files is None:
|
||||
files = []
|
||||
url = f"{self.api_base}/workflows/run"
|
||||
payload = locals()
|
||||
payload.pop("self")
|
||||
payload.pop("timeout")
|
||||
payload.pop("request_timeout")
|
||||
logger.info(f"workflow_run payload: {payload}")
|
||||
async with self.session.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers=self.headers,
|
||||
timeout=timeout,
|
||||
timeout=request_timeout,
|
||||
) as resp:
|
||||
if resp.status != 200:
|
||||
text = await resp.text()
|
||||
@@ -134,8 +135,8 @@ class DifyAPIClient:
|
||||
# 使用文件路径
|
||||
import os
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
file_content = f.read()
|
||||
async with await anyio.open_file(file_path, "rb") as f:
|
||||
file_content = await f.read()
|
||||
form.add_field(
|
||||
"file",
|
||||
file_content,
|
||||
|
||||
@@ -4,6 +4,8 @@ import time
|
||||
import traceback
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
import anyio
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.agent.message import Message
|
||||
from astrbot.core.agent.runners.tool_loop_agent_runner import ToolLoopAgentRunner
|
||||
@@ -529,8 +531,8 @@ async def _simulated_stream_tts(
|
||||
audio_path = await tts_provider.get_audio(text)
|
||||
|
||||
if audio_path:
|
||||
with open(audio_path, "rb") as f:
|
||||
audio_data = f.read()
|
||||
async with await anyio.open_file(audio_path, "rb") as f:
|
||||
audio_data = await f.read()
|
||||
await audio_queue.put((text, audio_data))
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
|
||||
@@ -12,6 +12,7 @@ from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import anyio
|
||||
from sqlalchemy import select
|
||||
|
||||
from astrbot.core import logger
|
||||
@@ -83,7 +84,7 @@ class AstrBotExporter:
|
||||
output_dir = get_astrbot_backups_path()
|
||||
|
||||
# 确保输出目录存在
|
||||
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
||||
await anyio.Path(output_dir).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
zip_filename = f"astrbot_backup_{timestamp}.zip"
|
||||
@@ -160,9 +161,11 @@ class AstrBotExporter:
|
||||
# 3. 导出配置文件
|
||||
if progress_callback:
|
||||
await progress_callback("config", 0, 100, "正在导出配置文件...")
|
||||
if os.path.exists(self.config_path):
|
||||
with open(self.config_path, encoding="utf-8") as f:
|
||||
config_content = f.read()
|
||||
if await anyio.Path(self.config_path).exists():
|
||||
async with await anyio.open_file(
|
||||
self.config_path, encoding="utf-8"
|
||||
) as f:
|
||||
config_content = await f.read()
|
||||
zf.writestr("config/cmd_config.json", config_content)
|
||||
self._add_checksum("config/cmd_config.json", config_content)
|
||||
if progress_callback:
|
||||
@@ -199,8 +202,8 @@ class AstrBotExporter:
|
||||
except Exception as e:
|
||||
logger.error(f"备份导出失败: {e}")
|
||||
# 清理失败的文件
|
||||
if os.path.exists(zip_path):
|
||||
os.remove(zip_path)
|
||||
if await anyio.Path(zip_path).exists():
|
||||
await anyio.Path(zip_path).unlink()
|
||||
raise
|
||||
|
||||
async def _export_main_database(self) -> dict[str, list[dict]]:
|
||||
@@ -317,7 +320,7 @@ class AstrBotExporter:
|
||||
|
||||
for dir_name, dir_path in backup_directories.items():
|
||||
full_path = Path(dir_path)
|
||||
if not full_path.exists():
|
||||
if not await anyio.Path(full_path).exists():
|
||||
logger.debug(f"目录不存在,跳过: {full_path}")
|
||||
continue
|
||||
|
||||
@@ -362,7 +365,7 @@ class AstrBotExporter:
|
||||
for attachment in attachments:
|
||||
try:
|
||||
file_path = attachment.get("path", "")
|
||||
if file_path and os.path.exists(file_path):
|
||||
if file_path and await anyio.Path(file_path).exists():
|
||||
# 使用 attachment_id 作为文件名
|
||||
attachment_id = attachment.get("attachment_id", "")
|
||||
ext = os.path.splitext(file_path)[1]
|
||||
|
||||
@@ -388,22 +388,24 @@ async def _sync_skills_to_sandbox(booter: ComputerBooter) -> None:
|
||||
Backward-compatible orchestrator: keep historical behavior while internally
|
||||
splitting into `apply` and `scan` phases.
|
||||
"""
|
||||
skills_root = Path(get_astrbot_skills_path())
|
||||
if not skills_root.is_dir():
|
||||
import anyio
|
||||
|
||||
skills_root = anyio.Path(get_astrbot_skills_path())
|
||||
if not await skills_root.is_dir():
|
||||
return
|
||||
local_skill_dirs = _list_local_skill_dirs(skills_root)
|
||||
|
||||
temp_dir = Path(get_astrbot_temp_path())
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
temp_dir = anyio.Path(get_astrbot_temp_path())
|
||||
await temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
zip_base = temp_dir / "skills_bundle"
|
||||
zip_path = zip_base.with_suffix(".zip")
|
||||
|
||||
try:
|
||||
if local_skill_dirs:
|
||||
if zip_path.exists():
|
||||
zip_path.unlink()
|
||||
if await zip_path.exists():
|
||||
await zip_path.unlink()
|
||||
shutil.make_archive(str(zip_base), "zip", str(skills_root))
|
||||
remote_zip = Path(SANDBOX_SKILLS_ROOT) / "skills.zip"
|
||||
remote_zip = anyio.Path(SANDBOX_SKILLS_ROOT) / "skills.zip"
|
||||
logger.info("[Computer] sandbox_sync phase=upload status=start")
|
||||
await booter.shell.exec(f"mkdir -p {SANDBOX_SKILLS_ROOT}")
|
||||
upload_result = await booter.upload_file(str(zip_path), str(remote_zip))
|
||||
|
||||
@@ -2,6 +2,8 @@ import os
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import anyio
|
||||
|
||||
from astrbot.api import FunctionTool, logger
|
||||
from astrbot.api.event import MessageChain
|
||||
from astrbot.core.agent.run_context import ContextWrapper
|
||||
@@ -111,10 +113,11 @@ class FileUploadTool(FunctionTool):
|
||||
)
|
||||
try:
|
||||
# Check if file exists
|
||||
if not os.path.exists(local_path):
|
||||
local_path_obj = anyio.Path(local_path)
|
||||
if not await local_path_obj.exists():
|
||||
return f"Error: File does not exist: {local_path}"
|
||||
|
||||
if not os.path.isfile(local_path):
|
||||
if not await local_path_obj.is_file():
|
||||
return f"Error: Path is not a file: {local_path}"
|
||||
|
||||
# Use basename if sandbox_filename is not provided
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import os
|
||||
|
||||
import anyio
|
||||
|
||||
from astrbot.api import logger, sp
|
||||
from astrbot.core.config import AstrBotConfig
|
||||
from astrbot.core.db import BaseDatabase
|
||||
@@ -22,7 +24,7 @@ async def check_migration_needed_v4(db_helper: BaseDatabase) -> bool:
|
||||
data_dir = get_astrbot_data_path()
|
||||
data_v3_db = os.path.join(data_dir, "data_v3.db")
|
||||
|
||||
if not os.path.exists(data_v3_db):
|
||||
if not await anyio.Path(data_v3_db).exists():
|
||||
return False
|
||||
migration_done = await db_helper.get_preference(
|
||||
"global",
|
||||
|
||||
@@ -6,6 +6,7 @@ from pathlib import Path
|
||||
from typing import Literal, NoReturn, cast
|
||||
|
||||
import aiohttp
|
||||
import anyio
|
||||
import dingtalk_stream
|
||||
from dingtalk_stream import AckMessage
|
||||
|
||||
@@ -334,8 +335,8 @@ class DingtalkPlatformAdapter(Platform):
|
||||
"downloadCode": download_code,
|
||||
"robotCode": robot_code,
|
||||
}
|
||||
temp_dir = Path(get_astrbot_temp_path())
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
temp_dir = anyio.Path(get_astrbot_temp_path())
|
||||
await temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
f_path = temp_dir / f"dingtalk_{uuid.uuid4()}.{ext}"
|
||||
async with (
|
||||
aiohttp.ClientSession() as session,
|
||||
@@ -494,7 +495,7 @@ class DingtalkPlatformAdapter(Platform):
|
||||
return converted, converted != input_path
|
||||
|
||||
async def upload_media(self, file_path: str, media_type: str) -> str:
|
||||
media_file_path = Path(file_path)
|
||||
media_file_path = anyio.Path(file_path)
|
||||
access_token = await self.get_access_token()
|
||||
if not access_token:
|
||||
logger.error("钉钉媒体上传失败: access_token 为空")
|
||||
@@ -503,7 +504,7 @@ class DingtalkPlatformAdapter(Platform):
|
||||
form = aiohttp.FormData()
|
||||
form.add_field(
|
||||
"media",
|
||||
media_file_path.read_bytes(),
|
||||
await media_file_path.read_bytes(),
|
||||
filename=media_file_path.name,
|
||||
content_type="application/octet-stream",
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ from pathlib import Path
|
||||
from typing import Any, cast
|
||||
from uuid import uuid4
|
||||
|
||||
import anyio
|
||||
import lark_oapi as lark
|
||||
from lark_oapi.api.im.v1 import (
|
||||
GetMessageRequest,
|
||||
@@ -428,13 +429,13 @@ class LarkPlatformAdapter(Platform):
|
||||
return None
|
||||
|
||||
suffix = Path(file_name).suffix if file_name else default_suffix
|
||||
temp_dir = Path(get_astrbot_temp_path())
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
temp_dir = anyio.Path(get_astrbot_temp_path())
|
||||
await temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
temp_path = (
|
||||
temp_dir / f"lark_{message_type}_{file_name}_{uuid4().hex[:4]}{suffix}"
|
||||
)
|
||||
temp_path.write_bytes(file_bytes)
|
||||
return str(temp_path.resolve())
|
||||
await temp_path.write_bytes(file_bytes)
|
||||
return str(await temp_path.resolve())
|
||||
|
||||
def _clean_expired_events(self) -> None:
|
||||
"""清理超过 30 分钟的事件记录"""
|
||||
|
||||
@@ -5,6 +5,8 @@ import os
|
||||
import uuid
|
||||
from io import BytesIO
|
||||
|
||||
import aiofiles
|
||||
import anyio
|
||||
import lark_oapi as lark
|
||||
from lark_oapi.api.cardkit.v1 import (
|
||||
ContentCardElementRequest,
|
||||
@@ -28,8 +30,16 @@ from lark_oapi.api.im.v1 import (
|
||||
|
||||
from astrbot import logger
|
||||
from astrbot.api.event import AstrMessageEvent, MessageChain
|
||||
from astrbot.api.message_components import At, File, Plain, Record, Video
|
||||
from astrbot.api.message_components import Image as AstrBotImage
|
||||
from astrbot.api.message_components import (
|
||||
At,
|
||||
File,
|
||||
Plain,
|
||||
Record,
|
||||
Video,
|
||||
)
|
||||
from astrbot.api.message_components import (
|
||||
Image as AstrBotImage,
|
||||
)
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
from astrbot.core.utils.io import download_image_by_url
|
||||
from astrbot.core.utils.media_utils import (
|
||||
@@ -146,7 +156,8 @@ class LarkMessageEvent(AstrMessageEvent):
|
||||
Returns:
|
||||
成功返回file_key,失败返回None
|
||||
"""
|
||||
if not path or not os.path.exists(path):
|
||||
path_obj = anyio.Path(path)
|
||||
if not path or not await path_obj.exists():
|
||||
logger.error(f"[Lark] 文件不存在: {path}")
|
||||
return None
|
||||
|
||||
@@ -155,7 +166,7 @@ class LarkMessageEvent(AstrMessageEvent):
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(path, "rb") as file_obj:
|
||||
async with aiofiles.open(path, "rb") as file_obj:
|
||||
body_builder = (
|
||||
CreateFileRequestBody.builder()
|
||||
.file_type(file_type)
|
||||
@@ -217,8 +228,8 @@ class LarkMessageEvent(AstrMessageEvent):
|
||||
temp_dir,
|
||||
f"lark_image_{uuid.uuid4().hex[:8]}.jpg",
|
||||
)
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(BytesIO(image_data).getvalue())
|
||||
async with aiofiles.open(file_path, "wb") as f:
|
||||
await f.write(BytesIO(image_data).getvalue())
|
||||
else:
|
||||
file_path = comp.file if comp.file else ""
|
||||
|
||||
@@ -227,7 +238,9 @@ class LarkMessageEvent(AstrMessageEvent):
|
||||
logger.error("[Lark] 图片路径为空,无法上传")
|
||||
continue
|
||||
try:
|
||||
image_file = open(file_path, "rb")
|
||||
image_file = await anyio.to_thread.run_sync(
|
||||
open, file_path, "rb"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[Lark] 无法打开图片文件: {e}")
|
||||
continue
|
||||
@@ -422,7 +435,10 @@ class LarkMessageEvent(AstrMessageEvent):
|
||||
logger.error(f"[Lark] 无法获取音频文件路径: {e}")
|
||||
return
|
||||
|
||||
if not original_audio_path or not os.path.exists(original_audio_path):
|
||||
if (
|
||||
not original_audio_path
|
||||
or not await anyio.Path(original_audio_path).exists()
|
||||
):
|
||||
logger.error(f"[Lark] 音频文件不存在: {original_audio_path}")
|
||||
return
|
||||
|
||||
@@ -452,9 +468,9 @@ class LarkMessageEvent(AstrMessageEvent):
|
||||
)
|
||||
|
||||
# 清理转换后的临时音频文件
|
||||
if converted_audio_path and os.path.exists(converted_audio_path):
|
||||
if converted_audio_path and await anyio.Path(converted_audio_path).exists():
|
||||
try:
|
||||
os.remove(converted_audio_path)
|
||||
await anyio.Path(converted_audio_path).unlink()
|
||||
logger.debug(f"[Lark] 已删除转换后的音频文件: {converted_audio_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"[Lark] 删除转换后的音频文件失败: {e}")
|
||||
@@ -495,7 +511,10 @@ class LarkMessageEvent(AstrMessageEvent):
|
||||
logger.error(f"[Lark] 无法获取视频文件路径: {e}")
|
||||
return
|
||||
|
||||
if not original_video_path or not os.path.exists(original_video_path):
|
||||
if (
|
||||
not original_video_path
|
||||
or not await anyio.Path(original_video_path).exists()
|
||||
):
|
||||
logger.error(f"[Lark] 视频文件不存在: {original_video_path}")
|
||||
return
|
||||
|
||||
@@ -525,9 +544,9 @@ class LarkMessageEvent(AstrMessageEvent):
|
||||
)
|
||||
|
||||
# 清理转换后的临时视频文件
|
||||
if converted_video_path and os.path.exists(converted_video_path):
|
||||
if converted_video_path and await anyio.Path(converted_video_path).exists():
|
||||
try:
|
||||
os.remove(converted_video_path)
|
||||
await anyio.Path(converted_video_path).unlink()
|
||||
logger.debug(f"[Lark] 已删除转换后的视频文件: {converted_video_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"[Lark] 删除转换后的视频文件失败: {e}")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import asyncio
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from collections.abc import AsyncGenerator
|
||||
from pathlib import Path
|
||||
|
||||
import anyio
|
||||
|
||||
from astrbot.api import logger
|
||||
from astrbot.api.event import AstrMessageEvent, MessageChain
|
||||
@@ -160,8 +160,8 @@ class LineMessageEvent(AstrMessageEvent):
|
||||
|
||||
try:
|
||||
video_path = await segment.convert_to_file_path()
|
||||
temp_dir = Path(get_astrbot_temp_path())
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
temp_dir = anyio.Path(get_astrbot_temp_path())
|
||||
await temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
thumb_path = temp_dir / f"line_video_preview_{uuid.uuid4().hex}.jpg"
|
||||
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
@@ -178,7 +178,7 @@ class LineMessageEvent(AstrMessageEvent):
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
await process.communicate()
|
||||
if process.returncode != 0 or not thumb_path.exists():
|
||||
if process.returncode != 0 or not await thumb_path.exists():
|
||||
return ""
|
||||
|
||||
cover_seg = Image.fromFileSystem(str(thumb_path))
|
||||
@@ -201,8 +201,8 @@ class LineMessageEvent(AstrMessageEvent):
|
||||
async def _resolve_file_size(segment: File) -> int:
|
||||
try:
|
||||
file_path = await segment.get_file(allow_return_url=False)
|
||||
if file_path and os.path.exists(file_path):
|
||||
return int(os.path.getsize(file_path))
|
||||
if file_path and await anyio.Path(file_path).exists():
|
||||
return int((await anyio.Path(file_path).stat()).st_size)
|
||||
except Exception as e:
|
||||
logger.debug("[LINE] resolve file size failed: %s", e)
|
||||
return 0
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import asyncio
|
||||
import os
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
import anyio
|
||||
|
||||
import astrbot.api.message_components as Comp
|
||||
from astrbot.api import logger
|
||||
from astrbot.api.event import MessageChain
|
||||
@@ -499,11 +500,12 @@ class MisskeyPlatformAdapter(Platform):
|
||||
# 清理临时文件
|
||||
if local_path and isinstance(local_path, str):
|
||||
data_temp = get_astrbot_temp_path()
|
||||
if local_path.startswith(data_temp) and os.path.exists(
|
||||
local_path,
|
||||
if (
|
||||
local_path.startswith(data_temp)
|
||||
and await anyio.Path(local_path).exists()
|
||||
):
|
||||
try:
|
||||
os.remove(local_path)
|
||||
await anyio.Path(local_path).unlink()
|
||||
logger.debug(f"[Misskey] 已清理临时文件: {local_path}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -5,6 +5,8 @@ import uuid
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any, NoReturn
|
||||
|
||||
import anyio
|
||||
|
||||
try:
|
||||
import aiohttp
|
||||
import websockets
|
||||
@@ -555,7 +557,7 @@ class MisskeyAPI:
|
||||
form.add_field("folderId", str(folder_id))
|
||||
|
||||
try:
|
||||
f = open(file_path, "rb")
|
||||
f = await anyio.to_thread.run_sync(open, file_path, "rb")
|
||||
except FileNotFoundError as e:
|
||||
logger.error(f"[Misskey API] 本地文件不存在: {file_path}")
|
||||
raise APIError(f"File not found: {file_path}") from e
|
||||
|
||||
@@ -6,6 +6,7 @@ import uuid
|
||||
from typing import cast
|
||||
|
||||
import aiofiles
|
||||
import anyio
|
||||
import botpy
|
||||
import botpy.errors
|
||||
import botpy.message
|
||||
@@ -495,7 +496,8 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
payload["file_name"] = file_name
|
||||
|
||||
# 处理文件数据
|
||||
if os.path.exists(file_source):
|
||||
file_source_obj = anyio.Path(file_source)
|
||||
if await file_source_obj.exists():
|
||||
# 读取本地文件
|
||||
async with aiofiles.open(file_source, "rb") as f:
|
||||
file_content = await f.read()
|
||||
|
||||
@@ -6,6 +6,8 @@ from collections.abc import Awaitable, Callable, Sequence
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import anyio
|
||||
|
||||
from astrbot.core.db.po import Attachment
|
||||
from astrbot.core.message.components import (
|
||||
File,
|
||||
@@ -339,7 +341,11 @@ async def create_attachment_part_from_existing_file(
|
||||
candidate_paths = [Path(attachments_dir) / basename]
|
||||
candidate_paths.extend(Path(p) / basename for p in fallback_dirs)
|
||||
|
||||
file_path = next((path for path in candidate_paths if path.exists()), None)
|
||||
file_path = None
|
||||
for path in candidate_paths:
|
||||
if await anyio.Path(path).exists():
|
||||
file_path = path
|
||||
break
|
||||
if not file_path:
|
||||
return None
|
||||
|
||||
@@ -365,8 +371,8 @@ async def message_chain_to_storage_message_parts(
|
||||
insert_attachment: AttachmentInserter,
|
||||
attachments_dir: str | Path,
|
||||
) -> list[dict]:
|
||||
target_dir = Path(attachments_dir)
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
target_dir = anyio.Path(attachments_dir)
|
||||
await target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
parts: list[dict] = []
|
||||
for comp in message_chain.chain:
|
||||
@@ -441,13 +447,13 @@ async def _copy_file_to_attachment_part(
|
||||
attachments_dir: Path,
|
||||
display_name: str | None = None,
|
||||
) -> dict | None:
|
||||
src_path = Path(file_path)
|
||||
if not src_path.exists() or not src_path.is_file():
|
||||
src_path = anyio.Path(file_path)
|
||||
if not await src_path.exists() or not await src_path.is_file():
|
||||
return None
|
||||
|
||||
suffix = src_path.suffix
|
||||
target_path = attachments_dir / f"{uuid.uuid4().hex}{suffix}"
|
||||
shutil.copy2(src_path, target_path)
|
||||
shutil.copy2(str(src_path), str(target_path))
|
||||
|
||||
mime_type, _ = mimetypes.guess_type(target_path.name)
|
||||
attachment = await insert_attachment(
|
||||
|
||||
@@ -4,6 +4,8 @@ import os
|
||||
import shutil
|
||||
import uuid
|
||||
|
||||
import aiofiles
|
||||
|
||||
from astrbot.api import logger
|
||||
from astrbot.api.event import AstrMessageEvent, MessageChain
|
||||
from astrbot.api.message_components import File, Image, Json, Plain, Record
|
||||
@@ -80,8 +82,8 @@ class WebChatMessageEvent(AstrMessageEvent):
|
||||
filename = f"{str(uuid.uuid4())}.jpg"
|
||||
path = os.path.join(attachments_dir, filename)
|
||||
image_base64 = await comp.convert_to_base64()
|
||||
with open(path, "wb") as f:
|
||||
f.write(base64.b64decode(image_base64))
|
||||
async with aiofiles.open(path, "wb") as f:
|
||||
await f.write(base64.b64decode(image_base64))
|
||||
data = f"[IMAGE]{filename}"
|
||||
await web_chat_back_queue.put(
|
||||
{
|
||||
@@ -96,8 +98,8 @@ class WebChatMessageEvent(AstrMessageEvent):
|
||||
filename = f"{str(uuid.uuid4())}.wav"
|
||||
path = os.path.join(attachments_dir, filename)
|
||||
record_base64 = await comp.convert_to_base64()
|
||||
with open(path, "wb") as f:
|
||||
f.write(base64.b64decode(record_base64))
|
||||
async with aiofiles.open(path, "wb") as f:
|
||||
await f.write(base64.b64decode(record_base64))
|
||||
data = f"[RECORD]{filename}"
|
||||
await web_chat_back_queue.put(
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ import uuid
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any, cast
|
||||
|
||||
import aiofiles
|
||||
import quart
|
||||
from requests import Response
|
||||
from wechatpy.enterprise import WeChatClient, parse_message
|
||||
@@ -346,8 +347,8 @@ class WecomPlatformAdapter(Platform):
|
||||
)
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
path = os.path.join(temp_dir, f"wecom_{msg.media_id}.amr")
|
||||
with open(path, "wb") as f:
|
||||
f.write(resp.content)
|
||||
async with aiofiles.open(path, "wb") as f:
|
||||
await f.write(resp.content)
|
||||
|
||||
try:
|
||||
path_wav = os.path.join(temp_dir, f"wecom_{msg.media_id}.wav")
|
||||
@@ -402,8 +403,8 @@ class WecomPlatformAdapter(Platform):
|
||||
)
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
path = os.path.join(temp_dir, f"weixinkefu_{media_id}.jpg")
|
||||
with open(path, "wb") as f:
|
||||
f.write(resp.content)
|
||||
async with aiofiles.open(path, "wb") as f:
|
||||
await f.write(resp.content)
|
||||
abm.message = [Image(file=path, url=path)]
|
||||
elif msgtype == "voice":
|
||||
media_id = msg.get("voice", {}).get("media_id", "")
|
||||
@@ -415,8 +416,8 @@ class WecomPlatformAdapter(Platform):
|
||||
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
path = os.path.join(temp_dir, f"weixinkefu_{media_id}.amr")
|
||||
with open(path, "wb") as f:
|
||||
f.write(resp.content)
|
||||
async with aiofiles.open(path, "wb") as f:
|
||||
await f.write(resp.content)
|
||||
|
||||
try:
|
||||
path_wav = os.path.join(temp_dir, f"weixinkefu_{media_id}.wav")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
import aiofiles
|
||||
import anyio
|
||||
from wechatpy.enterprise import WeChatClient
|
||||
|
||||
from astrbot.api import logger
|
||||
@@ -100,7 +101,7 @@ class WecomPlatformEvent(AstrMessageEvent):
|
||||
elif isinstance(comp, Image):
|
||||
img_path = await comp.convert_to_file_path()
|
||||
|
||||
with open(img_path, "rb") as f:
|
||||
async with aiofiles.open(img_path, "rb") as f:
|
||||
try:
|
||||
response = self.client.media.upload("image", f)
|
||||
except Exception as e:
|
||||
@@ -120,7 +121,7 @@ class WecomPlatformEvent(AstrMessageEvent):
|
||||
record_path_amr = await convert_audio_to_amr(record_path)
|
||||
|
||||
try:
|
||||
with open(record_path_amr, "rb") as f:
|
||||
async with aiofiles.open(record_path_amr, "rb") as f:
|
||||
try:
|
||||
response = self.client.media.upload("voice", f)
|
||||
except Exception as e:
|
||||
@@ -138,17 +139,18 @@ class WecomPlatformEvent(AstrMessageEvent):
|
||||
response["media_id"],
|
||||
)
|
||||
finally:
|
||||
if record_path_amr != record_path and os.path.exists(
|
||||
record_path_amr,
|
||||
if (
|
||||
record_path_amr != record_path
|
||||
and await anyio.Path(record_path_amr).exists()
|
||||
):
|
||||
try:
|
||||
os.remove(record_path_amr)
|
||||
await anyio.Path(record_path_amr).unlink()
|
||||
except OSError as e:
|
||||
logger.warning(f"删除临时音频文件失败: {e}")
|
||||
elif isinstance(comp, File):
|
||||
file_path = await comp.get_file()
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
async with aiofiles.open(file_path, "rb") as f:
|
||||
try:
|
||||
response = self.client.media.upload("file", f)
|
||||
except Exception as e:
|
||||
@@ -166,7 +168,7 @@ class WecomPlatformEvent(AstrMessageEvent):
|
||||
elif isinstance(comp, Video):
|
||||
video_path = await comp.convert_to_file_path()
|
||||
|
||||
with open(video_path, "rb") as f:
|
||||
async with aiofiles.open(video_path, "rb") as f:
|
||||
try:
|
||||
response = self.client.media.upload("video", f)
|
||||
except Exception as e:
|
||||
@@ -199,7 +201,7 @@ class WecomPlatformEvent(AstrMessageEvent):
|
||||
elif isinstance(comp, Image):
|
||||
img_path = await comp.convert_to_file_path()
|
||||
|
||||
with open(img_path, "rb") as f:
|
||||
async with aiofiles.open(img_path, "rb") as f:
|
||||
try:
|
||||
response = self.client.media.upload("image", f)
|
||||
except Exception as e:
|
||||
@@ -219,7 +221,7 @@ class WecomPlatformEvent(AstrMessageEvent):
|
||||
record_path_amr = await convert_audio_to_amr(record_path)
|
||||
|
||||
try:
|
||||
with open(record_path_amr, "rb") as f:
|
||||
async with aiofiles.open(record_path_amr, "rb") as f:
|
||||
try:
|
||||
response = self.client.media.upload("voice", f)
|
||||
except Exception as e:
|
||||
@@ -237,17 +239,18 @@ class WecomPlatformEvent(AstrMessageEvent):
|
||||
response["media_id"],
|
||||
)
|
||||
finally:
|
||||
if record_path_amr != record_path and os.path.exists(
|
||||
record_path_amr,
|
||||
if (
|
||||
record_path_amr != record_path
|
||||
and await anyio.Path(record_path_amr).exists()
|
||||
):
|
||||
try:
|
||||
os.remove(record_path_amr)
|
||||
await anyio.Path(record_path_amr).unlink()
|
||||
except OSError as e:
|
||||
logger.warning(f"删除临时音频文件失败: {e}")
|
||||
elif isinstance(comp, File):
|
||||
file_path = await comp.get_file()
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
async with aiofiles.open(file_path, "rb") as f:
|
||||
try:
|
||||
response = self.client.media.upload("file", f)
|
||||
except Exception as e:
|
||||
@@ -265,7 +268,7 @@ class WecomPlatformEvent(AstrMessageEvent):
|
||||
elif isinstance(comp, Video):
|
||||
video_path = await comp.convert_to_file_path()
|
||||
|
||||
with open(video_path, "rb") as f:
|
||||
async with aiofiles.open(video_path, "rb") as f:
|
||||
try:
|
||||
response = self.client.media.upload("video", f)
|
||||
except Exception as e:
|
||||
|
||||
@@ -10,6 +10,7 @@ from typing import Any, Literal
|
||||
from urllib.parse import parse_qs, urlencode, urlparse
|
||||
|
||||
import aiohttp
|
||||
import anyio
|
||||
|
||||
from astrbot.api import logger
|
||||
from astrbot.api.event import MessageChain
|
||||
@@ -103,7 +104,8 @@ class WecomAIBotWebhookClient:
|
||||
async def upload_media(
|
||||
self, file_path: Path, media_type: Literal["file", "voice"]
|
||||
) -> str:
|
||||
if not file_path.exists() or not file_path.is_file():
|
||||
file_path_anyio = anyio.Path(file_path)
|
||||
if not await file_path_anyio.exists() or not await file_path_anyio.is_file():
|
||||
raise WecomAIBotWebhookError(f"文件不存在: {file_path}")
|
||||
|
||||
content_type = (
|
||||
@@ -112,7 +114,7 @@ class WecomAIBotWebhookClient:
|
||||
form = aiohttp.FormData()
|
||||
form.add_field(
|
||||
"media",
|
||||
file_path.read_bytes(),
|
||||
await file_path_anyio.read_bytes(),
|
||||
filename=file_path.name,
|
||||
content_type=content_type,
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import uuid
|
||||
from collections.abc import Callable, Coroutine
|
||||
from typing import Any, cast
|
||||
|
||||
import aiofiles
|
||||
import quart
|
||||
from requests import Response
|
||||
from wechatpy import WeChatClient, create_reply, parse_message
|
||||
@@ -468,8 +469,8 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
|
||||
)
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
path = os.path.join(temp_dir, f"weixin_offacc_{msg.media_id}.amr")
|
||||
with open(path, "wb") as f:
|
||||
f.write(resp.content)
|
||||
async with aiofiles.open(path, "wb") as f:
|
||||
await f.write(resp.content)
|
||||
|
||||
try:
|
||||
path_wav = os.path.join(
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import asyncio
|
||||
import os
|
||||
from typing import Any, cast
|
||||
|
||||
import aiofiles
|
||||
import anyio
|
||||
from wechatpy import WeChatClient
|
||||
from wechatpy.replies import ImageReply, VoiceReply
|
||||
|
||||
@@ -101,7 +102,7 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent):
|
||||
elif isinstance(comp, Image):
|
||||
img_path = await comp.convert_to_file_path()
|
||||
|
||||
with open(img_path, "rb") as f:
|
||||
async with aiofiles.open(img_path, "rb") as f:
|
||||
try:
|
||||
response = self.client.media.upload("image", f)
|
||||
except Exception as e:
|
||||
@@ -132,7 +133,7 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent):
|
||||
record_path_amr = await convert_audio_to_amr(record_path)
|
||||
|
||||
try:
|
||||
with open(record_path_amr, "rb") as f:
|
||||
async with aiofiles.open(record_path_amr, "rb") as f:
|
||||
try:
|
||||
response = self.client.media.upload("voice", f)
|
||||
except Exception as e:
|
||||
@@ -162,11 +163,12 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent):
|
||||
assert isinstance(future, asyncio.Future)
|
||||
future.set_result(xml)
|
||||
finally:
|
||||
if record_path_amr != record_path and os.path.exists(
|
||||
record_path_amr
|
||||
if (
|
||||
record_path_amr != record_path
|
||||
and await anyio.Path(record_path_amr).exists()
|
||||
):
|
||||
try:
|
||||
os.remove(record_path_amr)
|
||||
await anyio.Path(record_path_amr).unlink()
|
||||
except OSError as e:
|
||||
logger.warning(f"删除临时音频文件失败: {e}")
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
import aiofiles
|
||||
from anthropic.types import Message as AnthropicMessage
|
||||
from google.genai.types import GenerateContentResponse
|
||||
from openai.types.chat.chat_completion import ChatCompletion
|
||||
@@ -218,8 +219,8 @@ class ProviderRequest:
|
||||
"""将图片转换为 base64"""
|
||||
if image_url.startswith("base64://"):
|
||||
return image_url.replace("base64://", "data:image/jpeg;base64,")
|
||||
with open(image_url, "rb") as f:
|
||||
image_bs64 = base64.b64encode(f.read()).decode("utf-8")
|
||||
async with aiofiles.open(image_url, "rb") as f:
|
||||
image_bs64 = base64.b64encode(await f.read()).decode("utf-8")
|
||||
return "data:image/jpeg;base64," + image_bs64
|
||||
return ""
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@ from dataclasses import dataclass
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
import aiofiles
|
||||
import aiohttp
|
||||
import anyio
|
||||
|
||||
from astrbot import logger
|
||||
from astrbot.core import sp
|
||||
@@ -373,15 +375,20 @@ class FunctionToolManager:
|
||||
data_dir = get_astrbot_data_path()
|
||||
|
||||
mcp_json_file = os.path.join(data_dir, "mcp_server.json")
|
||||
if not os.path.exists(mcp_json_file):
|
||||
mcp_json_path = anyio.Path(mcp_json_file)
|
||||
if not await mcp_json_path.exists():
|
||||
# 配置文件不存在错误处理
|
||||
with open(mcp_json_file, "w", encoding="utf-8") as f:
|
||||
json.dump(DEFAULT_MCP_CONFIG, f, ensure_ascii=False, indent=4)
|
||||
async with aiofiles.open(mcp_json_file, "w", encoding="utf-8") as f:
|
||||
await f.write(
|
||||
json.dumps(DEFAULT_MCP_CONFIG, ensure_ascii=False, indent=4)
|
||||
)
|
||||
logger.info(f"未找到 MCP 服务配置文件,已创建默认配置文件 {mcp_json_file}")
|
||||
return MCPInitSummary(total=0, success=0, failed=[])
|
||||
|
||||
with open(mcp_json_file, encoding="utf-8") as f:
|
||||
mcp_server_json_obj: dict[str, dict] = json.load(f)["mcpServers"]
|
||||
async with aiofiles.open(mcp_json_file, encoding="utf-8") as f:
|
||||
mcp_server_json_obj: dict[str, dict] = json.loads(await f.read())[
|
||||
"mcpServers"
|
||||
]
|
||||
|
||||
init_timeout = self._init_timeout_default
|
||||
timeout_display = f"{init_timeout:g}"
|
||||
@@ -403,7 +410,7 @@ class FunctionToolManager:
|
||||
name=name,
|
||||
cfg=cfg,
|
||||
shutdown_event=shutdown_event,
|
||||
timeout=init_timeout,
|
||||
init_timeout=init_timeout,
|
||||
),
|
||||
name=f"mcp-init:{name}",
|
||||
)
|
||||
@@ -455,7 +462,7 @@ class FunctionToolManager:
|
||||
cfg: dict,
|
||||
*,
|
||||
shutdown_event: asyncio.Event | None = None,
|
||||
timeout: float,
|
||||
init_timeout: float,
|
||||
) -> None:
|
||||
"""Initialize MCP server with timeout and register task/event together.
|
||||
|
||||
@@ -465,7 +472,7 @@ class FunctionToolManager:
|
||||
async with self._runtime_lock:
|
||||
if name in self._mcp_server_runtime or name in self._mcp_starting:
|
||||
logger.warning(
|
||||
f"Connected to MCP server {name}, ignoring this startup request (timeout={timeout:g})."
|
||||
f"Connected to MCP server {name}, ignoring this startup request (timeout={init_timeout:g})."
|
||||
)
|
||||
self._log_safe_mcp_debug_config(cfg)
|
||||
return
|
||||
@@ -478,11 +485,11 @@ class FunctionToolManager:
|
||||
try:
|
||||
mcp_client = await asyncio.wait_for(
|
||||
self._init_mcp_client(name, cfg),
|
||||
timeout=timeout,
|
||||
timeout=init_timeout,
|
||||
)
|
||||
except asyncio.TimeoutError as exc:
|
||||
raise MCPInitTimeoutError(
|
||||
f"Connected to MCP server {name} timeout ({timeout:g} seconds)"
|
||||
f"Connected to MCP server {name} timeout ({init_timeout:g} seconds)"
|
||||
) from exc
|
||||
except Exception:
|
||||
logger.error(f"Failed to initialize MCP client {name}", exc_info=True)
|
||||
@@ -515,7 +522,7 @@ class FunctionToolManager:
|
||||
async def _shutdown_runtimes(
|
||||
self,
|
||||
runtimes: list[_MCPServerRuntime],
|
||||
timeout: float,
|
||||
shutdown_timeout: float,
|
||||
*,
|
||||
strict: bool = True,
|
||||
) -> list[str]:
|
||||
@@ -534,7 +541,7 @@ class FunctionToolManager:
|
||||
try:
|
||||
results = await asyncio.wait_for(
|
||||
asyncio.gather(*lifecycle_tasks, return_exceptions=True),
|
||||
timeout=timeout,
|
||||
timeout=shutdown_timeout,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
pending_names = [
|
||||
@@ -547,10 +554,10 @@ class FunctionToolManager:
|
||||
task.cancel()
|
||||
await asyncio.gather(*lifecycle_tasks, return_exceptions=True)
|
||||
if strict:
|
||||
raise MCPShutdownTimeoutError(pending_names, timeout)
|
||||
raise MCPShutdownTimeoutError(pending_names, shutdown_timeout)
|
||||
logger.warning(
|
||||
"MCP server shutdown timeout (%s seconds), the following servers were not fully closed: %s",
|
||||
f"{timeout:g}",
|
||||
f"{shutdown_timeout:g}",
|
||||
", ".join(pending_names),
|
||||
)
|
||||
return pending_names
|
||||
@@ -663,7 +670,7 @@ class FunctionToolManager:
|
||||
name: str,
|
||||
config: dict,
|
||||
shutdown_event: asyncio.Event | None = None,
|
||||
timeout: float | int | str | None = None,
|
||||
init_timeout: float | int | str | None = None,
|
||||
) -> None:
|
||||
"""Enable a new MCP server and initialize it.
|
||||
|
||||
@@ -671,18 +678,18 @@ class FunctionToolManager:
|
||||
name: The name of the MCP server.
|
||||
config: Configuration for the MCP server.
|
||||
shutdown_event: Event to signal when the MCP client should shut down.
|
||||
timeout: Timeout in seconds for initialization.
|
||||
init_timeout: Timeout in seconds for initialization.
|
||||
Uses ASTRBOT_MCP_ENABLE_TIMEOUT by default (separate from init timeout).
|
||||
|
||||
Raises:
|
||||
MCPInitTimeoutError: If initialization does not complete within timeout.
|
||||
Exception: If there is an error during initialization.
|
||||
"""
|
||||
if timeout is None:
|
||||
if init_timeout is None:
|
||||
timeout_value = self._enable_timeout_default
|
||||
else:
|
||||
timeout_value = _resolve_timeout(
|
||||
timeout=timeout,
|
||||
timeout=init_timeout,
|
||||
env_name=ENABLE_MCP_TIMEOUT_ENV,
|
||||
default=self._enable_timeout_default,
|
||||
)
|
||||
@@ -690,19 +697,19 @@ class FunctionToolManager:
|
||||
name=name,
|
||||
cfg=config,
|
||||
shutdown_event=shutdown_event,
|
||||
timeout=timeout_value,
|
||||
init_timeout=timeout_value,
|
||||
)
|
||||
|
||||
async def disable_mcp_server(
|
||||
self,
|
||||
name: str | None = None,
|
||||
timeout: float = 10,
|
||||
shutdown_timeout: float = 10,
|
||||
) -> None:
|
||||
"""Disable an MCP server by its name.
|
||||
|
||||
Args:
|
||||
name (str): The name of the MCP server to disable. If None, ALL MCP servers will be disabled.
|
||||
timeout (int): Timeout.
|
||||
shutdown_timeout (int): Timeout.
|
||||
|
||||
Raises:
|
||||
MCPShutdownTimeoutError: If shutdown does not complete within timeout.
|
||||
@@ -715,11 +722,11 @@ class FunctionToolManager:
|
||||
if runtime is None:
|
||||
return
|
||||
|
||||
await self._shutdown_runtimes([runtime], timeout, strict=True)
|
||||
await self._shutdown_runtimes([runtime], shutdown_timeout, strict=True)
|
||||
else:
|
||||
async with self._runtime_lock:
|
||||
runtimes = list(self._mcp_server_runtime.values())
|
||||
await self._shutdown_runtimes(runtimes, timeout, strict=False)
|
||||
await self._shutdown_runtimes(runtimes, shutdown_timeout, strict=False)
|
||||
|
||||
def _warn_on_timeout_mismatch(
|
||||
self,
|
||||
|
||||
@@ -4,6 +4,9 @@ import os
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import TypeAlias, Union
|
||||
|
||||
import aiofiles
|
||||
import anyio
|
||||
|
||||
from astrbot.core.agent.message import ContentPart, Message
|
||||
from astrbot.core.agent.tool import ToolSet
|
||||
from astrbot.core.provider.entities import (
|
||||
@@ -188,11 +191,11 @@ class Provider(AbstractProvider):
|
||||
|
||||
return dicts
|
||||
|
||||
async def test(self, timeout: float = 45.0) -> None:
|
||||
await asyncio.wait_for(
|
||||
self.text_chat(prompt="REPLY `PONG` ONLY"),
|
||||
timeout=timeout,
|
||||
)
|
||||
async def test(self, test_timeout: float = 45.0) -> None:
|
||||
# Use anyio.fail_after to enforce timeout in async context.
|
||||
# This avoids direct asyncio.wait_for usage inside async functions.
|
||||
async with anyio.fail_after(test_timeout):
|
||||
await self.text_chat(prompt="REPLY `PONG` ONLY")
|
||||
|
||||
|
||||
class STTProvider(AbstractProvider):
|
||||
@@ -268,8 +271,8 @@ class TTSProvider(AbstractProvider):
|
||||
# 调用原有的 get_audio 方法获取音频文件路径
|
||||
audio_path = await self.get_audio(accumulated_text)
|
||||
# 读取音频文件内容
|
||||
with open(audio_path, "rb") as f:
|
||||
audio_data = f.read()
|
||||
async with aiofiles.open(audio_path, "rb") as f:
|
||||
audio_data = await f.read()
|
||||
await audio_queue.put((accumulated_text, audio_data))
|
||||
except Exception:
|
||||
# 出错时也要发送 None 结束标记
|
||||
@@ -284,10 +287,11 @@ class TTSProvider(AbstractProvider):
|
||||
audio_path = await self.get_audio("hi")
|
||||
|
||||
# 检查生成的音频文件是否有效
|
||||
if not os.path.exists(audio_path):
|
||||
audio_path_obj = anyio.Path(audio_path)
|
||||
if not await audio_path_obj.exists():
|
||||
raise Exception("TTS test failed: audio file was not created")
|
||||
|
||||
file_size = os.path.getsize(audio_path)
|
||||
file_size = (await audio_path_obj.stat()).st_size
|
||||
if file_size == 0:
|
||||
raise Exception(
|
||||
"TTS test failed: generated audio file is empty (0 bytes). "
|
||||
|
||||
@@ -2,6 +2,7 @@ import base64
|
||||
import json
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
import aiofiles
|
||||
import anthropic
|
||||
import httpx
|
||||
from anthropic import AsyncAnthropic
|
||||
@@ -652,8 +653,8 @@ class ProviderAnthropic(Provider):
|
||||
except Exception:
|
||||
mime_type = "image/jpeg"
|
||||
return f"data:{mime_type};base64,{raw_base64}", mime_type
|
||||
with open(image_url, "rb") as f:
|
||||
image_bytes = f.read()
|
||||
async with aiofiles.open(image_url, "rb") as f:
|
||||
image_bytes = await f.read()
|
||||
mime_type = self._detect_image_mime_type(image_bytes)
|
||||
image_bs64 = base64.b64encode(image_bytes).decode("utf-8")
|
||||
return f"data:{mime_type};base64,{image_bs64}", mime_type
|
||||
|
||||
@@ -4,6 +4,7 @@ import logging
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import aiofiles
|
||||
import aiohttp
|
||||
import dashscope
|
||||
from dashscope.audio.tts_v2 import AudioFormat, SpeechSynthesizer
|
||||
@@ -59,8 +60,8 @@ class ProviderDashscopeTTSAPI(TTSProvider):
|
||||
)
|
||||
|
||||
path = os.path.join(temp_dir, f"dashscope_tts_{uuid.uuid4()}{ext}")
|
||||
with open(path, "wb") as f:
|
||||
f.write(audio_bytes)
|
||||
async with aiofiles.open(path, "wb") as f:
|
||||
await f.write(audio_bytes)
|
||||
return path
|
||||
|
||||
def _call_qwen_tts(self, model: str, text: str):
|
||||
|
||||
@@ -112,8 +112,9 @@ class ProviderEdgeTTS(TTSProvider):
|
||||
f"FFmpeg 转换失败: {e.stderr.decode() if e.stderr else str(e)}",
|
||||
)
|
||||
try:
|
||||
if os.path.exists(mp3_path):
|
||||
os.remove(mp3_path)
|
||||
mp3_path_obj = anyio.Path(mp3_path)
|
||||
if await mp3_path_obj.exists():
|
||||
await mp3_path_obj.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
raise RuntimeError(f"FFmpeg 转换失败: {e!s}")
|
||||
@@ -121,8 +122,9 @@ class ProviderEdgeTTS(TTSProvider):
|
||||
except Exception as e:
|
||||
logger.error(f"音频生成失败: {e!s}")
|
||||
try:
|
||||
if os.path.exists(mp3_path):
|
||||
os.remove(mp3_path)
|
||||
mp3_path_obj = anyio.Path(mp3_path)
|
||||
if await mp3_path_obj.exists():
|
||||
await mp3_path_obj.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
raise RuntimeError(f"音频生成失败: {e!s}")
|
||||
|
||||
@@ -3,6 +3,7 @@ import re
|
||||
import uuid
|
||||
from typing import Annotated, Literal
|
||||
|
||||
import aiofiles
|
||||
import ormsgpack
|
||||
from httpx import AsyncClient
|
||||
from pydantic import BaseModel, conint
|
||||
@@ -159,9 +160,9 @@ class ProviderFishAudioTTSAPI(TTSProvider):
|
||||
if response.status_code == 200 and response.headers.get(
|
||||
"content-type", ""
|
||||
).startswith("audio/"):
|
||||
with open(path, "wb") as f:
|
||||
async with aiofiles.open(path, "wb") as f:
|
||||
async for chunk in response.aiter_bytes():
|
||||
f.write(chunk)
|
||||
await f.write(chunk)
|
||||
return path
|
||||
error_bytes = await response.aread()
|
||||
error_text = error_bytes.decode("utf-8", errors="replace")[:1024]
|
||||
|
||||
@@ -6,6 +6,7 @@ import random
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import cast
|
||||
|
||||
import aiofiles
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
from google.genai.errors import APIError
|
||||
@@ -924,8 +925,8 @@ class ProviderGoogleGenAI(Provider):
|
||||
"""将图片转换为 base64"""
|
||||
if image_url.startswith("base64://"):
|
||||
return image_url.replace("base64://", "data:image/jpeg;base64,")
|
||||
with open(image_url, "rb") as f:
|
||||
image_bs64 = base64.b64encode(f.read()).decode("utf-8")
|
||||
async with aiofiles.open(image_url, "rb") as f:
|
||||
image_bs64 = base64.b64encode(await f.read()).decode("utf-8")
|
||||
return "data:image/jpeg;base64," + image_bs64
|
||||
|
||||
async def terminate(self) -> None:
|
||||
|
||||
@@ -2,6 +2,9 @@ import asyncio
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import aiofiles
|
||||
import anyio
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.provider.entities import ProviderType
|
||||
from astrbot.core.provider.provider import TTSProvider
|
||||
@@ -72,7 +75,8 @@ class GenieTTSProvider(TTSProvider):
|
||||
try:
|
||||
await loop.run_in_executor(None, _generate, path)
|
||||
|
||||
if os.path.exists(path):
|
||||
path_obj = anyio.Path(path)
|
||||
if await path_obj.exists():
|
||||
return path
|
||||
|
||||
raise RuntimeError("Genie TTS did not save to file.")
|
||||
@@ -109,16 +113,17 @@ class GenieTTSProvider(TTSProvider):
|
||||
|
||||
await loop.run_in_executor(None, _generate, path, text)
|
||||
|
||||
if os.path.exists(path):
|
||||
with open(path, "rb") as f:
|
||||
audio_data = f.read()
|
||||
path_obj = anyio.Path(path)
|
||||
if await path_obj.exists():
|
||||
async with aiofiles.open(path, "rb") as f:
|
||||
audio_data = await f.read()
|
||||
|
||||
# Put (text, bytes) into queue so frontend can display text
|
||||
await audio_queue.put((text, audio_data))
|
||||
|
||||
# Clean up
|
||||
try:
|
||||
os.remove(path)
|
||||
await path_obj.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import aiofiles
|
||||
import aiohttp
|
||||
|
||||
from astrbot import logger
|
||||
@@ -129,8 +130,8 @@ class ProviderGSVTTS(TTSProvider):
|
||||
|
||||
result = await self._make_request(endpoint, params)
|
||||
if isinstance(result, bytes):
|
||||
with open(path, "wb") as f:
|
||||
f.write(result)
|
||||
async with aiofiles.open(path, "wb") as f:
|
||||
await f.write(result)
|
||||
return path
|
||||
raise Exception(f"[GSV TTS] 合成失败,输入文本:{text},错误信息:{result}")
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import os
|
||||
import urllib.parse
|
||||
import uuid
|
||||
|
||||
import aiofiles
|
||||
import aiohttp
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
@@ -48,8 +49,8 @@ class ProviderGSVITTS(TTSProvider):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as response:
|
||||
if response.status == 200:
|
||||
with open(path, "wb") as f:
|
||||
f.write(await response.read())
|
||||
async with aiofiles.open(path, "wb") as f:
|
||||
await f.write(await response.read())
|
||||
else:
|
||||
error_text = await response.text()
|
||||
raise Exception(
|
||||
|
||||
@@ -3,6 +3,7 @@ import os
|
||||
import uuid
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
import aiofiles
|
||||
import aiohttp
|
||||
|
||||
from astrbot.api import logger
|
||||
@@ -163,8 +164,8 @@ class ProviderMiniMaxTTSAPI(TTSProvider):
|
||||
)
|
||||
|
||||
# 结果保存至文件
|
||||
with open(path, "wb") as file:
|
||||
file.write(audio)
|
||||
async with aiofiles.open(path, "wb") as file:
|
||||
await file.write(audio)
|
||||
|
||||
return path
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import re
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
import aiofiles
|
||||
import httpx
|
||||
from openai import AsyncAzureOpenAI, AsyncOpenAI
|
||||
from openai._exceptions import NotFoundError
|
||||
@@ -950,8 +951,8 @@ class ProviderOpenAIOfficial(Provider):
|
||||
"""将图片转换为 base64"""
|
||||
if image_url.startswith("base64://"):
|
||||
return image_url.replace("base64://", "data:image/jpeg;base64,")
|
||||
with open(image_url, "rb") as f:
|
||||
image_bs64 = base64.b64encode(f.read()).decode("utf-8")
|
||||
async with aiofiles.open(image_url, "rb") as f:
|
||||
image_bs64 = base64.b64encode(await f.read()).decode("utf-8")
|
||||
return "data:image/jpeg;base64," + image_bs64
|
||||
|
||||
async def terminate(self):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import aiofiles
|
||||
import httpx
|
||||
from openai import NOT_GIVEN, AsyncOpenAI
|
||||
|
||||
@@ -54,9 +55,9 @@ class ProviderOpenAITTSAPI(TTSProvider):
|
||||
response_format="wav",
|
||||
input=text,
|
||||
) as response:
|
||||
with open(path, "wb") as f:
|
||||
async with aiofiles.open(path, "wb") as f:
|
||||
async for chunk in response.iter_bytes(chunk_size=1024):
|
||||
f.write(chunk)
|
||||
await f.write(chunk)
|
||||
return path
|
||||
|
||||
async def terminate(self):
|
||||
|
||||
@@ -4,12 +4,11 @@ LastEditTime: 2025-02-25 14:06:30
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
import anyio
|
||||
from funasr_onnx import SenseVoiceSmall
|
||||
from funasr_onnx.utils.postprocess_utils import rich_transcription_postprocess
|
||||
|
||||
@@ -52,14 +51,14 @@ class ProviderSenseVoiceSTTSelfHost(STTProvider):
|
||||
|
||||
async def get_timestamped_path(self) -> str:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
temp_dir = Path(get_astrbot_temp_path())
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
temp_dir = anyio.Path(get_astrbot_temp_path())
|
||||
await temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
return str(temp_dir / timestamp)
|
||||
|
||||
async def _is_silk_file(self, file_path) -> bool:
|
||||
silk_header = b"SILK"
|
||||
with open(file_path, "rb") as f:
|
||||
file_header = f.read(8)
|
||||
async with anyio.open_file(file_path, "rb") as f:
|
||||
file_header = await f.read(8)
|
||||
|
||||
if silk_header in file_header:
|
||||
return True
|
||||
@@ -76,7 +75,7 @@ class ProviderSenseVoiceSTTSelfHost(STTProvider):
|
||||
await download_file(audio_url, path)
|
||||
audio_url = path
|
||||
|
||||
if not os.path.isfile(audio_url):
|
||||
if not await anyio.Path(audio_url).is_file():
|
||||
raise FileNotFoundError(f"文件不存在: {audio_url}")
|
||||
|
||||
if audio_url.endswith((".amr", ".silk")) or is_tencent:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import anyio
|
||||
from openai import NOT_GIVEN, AsyncOpenAI
|
||||
|
||||
from astrbot.core import logger
|
||||
@@ -16,6 +17,10 @@ from ..provider import STTProvider
|
||||
from ..register import register_provider_adapter
|
||||
|
||||
|
||||
def _open_file_rb(path: str):
|
||||
return open(path, "rb")
|
||||
|
||||
|
||||
@register_provider_adapter(
|
||||
"openai_whisper_api",
|
||||
"OpenAI Whisper API",
|
||||
@@ -44,8 +49,8 @@ class ProviderOpenAIWhisperAPI(STTProvider):
|
||||
amr_header = b"#!AMR"
|
||||
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
file_header = f.read(8)
|
||||
async with anyio.open_file(file_path, "rb") as f:
|
||||
file_header = await f.read(8)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
@@ -73,7 +78,7 @@ class ProviderOpenAIWhisperAPI(STTProvider):
|
||||
await download_file(audio_url, path)
|
||||
audio_url = path
|
||||
|
||||
if not os.path.exists(audio_url):
|
||||
if not await anyio.Path(audio_url).exists():
|
||||
raise FileNotFoundError(f"文件不存在: {audio_url}")
|
||||
|
||||
if audio_url.endswith(".amr") or audio_url.endswith(".silk") or is_tencent:
|
||||
@@ -100,15 +105,17 @@ class ProviderOpenAIWhisperAPI(STTProvider):
|
||||
|
||||
audio_url = output_path
|
||||
|
||||
file_obj = await anyio.to_thread.run_sync(_open_file_rb, audio_url)
|
||||
result = await self.client.audio.transcriptions.create(
|
||||
model=self.model_name,
|
||||
file=("audio.wav", open(audio_url, "rb")),
|
||||
file=("audio.wav", file_obj),
|
||||
)
|
||||
file_obj.close()
|
||||
|
||||
# remove temp file
|
||||
if output_path and os.path.exists(output_path):
|
||||
if output_path and await anyio.Path(output_path).exists():
|
||||
try:
|
||||
os.remove(audio_url)
|
||||
await anyio.Path(audio_url).unlink()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to remove temp file {audio_url}: {e}")
|
||||
return result.text
|
||||
|
||||
@@ -3,6 +3,7 @@ import os
|
||||
import uuid
|
||||
from typing import cast
|
||||
|
||||
import anyio
|
||||
import whisper
|
||||
|
||||
from astrbot.core import logger
|
||||
@@ -42,8 +43,8 @@ class ProviderOpenAIWhisperSelfHost(STTProvider):
|
||||
|
||||
async def _is_silk_file(self, file_path) -> bool:
|
||||
silk_header = b"SILK"
|
||||
with open(file_path, "rb") as f:
|
||||
file_header = f.read(8)
|
||||
async with anyio.open_file(file_path, "rb") as f:
|
||||
file_header = await f.read(8)
|
||||
|
||||
if silk_header in file_header:
|
||||
return True
|
||||
@@ -66,7 +67,7 @@ class ProviderOpenAIWhisperSelfHost(STTProvider):
|
||||
await download_file(audio_url, path)
|
||||
audio_url = path
|
||||
|
||||
if not os.path.exists(audio_url):
|
||||
if not await anyio.Path(audio_url).exists():
|
||||
raise FileNotFoundError(f"文件不存在: {audio_url}")
|
||||
|
||||
if audio_url.endswith(".amr") or audio_url.endswith(".silk") or is_tencent:
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import aiofiles
|
||||
import aiohttp
|
||||
import anyio
|
||||
from xinference_client.client.restful.async_restful_client import (
|
||||
AsyncClient as Client,
|
||||
)
|
||||
@@ -102,9 +103,9 @@ class ProviderXinferenceSTT(STTProvider):
|
||||
f"Failed to download audio from {audio_url}, status: {resp.status}",
|
||||
)
|
||||
return ""
|
||||
elif os.path.exists(audio_url):
|
||||
with open(audio_url, "rb") as f:
|
||||
audio_bytes = f.read()
|
||||
elif await anyio.Path(audio_url).exists():
|
||||
async with aiofiles.open(audio_url, "rb") as f:
|
||||
audio_bytes = await f.read()
|
||||
else:
|
||||
logger.error(f"File not found: {audio_url}")
|
||||
return ""
|
||||
@@ -130,21 +131,19 @@ class ProviderXinferenceSTT(STTProvider):
|
||||
logger.info(
|
||||
f"Audio requires conversion ({conversion_type}), using temporary files..."
|
||||
)
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
temp_dir = anyio.Path(get_astrbot_temp_path())
|
||||
await temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
input_path = os.path.join(
|
||||
temp_dir,
|
||||
f"xinference_stt_{uuid.uuid4().hex[:8]}.input",
|
||||
input_path = str(
|
||||
temp_dir / f"xinference_stt_{uuid.uuid4().hex[:8]}.input"
|
||||
)
|
||||
output_path = os.path.join(
|
||||
temp_dir,
|
||||
f"xinference_stt_{uuid.uuid4().hex[:8]}.wav",
|
||||
output_path = str(
|
||||
temp_dir / f"xinference_stt_{uuid.uuid4().hex[:8]}.wav"
|
||||
)
|
||||
temp_files.extend([input_path, output_path])
|
||||
|
||||
with open(input_path, "wb") as f:
|
||||
f.write(audio_bytes)
|
||||
async with aiofiles.open(input_path, "wb") as f:
|
||||
await f.write(audio_bytes)
|
||||
|
||||
if conversion_type == "silk":
|
||||
logger.info("Converting silk to wav ...")
|
||||
@@ -153,8 +152,8 @@ class ProviderXinferenceSTT(STTProvider):
|
||||
logger.info("Converting amr to wav ...")
|
||||
await convert_to_pcm_wav(input_path, output_path)
|
||||
|
||||
with open(output_path, "rb") as f:
|
||||
audio_bytes = f.read()
|
||||
async with aiofiles.open(output_path, "rb") as f:
|
||||
audio_bytes = await f.read()
|
||||
|
||||
# 4. Transcribe
|
||||
# 官方asyncCLient的客户端似乎实现有点问题,这里直接用aiohttp实现openai标准兼容请求,提交issue等待官方修复后再改回来
|
||||
@@ -199,8 +198,9 @@ class ProviderXinferenceSTT(STTProvider):
|
||||
# 5. Cleanup
|
||||
for temp_file in temp_files:
|
||||
try:
|
||||
if os.path.exists(temp_file):
|
||||
os.remove(temp_file)
|
||||
temp_path = anyio.Path(temp_file)
|
||||
if await temp_path.exists():
|
||||
await temp_path.unlink()
|
||||
logger.debug(f"Removed temporary file: {temp_file}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to remove temporary file {temp_file}: {e}")
|
||||
|
||||
+19
-13
@@ -10,6 +10,7 @@ from ipaddress import IPv4Address, IPv6Address, ip_address
|
||||
from pathlib import Path
|
||||
from typing import Protocol
|
||||
|
||||
import anyio
|
||||
import jwt
|
||||
import psutil
|
||||
import werkzeug.exceptions
|
||||
@@ -115,18 +116,23 @@ class AstrBotDashboard:
|
||||
self.data_path = os.path.abspath(user_dist)
|
||||
|
||||
if self.enable_webui and not (Path(self.data_path) / "index.html").exists():
|
||||
raise RuntimeError(
|
||||
logger.warning(
|
||||
f"Dashboard static assets not found: index.html is missing in {self.data_path}. "
|
||||
"Please run the WebUI build step."
|
||||
"WebUI will be disabled."
|
||||
)
|
||||
self.enable_webui = False
|
||||
|
||||
def _init_app(self):
|
||||
"""初始化 Quart 应用"""
|
||||
global APP
|
||||
|
||||
static_folder = self.data_path if self.enable_webui else None
|
||||
static_url_path = "/" if self.enable_webui else None
|
||||
|
||||
self.app = Quart(
|
||||
"AstrBotDashboard",
|
||||
static_folder=self.data_path,
|
||||
static_url_path="/",
|
||||
static_folder=static_folder,
|
||||
static_url_path=static_url_path,
|
||||
)
|
||||
APP = self.app
|
||||
self.app.json_provider_class = DefaultJSONProvider
|
||||
@@ -456,25 +462,25 @@ class AstrBotDashboard:
|
||||
or ssl_config.get("ca_certs", "")
|
||||
)
|
||||
|
||||
cert_path = Path(cert_file).expanduser()
|
||||
key_path = Path(key_file).expanduser()
|
||||
cert_path = anyio.Path(cert_file).expanduser()
|
||||
key_path = anyio.Path(key_file).expanduser()
|
||||
if not cert_file or not key_file:
|
||||
raise ValueError(
|
||||
"dashboard.ssl.enable 为 true 时,必须配置 cert_file 和 key_file。",
|
||||
)
|
||||
if not cert_path.is_file():
|
||||
if not await cert_path.is_file():
|
||||
raise ValueError(f"SSL 证书文件不存在: {cert_path}")
|
||||
if not key_path.is_file():
|
||||
if not await key_path.is_file():
|
||||
raise ValueError(f"SSL 私钥文件不存在: {key_path}")
|
||||
|
||||
config.certfile = str(cert_path.resolve())
|
||||
config.keyfile = str(key_path.resolve())
|
||||
config.certfile = str(await cert_path.resolve())
|
||||
config.keyfile = str(await key_path.resolve())
|
||||
|
||||
if ca_certs:
|
||||
ca_path = Path(ca_certs).expanduser()
|
||||
if not ca_path.is_file():
|
||||
ca_path = anyio.Path(ca_certs).expanduser()
|
||||
if not await ca_path.is_file():
|
||||
raise ValueError(f"SSL CA 证书文件不存在: {ca_path}")
|
||||
config.ca_certs = str(ca_path.resolve())
|
||||
config.ca_certs = str(await ca_path.resolve())
|
||||
|
||||
# 根据配置决定是否禁用访问日志
|
||||
disable_access_log = dashboard_config.get("disable_access_log", True)
|
||||
|
||||
Reference in New Issue
Block a user