refactor(plugin): 移除已无调用者的物理分身源码改写器 (#6683)

物理分身是早期实现:复制整个插件目录,再用字符串替换改写副本源码里的类名、
plugin_config_prefix 和联邦构建产物。虚拟实例接管后,分身不再复制目录,
由装载器改写类身份、按实例 ID 隔离配置与数据,这套改写器随之失去调用者。

求证依据(全仓 git grep,覆盖 app/、tests/、scripts/、docs/、skills/):

- PluginPackageManager.clone() 无任何生产调用者。分身的唯一生产路径是
  POST /api/v1/plugin/clone/{plugin_id} -> PluginManager.clone_plugin()
  -> PluginCloneService.clone(),后者只写 PluginInstance 记录与隔离配置,
  不碰包适配器。clone() 也没有经 PluginSystemServices 暴露出去。
- clone() 的四个私有实现 _modify_plugin_files / _modify_python_file /
  _modify_federation_files / _rename_federation_assets,消费者只有 clone()
  自身与 PluginSystemServices 的同名透传。
- PluginSystemServices 的四个透传,消费者只有 PluginManager 的 _modify_* shim。
- PluginManager 的四个 shim 无任何调用者。动态可达性已单独排查:
  全仓没有 getattr/字符串反射指向这些名字;app/runtime/compat/manifest.py
  不登记它们的别名(只有 app.core.plugin -> 本模块的模块级映射);
  app/sdk/ 不再导出;PluginManager 与 PluginPackageManager 都没有
  __getattr__ 兜底,属性不存在即 AttributeError;官方插件基线
  official-plugin-baseline.json 零命中。
- @observe_compat_facade("PluginManager") 只包装类上已存在的方法,
  不会凭空合成属性,因此 shim 删除后 enforce_facade 再也匹配不到
  notices.py 里那四条 key,登记随之成为不可达数据,一并删除。

替代路径:这四条弃用声明当初承诺的替代目标是
get_plugin_system().package._modify_*(),那是一次「门面搬到适配器」的
重定位登记,本次连同能力一起下线;分身能力本身的替代路径是虚拟实例
PluginCloneService.clone(),它已在生产链路上,见
docs/v2-to-v3-overview.md §3.6「不复制插件源码,也能保持各实例配置隔离」。
既有磁盘上的旧物理分身不受影响:卸载路径读取 is_clone 属性并调用
remove_plugin_package() 的逻辑原样保留,只是不再产生新的物理分身。

这是对已公告弃用 API(DeprecationStage.SILENT,since v3.0.0)的正式移除。

连带改动:
- package.py 的 import re 仅服务于 _modify_python_file,同步移除;模块
  docstring 去掉「分身处理」。
- system.py 的 typing.cast 仅服务于被删透传,同步移除。
- tests/test_plugin_package_manager.py::test_clone_rewrites_python_and_federation_assets
  是这套改写器的唯一测试,且只覆盖已无调用者的路径,随实现一并删除。
  (删的是测试函数不是测试文件,tests/run.py 的分片装箱不变。)
- mypy 低水位随实现下降:app/runtime/extensions/plugin/manager.py 的
  assignment 8->4(四个 shim 的 version/icon 参数 str = None)、
  no-untyped-def 12->11(_rename_federation_assets 缺返回标注),
  fixture 与 docs/refactor/optimization-checklist.md 的全量 mypy 计数
  9,371 -> 9,366 同步更新。依赖、复杂度、ruff、并发等其余基线无变化。

Co-authored-by: jxxghp <jxxghp@gmail.com>
This commit is contained in:
Aqr-K
2026-09-14 07:19:46 +08:00
committed by GitHub
parent 5e232ca14d
commit dd4ae3e8d5
6 changed files with 4 additions and 389 deletions

View File

@@ -1,4 +1,4 @@
"""插件包文件安装、快照恢复和分身处理适配器。"""
"""插件包文件安装和快照恢复适配器。"""
from __future__ import annotations
@@ -6,7 +6,6 @@ import asyncio
import errno
import hashlib
import io
import re
import shutil
import stat
import uuid
@@ -2358,217 +2357,3 @@ class PluginPackageManager:
exc_info=True,
)
return False
def clone(
self,
*,
plugin_id: str,
clone_id: str,
original_class_name: str,
suffix: str,
name: str,
description: str,
version: Optional[str] = None,
icon: Optional[str] = None,
) -> tuple[bool, str]:
"""复制并改写插件分身文件,任一步失败都删除不完整目标。"""
original_dir = self.__plugin_dir(plugin_id)
clone_dir = self.__plugin_dir(clone_id)
if not original_dir.is_dir():
return False, f"原插件目录 {original_dir} 不存在"
if clone_dir.exists():
return False, f"分身插件 {clone_id} 已存在"
checkpoint = self.checkpoint(clone_id)
try:
shutil.copytree(original_dir, clone_dir)
success, message = self._modify_plugin_files(
plugin_dir=clone_dir,
original_class_name=original_class_name,
suffix=suffix,
name=name,
description=description,
version=version,
icon=icon,
)
if not success:
self.rollback(checkpoint)
return False, message
self.commit(checkpoint)
logger.info(f"已复制插件目录:{original_dir} -> {clone_dir}")
return True, "文件修改成功"
except Exception as err:
try:
self.rollback(checkpoint)
except Exception as rollback_err:
logger.error(
f"清理插件分身 {clone_id} 失败:{rollback_err}",
exc_info=True,
)
return False, f"创建插件分身文件失败:{err}"
def _modify_plugin_files(
self,
*,
plugin_dir: Path,
original_class_name: str,
suffix: str,
name: str,
description: str,
version: Optional[str],
icon: Optional[str],
) -> tuple[bool, str]:
"""改写分身的 Python 元数据和联邦前端资源。"""
clone_class_name = f"{original_class_name}{suffix}"
init_file = plugin_dir / "__init__.py"
if init_file.exists():
success, message = self._modify_python_file(
file_path=init_file,
original_class_name=original_class_name,
clone_class_name=clone_class_name,
name=name,
description=description,
version=version,
icon=icon,
)
if not success:
return False, message
dist_dir = plugin_dir / "dist"
if dist_dir.exists():
success, message = self._modify_federation_files(
dist_dir=dist_dir,
original_class_name=original_class_name,
clone_class_name=clone_class_name,
)
if not success:
return False, message
return True, "文件修改成功"
@staticmethod
def _modify_python_file(
*,
file_path: Path,
original_class_name: str,
clone_class_name: str,
name: str,
description: str,
version: Optional[str],
icon: Optional[str],
) -> tuple[bool, str]:
"""改写插件主类名称、展示元数据和独立配置前缀。"""
try:
content = file_path.read_text(encoding="utf-8", errors="replace")
content = content.replace(
f"class {original_class_name}",
f"class {clone_class_name}",
)
if name:
content = re.sub(
r'plugin_name\s*=\s*["\'][^"\']*["\']',
f'plugin_name = "{name}"',
content,
)
if description:
content = re.sub(
r'plugin_desc\s*=\s*["\'][^"\']*["\']',
f'plugin_desc = "{description}"',
content,
)
content = re.sub(
r'plugin_config_prefix\s*=\s*["\'][^"\']*["\']',
f'plugin_config_prefix = "{clone_class_name.lower()}_"',
content,
)
if version:
content = re.sub(
r'plugin_version\s*=\s*["\'][^"\']*["\']',
f'plugin_version = "{version}"',
content,
)
if icon and icon.strip():
content = re.sub(
r'plugin_icon\s*=\s*["\'][^"\']*["\']',
f'plugin_icon = "{icon}"',
content,
)
if "def init_plugin(self" in content:
init_index = content.index("def init_plugin(self")
content = (
content[:init_index]
+ "is_clone = True\n\n "
+ content[init_index:]
)
file_path.write_text(content, encoding="utf-8")
return True, "Python文件修改成功"
except Exception as err:
logger.error(f"修改Python文件失败:{err}")
return False, f"修改Python文件失败:{err}"
def _modify_federation_files(
self,
*,
dist_dir: Path,
original_class_name: str,
clone_class_name: str,
) -> tuple[bool, str]:
"""改写联邦构建产物中的插件类名和样式命名空间。"""
try:
for file_path in dist_dir.rglob("*"):
if not file_path.is_file() or file_path.suffix not in {".js", ".css"}:
continue
try:
content = file_path.read_text(encoding="utf-8", errors="replace")
if file_path.suffix == ".js":
content = content.replace(original_class_name, clone_class_name)
content = content.replace(
f'"{original_class_name}"',
f'"{clone_class_name}"',
)
content = content.replace(
f"'{original_class_name}'",
f"'{clone_class_name}'",
)
content = content.replace(
f"css__{original_class_name}__",
f"css__{clone_class_name}__",
)
content = content.replace(
original_class_name.lower(),
clone_class_name.lower(),
)
file_path.write_text(content, encoding="utf-8")
except Exception as err:
logger.warning(f"修改联邦插件文件 {file_path} 失败:{err}")
self._rename_federation_assets(
dist_dir,
original_class_name,
clone_class_name,
)
return True, "联邦插件文件修改完成"
except Exception as err:
logger.error(f"修改联邦插件文件失败:{err}")
return False, f"修改联邦插件文件失败:{err}"
@staticmethod
def _rename_federation_assets(
dist_dir: Path,
original_class_name: str,
clone_class_name: str,
) -> None:
"""重命名包含原类名的顶层联邦资源,避免分身资源冲突。"""
try:
for file_path in dist_dir.glob("*"):
if not file_path.is_file():
continue
if original_class_name.lower() not in file_path.name.lower():
continue
new_name = file_path.name.replace(
original_class_name.lower(),
clone_class_name.lower(),
)
new_path = file_path.parent / new_name
if not new_path.exists():
file_path.rename(new_path)
except Exception as err:
logger.warning(f"重命名联邦插件资源文件失败:{err}")

View File

@@ -68,38 +68,6 @@ class DeprecationNotice:
NOTICES: Dict[str, DeprecationNotice] = {
notice.key: notice
for notice in (
DeprecationNotice(
key="PluginManager._modify_plugin_files",
subject="PluginManager._modify_plugin_files()",
stage=DeprecationStage.SILENT,
since="v3.0.0",
replacement="get_plugin_system().package._modify_plugin_files()",
reason="分身文件改写已由插件包适配器实现,此处只为旧内部调用保留转发",
),
DeprecationNotice(
key="PluginManager._modify_python_file",
subject="PluginManager._modify_python_file()",
stage=DeprecationStage.SILENT,
since="v3.0.0",
replacement="get_plugin_system().package._modify_python_file()",
reason="Python 文件改写已由插件包适配器实现,此处只为旧内部调用保留转发",
),
DeprecationNotice(
key="PluginManager._modify_federation_files",
subject="PluginManager._modify_federation_files()",
stage=DeprecationStage.SILENT,
since="v3.0.0",
replacement="get_plugin_system().package._modify_federation_files()",
reason="联邦文件改写已由插件包适配器实现,此处只为旧内部调用保留转发",
),
DeprecationNotice(
key="PluginManager._rename_federation_assets",
subject="PluginManager._rename_federation_assets()",
stage=DeprecationStage.SILENT,
since="v3.0.0",
replacement="get_plugin_system().package._rename_federation_assets()",
reason="联邦资源重命名已由插件包适配器实现,此处只为旧内部调用保留转发",
),
DeprecationNotice(
key="PluginHelper.find_missing_dependencies",
subject="PluginHelper.find_missing_dependencies()",

View File

@@ -1460,69 +1460,3 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
:raise LookupError: 插件不存在,或 plugin_id 实为某个分身自身的实例ID
"""
self._plugin_default_target.clear_target(plugin_id, instance_id)
def _modify_plugin_files(self, plugin_dir: Path, original_id: str, suffix: str,
name: str, description: str, version: str = None,
icon: str = None) -> Tuple[bool, str]:
"""
兼容旧内部调用,将分身文件改写委托给包适配器。
:param plugin_dir: 插件目录
:param original_id: 原插件ID
:param suffix: 分身后缀
:param name: 分身名称
:param description: 分身描述
:param version: 自定义版本号
:param icon: 自定义图标URL
:return: (是否成功, 错误信息)
"""
original_plugin_class = self._plugins.get(original_id)
if not original_plugin_class:
return False, f"无法获取原插件类 {original_id}"
return self._plugin_runtime.system().modify_plugin_files(
plugin_dir=plugin_dir,
original_class_name=original_plugin_class.__name__,
suffix=suffix,
name=name,
description=description,
version=version,
icon=icon,
)
@staticmethod
def _modify_python_file(file_path: Path, original_class_name: str,
clone_class_name: str, name: str, description: str,
version: str = None, icon: str = None) -> Tuple[bool, str]:
"""
兼容旧内部调用,将 Python 文件改写委托给包适配器。
"""
return PluginManager()._plugin_runtime.system().modify_python_file(
file_path=file_path,
original_class_name=original_class_name,
clone_class_name=clone_class_name,
name=name,
description=description,
version=version,
icon=icon,
)
def _modify_federation_files(self, dist_dir: Path, original_class_name: str,
clone_class_name: str) -> Tuple[bool, str]:
"""
兼容旧内部调用,将联邦文件改写委托给包适配器。
"""
return self._plugin_runtime.system().modify_federation_files(
dist_dir=dist_dir,
original_class_name=original_class_name,
clone_class_name=clone_class_name,
)
@staticmethod
def _rename_federation_assets(dist_dir: Path, original_class_name: str, clone_class_name: str):
"""
兼容旧内部调用,将资源重命名委托给包适配器。
"""
PluginManager()._plugin_runtime.system().rename_federation_assets(
dist_dir,
original_class_name,
clone_class_name,
)

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
from typing import Any, Optional, cast
from typing import Any, Optional
class PluginSystemServices:
@@ -95,34 +95,6 @@ class PluginSystemServices:
"""通过唯一包事务 owner 删除插件物理目录。"""
return bool(self.package.remove_plugin(plugin_id))
def modify_plugin_files(self, **kwargs: Any) -> tuple[bool, str]:
"""把旧分身源码改写调用收口到唯一包适配器。"""
return cast(tuple[bool, str], self.package._modify_plugin_files(**kwargs))
def modify_python_file(self, **kwargs: Any) -> tuple[bool, str]:
"""把旧 Python 类改写调用收口到唯一包适配器。"""
return cast(tuple[bool, str], self.package._modify_python_file(**kwargs))
def modify_federation_files(self, **kwargs: Any) -> tuple[bool, str]:
"""把旧联邦构建产物改写调用收口到唯一包适配器。"""
return cast(
tuple[bool, str],
self.package._modify_federation_files(**kwargs),
)
def rename_federation_assets(
self,
dist_dir: Path,
original_class_name: str,
clone_class_name: str,
) -> None:
"""把旧联邦资源重命名调用收口到唯一包适配器。"""
self.package._rename_federation_assets(
dist_dir,
original_class_name,
clone_class_name,
)
_services: Optional[PluginSystemServices] = None

View File

@@ -2533,10 +2533,10 @@
"type-arg": 2
},
"app/runtime/extensions/plugin/manager.py": {
"assignment": 8,
"assignment": 4,
"attr-defined": 1,
"no-untyped-call": 1,
"no-untyped-def": 12,
"no-untyped-def": 11,
"type-arg": 10
},
"app/runtime/extensions/plugin/metadata.py": {

View File

@@ -447,47 +447,3 @@ def test_local_sync_failure_restores_previous_runtime_copy(monkeypatch, tmp_path
assert manager.sync_local("DemoPlugin", missing_source) is False
assert source_file.read_text(encoding="utf-8") == "stable"
def test_clone_rewrites_python_and_federation_assets(monkeypatch, tmp_path):
"""插件分身文件处理应由包适配器完成并隔离配置命名空间。"""
manager = _manager(monkeypatch, tmp_path)
plugin_dir = tmp_path / "app" / "plugins" / "demoplugin"
dist_dir = plugin_dir / "dist"
dist_dir.mkdir(parents=True)
(plugin_dir / "__init__.py").write_text(
"class DemoPlugin:\n"
" plugin_name = 'Demo'\n"
" plugin_desc = 'Description'\n"
" plugin_config_prefix = 'demo_'\n"
" plugin_version = '1.0.0'\n"
" plugin_icon = 'old.png'\n"
" def init_plugin(self, config=None):\n"
" pass\n",
encoding="utf-8",
)
(dist_dir / "demoplugin.js").write_text(
"const name = 'DemoPlugin'; const css = 'css__DemoPlugin__root';",
encoding="utf-8",
)
success, message = manager.clone(
plugin_id="DemoPlugin",
clone_id="DemoPluginBlue",
original_class_name="DemoPlugin",
suffix="blue",
name="Demo Blue",
description="Blue clone",
version="2.0.0",
icon="blue.png",
)
clone_dir = tmp_path / "app" / "plugins" / "demopluginblue"
clone_source = (clone_dir / "__init__.py").read_text(encoding="utf-8")
assert success is True
assert message == "文件修改成功"
assert "class DemoPluginblue" in clone_source
assert 'plugin_name = "Demo Blue"' in clone_source
assert 'plugin_config_prefix = "demopluginblue_"' in clone_source
assert "is_clone = True" in clone_source
assert (clone_dir / "dist" / "demopluginblue.js").is_file()