From 770201c48c2aa1d19490347569ef33b1bedc918e Mon Sep 17 00:00:00 2001 From: InfinityPacer <160988576+InfinityPacer@users.noreply.github.com> Date: Sat, 11 Jul 2026 18:15:00 +0800 Subject: [PATCH] fix(plugin): exclude build dependencies from runtime copies (#6096) --- app/core/plugin.py | 2 +- app/helper/plugin.py | 2 +- tests/test_plugin_helper.py | 40 ++++++++++++++++++++++++++++----- tests/test_plugin_local_sync.py | 10 ++++++++- 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/app/core/plugin.py b/app/core/plugin.py index d2f8079d..9d6b4863 100644 --- a/app/core/plugin.py +++ b/app/core/plugin.py @@ -517,7 +517,7 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton): source_dir, dest_dir, dirs_exist_ok=True, - ignore=shutil.ignore_patterns("__pycache__", "*.pyc", ".DS_Store") + ignore=shutil.ignore_patterns("__pycache__", "*.pyc", ".DS_Store", "node_modules") ) PluginManager()._recent_local_sync[pid] = time.time() logger.info(f"已同步本地插件 {pid}:{source_dir} -> {dest_dir}") diff --git a/app/helper/plugin.py b/app/helper/plugin.py index f04477d0..ebc4810e 100644 --- a/app/helper/plugin.py +++ b/app/helper/plugin.py @@ -758,7 +758,7 @@ class PluginHelper(metaclass=WeakSingleton): source_dir, dest_dir, dirs_exist_ok=True, - ignore=shutil.ignore_patterns("__pycache__", "*.pyc", ".DS_Store") + ignore=shutil.ignore_patterns("__pycache__", "*.pyc", ".DS_Store", "node_modules") ) return True, "" except Exception as e: diff --git a/tests/test_plugin_helper.py b/tests/test_plugin_helper.py index eeb6eed0..8933f40d 100644 --- a/tests/test_plugin_helper.py +++ b/tests/test_plugin_helper.py @@ -1534,22 +1534,50 @@ class TestPluginHelper: assert "" == message assert seen_versions - def test_install_local_delegates_local_repo_url(self, monkeypatch): + def test_install_local_copies_runtime_assets_without_build_dependencies(self, monkeypatch, tmp_path): """ - local:// 来源由本地插件安装路径处理,不访问远端仓库。 + local:// 来源保留运行资产,但不把本地前端构建依赖复制到运行目录。 """ try: from app.helper.plugin import PluginHelper except ModuleNotFoundError as exc: pytest.skip(f"missing dependency: {exc}") - helper = PluginHelper() - monkeypatch.setattr(helper, "install_local", lambda pid, repo_url, force_install=False: (True, f"{pid}:{repo_url}")) + repo_path = tmp_path / "local-plugins" + source_dir = repo_path / "plugins.v2" / PLUGIN_ID.lower() + remote_entry = source_dir / "dist" / "assets" / "remoteEntry.js" + remote_entry.parent.mkdir(parents=True) + remote_entry.write_text("export default {}\n", encoding="utf-8") + dependency_file = source_dir / "node_modules" / "example" / "index.js" + dependency_file.parent.mkdir(parents=True) + dependency_file.write_text("module.exports = {}\n", encoding="utf-8") - success, message = helper.install(PLUGIN_ID, f"local://{PLUGIN_ID}?path=/tmp/plugins") + runtime_root = tmp_path / "runtime-plugins" + helper = PluginHelper() + monkeypatch.setattr( + helper, + "get_local_plugin_candidate", + lambda *_args, **_kwargs: { + "path": source_dir, + "repo_path": repo_path, + "package_version": "v2", + "version": "1.0.0", + }, + ) + monkeypatch.setattr("app.helper.plugin.PLUGIN_DIR", runtime_root) + monkeypatch.setattr(helper, "refresh_persistent_plugin_backup", lambda _pid: True) + + success, message = helper.install( + PLUGIN_ID, + helper.make_local_repo_url(PLUGIN_ID, repo_path, "v2"), + force_install=True, + ) assert success - assert message.startswith(f"{PLUGIN_ID}:local://{PLUGIN_ID}") + assert "" == message + runtime_dir = runtime_root / PLUGIN_ID.lower() + assert (runtime_dir / "dist" / "assets" / "remoteEntry.js").is_file() + assert not (runtime_dir / "node_modules").exists() def test_install_release_download_failure_falls_back_to_filelist(self, monkeypatch): """ diff --git a/tests/test_plugin_local_sync.py b/tests/test_plugin_local_sync.py index f7e13cf0..90319b4b 100644 --- a/tests/test_plugin_local_sync.py +++ b/tests/test_plugin_local_sync.py @@ -21,7 +21,7 @@ def plugin_manager() -> Iterator[PluginManager]: def _build_local_plugin_repo(tmp_path: Path) -> tuple[Path, Path]: - """构造带系统版本要求的本地 v2 插件仓库。""" + """构造带运行资产、构建依赖和系统版本要求的本地 v2 插件仓库。""" repo_path = tmp_path / "local-plugins" source_dir = repo_path / "plugins.v2" / "demoplugin" source_file = source_dir / "__init__.py" @@ -32,6 +32,12 @@ def _build_local_plugin_repo(tmp_path: Path) -> tuple[Path, Path]: " plugin_name = 'Demo'\n", encoding="utf-8", ) + remote_entry = source_dir / "dist" / "assets" / "remoteEntry.js" + remote_entry.parent.mkdir(parents=True) + remote_entry.write_text("export default {}\n", encoding="utf-8") + dependency_file = source_dir / "node_modules" / "example" / "index.js" + dependency_file.parent.mkdir(parents=True) + dependency_file.write_text("module.exports = {}\n", encoding="utf-8") (repo_path / "package.v2.json").write_text( '{"DemoPlugin": {"version": "1.0.0", "system_version": ">=2.13.11"}}', encoding="utf-8", @@ -62,6 +68,8 @@ def test_dev_local_plugin_candidate_keeps_hot_sync_allowed_when_system_version_l assert candidate.get("compatible") is not False assert plugin_manager._sync_local_plugin_if_installed("DemoPlugin", candidate) assert (runtime_dir / "__init__.py").read_text(encoding="utf-8") == source_file.read_text(encoding="utf-8") + assert (runtime_dir / "dist" / "assets" / "remoteEntry.js").is_file() + assert not (runtime_dir / "node_modules").exists() def test_local_plugin_candidate_keeps_system_version_gate_outside_dev(