feat: temporary file handling and introduce TempDirCleaner (#5026)
* feat: temporary file handling and introduce TempDirCleaner - Updated various modules to use `get_astrbot_temp_path()` instead of `get_astrbot_data_path()` for temporary file storage. - Renamed temporary files for better identification and organization. - Introduced `TempDirCleaner` to manage the size of the temporary directory, ensuring it does not exceed a specified limit by deleting the oldest files. - Added configuration option for maximum temporary directory size in the dashboard. - Implemented tests for `TempDirCleaner` to verify cleanup functionality and size management. * ruff
This commit is contained in:
@@ -10,7 +10,7 @@ from astrbot.core.provider.entities import (
|
||||
LLMResponse,
|
||||
ProviderRequest,
|
||||
)
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
from astrbot.core.utils.io import download_file
|
||||
|
||||
from ...hooks import BaseAgentRunHooks
|
||||
@@ -291,8 +291,8 @@ class DifyAgentRunner(BaseAgentRunner[TContext]):
|
||||
return Comp.Image(file=item["url"], url=item["url"])
|
||||
case "audio":
|
||||
# 仅支持 wav
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
path = os.path.join(temp_dir, f"{item['filename']}.wav")
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
path = os.path.join(temp_dir, f"dify_{item['filename']}.wav")
|
||||
await download_file(item["url"], path)
|
||||
return Comp.Image(file=item["url"], url=item["url"])
|
||||
case "video":
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic.dataclasses import dataclass
|
||||
@@ -240,7 +241,9 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]):
|
||||
if "_&exists_" in json.dumps(result):
|
||||
# Download the file from sandbox
|
||||
name = os.path.basename(path)
|
||||
local_path = os.path.join(get_astrbot_temp_path(), name)
|
||||
local_path = os.path.join(
|
||||
get_astrbot_temp_path(), f"sandbox_{uuid.uuid4().hex[:4]}_{name}"
|
||||
)
|
||||
await sb.download_file(path, local_path)
|
||||
logger.info(f"Downloaded file from sandbox: {path} -> {local_path}")
|
||||
return local_path, True
|
||||
@@ -352,11 +355,11 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]):
|
||||
MessageChain(chain=components),
|
||||
)
|
||||
|
||||
if file_from_sandbox:
|
||||
try:
|
||||
os.remove(local_path)
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing temp file {local_path}: {e}")
|
||||
# if file_from_sandbox:
|
||||
# try:
|
||||
# os.remove(local_path)
|
||||
# except Exception as e:
|
||||
# logger.error(f"Error removing temp file {local_path}: {e}")
|
||||
|
||||
return f"Message sent to session {target_session}"
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from astrbot.api import FunctionTool, logger
|
||||
@@ -167,7 +168,9 @@ class FileDownloadTool(FunctionTool):
|
||||
try:
|
||||
name = os.path.basename(remote_path)
|
||||
|
||||
local_path = os.path.join(get_astrbot_temp_path(), name)
|
||||
local_path = os.path.join(
|
||||
get_astrbot_temp_path(), f"sandbox_{uuid.uuid4().hex[:4]}_{name}"
|
||||
)
|
||||
|
||||
# Download file from sandbox
|
||||
await sb.download_file(remote_path, local_path)
|
||||
@@ -183,12 +186,12 @@ class FileDownloadTool(FunctionTool):
|
||||
logger.error(f"Error sending file message: {e}")
|
||||
|
||||
# remove
|
||||
try:
|
||||
os.remove(local_path)
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing temp file {local_path}: {e}")
|
||||
# try:
|
||||
# os.remove(local_path)
|
||||
# except Exception as e:
|
||||
# logger.error(f"Error removing temp file {local_path}: {e}")
|
||||
|
||||
return f"File downloaded successfully to {local_path} and sent to user. The file has been removed from local storage."
|
||||
return f"File downloaded successfully to {local_path} and sent to user."
|
||||
|
||||
return f"File downloaded successfully to {local_path}"
|
||||
except Exception as e:
|
||||
|
||||
@@ -203,6 +203,7 @@ DEFAULT_CONFIG = {
|
||||
"log_file_enable": False,
|
||||
"log_file_path": "logs/astrbot.log",
|
||||
"log_file_max_mb": 20,
|
||||
"temp_dir_max_size": 1024,
|
||||
"trace_enable": False,
|
||||
"trace_log_enable": False,
|
||||
"trace_log_path": "logs/astrbot.trace.log",
|
||||
@@ -2394,6 +2395,7 @@ CONFIG_METADATA_2 = {
|
||||
"log_file_enable": {"type": "bool"},
|
||||
"log_file_path": {"type": "string", "condition": {"log_file_enable": True}},
|
||||
"log_file_max_mb": {"type": "int", "condition": {"log_file_enable": True}},
|
||||
"temp_dir_max_size": {"type": "int"},
|
||||
"trace_log_enable": {"type": "bool"},
|
||||
"trace_log_path": {
|
||||
"type": "string",
|
||||
@@ -3372,6 +3374,11 @@ CONFIG_METADATA_3_SYSTEM = {
|
||||
"type": "int",
|
||||
"hint": "超过大小后自动轮转,默认 20MB。",
|
||||
},
|
||||
"temp_dir_max_size": {
|
||||
"description": "临时目录大小上限 (MB)",
|
||||
"type": "int",
|
||||
"hint": "用于限制 data/temp 目录总大小,单位为 MB。系统每 10 分钟检查一次,超限时按文件修改时间从旧到新删除,释放约 30% 当前体积。",
|
||||
},
|
||||
"trace_log_enable": {
|
||||
"description": "启用 Trace 文件日志",
|
||||
"type": "bool",
|
||||
|
||||
@@ -37,6 +37,7 @@ from astrbot.core.umop_config_router import UmopConfigRouter
|
||||
from astrbot.core.updator import AstrBotUpdator
|
||||
from astrbot.core.utils.llm_metadata import update_llm_metadata
|
||||
from astrbot.core.utils.migra_helper import migra
|
||||
from astrbot.core.utils.temp_dir_cleaner import TempDirCleaner
|
||||
|
||||
from . import astrbot_config, html_renderer
|
||||
from .event_bus import EventBus
|
||||
@@ -57,6 +58,7 @@ class AstrBotCoreLifecycle:
|
||||
|
||||
self.subagent_orchestrator: SubAgentOrchestrator | None = None
|
||||
self.cron_manager: CronJobManager | None = None
|
||||
self.temp_dir_cleaner: TempDirCleaner | None = None
|
||||
|
||||
# 设置代理
|
||||
proxy_config = self.astrbot_config.get("http_proxy", "")
|
||||
@@ -125,6 +127,12 @@ class AstrBotCoreLifecycle:
|
||||
ucr=self.umop_config_router,
|
||||
sp=sp,
|
||||
)
|
||||
self.temp_dir_cleaner = TempDirCleaner(
|
||||
max_size_getter=lambda: self.astrbot_config_mgr.default_conf.get(
|
||||
TempDirCleaner.CONFIG_KEY,
|
||||
TempDirCleaner.DEFAULT_MAX_SIZE,
|
||||
),
|
||||
)
|
||||
|
||||
# apply migration
|
||||
try:
|
||||
@@ -238,6 +246,12 @@ class AstrBotCoreLifecycle:
|
||||
self.cron_manager.start(self.star_context),
|
||||
name="cron_manager",
|
||||
)
|
||||
temp_dir_cleaner_task = None
|
||||
if self.temp_dir_cleaner:
|
||||
temp_dir_cleaner_task = asyncio.create_task(
|
||||
self.temp_dir_cleaner.run(),
|
||||
name="temp_dir_cleaner",
|
||||
)
|
||||
|
||||
# 把插件中注册的所有协程函数注册到事件总线中并执行
|
||||
extra_tasks = []
|
||||
@@ -247,6 +261,8 @@ class AstrBotCoreLifecycle:
|
||||
tasks_ = [event_bus_task, *(extra_tasks if extra_tasks else [])]
|
||||
if cron_task:
|
||||
tasks_.append(cron_task)
|
||||
if temp_dir_cleaner_task:
|
||||
tasks_.append(temp_dir_cleaner_task)
|
||||
for task in tasks_:
|
||||
self.curr_tasks.append(
|
||||
asyncio.create_task(self._task_wrapper(task), name=task.get_name()),
|
||||
@@ -298,6 +314,9 @@ class AstrBotCoreLifecycle:
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""停止 AstrBot 核心生命周期管理类, 取消所有当前任务并终止各个管理器."""
|
||||
if self.temp_dir_cleaner:
|
||||
await self.temp_dir_cleaner.stop()
|
||||
|
||||
# 请求停止所有正在运行的异步任务
|
||||
for task in self.curr_tasks:
|
||||
task.cancel()
|
||||
|
||||
@@ -31,7 +31,7 @@ from enum import Enum
|
||||
from pydantic.v1 import BaseModel
|
||||
|
||||
from astrbot.core import astrbot_config, file_token_service, logger
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
from astrbot.core.utils.io import download_file, download_image_by_url, file_to_base64
|
||||
|
||||
|
||||
@@ -156,8 +156,9 @@ class Record(BaseMessageComponent):
|
||||
if self.file.startswith("base64://"):
|
||||
bs64_data = self.file.removeprefix("base64://")
|
||||
image_bytes = base64.b64decode(bs64_data)
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
file_path = os.path.join(temp_dir, f"{uuid.uuid4()}.jpg")
|
||||
file_path = os.path.join(
|
||||
get_astrbot_temp_path(), f"recordseg_{uuid.uuid4()}.jpg"
|
||||
)
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(image_bytes)
|
||||
return os.path.abspath(file_path)
|
||||
@@ -245,8 +246,9 @@ class Video(BaseMessageComponent):
|
||||
if url and url.startswith("file:///"):
|
||||
return url[8:]
|
||||
if url and url.startswith("http"):
|
||||
download_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
video_file_path = os.path.join(download_dir, f"{uuid.uuid4().hex}")
|
||||
video_file_path = os.path.join(
|
||||
get_astrbot_temp_path(), f"videoseg_{uuid.uuid4().hex}"
|
||||
)
|
||||
await download_file(url, video_file_path)
|
||||
if os.path.exists(video_file_path):
|
||||
return os.path.abspath(video_file_path)
|
||||
@@ -445,8 +447,9 @@ class Image(BaseMessageComponent):
|
||||
if url.startswith("base64://"):
|
||||
bs64_data = url.removeprefix("base64://")
|
||||
image_bytes = base64.b64decode(bs64_data)
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
image_file_path = os.path.join(temp_dir, f"{uuid.uuid4()}.jpg")
|
||||
image_file_path = os.path.join(
|
||||
get_astrbot_temp_path(), f"imgseg_{uuid.uuid4()}.jpg"
|
||||
)
|
||||
with open(image_file_path, "wb") as f:
|
||||
f.write(image_bytes)
|
||||
return os.path.abspath(image_file_path)
|
||||
@@ -725,13 +728,12 @@ class File(BaseMessageComponent):
|
||||
"""下载文件"""
|
||||
if not self.url:
|
||||
raise ValueError("Download failed: No URL provided in File component.")
|
||||
download_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
os.makedirs(download_dir, exist_ok=True)
|
||||
download_dir = get_astrbot_temp_path()
|
||||
if self.name:
|
||||
name, ext = os.path.splitext(self.name)
|
||||
filename = f"{name}_{uuid.uuid4().hex[:8]}{ext}"
|
||||
filename = f"fileseg_{name}_{uuid.uuid4().hex[:8]}{ext}"
|
||||
else:
|
||||
filename = f"{uuid.uuid4().hex}"
|
||||
filename = f"fileseg_{uuid.uuid4().hex}"
|
||||
file_path = os.path.join(download_dir, filename)
|
||||
await download_file(self.url, file_path)
|
||||
self.file_ = os.path.abspath(file_path)
|
||||
|
||||
@@ -21,7 +21,7 @@ from astrbot.api.platform import (
|
||||
)
|
||||
from astrbot.core import sp
|
||||
from astrbot.core.platform.astr_message_event import MessageSesion
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
from astrbot.core.utils.io import download_file
|
||||
from astrbot.core.utils.media_utils import (
|
||||
convert_audio_format,
|
||||
@@ -253,9 +253,9 @@ class DingtalkPlatformAdapter(Platform):
|
||||
"downloadCode": download_code,
|
||||
"robotCode": robot_code,
|
||||
}
|
||||
temp_dir = Path(get_astrbot_data_path()) / "temp"
|
||||
temp_dir = Path(get_astrbot_temp_path())
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
f_path = temp_dir / f"dingtalk_file_{uuid.uuid4()}.{ext}"
|
||||
f_path = temp_dir / f"dingtalk_{uuid.uuid4()}.{ext}"
|
||||
async with (
|
||||
aiohttp.ClientSession() as session,
|
||||
session.post(
|
||||
|
||||
@@ -21,7 +21,7 @@ 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.core.utils.astrbot_path import get_astrbot_data_path
|
||||
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 (
|
||||
convert_audio_to_opus,
|
||||
@@ -202,8 +202,11 @@ class LarkMessageEvent(AstrMessageEvent):
|
||||
base64_str = comp.file.removeprefix("base64://")
|
||||
image_data = base64.b64decode(base64_str)
|
||||
# save as temp file
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
file_path = os.path.join(temp_dir, f"{uuid.uuid4()}_test.jpg")
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
file_path = os.path.join(
|
||||
temp_dir,
|
||||
f"lark_image_{uuid.uuid4().hex[:8]}.jpg",
|
||||
)
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(BytesIO(image_data).getvalue())
|
||||
else:
|
||||
|
||||
@@ -21,7 +21,7 @@ try:
|
||||
except Exception:
|
||||
magic = None
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
from .misskey_event import MisskeyPlatformEvent
|
||||
from .misskey_utils import (
|
||||
@@ -498,7 +498,7 @@ class MisskeyPlatformAdapter(Platform):
|
||||
finally:
|
||||
# 清理临时文件
|
||||
if local_path and isinstance(local_path, str):
|
||||
data_temp = os.path.join(get_astrbot_data_path(), "temp")
|
||||
data_temp = get_astrbot_temp_path()
|
||||
if local_path.startswith(data_temp) and os.path.exists(
|
||||
local_path,
|
||||
):
|
||||
|
||||
@@ -19,7 +19,7 @@ from astrbot.api import logger
|
||||
from astrbot.api.event import AstrMessageEvent, MessageChain
|
||||
from astrbot.api.message_components import Image, Plain, Record
|
||||
from astrbot.api.platform import AstrBotMessage, PlatformMetadata
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
from astrbot.core.utils.io import download_image_by_url, file_to_base64
|
||||
from astrbot.core.utils.tencent_record_helper import wav_to_tencent_silk
|
||||
|
||||
@@ -350,10 +350,10 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
elif isinstance(i, Record):
|
||||
if i.file:
|
||||
record_wav_path = await i.convert_to_file_path() # wav 路径
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
record_tecent_silk_path = os.path.join(
|
||||
temp_dir,
|
||||
f"{uuid.uuid4()}.silk",
|
||||
f"qqofficial_{uuid.uuid4()}.silk",
|
||||
)
|
||||
try:
|
||||
duration = await wav_to_tencent_silk(
|
||||
|
||||
@@ -25,7 +25,7 @@ from astrbot.api.platform import (
|
||||
)
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.platform.astr_message_event import MessageSesion
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
from astrbot.core.utils.media_utils import convert_audio_to_wav
|
||||
from astrbot.core.utils.webhook_utils import log_webhook_info
|
||||
|
||||
@@ -344,7 +344,7 @@ class WecomPlatformAdapter(Platform):
|
||||
self.client.media.download,
|
||||
msg.media_id,
|
||||
)
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
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)
|
||||
@@ -400,7 +400,8 @@ class WecomPlatformAdapter(Platform):
|
||||
self.client.media.download,
|
||||
media_id,
|
||||
)
|
||||
path = f"data/temp/wechat_kf_{media_id}.jpg"
|
||||
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)
|
||||
abm.message = [Image(file=path, url=path)]
|
||||
@@ -412,7 +413,7 @@ class WecomPlatformAdapter(Platform):
|
||||
media_id,
|
||||
)
|
||||
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
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)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
from collections.abc import Awaitable, Callable
|
||||
@@ -24,6 +25,7 @@ from astrbot.api.platform import (
|
||||
)
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.platform.astr_message_event import MessageSesion
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
from astrbot.core.utils.media_utils import convert_audio_to_wav
|
||||
from astrbot.core.utils.webhook_utils import log_webhook_info
|
||||
|
||||
@@ -290,12 +292,16 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
|
||||
self.client.media.download,
|
||||
msg.media_id,
|
||||
)
|
||||
path = f"data/temp/wecom_{msg.media_id}.amr"
|
||||
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)
|
||||
|
||||
try:
|
||||
path_wav = f"data/temp/wecom_{msg.media_id}.wav"
|
||||
path_wav = os.path.join(
|
||||
temp_dir,
|
||||
f"weixin_offacc_{msg.media_id}.wav",
|
||||
)
|
||||
path_wav = await convert_audio_to_wav(path, path_wav)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
|
||||
@@ -12,12 +12,13 @@ from httpx import AsyncClient, Timeout
|
||||
|
||||
from astrbot import logger
|
||||
from astrbot.core.config.default import VERSION
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
from ..entities import ProviderType
|
||||
from ..provider import TTSProvider
|
||||
from ..register import register_provider_adapter
|
||||
|
||||
TEMP_DIR = Path("data/temp/azure_tts")
|
||||
TEMP_DIR = Path(get_astrbot_temp_path()) / "azure_tts"
|
||||
TEMP_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ except (
|
||||
): # pragma: no cover - older dashscope versions without Qwen TTS support
|
||||
MultiModalConversation = None
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
from ..entities import ProviderType
|
||||
from ..provider import TTSProvider
|
||||
@@ -45,7 +45,7 @@ class ProviderDashscopeTTSAPI(TTSProvider):
|
||||
if not model:
|
||||
raise RuntimeError("Dashscope TTS model is not configured.")
|
||||
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
if self._is_qwen_tts_model(model):
|
||||
|
||||
@@ -6,7 +6,7 @@ import uuid
|
||||
import edge_tts
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
from ..entities import ProviderType
|
||||
from ..provider import TTSProvider
|
||||
@@ -46,7 +46,7 @@ class ProviderEdgeTTS(TTSProvider):
|
||||
self.set_model("edge_tts")
|
||||
|
||||
async def get_audio(self, text: str) -> str:
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
mp3_path = os.path.join(temp_dir, f"edge_tts_temp_{uuid.uuid4()}.mp3")
|
||||
wav_path = os.path.join(temp_dir, f"edge_tts_{uuid.uuid4()}.wav")
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from httpx import AsyncClient
|
||||
from pydantic import BaseModel, conint
|
||||
|
||||
from astrbot import logger
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
from ..entities import ProviderType
|
||||
from ..provider import TTSProvider
|
||||
@@ -142,7 +142,7 @@ class ProviderFishAudioTTSAPI(TTSProvider):
|
||||
)
|
||||
|
||||
async def get_audio(self, text: str) -> str:
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
path = os.path.join(temp_dir, f"fishaudio_tts_api_{uuid.uuid4()}.wav")
|
||||
self.headers["content-type"] = "application/msgpack"
|
||||
request = await self._generate_request(text)
|
||||
|
||||
@@ -6,7 +6,7 @@ from google import genai
|
||||
from google.genai import types
|
||||
|
||||
from astrbot import logger
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
from ..entities import ProviderType
|
||||
from ..provider import TTSProvider
|
||||
@@ -49,7 +49,7 @@ class ProviderGeminiTTSAPI(TTSProvider):
|
||||
self.voice_name: str = provider_config.get("gemini_tts_voice_name", "Leda")
|
||||
|
||||
async def get_audio(self, text: str) -> str:
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
path = os.path.join(temp_dir, f"gemini_tts_{uuid.uuid4()}.wav")
|
||||
prompt = f"{self.prefix}: {text}" if self.prefix else text
|
||||
response = await self.client.models.generate_content(
|
||||
|
||||
@@ -6,7 +6,7 @@ from astrbot.core import logger
|
||||
from astrbot.core.provider.entities import ProviderType
|
||||
from astrbot.core.provider.provider import TTSProvider
|
||||
from astrbot.core.provider.register import register_provider_adapter
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
try:
|
||||
import genie_tts as genie # type: ignore
|
||||
@@ -54,7 +54,7 @@ class GenieTTSProvider(TTSProvider):
|
||||
return True
|
||||
|
||||
async def get_audio(self, text: str) -> str:
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
filename = f"genie_tts_{uuid.uuid4()}.wav"
|
||||
path = os.path.join(temp_dir, filename)
|
||||
@@ -94,7 +94,7 @@ class GenieTTSProvider(TTSProvider):
|
||||
break
|
||||
|
||||
try:
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
filename = f"genie_tts_{uuid.uuid4()}.wav"
|
||||
path = os.path.join(temp_dir, filename)
|
||||
|
||||
@@ -5,7 +5,7 @@ import uuid
|
||||
import aiohttp
|
||||
|
||||
from astrbot import logger
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
from ..entities import ProviderType
|
||||
from ..provider import TTSProvider
|
||||
@@ -121,7 +121,7 @@ class ProviderGSVTTS(TTSProvider):
|
||||
|
||||
params = self.build_synthesis_params(text)
|
||||
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
path = os.path.join(temp_dir, f"gsv_tts_{uuid.uuid4().hex}.wav")
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import uuid
|
||||
|
||||
import aiohttp
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
from ..entities import ProviderType
|
||||
from ..provider import TTSProvider
|
||||
@@ -29,7 +29,7 @@ class ProviderGSVITTS(TTSProvider):
|
||||
self.emotion = provider_config.get("emotion")
|
||||
|
||||
async def get_audio(self, text: str) -> str:
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
path = os.path.join(temp_dir, f"gsvi_tts_{uuid.uuid4()}.wav")
|
||||
params = {"text": text}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections.abc import AsyncIterator
|
||||
import aiohttp
|
||||
|
||||
from astrbot.api import logger
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
from ..entities import ProviderType
|
||||
from ..provider import TTSProvider
|
||||
@@ -145,7 +145,7 @@ class ProviderMiniMaxTTSAPI(TTSProvider):
|
||||
return b"".join(chunks)
|
||||
|
||||
async def get_audio(self, text: str) -> str:
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
path = os.path.join(temp_dir, f"minimax_tts_api_{uuid.uuid4()}.mp3")
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import httpx
|
||||
from openai import NOT_GIVEN, AsyncOpenAI
|
||||
|
||||
from astrbot import logger
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
from ..entities import ProviderType
|
||||
from ..provider import TTSProvider
|
||||
@@ -46,7 +46,7 @@ class ProviderOpenAITTSAPI(TTSProvider):
|
||||
self.set_model(provider_config.get("model", ""))
|
||||
|
||||
async def get_audio(self, text: str) -> str:
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
path = os.path.join(temp_dir, f"openai_tts_api_{uuid.uuid4()}.wav")
|
||||
async with self.client.audio.speech.with_streaming_response.create(
|
||||
model=self.model_name,
|
||||
|
||||
@@ -8,6 +8,7 @@ import uuid
|
||||
import aiohttp
|
||||
|
||||
from astrbot import logger
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
from ..entities import ProviderType
|
||||
from ..provider import TTSProvider
|
||||
@@ -92,9 +93,12 @@ class ProviderVolcengineTTS(TTSProvider):
|
||||
if "data" in resp_data:
|
||||
audio_data = base64.b64decode(resp_data["data"])
|
||||
|
||||
os.makedirs("data/temp", exist_ok=True)
|
||||
|
||||
file_path = f"data/temp/volcengine_tts_{uuid.uuid4()}.mp3"
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
file_path = os.path.join(
|
||||
temp_dir,
|
||||
f"volcengine_tts_{uuid.uuid4()}.mp3",
|
||||
)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
await loop.run_in_executor(
|
||||
|
||||
@@ -4,7 +4,7 @@ import uuid
|
||||
from openai import NOT_GIVEN, AsyncOpenAI
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
from astrbot.core.utils.io import download_file
|
||||
from astrbot.core.utils.tencent_record_helper import (
|
||||
convert_to_pcm_wav,
|
||||
@@ -65,9 +65,11 @@ class ProviderOpenAIWhisperAPI(STTProvider):
|
||||
if "multimedia.nt.qq.com.cn" in audio_url:
|
||||
is_tencent = True
|
||||
|
||||
name = str(uuid.uuid4())
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
path = os.path.join(temp_dir, name)
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
path = os.path.join(
|
||||
temp_dir,
|
||||
f"whisper_api_{uuid.uuid4().hex[:8]}.input",
|
||||
)
|
||||
await download_file(audio_url, path)
|
||||
audio_url = path
|
||||
|
||||
@@ -79,8 +81,11 @@ class ProviderOpenAIWhisperAPI(STTProvider):
|
||||
|
||||
# 判断是否需要转换
|
||||
if file_format in ["silk", "amr"]:
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
output_path = os.path.join(temp_dir, str(uuid.uuid4()) + ".wav")
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
output_path = os.path.join(
|
||||
temp_dir,
|
||||
f"whisper_api_{uuid.uuid4().hex[:8]}.wav",
|
||||
)
|
||||
|
||||
if file_format == "silk":
|
||||
logger.info(
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import cast
|
||||
import whisper
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
from astrbot.core.utils.io import download_file
|
||||
from astrbot.core.utils.tencent_record_helper import tencent_silk_to_wav
|
||||
|
||||
@@ -58,9 +58,11 @@ class ProviderOpenAIWhisperSelfHost(STTProvider):
|
||||
if "multimedia.nt.qq.com.cn" in audio_url:
|
||||
is_tencent = True
|
||||
|
||||
name = str(uuid.uuid4())
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
path = os.path.join(temp_dir, name)
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
path = os.path.join(
|
||||
temp_dir,
|
||||
f"whisper_selfhost_{uuid.uuid4().hex[:8]}.input",
|
||||
)
|
||||
await download_file(audio_url, path)
|
||||
audio_url = path
|
||||
|
||||
@@ -71,8 +73,11 @@ class ProviderOpenAIWhisperSelfHost(STTProvider):
|
||||
is_silk = await self._is_silk_file(audio_url)
|
||||
if is_silk:
|
||||
logger.info("Converting silk file to wav ...")
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
output_path = os.path.join(temp_dir, str(uuid.uuid4()) + ".wav")
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
output_path = os.path.join(
|
||||
temp_dir,
|
||||
f"whisper_selfhost_{uuid.uuid4().hex[:8]}.wav",
|
||||
)
|
||||
await tencent_silk_to_wav(audio_url, output_path)
|
||||
audio_url = output_path
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from xinference_client.client.restful.async_restful_client import (
|
||||
)
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
from astrbot.core.utils.tencent_record_helper import (
|
||||
convert_to_pcm_wav,
|
||||
tencent_silk_to_wav,
|
||||
@@ -130,11 +130,17 @@ class ProviderXinferenceSTT(STTProvider):
|
||||
logger.info(
|
||||
f"Audio requires conversion ({conversion_type}), using temporary files..."
|
||||
)
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
input_path = os.path.join(temp_dir, str(uuid.uuid4()))
|
||||
output_path = os.path.join(temp_dir, str(uuid.uuid4()) + ".wav")
|
||||
input_path = os.path.join(
|
||||
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",
|
||||
)
|
||||
temp_files.extend([input_path, output_path])
|
||||
|
||||
with open(input_path, "wb") as f:
|
||||
|
||||
@@ -93,7 +93,6 @@ class SkillManager:
|
||||
self.skills_root = skills_root or get_astrbot_skills_path()
|
||||
self.config_path = os.path.join(get_astrbot_data_path(), SKILLS_CONFIG_FILENAME)
|
||||
os.makedirs(self.skills_root, exist_ok=True)
|
||||
os.makedirs(get_astrbot_temp_path(), exist_ok=True)
|
||||
|
||||
def _load_config(self) -> dict:
|
||||
if not os.path.exists(self.config_path):
|
||||
|
||||
@@ -14,7 +14,7 @@ import certifi
|
||||
import psutil
|
||||
from PIL import Image
|
||||
|
||||
from .astrbot_path import get_astrbot_data_path
|
||||
from .astrbot_path import get_astrbot_data_path, get_astrbot_temp_path
|
||||
|
||||
logger = logging.getLogger("astrbot")
|
||||
|
||||
@@ -50,21 +50,10 @@ def port_checker(port: int, host: str = "localhost") -> bool:
|
||||
|
||||
|
||||
def save_temp_img(img: Image.Image | bytes) -> str:
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
# 获得文件创建时间,清除超过 12 小时的
|
||||
try:
|
||||
for f in os.listdir(temp_dir):
|
||||
path = os.path.join(temp_dir, f)
|
||||
if os.path.isfile(path):
|
||||
ctime = os.path.getctime(path)
|
||||
if time.time() - ctime > 3600 * 12:
|
||||
os.remove(path)
|
||||
except Exception as e:
|
||||
print(f"清除临时文件失败: {e}")
|
||||
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
# 获得时间戳
|
||||
timestamp = f"{int(time.time())}_{uuid.uuid4().hex[:8]}"
|
||||
p = os.path.join(temp_dir, f"{timestamp}.jpg")
|
||||
p = os.path.join(temp_dir, f"io_temp_img_{timestamp}.jpg")
|
||||
|
||||
if isinstance(img, Image.Image):
|
||||
img.save(p)
|
||||
|
||||
@@ -10,7 +10,7 @@ import uuid
|
||||
from pathlib import Path
|
||||
|
||||
from astrbot import logger
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
|
||||
async def get_media_duration(file_path: str) -> int | None:
|
||||
@@ -77,9 +77,9 @@ async def convert_audio_to_opus(audio_path: str, output_path: str | None = None)
|
||||
|
||||
# 生成输出文件路径
|
||||
if output_path is None:
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
output_path = os.path.join(temp_dir, f"{uuid.uuid4()}.opus")
|
||||
output_path = os.path.join(temp_dir, f"media_audio_{uuid.uuid4().hex}.opus")
|
||||
|
||||
try:
|
||||
# 使用ffmpeg转换为opus格式
|
||||
@@ -156,9 +156,12 @@ async def convert_video_format(
|
||||
|
||||
# 生成输出文件路径
|
||||
if output_path is None:
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
output_path = os.path.join(temp_dir, f"{uuid.uuid4()}.{output_format}")
|
||||
output_path = os.path.join(
|
||||
temp_dir,
|
||||
f"media_video_{uuid.uuid4().hex}.{output_format}",
|
||||
)
|
||||
|
||||
try:
|
||||
# 使用ffmpeg转换视频格式
|
||||
@@ -227,9 +230,9 @@ async def convert_audio_format(
|
||||
return audio_path
|
||||
|
||||
if output_path is None:
|
||||
temp_dir = Path(get_astrbot_data_path()) / "temp"
|
||||
temp_dir = Path(get_astrbot_temp_path())
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_path = str(temp_dir / f"{uuid.uuid4()}.{output_format}")
|
||||
output_path = str(temp_dir / f"media_audio_{uuid.uuid4().hex}.{output_format}")
|
||||
|
||||
args = ["ffmpeg", "-y", "-i", audio_path]
|
||||
if output_format == "amr":
|
||||
@@ -283,9 +286,9 @@ async def extract_video_cover(
|
||||
) -> str:
|
||||
"""从视频中提取封面图(JPG)。"""
|
||||
if output_path is None:
|
||||
temp_dir = Path(get_astrbot_data_path()) / "temp"
|
||||
temp_dir = Path(get_astrbot_temp_path())
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_path = str(temp_dir / f"{uuid.uuid4()}.jpg")
|
||||
output_path = str(temp_dir / f"media_cover_{uuid.uuid4().hex}.jpg")
|
||||
|
||||
try:
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from astrbot import logger
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
|
||||
def parse_size_to_bytes(value: str | int | float | None) -> int:
|
||||
"""Parse size in MB to bytes."""
|
||||
if value is None:
|
||||
return 0
|
||||
|
||||
try:
|
||||
size_mb = float(str(value).strip())
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
if size_mb <= 0:
|
||||
return 0
|
||||
|
||||
return int(size_mb * 1024**2)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TempFileInfo:
|
||||
path: Path
|
||||
size: int
|
||||
mtime: float
|
||||
|
||||
|
||||
class TempDirCleaner:
|
||||
CONFIG_KEY = "temp_dir_max_size"
|
||||
DEFAULT_MAX_SIZE = 1024
|
||||
CHECK_INTERVAL_SECONDS = 10 * 60
|
||||
CLEANUP_RATIO = 0.30
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
max_size_getter: Callable[[], str | int | float | None],
|
||||
temp_dir: Path | None = None,
|
||||
) -> None:
|
||||
self._max_size_getter = max_size_getter
|
||||
self._temp_dir = temp_dir or Path(get_astrbot_temp_path())
|
||||
self._stop_event = asyncio.Event()
|
||||
|
||||
def _limit_bytes(self) -> int:
|
||||
configured = self._max_size_getter()
|
||||
parsed = parse_size_to_bytes(configured)
|
||||
if parsed <= 0:
|
||||
fallback = parse_size_to_bytes(self.DEFAULT_MAX_SIZE)
|
||||
logger.warning(
|
||||
f"Invalid {self.CONFIG_KEY}={configured!r}, fallback to {self.DEFAULT_MAX_SIZE}MB.",
|
||||
)
|
||||
return fallback
|
||||
return parsed
|
||||
|
||||
def _scan_temp_files(self) -> tuple[int, list[TempFileInfo]]:
|
||||
if not self._temp_dir.exists():
|
||||
return 0, []
|
||||
|
||||
total_size = 0
|
||||
files: list[TempFileInfo] = []
|
||||
for path in self._temp_dir.rglob("*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
try:
|
||||
stat = path.stat()
|
||||
except OSError as e:
|
||||
logger.debug(f"Skip temp file {path} due to stat error: {e}")
|
||||
continue
|
||||
total_size += stat.st_size
|
||||
files.append(
|
||||
TempFileInfo(path=path, size=stat.st_size, mtime=stat.st_mtime)
|
||||
)
|
||||
|
||||
return total_size, files
|
||||
|
||||
def _cleanup_empty_dirs(self) -> None:
|
||||
if not self._temp_dir.exists():
|
||||
return
|
||||
for path in sorted(
|
||||
self._temp_dir.rglob("*"), key=lambda p: len(p.parts), reverse=True
|
||||
):
|
||||
if not path.is_dir():
|
||||
continue
|
||||
try:
|
||||
path.rmdir()
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
def cleanup_once(self) -> None:
|
||||
limit = self._limit_bytes()
|
||||
if limit <= 0:
|
||||
return
|
||||
|
||||
total_size, files = self._scan_temp_files()
|
||||
if total_size <= limit:
|
||||
return
|
||||
|
||||
target_release = max(int(total_size * self.CLEANUP_RATIO), 1)
|
||||
released = 0
|
||||
removed_files = 0
|
||||
|
||||
for file_info in sorted(files, key=lambda item: item.mtime):
|
||||
try:
|
||||
file_info.path.unlink()
|
||||
except OSError as e:
|
||||
logger.warning(f"Failed to delete temp file {file_info.path}: {e}")
|
||||
continue
|
||||
|
||||
released += file_info.size
|
||||
removed_files += 1
|
||||
if released >= target_release:
|
||||
break
|
||||
|
||||
self._cleanup_empty_dirs()
|
||||
|
||||
logger.warning(
|
||||
f"Temp dir exceeded limit ({total_size} > {limit}). "
|
||||
f"Removed {removed_files} files, released {released} bytes "
|
||||
f"(target {target_release} bytes).",
|
||||
)
|
||||
|
||||
async def run(self) -> None:
|
||||
logger.info(
|
||||
f"TempDirCleaner started. interval={self.CHECK_INTERVAL_SECONDS}s "
|
||||
f"cleanup_ratio={self.CLEANUP_RATIO}",
|
||||
)
|
||||
while not self._stop_event.is_set():
|
||||
try:
|
||||
# File-system traversal and deletion are blocking operations.
|
||||
# Run cleanup in a worker thread to avoid blocking the event loop.
|
||||
await asyncio.to_thread(self.cleanup_once)
|
||||
except Exception as e:
|
||||
logger.error(f"TempDirCleaner run failed: {e}", exc_info=True)
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
self._stop_event.wait(),
|
||||
timeout=self.CHECK_INTERVAL_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
continue
|
||||
|
||||
logger.info("TempDirCleaner stopped.")
|
||||
|
||||
async def stop(self) -> None:
|
||||
self._stop_event.set()
|
||||
@@ -7,7 +7,7 @@ import wave
|
||||
from io import BytesIO
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
|
||||
async def tencent_silk_to_wav(silk_path: str, output_path: str) -> str:
|
||||
@@ -117,12 +117,13 @@ async def audio_to_tencent_silk_base64(audio_path: str) -> tuple[str, float]:
|
||||
except ImportError as e:
|
||||
raise Exception("未安装 pilk: pip install pilk") from e
|
||||
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
# 是否需要转换为 WAV
|
||||
ext = os.path.splitext(audio_path)[1].lower()
|
||||
temp_wav = tempfile.NamedTemporaryFile(
|
||||
prefix="tencent_record_",
|
||||
suffix=".wav",
|
||||
delete=False,
|
||||
dir=temp_dir,
|
||||
@@ -140,6 +141,7 @@ async def audio_to_tencent_silk_base64(audio_path: str) -> tuple[str, float]:
|
||||
rate = wav_file.getframerate()
|
||||
|
||||
silk_path = tempfile.NamedTemporaryFile(
|
||||
prefix="tencent_record_",
|
||||
suffix=".silk",
|
||||
delete=False,
|
||||
dir=temp_dir,
|
||||
|
||||
@@ -12,6 +12,7 @@ from quart import request
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
||||
from astrbot.core.provider.provider import EmbeddingProvider, RerankProvider
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
from ..utils import generate_tsne_visualization
|
||||
from .route import Response, Route, RouteContext
|
||||
@@ -703,7 +704,10 @@ class KnowledgeBaseRoute(Route):
|
||||
file_name = file.filename
|
||||
|
||||
# 保存到临时文件
|
||||
temp_file_path = f"data/temp/{uuid.uuid4()}_{file_name}"
|
||||
temp_file_path = os.path.join(
|
||||
get_astrbot_temp_path(),
|
||||
f"kb_upload_{uuid.uuid4()}_{file_name}",
|
||||
)
|
||||
await file.save(temp_file_path)
|
||||
|
||||
try:
|
||||
|
||||
@@ -12,7 +12,7 @@ from quart import websocket
|
||||
from astrbot import logger
|
||||
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
||||
from astrbot.core.platform.sources.webchat.webchat_queue_mgr import webchat_queue_mgr
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
from .route import Route, RouteContext
|
||||
|
||||
@@ -60,7 +60,7 @@ class LiveChatSession:
|
||||
|
||||
# 组装 WAV 文件
|
||||
try:
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
audio_path = os.path.join(temp_dir, f"live_audio_{uuid.uuid4()}.wav")
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from astrbot.core.star.filter.permission import PermissionTypeFilter
|
||||
from astrbot.core.star.filter.regex import RegexFilter
|
||||
from astrbot.core.star.star_handler import EventType, star_handlers_registry
|
||||
from astrbot.core.star.star_manager import PluginManager
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
from .route import Response, Route, RouteContext
|
||||
|
||||
@@ -431,7 +432,10 @@ class PluginRoute(Route):
|
||||
file = await request.files
|
||||
file = file["file"]
|
||||
logger.info(f"正在安装用户上传的插件 {file.filename}")
|
||||
file_path = f"data/temp/{file.filename}"
|
||||
file_path = os.path.join(
|
||||
get_astrbot_temp_path(),
|
||||
f"plugin_upload_{file.filename}",
|
||||
)
|
||||
await file.save(file_path)
|
||||
plugin_info = await self.plugin_manager.install_plugin_from_file(file_path)
|
||||
# self.core_lifecycle.restart()
|
||||
|
||||
@@ -819,6 +819,10 @@
|
||||
"description": "Log File Max Size (MB)",
|
||||
"hint": "Rotate when exceeding this size; default 20MB."
|
||||
},
|
||||
"temp_dir_max_size": {
|
||||
"description": "Temp Directory Size Limit (MB)",
|
||||
"hint": "Limits total size of data/temp in MB. The system checks every 10 minutes, and when exceeded, deletes oldest files first to release about 30% of current size."
|
||||
},
|
||||
"trace_log_enable": {
|
||||
"description": "Enable Trace File Logging",
|
||||
"hint": "Write trace events to a separate file (does not change console output)."
|
||||
|
||||
@@ -822,6 +822,10 @@
|
||||
"description": "日志文件大小上限 (MB)",
|
||||
"hint": "超过大小后自动轮转,默认 20MB。"
|
||||
},
|
||||
"temp_dir_max_size": {
|
||||
"description": "临时目录大小上限 (MB)",
|
||||
"hint": "用于限制 data/temp 目录总大小,单位为 MB。系统每 10 分钟检查一次,超限时按文件修改时间从旧到新删除,释放约 30% 当前体积。"
|
||||
},
|
||||
"trace_log_enable": {
|
||||
"description": "启用 Trace 文件日志",
|
||||
"hint": "将 Trace 事件写入独立文件(不影响控制台输出)。"
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from astrbot.core.utils.temp_dir_cleaner import TempDirCleaner, parse_size_to_bytes
|
||||
|
||||
|
||||
def test_parse_size_to_bytes():
|
||||
assert parse_size_to_bytes("1024") == 1024 * 1024**2
|
||||
assert parse_size_to_bytes(2048) == 2048 * 1024**2
|
||||
assert parse_size_to_bytes("0.5") == int(0.5 * 1024**2)
|
||||
assert parse_size_to_bytes(0) == 0
|
||||
assert parse_size_to_bytes("invalid") == 0
|
||||
|
||||
|
||||
def _write_file(path: Path, size: int, mtime: float) -> None:
|
||||
path.write_bytes(b"x" * size)
|
||||
os.utime(path, (mtime, mtime))
|
||||
|
||||
|
||||
def test_cleanup_once_releases_30_percent_and_prefers_old_files(tmp_path):
|
||||
temp_dir = tmp_path / "temp"
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
base_time = time.time() - 1000
|
||||
file_old = temp_dir / "old.bin"
|
||||
file_mid = temp_dir / "mid.bin"
|
||||
file_new = temp_dir / "new.bin"
|
||||
_write_file(file_old, 400, base_time)
|
||||
_write_file(file_mid, 300, base_time + 10)
|
||||
_write_file(file_new, 300, base_time + 20)
|
||||
|
||||
cleaner = TempDirCleaner(max_size_getter=lambda: "0.0008", temp_dir=temp_dir)
|
||||
cleaner.cleanup_once()
|
||||
|
||||
remaining_size = sum(f.stat().st_size for f in temp_dir.rglob("*") if f.is_file())
|
||||
assert remaining_size <= 600
|
||||
assert not file_old.exists()
|
||||
assert file_mid.exists()
|
||||
assert file_new.exists()
|
||||
|
||||
|
||||
def test_cleanup_once_noop_when_below_limit(tmp_path):
|
||||
temp_dir = tmp_path / "temp"
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
file_path = temp_dir / "a.bin"
|
||||
_write_file(file_path, 100, time.time())
|
||||
|
||||
cleaner = TempDirCleaner(max_size_getter=lambda: "1", temp_dir=temp_dir)
|
||||
cleaner.cleanup_once()
|
||||
|
||||
assert file_path.exists()
|
||||
Reference in New Issue
Block a user