refactor(api): 渠道与存储管理端点通用化,端点层零目标特色

- schemas 新增公共 ManageRequest(target+action+params) 与 StorageAction 词汇表
- endpoint 层收敛为 POST /notification/manage、/storage/manage 两个通用接口,
  不再定义任何渠道/存储特定的名称、参数与响应字段,前端上送参数原样透传
- chain 层 manage_channel/manage_storage 接受字符串标识纯透明转发,
  StorageChain 移除全部特色管理方法
- FileManagerModule 新增 storage_manage 统一入口,模块返回归一化为
  {success, message, data};wechatclawbot channel_manage 同步归一化,
  路由标识兼容枚举名/值/对象
- dashboard 与 agentopsassistant 的用量查询改走 manage_storage(usage)
- 新增 10 项存储契约守护测试,通知侧补充 3 项链透传与字符串路由测试,
  API 守护测试对 ManageRequest 开放映射按设计豁免
This commit is contained in:
jxxghp
2026-08-16 07:18:49 +08:00
parent 5b0c631f80
commit a6dbd799d5
14 changed files with 354 additions and 380 deletions
+48 -72
View File
@@ -1,5 +1,5 @@
from pathlib import Path
from typing import Optional, List, Tuple, Union, Dict, Callable
from typing import Any, Optional, List, Tuple, Union, Dict, Callable
from app.runtime.config import settings
from app.domain.context import MediaInfo, MusicInfo
@@ -13,8 +13,8 @@ from app.runtime.log import logger
from app.modules import _ModuleBase
from app.modules.filemanager.storages import StorageBase
from app.modules.filemanager.transhandler import TransHandler
from app.schemas import TransferInfo, ExistMediaInfo, TmdbEpisode, TransferDirectoryConf, FileItem, StorageUsage
from app.schemas.types import MUSIC_ENTITY_ALBUM, MediaType, ModuleType, OtherModulesType
from app.schemas import TransferInfo, ExistMediaInfo, TmdbEpisode, TransferDirectoryConf, FileItem
from app.schemas.types import MUSIC_ENTITY_ALBUM, MediaType, ModuleType, OtherModulesType, StorageAction
from app.adapters.system.host import SystemUtils
from app.foundation import text as text_tools
@@ -122,17 +122,55 @@ class FileManagerModule(_ModuleBase):
def init_setting(self) -> Tuple[str, Union[str, bool]]:
pass
def support_transtype(self, storage: str) -> Optional[dict]:
def storage_manage(self, storage: str, action: StorageAction, **params) -> Dict[str, Any]:
"""
支持的整理方式
网盘存储统一管理入口,按存储标识路由
动作语义与参数解释交给具体存储实现,
统一返回 {"success": bool, "message": ..., "data": ...}
"""
try:
action = StorageAction(action)
except ValueError:
return {"success": False, "message": f"不支持的存储管理动作:{action}"}
if storage not in self._support_storages:
return None
storage_oper = self.__get_storage_oper(storage)
return {"success": False, "message": f"不支持的存储类型:{storage}"}
if action == StorageAction.SAVE_CONFIG:
storage_oper = self.__get_storage_oper(storage)
if not storage_oper:
return {"success": False, "message": f"不支持 {storage} 的配置保存"}
storage_oper.set_config(params.get("conf") or {})
return {"success": True}
if action == StorageAction.RESET_CONFIG:
storage_oper = self.__get_storage_oper(storage)
if not storage_oper:
return {"success": False, "message": f"不支持 {storage} 的重置存储配置"}
storage_oper.reset_config()
return {"success": True}
if action == StorageAction.SUPPORT_TRANSTYPE:
storage_oper = self.__get_storage_oper(storage)
if not storage_oper:
return {"success": False, "message": f"不支持 {storage} 的整理方式获取"}
transtype = storage_oper.support_transtype()
return {"success": bool(transtype), "data": transtype}
if action == StorageAction.USAGE:
storage_oper = self.__get_storage_oper(storage)
if not storage_oper:
return {"success": False, "message": f"不支持 {storage} 的存储使用情况"}
usage = storage_oper.usage()
return {"success": bool(usage), "data": usage}
# 登录类动作:存储实现不支持时返回失败信息
oper_method = action.value
storage_oper = self.__get_storage_oper(storage, oper_method)
if not storage_oper:
logger.error(f"不支持 {storage} 的整理方式获取")
return None
return storage_oper.support_transtype()
return {"success": False, "message": f"{storage} 不支持 {oper_method}"}
result = getattr(storage_oper, oper_method)(**params)
if result is None:
return {"success": False, "message": f"{storage}{oper_method} 执行失败"}
data, errmsg = result
return {"success": bool(data), "message": errmsg, "data": data}
@staticmethod
def recommend_name(meta: MetaBase, mediainfo: MediaInfo,
@@ -157,56 +195,6 @@ class FileManagerModule(_ModuleBase):
)
return path.as_posix() if path else ""
def save_config(self, storage: str, conf: Dict) -> None:
"""
保存存储配置
"""
storage_oper = self.__get_storage_oper(storage)
if not storage_oper:
logger.error(f"不支持 {storage} 的配置保存")
return
storage_oper.set_config(conf)
def reset_config(self, storage: str) -> None:
"""
重置存储配置
"""
storage_oper = self.__get_storage_oper(storage)
if not storage_oper:
logger.error(f"不支持 {storage} 的重置存储配置")
return
storage_oper.reset_config()
def generate_qrcode(self, storage: str) -> Optional[Tuple[dict, str]]:
"""
生成二维码
"""
storage_oper = self.__get_storage_oper(storage, "generate_qrcode")
if not storage_oper:
logger.error(f"不支持 {storage} 的二维码生成")
return None
return storage_oper.generate_qrcode()
def generate_auth_url(self, storage: str) -> Optional[Tuple[dict, str]]:
"""
生成 OAuth2 授权 URL
"""
storage_oper = self.__get_storage_oper(storage, "generate_auth_url")
if not storage_oper:
logger.error(f"不支持 {storage} 的 OAuth2 授权")
return {}, f"不支持 {storage} 的 OAuth2 授权"
return storage_oper.generate_auth_url()
def check_login(self, storage: str, **kwargs) -> Optional[Dict[str, str]]:
"""
登录确认
"""
storage_oper = self.__get_storage_oper(storage, "check_login")
if not storage_oper:
logger.error(f"不支持 {storage} 的登录确认")
return None
return storage_oper.check_login(**kwargs)
def list_files(self, fileitem: FileItem, recursion: Optional[bool] = False) -> Optional[List[FileItem]]:
"""
浏览文件
@@ -397,18 +385,6 @@ class FileManagerModule(_ModuleBase):
previous_snapshot=previous_snapshot
)
def storage_usage(self, storage: str) -> Optional[StorageUsage]:
"""
存储使用情况
"""
if storage not in self._support_storages:
return None
storage_oper = self.__get_storage_oper(storage)
if not storage_oper:
logger.error(f"不支持 {storage} 的存储使用情况")
return None
return storage_oper.usage()
def transfer(self, fileitem: FileItem, meta: MetaBase, mediainfo: MediaInfo,
target_directory: TransferDirectoryConf = None,
target_storage: Optional[str] = None, target_path: Path = None,
+11 -5
View File
@@ -93,9 +93,12 @@ class WechatClawBotModule(_ModuleBase, _MessageBase[WechatClawBot]):
动作语义与表单参数全部由模块自行解释:优先使用已保存配置实例,
无匹配配置时可基于表单参数构造临时实例(未保存配置的扫码预览)。
统一返回 {"success": bool, "message": ..., ...} 结构。
统一返回 {"success": bool, "message": ..., "data": ...} 结构。
"""
if channel != self.get_subtype():
# 路由标识归一化:兼容枚举名、枚举值与原始枚举对象
if isinstance(channel, str) and channel not in (self.get_subtype().name, self.get_subtype().value):
return None
if not isinstance(channel, str) and channel != self.get_subtype():
return None
try:
action = NotificationAction(action)
@@ -116,14 +119,17 @@ class WechatClawBotModule(_ModuleBase, _MessageBase[WechatClawBot]):
return {"success": False, "message": errmsg}
if action == NotificationAction.STATUS:
return client.get_status(
data = client.get_status(
refresh_remote=bool(params.get("refresh_remote", True)),
auto_generate_qrcode=bool(params.get("auto_generate_qrcode", True)),
)
return {"success": bool(data.get("success")), "message": data.get("message"), "data": data}
if action == NotificationAction.REFRESH_QRCODE:
return client.refresh_qrcode()
data = client.refresh_qrcode()
return {"success": bool(data.get("success")), "message": data.get("message"), "data": data}
if action == NotificationAction.LOGOUT:
return client.logout()
data = client.logout()
return {"success": bool(data.get("success")), "message": data.get("message"), "data": data}
if action == NotificationAction.TEST_CONNECTION:
state, message = client.test_connection()
return {"success": state, "message": message}