diff --git a/app/helper/package_installer.py b/app/helper/package.py similarity index 100% rename from app/helper/package_installer.py rename to app/helper/package.py diff --git a/app/helper/plugin.py b/app/helper/plugin.py index e8f3cc22..c2681d9d 100644 --- a/app/helper/plugin.py +++ b/app/helper/plugin.py @@ -29,7 +29,7 @@ from requests import Response from app.core.cache import cached, is_fresh from app.core.config import settings from app.db.systemconfig_oper import SystemConfigOper -from app.helper.package_installer import PackageInstallRequest, build_package_install_strategies +from app.helper.package import PackageInstallRequest, build_package_install_strategies from app.log import logger from app.schemas.types import SystemConfigKey from app.utils.http import RequestUtils, AsyncRequestUtils diff --git a/docker/Dockerfile b/docker/Dockerfile index 716b22c7..b0a8b08a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -28,6 +28,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ wget \ git \ + gh \ busybox \ tini \ cron \ diff --git a/docs/rules/05-architecture.md b/docs/rules/05-architecture.md index 498ac258..10db1667 100644 --- a/docs/rules/05-architecture.md +++ b/docs/rules/05-architecture.md @@ -31,6 +31,10 @@ The application is structured as four distinct layers. Each layer has a defined ## Layer Responsibilities and Boundaries +### Shared File Placement Rule + +Before creating a new file under `app/api/endpoints/`, `app/chain/`, `app/helper/`, or `app/utils/`, first check whether the capability belongs in an existing domain file. Prefer extending that file when the domain already exists. Create a new file only for a genuinely new domain or standalone reusable concern, and name it with a single noun according to `07-naming-conventions.md`. + ### Entrypoint Layer **Directories:** `app/api/endpoints/`, `moviepilot` (CLI), `app/agent/`, scheduler callbacks, webhook handlers, message interactions. @@ -41,7 +45,7 @@ The application is structured as four distinct layers. Each layer has a defined - Any logic that coordinates multiple modules, triggers events, touches caches, or combines workflows must be moved into `chain`. **Rules:** -- Prefer adding new endpoints to an existing domain file. Create a new endpoint file only when introducing a new top-level resource domain. +- Prefer adding new endpoints to an existing domain file. Create a new endpoint file only when introducing a new top-level resource domain, and use a single-noun filename. - After adding a new endpoint, register it in `app/api/apiv1.py`. - Endpoints must not contain business logic that belongs in `chain`. @@ -166,4 +170,4 @@ The application is structured as four distinct layers. Each layer has a defined | Few dozen lines of private logic in one chain or module | Private function in the same file; do not create a new helper | | New module category or subtype | Also update `app/schemas/types.py` | -*Last Updated: 2026-05-25* +*Last Updated: 2026-06-23* diff --git a/docs/rules/06-code-styles.md b/docs/rules/06-code-styles.md index 5ac2fe80..80141f1d 100644 --- a/docs/rules/06-code-styles.md +++ b/docs/rules/06-code-styles.md @@ -105,6 +105,8 @@ except: - One primary class per file is the norm for chains, modules, and helpers. - Private helper functions in the same file are preferable to extracting a new helper for single-use logic. +- Under `app/api/endpoints/`, `app/chain/`, `app/helper/`, and `app/utils/`, add code to an existing domain file whenever the domain already exists. +- New files under those directories must use a single noun filename such as `package.py`; avoid role-suffix names such as `package_installer.py` unless an established framework convention requires it. - Keep files focused on one domain concern. --- @@ -118,4 +120,4 @@ except: - Do not add noisy markers like `# change starts here`, `# important`, or `# this is a fix`. - Do not write comments that restate what the code already clearly says. -*Last Updated: 2026-05-25* +*Last Updated: 2026-06-23* diff --git a/docs/rules/07-naming-conventions.md b/docs/rules/07-naming-conventions.md index dbb6f369..83c3344e 100644 --- a/docs/rules/07-naming-conventions.md +++ b/docs/rules/07-naming-conventions.md @@ -8,7 +8,8 @@ All new code must follow these conventions. Consistent naming is how the codebas | Context | Convention | Examples | |---|---|---| -| Python source files | `snake_case.py` | `download_chain.py`, `qbittorrent.py` | +| Python source files | `snake_case.py` | `download.py`, `qbittorrent.py`, `package.py` | +| New domain files under `app/api/endpoints/`, `app/chain/`, `app/helper/`, `app/utils/` | Single noun `snake_case.py`; prefer an existing domain file before adding a new one | `package.py`, `plugin.py`, `torrent.py` | | Module package directories | `snake_case/` | `qbittorrent/`, `synologychat/` | | Test files | `test_.py` | `test_download_chain.py`, `test_subscribe_endpoint.py` | | Alembic migrations | Auto-generated by Alembic; do not rename | `20240101_add_column.py` | @@ -99,4 +100,4 @@ All new code must follow these conventions. Consistent naming is how the codebas | `SystemConfigOper().get("RssUrls")` | `SystemConfigOper().get(SystemConfigKey.RssUrls)` | | `class subscribe_oper:` | `class SubscribeOper:` | -*Last Updated: 2026-05-25* +*Last Updated: 2026-06-23* diff --git a/scripts/dev/simulate_package_installer.py b/scripts/dev/simulate_package_installer.py index 0c7819d3..b244ae48 100644 --- a/scripts/dev/simulate_package_installer.py +++ b/scripts/dev/simulate_package_installer.py @@ -8,7 +8,7 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) -from app.helper.package_installer import PackageInstallRequest, build_package_install_strategies +from app.helper.package import PackageInstallRequest, build_package_install_strategies def sample(name: str, request: PackageInstallRequest) -> None: diff --git a/tests/test_package_installer.py b/tests/test_package_installer.py index fd9a0fa2..73afa230 100644 --- a/tests/test_package_installer.py +++ b/tests/test_package_installer.py @@ -3,7 +3,7 @@ from __future__ import annotations from pathlib import Path from unittest.mock import patch -from app.helper.package_installer import ( +from app.helper.package import ( PackageInstallRequest, build_package_install_env, build_package_install_strategies, @@ -106,7 +106,7 @@ def test_build_strategies_uses_pip_only_when_uv_missing(tmp_path): config_dir=tmp_path / "config", ) - with patch("app.helper.package_installer._find_uv", return_value=None): + with patch("app.helper.package._find_uv", return_value=None): strategies = build_package_install_strategies(request) assert [strategy.strategy_name for strategy in strategies] == ["pip:直连"] diff --git a/tests/test_plugin_helper.py b/tests/test_plugin_helper.py index 89fb7700..c566808c 100644 --- a/tests/test_plugin_helper.py +++ b/tests/test_plugin_helper.py @@ -667,7 +667,7 @@ class TestPluginHelper: uv_bin.parent.mkdir(parents=True) uv_bin.write_text("", encoding="utf-8") - with patch("app.helper.package_installer._find_uv", return_value=uv_bin), \ + with patch("app.helper.package._find_uv", return_value=uv_bin), \ patch.object(PluginHelper, "_PluginHelper__get_protected_runtime_packages", return_value={}), \ patch.object(PluginHelper, "_PluginHelper__run_runtime_healthcheck", return_value=(True, "")), \ patch("app.helper.plugin.SystemUtils.execute_with_subprocess", side_effect=fake_execute), \ @@ -830,7 +830,7 @@ class TestPluginHelper: return_value={} ): with patch("app.helper.plugin.SystemUtils.execute_with_subprocess", side_effect=fake_execute): - with patch("app.helper.package_installer._find_uv", return_value=None): + with patch("app.helper.package._find_uv", return_value=None): success, message = PluginHelper.pip_install_with_fallback(requirements_file) assert success @@ -867,7 +867,7 @@ class TestPluginHelper: return_value={"fastapi": Version("0.115.14")} ): with patch("app.helper.plugin.SystemUtils.execute_with_subprocess", side_effect=fake_execute): - with patch("app.helper.package_installer._find_uv", return_value=None): + with patch("app.helper.package._find_uv", return_value=None): success, message = PluginHelper.pip_install_with_fallback(requirements_file) assert success @@ -913,7 +913,7 @@ class TestPluginHelper: return_value={"fastapi": Version("0.115.14")} ): with patch("app.helper.plugin.SystemUtils.execute_with_subprocess", side_effect=fake_execute): - with patch("app.helper.package_installer._find_uv", return_value=None): + with patch("app.helper.package._find_uv", return_value=None): success, message = PluginHelper.pip_install_with_fallback(requirements_file) assert not success @@ -942,7 +942,7 @@ class TestPluginHelper: req = root / "plugin-requirements.txt" req.write_text("demo\n", encoding="utf-8") - with patch("app.helper.package_installer._find_uv", return_value=None), \ + with patch("app.helper.package._find_uv", return_value=None), \ patch.object(PluginHelper, "_PluginHelper__get_protected_runtime_packages", return_value={}), \ patch.object( PluginHelper, @@ -990,7 +990,7 @@ class TestPluginHelper: uv_bin.parent.mkdir(parents=True) uv_bin.write_text("", encoding="utf-8") - with patch("app.helper.package_installer._find_uv", return_value=uv_bin), \ + with patch("app.helper.package._find_uv", return_value=uv_bin), \ patch.object(PluginHelper, "_PluginHelper__get_protected_runtime_packages", return_value={}), \ patch.object( PluginHelper, @@ -1014,7 +1014,7 @@ class TestPluginHelper: assert len(seen_install_commands) == 1 assert repair_calls - def test_repair_main_runtime_dependencies_uses_package_installer_semantics(self): + def test_repair_main_runtime_dependencies_uses_package_helper_semantics(self): """ 主运行环境恢复与插件安装使用同一套 cache、index、proxy 和安全日志语义。 """ @@ -1037,7 +1037,7 @@ class TestPluginHelper: uv_bin.parent.mkdir(parents=True) uv_bin.write_text("", encoding="utf-8") - with patch("app.helper.package_installer._find_uv", return_value=uv_bin), \ + with patch("app.helper.package._find_uv", return_value=uv_bin), \ patch("app.helper.plugin.settings.CONFIG_DIR", str(root / "config")), \ patch("app.helper.plugin.settings.PACKAGE_CACHE_ROOT", str(root / "custom-package-cache")), \ patch("app.helper.plugin.settings.PIP_PROXY", "https://user:pass@mirror.example/simple"), \