Files
DDSRem 2de976f6b3 Merge pull request #7050 from DDSRem-Dev/codex-comment-migration
docs: 统一全项目中文注释和结构化契约说明
2026-10-17 19:26:29 +08:00

391 lines
16 KiB
Python

"""
插件文件夹查询、变更和插件归属维护用例。
"""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from contextlib import AbstractContextManager
from copy import deepcopy
from dataclasses import dataclass
from typing import Any
from app.application.configuration import get_configured_system_config
from app.application.plugin.runtime import get_plugin_manager
from app.runtime.log import logger
from app.schemas.exception import (
PersistenceUnavailableError,
PluginMutationRejectedError,
)
from app.schemas.types import SystemConfigKey
PluginFolders = dict[str, Any]
FolderReader = Callable[[], PluginFolders]
FolderWriter = Callable[[PluginFolders], Awaitable[object]]
FolderSyncWriter = Callable[[PluginFolders], object]
FolderMutation = Callable[[str], AbstractContextManager[None]]
@dataclass(frozen=True, slots=True)
class PluginFolderResult:
"""
描述一次插件文件夹变更结果和用户提示。
:param success: 操作是否完成其预期目标
:param message: 操作结果或失败原因的说明,默认空字符串
"""
success: bool
message: str = ""
FolderChange = Callable[[PluginFolders], tuple[PluginFolderResult, PluginFolders]]
FolderAtomicWriter = Callable[[FolderChange], Awaitable[PluginFolderResult]]
class PluginFolderService:
"""
集中管理插件文件夹快照和带准入的持久化变更。
"""
def __init__(
self,
*,
read: FolderReader,
write: FolderWriter,
write_sync: FolderSyncWriter,
mutation: FolderMutation,
update: FolderAtomicWriter | None = None,
) -> None:
"""
保存配置读写和插件运行态变更准入端口。
"""
self._read = read
self._write = write
self._write_sync = write_sync
self._mutation = mutation
self._update = update
def get(self) -> PluginFolders:
"""
返回与配置存储隔离的当前文件夹快照。
"""
return deepcopy(self._read())
def get_or_empty(self) -> PluginFolders:
"""
读取失败时记录诊断并返回兼容的空文件夹快照。
"""
try:
return self.get()
# 查询入口保留旧空结果语义
except Exception as error: # noqa: BLE001
logger.error(f"[文件夹API] 获取文件夹配置失败: {error}")
return {}
async def save(self, folders: PluginFolders) -> PluginFolderResult:
"""
完整替换文件夹配置并取得插件变更准入。
"""
try:
with self._mutation("保存插件文件夹配置"):
await self._write(deepcopy(folders))
return PluginFolderResult(True)
except PersistenceUnavailableError:
raise
except PluginMutationRejectedError as error:
return PluginFolderResult(False, str(error))
# 兼容入口以结果表达保存失败
except Exception as error: # noqa: BLE001
logger.error(f"[文件夹API] 保存文件夹配置失败: {error}")
return PluginFolderResult(False, str(error))
async def create(self, folder_name: str) -> PluginFolderResult:
"""
创建不存在的文件夹,保留旧列表格式的兼容形态。
"""
folder_name = folder_name.strip()
if not folder_name:
return PluginFolderResult(False, "文件夹名称不能为空")
def change(folders: PluginFolders) -> tuple[PluginFolderResult, PluginFolders]:
"""
只在名称尚未占用时向最新快照追加空文件夹。
"""
if folder_name in folders:
return PluginFolderResult(False, f"文件夹 '{folder_name}' 已存在"), folders
folders[folder_name] = []
return PluginFolderResult(True, f"文件夹 '{folder_name}' 创建成功"), folders
return await self._change(f"创建插件文件夹 {folder_name}", change)
async def delete(self, folder_name: str) -> PluginFolderResult:
"""
删除存在的文件夹并返回稳定业务结果。
"""
def change(folders: PluginFolders) -> tuple[PluginFolderResult, PluginFolders]:
"""
只从最新快照移除目标文件夹。
"""
if folder_name not in folders:
return PluginFolderResult(False, f"文件夹 '{folder_name}' 不存在"), folders
del folders[folder_name]
return PluginFolderResult(True, f"文件夹 '{folder_name}' 删除成功"), folders
return await self._change(f"删除插件文件夹 {folder_name}", change)
async def update_folder(
self,
folder_name: str,
*,
new_name: str | None = None,
changes: dict[str, Any] | None = None,
) -> PluginFolderResult:
"""
增量更新文件夹名称或展示配置,同时保留成员与未修改字段。
"""
normalized_name = new_name.strip() if new_name is not None else folder_name
if not normalized_name:
return PluginFolderResult(False, "文件夹名称不能为空")
folder_changes = deepcopy(changes or {})
def change(folders: PluginFolders) -> tuple[PluginFolderResult, PluginFolders]:
"""
在最新快照中合并展示字段,并保持重命名前的字典位置。
"""
if folder_name not in folders:
return PluginFolderResult(False, f"文件夹 '{folder_name}' 不存在"), folders
if normalized_name != folder_name and normalized_name in folders:
return PluginFolderResult(False, f"文件夹 '{normalized_name}' 已存在"), folders
current = folders[folder_name]
if folder_changes:
current = (
{"plugins": list(current)}
if isinstance(current, list)
else deepcopy(current) if isinstance(current, dict) else {"plugins": []}
)
current.update(folder_changes)
if normalized_name == folder_name:
folders[folder_name] = current
else:
folders = {
(normalized_name if name == folder_name else name): (current if name == folder_name else value)
for name, value in folders.items()
}
return PluginFolderResult(True, f"文件夹 '{normalized_name}' 已更新"), folders
return await self._change(f"更新插件文件夹 {folder_name}", change)
async def update_plugins(
self,
folder_name: str,
plugin_ids: list[str],
expected_plugin_ids: list[str] | None = None,
) -> PluginFolderResult:
"""
条件更新指定文件夹的插件顺序和成员,并保留展示配置。
"""
next_plugin_ids = list(plugin_ids)
def change(folders: PluginFolders) -> tuple[PluginFolderResult, PluginFolders]:
"""
基于最新成员列表检查预期快照并替换目标列表。
"""
if folder_name not in folders:
return PluginFolderResult(False, f"文件夹 '{folder_name}' 不存在"), folders
folder_data = folders[folder_name]
current_plugin_ids = _folder_plugins(folder_data) or []
if expected_plugin_ids is not None and current_plugin_ids != expected_plugin_ids:
return PluginFolderResult(False, "插件文件夹已被其他请求修改,请重新读取后再试"), folders
folders[folder_name] = _with_folder_plugins(folder_data, next_plugin_ids)
return PluginFolderResult(True, f"文件夹 '{folder_name}' 中的插件已更新"), folders
return await self._change(f"更新插件文件夹 {folder_name}", change)
async def assign_plugin(self, folder_name: str, plugin_id: str) -> PluginFolderResult:
"""
把一个插件原子迁移到目标文件夹,并从其他文件夹移除。
"""
def change(folders: PluginFolders) -> tuple[PluginFolderResult, PluginFolders]:
"""
在同一最新快照内完成跨文件夹成员迁移。
"""
if folder_name not in folders:
return PluginFolderResult(False, f"文件夹 '{folder_name}' 不存在"), folders
for name, folder_data in folders.items():
plugins = _folder_plugins(folder_data)
if plugins is None:
if name == folder_name:
folders[name] = _with_folder_plugins(folder_data, [])
continue
folders[name] = _with_folder_plugins(
folder_data,
[item for item in plugins if item != plugin_id],
)
target = folders[folder_name]
target_plugins = list(_folder_plugins(target) or [])
target_plugins.append(plugin_id)
folders[folder_name] = _with_folder_plugins(target, target_plugins)
return PluginFolderResult(True, f"插件已移动到文件夹 '{folder_name}'"), folders
return await self._change(f"移动插件到文件夹 {folder_name}", change)
async def remove_plugin_from_folder(
self,
folder_name: str,
plugin_id: str,
) -> PluginFolderResult:
"""
只从指定文件夹移除一个插件,不影响其他文件夹。
"""
def change(folders: PluginFolders) -> tuple[PluginFolderResult, PluginFolders]:
"""
在最新快照内删除目标文件夹中的指定成员。
"""
if folder_name not in folders:
return PluginFolderResult(False, f"文件夹 '{folder_name}' 不存在"), folders
folder_data = folders[folder_name]
plugins = _folder_plugins(folder_data) or []
if plugin_id not in plugins:
return PluginFolderResult(False, f"插件不在文件夹 '{folder_name}' 中"), folders
folders[folder_name] = _with_folder_plugins(
folder_data,
[item for item in plugins if item != plugin_id],
)
return PluginFolderResult(True, f"插件已从文件夹 '{folder_name}' 移除"), folders
return await self._change(f"从文件夹 {folder_name} 移除插件", change)
async def _change(
self,
operation: str,
change: FolderChange,
) -> PluginFolderResult:
"""
统一执行带运行时准入的增量文件夹写入并映射稳定失败结果。
"""
try:
with self._mutation(operation):
if self._update is not None:
return await self._update(change)
folders = self.get()
result, changed_folders = change(folders)
if result.success:
await self._write(changed_folders)
return result
except PersistenceUnavailableError:
raise
except PluginMutationRejectedError as error:
return PluginFolderResult(False, str(error))
# HTTP 兼容入口以业务结果表达失败
except Exception as error: # noqa: BLE001
logger.error(f"[文件夹API] {operation}失败: {error}")
return PluginFolderResult(False, str(error))
def remove_plugin(self, plugin_id: str) -> None:
"""
从当前和旧版文件夹形态中移除插件且不阻断卸载。
"""
try:
folders = self.get()
modified = False
for folder_name, folder_data in folders.items():
plugins = _folder_plugins(folder_data)
if plugins is None or plugin_id not in plugins:
continue
plugins.remove(plugin_id)
logger.info(f"已从文件夹 '{folder_name}' 中移除插件 {plugin_id}")
modified = True
if modified:
self._write_sync(folders)
else:
logger.debug(f"插件 {plugin_id} 不在任何文件夹中,无需移除")
# 文件夹配置损坏不应阻断插件代码、数据和定时任务的卸载流程。
except Exception as error: # noqa: BLE001
logger.error(f"从文件夹中移除插件时出错:{error}")
def add_clone(self, original_plugin_id: str, clone_plugin_id: str) -> None:
"""
让分身继承原插件所在的首个文件夹且不阻断创建。
"""
try:
folders = self.get()
for folder_name, folder_data in folders.items():
plugins = _folder_plugins(folder_data)
if plugins is None or original_plugin_id not in plugins:
continue
if clone_plugin_id not in plugins:
plugins.append(clone_plugin_id)
self._write_sync(folders)
logger.info(
f"已将分身插件 {clone_plugin_id} 添加到文件夹 "
f"'{folder_name}' 中"
)
return
logger.info(
f"原插件 {original_plugin_id} 不在任何文件夹中,"
f"分身插件 {clone_plugin_id} 将保持独立"
)
# 文件夹处理失败不影响插件分身创建的整体流程。
except Exception as error: # noqa: BLE001
logger.error(f"处理插件文件夹时出错:{error}")
def get_plugin_folder_service() -> PluginFolderService:
"""
基于当前 lifespan 的 Application 配置和插件 Runtime 构造文件夹服务。
"""
return PluginFolderService(
read=lambda: (
get_configured_system_config().get(SystemConfigKey.PluginFolders) or {}
),
write=lambda folders: get_configured_system_config().async_set(
SystemConfigKey.PluginFolders, folders
),
write_sync=lambda folders: get_configured_system_config().set(
SystemConfigKey.PluginFolders, folders
),
mutation=lambda operation: get_plugin_manager().mutation(operation),
update=lambda change: get_configured_system_config().async_update_atomically(
SystemConfigKey.PluginFolders,
lambda current: change(deepcopy(current) if isinstance(current, dict) else {}),
),
)
def remove_plugin_from_folders(plugin_id: str) -> None:
"""
保留卸载调用方使用的稳定 Application 入口。
"""
get_plugin_folder_service().remove_plugin(plugin_id)
def add_clone_to_plugin_folder(original_plugin_id: str, clone_plugin_id: str) -> None:
"""
保留分身创建调用方使用的稳定 Application 入口。
"""
get_plugin_folder_service().add_clone(original_plugin_id, clone_plugin_id)
def _folder_plugins(folder_data: Any) -> list[str] | None:
"""
返回当前字典形态或旧列表形态中的可变插件列表。
"""
if isinstance(folder_data, dict):
plugins = folder_data.get("plugins")
return plugins if isinstance(plugins, list) else None
return folder_data if isinstance(folder_data, list) else None
def _with_folder_plugins(folder_data: Any, plugin_ids: list[str]) -> Any:
"""
替换成员列表,同时保留对象格式中的展示配置。
"""
if isinstance(folder_data, dict):
return {**folder_data, "plugins": list(plugin_ids)}
return list(plugin_ids)