From 212d8cc10122bd58a40b5db339c370ba26bff03b Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Mon, 3 Nov 2025 00:02:21 +0800 Subject: [PATCH] =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=8F=B7=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=EF=BC=8C=E5=B8=B8=E9=87=8F=E7=A7=BB=E5=8A=A8=E8=87=B3base?= =?UTF-8?q?=E5=8C=85=20(#3281)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 赵天乐(tyler zhao) <189870321+tyler-ztl@users.noreply.github.com> --- .github/copilot-instructions.md | 98 ++++++++-------- astrbot/__main__.py | 95 ++++++++++++++++ astrbot/base/__init__.py | 2 + astrbot/base/abc.py | 10 ++ astrbot/base/const.py | 9 ++ astrbot/base/paths.py | 23 +++- astrbot/cli/__init__.py | 2 +- astrbot/cli/__main__.py | 15 +-- astrbot/cli/commands/cmd_plug.py | 5 +- astrbot/cli/utils/basic.py | 13 ++- astrbot/core/__init__.py | 4 - astrbot/core/config/astrbot_config.py | 4 +- astrbot/core/config/default.py | 10 +- astrbot/core/message/components.py | 54 ++++----- .../sources/webchat/webchat_adapter.py | 4 +- .../wechatpadpro/wechatpadpro_adapter.py | 18 ++- .../core/provider/sources/dashscope_tts.py | 4 +- .../core/provider/sources/edge_tts_source.py | 8 +- .../core/provider/sources/volcengine_tts.py | 7 +- .../provider/sources/whisper_api_source.py | 10 +- astrbot/core/star/config.py | 9 +- astrbot/core/utils/io.py | 12 +- astrbot/dashboard/routes/chat.py | 4 +- astrbot/dashboard/server.py | 4 +- main.py | 105 +----------------- packages/python_interpreter/main.py | 14 ++- packages/reminder/main.py | 3 +- 27 files changed, 284 insertions(+), 262 deletions(-) create mode 100644 astrbot/__main__.py create mode 100644 astrbot/base/const.py diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 05a4559e..30d71d61 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,63 +1,63 @@ -# AstrBot Development Instructions +# AstrBot 开发指南 -AstrBot is a multi-platform LLM chatbot and development framework written in Python with a Vue.js dashboard. It supports multiple messaging platforms (QQ, Telegram, Discord, etc.) and various LLM providers (OpenAI, Anthropic, Google Gemini, etc.). +AstrBot 是一个使用 Python 编写、配备 Vue.js 仪表盘的多平台 LLM 聊天机器人开发框架。它支持多个消息平台(QQ、Telegram、Discord 等)和多种 LLM 提供商(OpenAI、Anthropic、Google Gemini 等)。 -Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here. +始终优先参考这些指南,仅在遇到与此处信息不符的意外情况时才回退到搜索或 bash 命令。 -## Working Effectively +## 高效工作 -### Bootstrap and Install Dependencies -- **Python 3.10+ required** - Check `.python-version` file -- Install UV package manager: `pip install uv` -- Install project dependencies: `uv sync` -- takes 6-7 minutes. NEVER CANCEL. Set timeout to 10+ minutes. -- Create required directories: `mkdir -p data/plugins data/config data/temp` +### 引导和安装依赖 +- **需要 Python 3.10+** - 检查 `.python-version` 文件 +- 安装 UV 包管理器:`pip install uv` +- 安装项目依赖:`uv sync` -- 很快几分钟。绝不要取消。设置超时时间为 10+ 分钟。 +- 创建必需的目录:`mkdir -p data/plugins data/config data/temp` -### Running the Application -- Run main application: `uv run main.py` -- starts in ~3 seconds -- Application creates WebUI on http://localhost:6185 (default credentials: `astrbot`/`astrbot`) -- Application loads plugins automatically from `packages/` and `data/plugins/` directories +### 运行应用程序 +- 运行主应用程序:`uv run main.py` -- 约 3 秒启动 +- 应用程序在 http://localhost:6185 创建 WebUI(默认凭据:`astrbot`/`astrbot`) +- 应用程序自动从 `packages/` 和 `data/plugins/` 目录加载插件 -### Dashboard Build (Vue.js/Node.js) -- **Prerequisites**: Node.js 20+ and npm 10+ required -- Navigate to dashboard: `cd dashboard` -- Install dashboard dependencies: `npm install` -- takes 2-3 minutes. NEVER CANCEL. Set timeout to 5+ minutes. -- Build dashboard: `npm run build` -- takes 25-30 seconds. NEVER CANCEL. -- Dashboard creates optimized production build in `dashboard/dist/` +### 仪表盘构建(Vue.js/Node.js) +- **前置要求**:需要 Node.js 20+ 和 npm 10+ +- 导航到仪表盘:`cd dashboard` +- 安装仪表盘依赖:`npm install` -- 需要 2-3 分钟。绝不要取消。设置超时时间为 5+ 分钟。 +- 构建仪表盘:`npm run build` -- 需要 25-30 秒。绝不要取消。 +- 仪表盘在 `dashboard/dist/` 创建优化的生产构建 -### Testing -- Do not generate test files for now. +### 测试 +- 暂时不要生成测试文件。 -### Code Quality and Linting -- Install ruff linter: `uv add --dev ruff` -- Check code style: `uv run ruff check .` -- takes <1 second -- Check formatting: `uv run ruff format --check .` -- takes <1 second -- Fix formatting: `uv run ruff format .` -- **ALWAYS** run `uv run ruff check .` and `uv run ruff format .` before committing changes +### 代码质量和检查 +- 安装 ruff 检查器:`uv add --dev ruff` +- 检查代码风格:`uv run ruff check .` -- 耗时 <1 秒 +- 检查格式:`uv run ruff format --check .` -- 耗时 <1 秒 +- 修复格式:`uv run ruff format .` +- **始终**在提交更改前运行 `uv run ruff check .` 和 `uv run ruff format .` -### Plugin Development -- Plugins load from `packages/` (built-in) and `data/plugins/` (user-installed) -- Plugin system supports function tools and message handlers -- Key plugins: python_interpreter, web_searcher, astrbot, reminder, session_controller +### 插件开发 +- 插件从 `packages/`(内置)和 `data/plugins/`(用户安装)加载 +- 插件系统支持函数工具和消息处理器 +- 关键插件:python_interpreter、web_searcher、astrbot、reminder、session_controller -### Common Issues and Workarounds -- **Dashboard download fails**: Known issue with "division by zero" error - application still works -- **Import errors in tests**: Ensure `uv run` is used to run tests in proper environment -=- **Build timeouts**: Always set appropriate timeouts (10+ minutes for uv sync, 5+ minutes for npm install) +### 常见问题和解决方法 +- **仪表盘下载失败**:已知的"除以零"错误问题 - 应用程序仍可正常工作 +- **测试中的导入错误**:确保使用 `uv run` 在适当的环境中运行测试 +- **构建超时**:始终设置适当的超时时间(uv sync 为 10+ 分钟,npm install 为 5+ 分钟) -## CI/CD Integration -- GitHub Actions workflows in `.github/workflows/` -- Docker builds supported via `Dockerfile` -- Pre-commit hooks enforce ruff formatting and linting +## CI/CD 集成 +- GitHub Actions 工作流在 `.github/workflows/` 中 +- 通过 `Dockerfile` 支持 Docker 构建 +- Pre-commit 钩子强制执行 ruff 格式化和检查 -## Docker Support -- Primary deployment method: `docker run soulter/astrbot:latest` -- Compose file available: `compose.yml` -- Exposes ports: 6185 (WebUI), 6195 (WeChat), 6199 (QQ), etc. -- Volume mount required: `./data:/AstrBot/data` +## Docker 支持 +- 主要部署方法:`docker run soulter/astrbot:latest` +- 可用的 Compose 文件:`compose.yml` +- 暴露端口:6185(WebUI)、6195(WeChat)、6199(QQ)等 +- 需要挂载卷:`./data:/AstrBot/data` -## Multi-language Support -- Documentation in Chinese (README.md), English (README_en.md), Japanese (README_ja.md) -- UI supports internationalization -- Default language is Chinese +## 多语言支持 +- 文档包括中文(README.md)、英文(README_en.md)、日文(README_ja.md) +- UI 支持国际化 +- 默认语言为中文 -Remember: This is a production chatbot framework with real users. Always test thoroughly and ensure changes don't break existing functionality. +请记住:这是一个有真实用户的生产聊天机器人框架。始终进行彻底测试,确保更改不会破坏现有功能。 diff --git a/astrbot/__main__.py b/astrbot/__main__.py new file mode 100644 index 00000000..c3620ce8 --- /dev/null +++ b/astrbot/__main__.py @@ -0,0 +1,95 @@ +import argparse +import asyncio +import mimetypes +import os +import sys + +from astrbot.base import LOGO, AstrbotPaths +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.io import download_dashboard, get_dashboard_version + + +def check_env(): + if not (sys.version_info.major == 3 and sys.version_info.minor >= 10): + logger.error("请使用 Python3.10+ 运行本项目。") + exit() + + # os.makedirs("data/config", exist_ok=True) + # os.makedirs("data/plugins", exist_ok=True) + # os.makedirs("data/temp", 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 os.path.exists(webui_dir): + logger.info(f"使用指定的 WebUI 目录: {webui_dir}") + return webui_dir + logger.warning(f"指定的 WebUI 目录 {webui_dir} 不存在,将使用默认逻辑。") + + data_dist_path = str(AstrbotPaths.astrbot_root / "dist") + if os.path.exists(data_dist_path): + 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.critical(f"下载管理面板文件失败: {e}。") + return None + + logger.info("管理面板下载完成。") + return data_dist_path + + +def 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) + + # 检查仪表板文件 + webui_dir = asyncio.run(check_dashboard_files(args.webui_dir)) + + db = db_helper + + # 打印 logo + logger.info(LOGO) + + core_lifecycle = InitialLoader(db, log_broker) + core_lifecycle.webui_dir = webui_dir + asyncio.run(core_lifecycle.start()) + + +if __name__ == "__main__": + main() diff --git a/astrbot/base/__init__.py b/astrbot/base/__init__.py index dfca4cd8..069d4804 100644 --- a/astrbot/base/__init__.py +++ b/astrbot/base/__init__.py @@ -1,7 +1,9 @@ from .abc import IAstrbotPaths +from .const import LOGO from .paths import AstrbotPaths __all__ = [ "IAstrbotPaths", "AstrbotPaths", + "LOGO", ] diff --git a/astrbot/base/abc.py b/astrbot/base/abc.py index 27820609..78642fac 100644 --- a/astrbot/base/abc.py +++ b/astrbot/base/abc.py @@ -43,6 +43,16 @@ class IAstrbotPaths(ABC): def log(self) -> Path: """获取模块日志目录.""" + @property + @abstractmethod + def temp(self) -> Path: + """获取模块临时目录.""" + + @property + @abstractmethod + def plugins(self) -> Path: + """获取插件目录.""" + @abstractmethod def reload(self) -> None: """重新加载环境变量.""" diff --git a/astrbot/base/const.py b/astrbot/base/const.py new file mode 100644 index 00000000..8478cebe --- /dev/null +++ b/astrbot/base/const.py @@ -0,0 +1,9 @@ +LOGO = r""" + ___ _______.___________..______ .______ ______ .___________. + / \ / | || _ \ | _ \ / __ \ | | + / ^ \ | (----`---| |----`| |_) | | |_) | | | | | `---| |----` + / /_\ \ \ \ | | | / | _ < | | | | | | + / _____ \ .----) | | | | |\ \----.| |_) | | `--' | | | +/__/ \__\ |_______/ |__| | _| `._____||______/ \______/ |__| + +""" diff --git a/astrbot/base/paths.py b/astrbot/base/paths.py index e8f02883..3cd21fe8 100644 --- a/astrbot/base/paths.py +++ b/astrbot/base/paths.py @@ -19,13 +19,15 @@ if TYPE_CHECKING: class AstrbotPaths(IAstrbotPaths): - """Class to manage and provide paths used by Astrbot Canary.""" + """统一化路径获取.""" load_dotenv() astrbot_root: ClassVar[Path] = Path( getenv("ASTRBOT_ROOT", Path.home() / ".astrbot") ).absolute() + _instances: ClassVar[dict[str, AstrbotPaths]] = {} + def __init__(self, name: str) -> None: self.name: str = name # 确保根目录存在 @@ -35,8 +37,11 @@ class AstrbotPaths(IAstrbotPaths): def getPaths(cls, name: str) -> AstrbotPaths: """返回Paths实例,用于访问模块的各类目录.""" normalized_name: NormalizedName = canonicalize_name(name) + if normalized_name in cls._instances: + return cls._instances[normalized_name] instance: AstrbotPaths = cls(normalized_name) instance.name = normalized_name + cls._instances[normalized_name] = instance return instance @property @@ -68,7 +73,7 @@ class AstrbotPaths(IAstrbotPaths): @property def data(self) -> Path: - """返回模块数据目录.""" + """返回模块/插件数据目录.""" data_path = self.astrbot_root / "data" / self.name data_path.mkdir(parents=True, exist_ok=True) return data_path @@ -80,6 +85,20 @@ class AstrbotPaths(IAstrbotPaths): log_path.mkdir(parents=True, exist_ok=True) return log_path + @property + def temp(self) -> Path: + """返回模块临时文件目录.""" + temp_path = self.astrbot_root / "temp" / self.name + temp_path.mkdir(parents=True, exist_ok=True) + return temp_path + + @property + def plugins(self) -> Path: + """返回插件目录.""" + plugin_path = self.astrbot_root / "plugins" / self.name + plugin_path.mkdir(parents=True, exist_ok=True) + return plugin_path + @classmethod def is_root(cls, path: Path) -> bool: """检查路径是否为 Astrbot 根目录.""" diff --git a/astrbot/cli/__init__.py b/astrbot/cli/__init__.py index 8d1eee0b..86b0e6a0 100644 --- a/astrbot/cli/__init__.py +++ b/astrbot/cli/__init__.py @@ -1 +1 @@ -__version__ = "3.5.23" +"""AstrBot CLI入口""" diff --git a/astrbot/cli/__main__.py b/astrbot/cli/__main__.py index 40c46de7..24f6c3a4 100644 --- a/astrbot/cli/__main__.py +++ b/astrbot/cli/__main__.py @@ -1,27 +1,22 @@ """AstrBot CLI入口""" import sys +from importlib.metadata import version import click -from . import __version__ +from astrbot.base import LOGO + from .commands import conf, init, plug, run -logo_tmpl = r""" - ___ _______.___________..______ .______ ______ .___________. - / \ / | || _ \ | _ \ / __ \ | | - / ^ \ | (----`---| |----`| |_) | | |_) | | | | | `---| |----` - / /_\ \ \ \ | | | / | _ < | | | | | | - / _____ \ .----) | | | | |\ \----.| |_) | | `--' | | | -/__/ \__\ |_______/ |__| | _| `._____||______/ \______/ |__| -""" +__version__ = version("astrbot") @click.group() @click.version_option(__version__, prog_name="AstrBot") def cli() -> None: """The AstrBot CLI""" - click.echo(logo_tmpl) + click.echo(LOGO) click.echo("Welcome to AstrBot CLI!") click.echo(f"AstrBot CLI version: {__version__}") diff --git a/astrbot/cli/commands/cmd_plug.py b/astrbot/cli/commands/cmd_plug.py index a1099de1..5118ae37 100644 --- a/astrbot/cli/commands/cmd_plug.py +++ b/astrbot/cli/commands/cmd_plug.py @@ -4,6 +4,8 @@ from pathlib import Path import click +from astrbot.base import AstrbotPaths + from ..utils import ( PluginStatus, build_plug_list, @@ -47,8 +49,7 @@ def display_plugins(plugins, title=None, color=None): @click.argument("name") def new(name: str): """创建新插件""" - base_path = _get_data_path() - plug_path = base_path / "plugins" / name + plug_path = AstrbotPaths.getPaths(name).plugins if plug_path.exists(): raise click.ClickException(f"插件 {name} 已存在") diff --git a/astrbot/cli/utils/basic.py b/astrbot/cli/utils/basic.py index 84535564..d73be4f5 100644 --- a/astrbot/cli/utils/basic.py +++ b/astrbot/cli/utils/basic.py @@ -9,18 +9,19 @@ from astrbot.base import AstrbotPaths def check_astrbot_root(path: str | Path) -> bool: """检查路径是否为 AstrBot 根目录""" warnings.warn( - "请使用 AstrbotPaths 类代替本模块中的函数", - DeprecationWarning, - stacklevel=2, + "请使用 AstrbotPaths 类代替本模块中的函数", + DeprecationWarning, + stacklevel=2, ) return AstrbotPaths.is_root(Path(path)) + def get_astrbot_root() -> Path: """获取Astrbot根目录路径""" warnings.warn( - "请使用 AstrbotPaths 类代替本模块中的函数", - DeprecationWarning, - stacklevel=2, + "请使用 AstrbotPaths 类代替本模块中的函数", + DeprecationWarning, + stacklevel=2, ) return AstrbotPaths.astrbot_root diff --git a/astrbot/core/__init__.py b/astrbot/core/__init__.py index 30b81af6..86e3ba5c 100644 --- a/astrbot/core/__init__.py +++ b/astrbot/core/__init__.py @@ -9,10 +9,6 @@ from astrbot.core.utils.shared_preferences import SharedPreferences from astrbot.core.utils.t2i.renderer import HtmlRenderer from .log import LogBroker, LogManager # noqa -from .utils.astrbot_path import get_astrbot_data_path - -# 初始化数据存储文件夹 -os.makedirs(get_astrbot_data_path(), exist_ok=True) DEMO_MODE = os.getenv("DEMO_MODE", False) diff --git a/astrbot/core/config/astrbot_config.py b/astrbot/core/config/astrbot_config.py index 786d29c8..08e68a25 100644 --- a/astrbot/core/config/astrbot_config.py +++ b/astrbot/core/config/astrbot_config.py @@ -3,11 +3,11 @@ import json import logging import os -from astrbot.core.utils.astrbot_path import get_astrbot_data_path +from astrbot.base import AstrbotPaths from .default import DEFAULT_CONFIG, DEFAULT_VALUE_MAP -ASTRBOT_CONFIG_PATH = os.path.join(get_astrbot_data_path(), "cmd_config.json") +ASTRBOT_CONFIG_PATH = str(AstrbotPaths.astrbot_root / "cmd_config.json") logger = logging.getLogger("astrbot") diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index d8ccb1a2..c59dcce7 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -1,11 +1,13 @@ """如需修改配置,请在 `data/cmd_config.json` 中修改或者在管理面板中可视化修改。""" -import os +from importlib.metadata import version -from astrbot.core.utils.astrbot_path import get_astrbot_data_path +from astrbot.base import AstrbotPaths -VERSION = "4.5.1" -DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db") +VERSION = version("astrbot") +""" 警告,请使用version函数获取版本,此变量兼容保留 """ + +DB_PATH = str(AstrbotPaths.astrbot_root / "data_v4.db") # 默认配置 DEFAULT_CONFIG = { diff --git a/astrbot/core/message/components.py b/astrbot/core/message/components.py index 43e3bf0e..5ad3098e 100644 --- a/astrbot/core/message/components.py +++ b/astrbot/core/message/components.py @@ -21,7 +21,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -import asyncio import base64 import json import os @@ -30,8 +29,8 @@ from enum import Enum from pydantic.v1 import BaseModel +from astrbot.base import AstrbotPaths 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.io import download_file, download_image_by_url, file_to_base64 @@ -153,8 +152,8 @@ 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") + temp_dir = str(AstrbotPaths.astrbot_root / "temp") + file_path = str(AstrbotPaths.astrbot_root / "temp" / f"{uuid.uuid4()}.jpg") with open(file_path, "wb") as f: f.write(image_bytes) return os.path.abspath(file_path) @@ -242,8 +241,8 @@ 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}") + download_dir = str(AstrbotPaths.astrbot_root / "temp") + video_file_path = str(AstrbotPaths.astrbot_root / "temp" / f"{uuid.uuid4().hex}") await download_file(url, video_file_path) if os.path.exists(video_file_path): return os.path.abspath(video_file_path) @@ -442,8 +441,8 @@ 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") + temp_dir = str(AstrbotPaths.astrbot_root / "temp") + image_file_path = str(AstrbotPaths.astrbot_root / "temp" / f"{uuid.uuid4()}.jpg") with open(image_file_path, "wb") as f: f.write(image_bytes) return os.path.abspath(image_file_path) @@ -527,7 +526,7 @@ class Reply(BaseMessageComponent): class Poke(BaseMessageComponent): - type: str = ComponentType.Poke + type = ComponentType.Poke id: int | None = 0 qq: int | None = 0 @@ -654,33 +653,19 @@ class File(BaseMessageComponent): @property def file(self) -> str: - """获取文件路径,如果文件不存在但有URL,则同步下载文件 + """获取本地文件路径(仅返回已存在的文件) + + ⚠️ 警告:此属性不会自动下载文件! + - 如果文件已存在,返回绝对路径 + - 如果只有 URL 没有本地文件,返回空字符串 + - 需要下载文件请使用 `await get_file()` 方法 Returns: - str: 文件路径 + str: 文件的绝对路径,如果文件不存在则返回空字符串 """ if self.file_ and os.path.exists(self.file_): return os.path.abspath(self.file_) - - if self.url: - try: - loop = asyncio.get_event_loop() - if loop.is_running(): - logger.warning( - "不可以在异步上下文中同步等待下载! " - "这个警告通常发生于某些逻辑试图通过 .file 获取文件消息段的文件内容。" - "请使用 await get_file() 代替直接获取 .file 字段", - ) - return "" - # 等待下载完成 - loop.run_until_complete(self._download_file()) - - if self.file_ and os.path.exists(self.file_): - return os.path.abspath(self.file_) - except Exception as e: - logger.error(f"文件下载失败: {e}") - return "" @file.setter @@ -714,15 +699,18 @@ class File(BaseMessageComponent): if self.url: await self._download_file() - return os.path.abspath(self.file_) + if self.file_: + return os.path.abspath(self.file_) return "" async def _download_file(self): """下载文件""" - download_dir = os.path.join(get_astrbot_data_path(), "temp") + if not self.url: + raise ValueError("No URL provided for download") + download_dir = str(AstrbotPaths.astrbot_root / "temp") os.makedirs(download_dir, exist_ok=True) - file_path = os.path.join(download_dir, f"{uuid.uuid4().hex}") + file_path = str(AstrbotPaths.astrbot_root / "temp" / f"{uuid.uuid4().hex}") await download_file(self.url, file_path) self.file_ = os.path.abspath(file_path) diff --git a/astrbot/core/platform/sources/webchat/webchat_adapter.py b/astrbot/core/platform/sources/webchat/webchat_adapter.py index ad6a5077..2c851a58 100644 --- a/astrbot/core/platform/sources/webchat/webchat_adapter.py +++ b/astrbot/core/platform/sources/webchat/webchat_adapter.py @@ -6,6 +6,7 @@ from collections.abc import Awaitable, Callable from typing import Any from astrbot import logger +from astrbot.base import AstrbotPaths from astrbot.core.message.components import Image, Plain, Record from astrbot.core.message.message_event_result import MessageChain from astrbot.core.platform import ( @@ -16,7 +17,6 @@ from astrbot.core.platform import ( PlatformMetadata, ) from astrbot.core.platform.astr_message_event import MessageSesion -from astrbot.core.utils.astrbot_path import get_astrbot_data_path from ...register import register_platform_adapter from .webchat_event import WebChatMessageEvent @@ -79,7 +79,7 @@ class WebChatAdapter(Platform): self.config = platform_config self.settings = platform_settings self.unique_session = platform_settings["unique_session"] - self.imgs_dir = os.path.join(get_astrbot_data_path(), "webchat", "imgs") + self.imgs_dir = str(AstrbotPaths.astrbot_root / "webchat" / "imgs") os.makedirs(self.imgs_dir, exist_ok=True) self.metadata = PlatformMetadata( diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index 165375cd..e3516891 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -12,6 +12,7 @@ import websockets from astrbot import logger from astrbot.api.message_components import At, Image, Plain, Record from astrbot.api.platform import Platform, PlatformMetadata +from astrbot.base import AstrbotPaths from astrbot.core.message.message_event_result import MessageChain from astrbot.core.platform.astr_message_event import MessageSesion from astrbot.core.platform.astrbot_message import ( @@ -19,7 +20,6 @@ from astrbot.core.platform.astrbot_message import ( MessageMember, MessageType, ) -from astrbot.core.utils.astrbot_path import get_astrbot_data_path from ...register import register_platform_adapter from .wechatpadpro_message_event import WeChatPadProMessageEvent @@ -68,9 +68,8 @@ class WeChatPadProAdapter(Platform): self.base_url = f"http://{self.host}:{self.port}" self.auth_key = None # 用于保存生成的授权码 self.wxid = None # 用于保存登录成功后的 wxid - self.credentials_file = os.path.join( - get_astrbot_data_path(), - "wechatpadpro_credentials.json", + self.credentials_file = str( + AstrbotPaths.astrbot_root / "wechatpadpro_credentials.json" ) # 持久化文件路径 self.ws_handle_task = None @@ -155,8 +154,8 @@ class WeChatPadProAdapter(Platform): } try: # 确保数据目录存在 - data_dir = os.path.dirname(self.credentials_file) - os.makedirs(data_dir, exist_ok=True) + config_dir = AstrbotPaths.astrbot_root / "config" + config_dir.mkdir(parents=True, exist_ok=True) with open(self.credentials_file, "w") as f: json.dump(credentials, f) except Exception as e: @@ -787,10 +786,9 @@ class WeChatPadProAdapter(Platform): voice_bs64_data = voice_resp.get("Data", {}).get("Base64", None) if voice_bs64_data: voice_bs64_data = base64.b64decode(voice_bs64_data) - temp_dir = os.path.join(get_astrbot_data_path(), "temp") - file_path = os.path.join( - temp_dir, - f"wechatpadpro_voice_{abm.message_id}.silk", + temp_dir = str(AstrbotPaths.astrbot_root / "temp") + file_path = str( + AstrbotPaths.astrbot_root / "temp" / f"wechatpadpro_voice_{abm.message_id}.silk" ) async with await anyio.open_file(file_path, "wb") as f: diff --git a/astrbot/core/provider/sources/dashscope_tts.py b/astrbot/core/provider/sources/dashscope_tts.py index 44e9965c..8d2475c9 100644 --- a/astrbot/core/provider/sources/dashscope_tts.py +++ b/astrbot/core/provider/sources/dashscope_tts.py @@ -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.base import AstrbotPaths 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 = str(AstrbotPaths.astrbot_root / "temp") os.makedirs(temp_dir, exist_ok=True) if self._is_qwen_tts_model(model): diff --git a/astrbot/core/provider/sources/edge_tts_source.py b/astrbot/core/provider/sources/edge_tts_source.py index 8bbf6232..75225e87 100644 --- a/astrbot/core/provider/sources/edge_tts_source.py +++ b/astrbot/core/provider/sources/edge_tts_source.py @@ -5,8 +5,8 @@ import uuid import edge_tts +from astrbot.base import AstrbotPaths from astrbot.core import logger -from astrbot.core.utils.astrbot_path import get_astrbot_data_path from ..entities import ProviderType from ..provider import TTSProvider @@ -46,8 +46,10 @@ 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") - mp3_path = os.path.join(temp_dir, f"edge_tts_temp_{uuid.uuid4()}.mp3") + temp_dir = str(AstrbotPaths.astrbot_root / "temp") + mp3_path = str( + AstrbotPaths.astrbot_root / "temp" / f"edge_tts_temp_{uuid.uuid4()}.mp3" + ) wav_path = os.path.join(temp_dir, f"edge_tts_{uuid.uuid4()}.wav") # 构建 Edge TTS 参数 diff --git a/astrbot/core/provider/sources/volcengine_tts.py b/astrbot/core/provider/sources/volcengine_tts.py index f5d758f5..1bdcbf7c 100644 --- a/astrbot/core/provider/sources/volcengine_tts.py +++ b/astrbot/core/provider/sources/volcengine_tts.py @@ -1,13 +1,13 @@ import asyncio import base64 import json -import os import traceback import uuid import aiohttp from astrbot import logger +from astrbot.base import AstrbotPaths from ..entities import ProviderType from ..provider import TTSProvider @@ -92,9 +92,10 @@ class ProviderVolcengineTTS(TTSProvider): if "data" in resp_data: audio_data = base64.b64decode(resp_data["data"]) - os.makedirs("data/temp", exist_ok=True) + temp_dir = AstrbotPaths.astrbot_root / "temp" + temp_dir.mkdir(parents=True, exist_ok=True) - file_path = f"data/temp/volcengine_tts_{uuid.uuid4()}.mp3" + file_path = str(temp_dir / f"volcengine_tts_{uuid.uuid4()}.mp3") loop = asyncio.get_running_loop() await loop.run_in_executor( diff --git a/astrbot/core/provider/sources/whisper_api_source.py b/astrbot/core/provider/sources/whisper_api_source.py index 8f6d9e29..c6b57ce9 100644 --- a/astrbot/core/provider/sources/whisper_api_source.py +++ b/astrbot/core/provider/sources/whisper_api_source.py @@ -3,8 +3,8 @@ import uuid from openai import NOT_GIVEN, AsyncOpenAI +from astrbot.base import AstrbotPaths from astrbot.core import logger -from astrbot.core.utils.astrbot_path import get_astrbot_data_path from astrbot.core.utils.io import download_file from astrbot.core.utils.tencent_record_helper import tencent_silk_to_wav @@ -53,8 +53,7 @@ class ProviderOpenAIWhisperAPI(STTProvider): is_tencent = True name = str(uuid.uuid4()) - temp_dir = os.path.join(get_astrbot_data_path(), "temp") - path = os.path.join(temp_dir, name) + path = str(AstrbotPaths.astrbot_root / "temp" / name) await download_file(audio_url, path) audio_url = path @@ -65,8 +64,9 @@ class ProviderOpenAIWhisperAPI(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") + output_path = str( + AstrbotPaths.astrbot_root / "temp" / f"{uuid.uuid4()}.wav" + ) await tencent_silk_to_wav(audio_url, output_path) audio_url = output_path diff --git a/astrbot/core/star/config.py b/astrbot/core/star/config.py index a9af974c..2ddb70e7 100644 --- a/astrbot/core/star/config.py +++ b/astrbot/core/star/config.py @@ -3,7 +3,7 @@ import json import os -from astrbot.core.utils.astrbot_path import get_astrbot_data_path +from astrbot.base import AstrbotPaths def load_config(namespace: str) -> dict | bool: @@ -11,7 +11,7 @@ def load_config(namespace: str) -> dict | bool: namespace: str, 配置的唯一识别符,也就是配置文件的名字。 返回值: 当配置文件存在时,返回 namespace 对应配置文件的内容dict,否则返回 False。 """ - path = os.path.join(get_astrbot_data_path(), "config", f"{namespace}.json") + path = str(AstrbotPaths.astrbot_root / "config" / f"{namespace}.json") if not os.path.exists(path): return False with open(path, encoding="utf-8-sig") as f: @@ -41,8 +41,7 @@ def put_config(namespace: str, name: str, key: str, value, description: str): if not isinstance(value, (str, int, float, bool, list)): raise ValueError("value 只支持 str, int, float, bool, list 类型。") - config_dir = os.path.join(get_astrbot_data_path(), "config") - path = os.path.join(config_dir, f"{namespace}.json") + path = str(AstrbotPaths.astrbot_root / "config" / f"{namespace}.json") if not os.path.exists(path): with open(path, "w", encoding="utf-8-sig") as f: @@ -70,7 +69,7 @@ def update_config(namespace: str, key: str, value): key: str, 配置项的键。 value: str, int, float, bool, list, 配置项的值。 """ - path = os.path.join(get_astrbot_data_path(), "config", f"{namespace}.json") + path = str(AstrbotPaths.astrbot_root / "config" / f"{namespace}.json") if not os.path.exists(path): raise FileNotFoundError(f"配置文件 {namespace}.json 不存在。") with open(path, encoding="utf-8-sig") as f: diff --git a/astrbot/core/utils/io.py b/astrbot/core/utils/io.py index 03549dc9..6eb2a28a 100644 --- a/astrbot/core/utils/io.py +++ b/astrbot/core/utils/io.py @@ -14,6 +14,8 @@ import certifi import psutil from PIL import Image +from astrbot.base import AstrbotPaths + from .astrbot_path import get_astrbot_data_path logger = logging.getLogger("astrbot") @@ -50,11 +52,11 @@ def port_checker(port: int, host: str = "localhost"): def save_temp_img(img: Image.Image | str) -> str: - temp_dir = os.path.join(get_astrbot_data_path(), "temp") + temp_dir = str(AstrbotPaths.astrbot_root / "temp") # 获得文件创建时间,清除超过 12 小时的 try: for f in os.listdir(temp_dir): - path = os.path.join(temp_dir, f) + path = str(AstrbotPaths.astrbot_root / "temp" / f) if os.path.isfile(path): ctime = os.path.getctime(path) if time.time() - ctime > 3600 * 12: @@ -64,7 +66,7 @@ def save_temp_img(img: Image.Image | str) -> str: # 获得时间戳 timestamp = f"{int(time.time())}_{uuid.uuid4().hex[:8]}" - p = os.path.join(temp_dir, f"{timestamp}.jpg") + p = str(AstrbotPaths.astrbot_root / "temp" / f"{timestamp}.jpg") if isinstance(img, Image.Image): img.save(p) @@ -205,9 +207,9 @@ def get_local_ip_addresses(): async def get_dashboard_version(): - dist_dir = os.path.join(get_astrbot_data_path(), "dist") + dist_dir = str(AstrbotPaths.astrbot_root / "dist") if os.path.exists(dist_dir): - version_file = os.path.join(dist_dir, "assets", "version") + version_file = str(AstrbotPaths.astrbot_root / "dist" / "assets" / "version") if os.path.exists(version_file): with open(version_file, encoding="utf-8") as f: v = f.read().strip() diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py index 5156e14e..b1175143 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -7,12 +7,12 @@ from contextlib import asynccontextmanager from quart import Response as QuartResponse from quart import g, make_response, request +from astrbot.base import AstrbotPaths from astrbot.core import logger from astrbot.core.core_lifecycle import AstrBotCoreLifecycle from astrbot.core.db import BaseDatabase from astrbot.core.platform.astr_message_event import MessageSession 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 .route import Response, Route, RouteContext @@ -47,7 +47,7 @@ class ChatRoute(Route): } self.core_lifecycle = core_lifecycle self.register_routes() - self.imgs_dir = os.path.join(get_astrbot_data_path(), "webchat", "imgs") + self.imgs_dir = str(AstrbotPaths.astrbot_root / "webchat" / "imgs") os.makedirs(self.imgs_dir, exist_ok=True) self.supported_imgs = ["jpg", "jpeg", "png", "gif", "webp"] diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index 84976f2b..9f4f7f4d 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -8,11 +8,11 @@ import psutil from quart import Quart, g, jsonify, request from quart.logging import default_handler +from astrbot.base import AstrbotPaths from astrbot.core import logger from astrbot.core.config.default import VERSION from astrbot.core.core_lifecycle import AstrBotCoreLifecycle from astrbot.core.db import BaseDatabase -from astrbot.core.utils.astrbot_path import get_astrbot_data_path from astrbot.core.utils.io import get_local_ip_addresses from .routes import * @@ -39,7 +39,7 @@ class AstrBotDashboard: self.data_path = os.path.abspath(webui_dir) else: self.data_path = os.path.abspath( - os.path.join(get_astrbot_data_path(), "dist"), + str(AstrbotPaths.astrbot_root / "dist") ) self.app = Quart("dashboard", static_folder=self.data_path, static_url_path="/") diff --git a/main.py b/main.py index 60879f06..33d48126 100644 --- a/main.py +++ b/main.py @@ -1,105 +1,4 @@ -import argparse -import asyncio -import mimetypes -import os -import sys -from pathlib import Path - -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_data_path -from astrbot.core.utils.io import download_dashboard, get_dashboard_version - -# 将父目录添加到 sys.path -sys.path.append(Path(__file__).parent.as_posix()) - -logo_tmpl = r""" - ___ _______.___________..______ .______ ______ .___________. - / \ / | || _ \ | _ \ / __ \ | | - / ^ \ | (----`---| |----`| |_) | | |_) | | | | | `---| |----` - / /_\ \ \ \ | | | / | _ < | | | | | | - / _____ \ .----) | | | | |\ \----.| |_) | | `--' | | | -/__/ \__\ |_______/ |__| | _| `._____||______/ \______/ |__| - -""" - - -def check_env(): - if not (sys.version_info.major == 3 and sys.version_info.minor >= 10): - logger.error("请使用 Python3.10+ 运行本项目。") - exit() - - os.makedirs("data/config", exist_ok=True) - os.makedirs("data/plugins", exist_ok=True) - os.makedirs("data/temp", 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 os.path.exists(webui_dir): - 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 os.path.exists(data_dist_path): - 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.critical(f"下载管理面板文件失败: {e}。") - return None - - logger.info("管理面板下载完成。") - return data_dist_path - +from astrbot.__main__ import main 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) - - # 检查仪表板文件 - webui_dir = asyncio.run(check_dashboard_files(args.webui_dir)) - - db = db_helper - - # 打印 logo - logger.info(logo_tmpl) - - core_lifecycle = InitialLoader(db, log_broker) - core_lifecycle.webui_dir = webui_dir - asyncio.run(core_lifecycle.start()) + main() diff --git a/packages/python_interpreter/main.py b/packages/python_interpreter/main.py index 35a2f269..c6c1b743 100644 --- a/packages/python_interpreter/main.py +++ b/packages/python_interpreter/main.py @@ -14,7 +14,7 @@ from astrbot.api import llm_tool, logger, star from astrbot.api.event import AstrMessageEvent, MessageEventResult, filter from astrbot.api.message_components import File, Image from astrbot.api.provider import ProviderRequest -from astrbot.core.utils.astrbot_path import get_astrbot_data_path +from astrbot.base import AstrbotPaths from astrbot.core.utils.io import download_file, download_image_by_url PROMPT = """ @@ -91,7 +91,7 @@ DEFAULT_CONFIG = { }, "docker_host_astrbot_abs_path": "", } -PATH = os.path.join(get_astrbot_data_path(), "config", "python_interpreter.json") +PATH = str(AstrbotPaths.astrbot_root / "config" / "python_interpreter.json") class Main(star.Star): @@ -101,13 +101,15 @@ class Main(star.Star): self.context = context self.curr_dir = os.path.dirname(os.path.abspath(__file__)) - self.shared_path = os.path.join("data", "py_interpreter_shared") + self.shared_path = str(AstrbotPaths.astrbot_root / "py_interpreter_shared") if not os.path.exists(self.shared_path): # 复制 api.py 到 shared 目录 os.makedirs(self.shared_path, exist_ok=True) shared_api_file = os.path.join(self.curr_dir, "shared", "api.py") shutil.copy(shared_api_file, self.shared_path) - self.workplace_path = os.path.join("data", "py_interpreter_workplace") + self.workplace_path = str( + AstrbotPaths.astrbot_root / "py_interpreter_workplace" + ) os.makedirs(self.workplace_path, exist_ok=True) self.user_file_msg_buffer = defaultdict(list) @@ -212,8 +214,8 @@ class Main(star.Star): file_path = await comp.get_file() if file_path.startswith("http"): name = comp.name if comp.name else uuid.uuid4().hex[:8] - temp_dir = os.path.join(get_astrbot_data_path(), "temp") - path = os.path.join(temp_dir, name) + temp_dir = str(AstrbotPaths.astrbot_root / "temp") + path = str(AstrbotPaths.astrbot_root / "temp" / name) await download_file(file_path, path) else: path = file_path diff --git a/packages/reminder/main.py b/packages/reminder/main.py index eaeec8d7..52d45e6b 100644 --- a/packages/reminder/main.py +++ b/packages/reminder/main.py @@ -8,6 +8,7 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler from astrbot.api import llm_tool, logger, star from astrbot.api.event import AstrMessageEvent, MessageEventResult, filter +from astrbot.base import AstrbotPaths from astrbot.core.utils.astrbot_path import get_astrbot_data_path @@ -27,7 +28,7 @@ class Main(star.Star): self.scheduler = AsyncIOScheduler(timezone=self.timezone) # set and load config - reminder_file = os.path.join(get_astrbot_data_path(), "astrbot-reminder.json") + reminder_file = str(AstrbotPaths.astrbot_root / "astrbot-reminder.json") if not os.path.exists(reminder_file): with open(reminder_file, "w", encoding="utf-8") as f: f.write("{}")