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
+3 -1
View File
@@ -13,6 +13,7 @@ from app.application.security.access import verify_apitoken
from app.db import get_db
from app.db.models.transferhistory import TransferHistory
from app.api.deps import get_current_active_superuser
from app.schemas.types import StorageAction
from app.application.directory import DirectoryHelper
from app.scheduler import Scheduler
from app.adapters.system.host import SystemUtils
@@ -65,7 +66,8 @@ def _build_storage() -> schemas.Storage:
return schemas.Storage(total_storage=total, used_storage=total - available)
storages = set([d.library_storage for d in dirs if d.library_storage])
for _storage in storages:
_usage = StorageChain().storage_usage(_storage)
_result = StorageChain().manage_storage(storage=_storage, action=StorageAction.USAGE.value)
_usage = _result.get("data") if _result.get("success") else None
if _usage:
total += _usage.total
available += _usage.available
+18 -143
View File
@@ -1,4 +1,4 @@
from typing import Optional
from typing import Any, Dict
from fastapi import Depends
@@ -7,157 +7,32 @@ from app.api.response import ResponseAPIRouter
from app.chain.notification import NotificationChain
from app.db.models import User
from app.api.deps import get_current_active_superuser
from app.schemas.types import MessageChannel, NotificationAction
router = ResponseAPIRouter()
@router.get(
"/wechatclawbot/status",
summary="查询微信 ClawBot 登录状态",
response_model=schemas.Response[schemas.WechatClawBotData],
@router.post(
"/manage",
summary="通知渠道统一管理",
response_model=schemas.Response[Dict[str, Any]],
)
def wechatclawbot_status(
source: Optional[str] = None,
fallback_source: Optional[str] = None,
refresh_remote: bool = True,
auto_generate_qrcode: bool = True,
WECHATCLAWBOT_BASE_URL: Optional[str] = None,
WECHATCLAWBOT_DEFAULT_TARGET: Optional[str] = None,
WECHATCLAWBOT_ADMINS: Optional[str] = None,
WECHATCLAWBOT_POLL_TIMEOUT: Optional[int] = None,
def manage_channel(
request: schemas.ManageRequest,
_: User = Depends(get_current_active_superuser),
):
"""查询微信 ClawBot 登录状态和二维码。"""
"""
通知渠道统一管理入口
端点层不定义任何渠道特定的名称与参数,
渠道标识、管理动作与表单参数由前端上送并原样透传给渠道模块
"""
result = NotificationChain().manage_channel(
channel=MessageChannel.WechatClawBot,
action=NotificationAction.STATUS,
source=source,
fallback_source=fallback_source,
refresh_remote=refresh_remote,
auto_generate_qrcode=auto_generate_qrcode,
WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL,
WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET,
WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS,
WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT,
channel=request.target,
action=request.action,
**request.params,
)
return schemas.Response(
success=bool(result.get("success")),
message=result.get("message"),
data=result if result.get("success") else None,
data=result.get("data"),
)
@router.post(
"/wechatclawbot/refresh",
summary="刷新微信 ClawBot 二维码",
response_model=schemas.Response[schemas.WechatClawBotData],
)
def refresh_wechatclawbot_qrcode(
source: Optional[str] = None,
fallback_source: Optional[str] = None,
WECHATCLAWBOT_BASE_URL: Optional[str] = None,
WECHATCLAWBOT_DEFAULT_TARGET: Optional[str] = None,
WECHATCLAWBOT_ADMINS: Optional[str] = None,
WECHATCLAWBOT_POLL_TIMEOUT: Optional[int] = None,
_: User = Depends(get_current_active_superuser),
):
"""刷新微信 ClawBot 二维码。"""
result = NotificationChain().manage_channel(
channel=MessageChannel.WechatClawBot,
action=NotificationAction.REFRESH_QRCODE,
source=source,
fallback_source=fallback_source,
WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL,
WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET,
WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS,
WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT,
)
return schemas.Response(
success=bool(result.get("success")),
message=result.get("message"),
data=result,
)
@router.post(
"/wechatclawbot/logout",
summary="退出微信 ClawBot 登录",
response_model=schemas.Response[schemas.WechatClawBotData],
)
def logout_wechatclawbot(
source: Optional[str] = None,
fallback_source: Optional[str] = None,
WECHATCLAWBOT_BASE_URL: Optional[str] = None,
WECHATCLAWBOT_DEFAULT_TARGET: Optional[str] = None,
WECHATCLAWBOT_ADMINS: Optional[str] = None,
WECHATCLAWBOT_POLL_TIMEOUT: Optional[int] = None,
_: User = Depends(get_current_active_superuser),
):
"""退出微信 ClawBot 登录。"""
result = NotificationChain().manage_channel(
channel=MessageChannel.WechatClawBot,
action=NotificationAction.LOGOUT,
source=source,
fallback_source=fallback_source,
WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL,
WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET,
WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS,
WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT,
)
return schemas.Response(
success=bool(result.get("success")),
message=result.get("message"),
data=result,
)
@router.get(
"/wechatclawbot/test",
summary="测试微信 ClawBot 连通性",
response_model=schemas.Response[None],
)
def test_wechatclawbot(
source: Optional[str] = None,
fallback_source: Optional[str] = None,
WECHATCLAWBOT_BASE_URL: Optional[str] = None,
WECHATCLAWBOT_DEFAULT_TARGET: Optional[str] = None,
WECHATCLAWBOT_ADMINS: Optional[str] = None,
WECHATCLAWBOT_POLL_TIMEOUT: Optional[int] = None,
_: User = Depends(get_current_active_superuser),
):
"""测试微信 ClawBot 当前登录态是否可用。"""
result = NotificationChain().manage_channel(
channel=MessageChannel.WechatClawBot,
action=NotificationAction.TEST_CONNECTION,
source=source,
fallback_source=fallback_source,
WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL,
WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET,
WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS,
WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT,
)
return schemas.Response(success=bool(result.get("success")), message=result.get("message"))
@router.post(
"/wechatclawbot/migrate",
summary="迁移微信 ClawBot 登录缓存",
response_model=schemas.Response[None],
)
def migrate_wechatclawbot_cache(
old_source: str,
new_source: str,
cleanup_old: bool = False,
overwrite: bool = False,
_: User = Depends(get_current_active_superuser),
):
"""在通知名称变更时迁移对应的微信 ClawBot 登录缓存。"""
result = NotificationChain().manage_channel(
channel=MessageChannel.WechatClawBot,
action=NotificationAction.MIGRATE_CACHE,
old_name=old_source,
new_name=new_source,
cleanup_old=cleanup_old,
overwrite=overwrite,
)
return schemas.Response(success=bool(result.get("success")), message=result.get("message"))
+18 -97
View File
@@ -2,7 +2,7 @@ import fnmatch
import math
import re
from pathlib import Path
from typing import Any, List, Optional
from typing import Any, Dict, List, Optional
from fastapi import Depends, HTTPException
from starlette.responses import FileResponse, Response
@@ -13,12 +13,10 @@ from app.chain.media import MediaChain
from app.chain.storage import StorageChain
from app.chain.transfer import TransferChain
from app.runtime.config import settings
from app.application.security.access import verify_token
from app.db.models import User
from app.api.deps import (
get_current_active_manage_user,
get_current_active_superuser,
get_current_active_superuser_async,
)
from app.runtime.progress import ProgressHelper
from app.schemas.types import ProgressKey
@@ -27,75 +25,28 @@ from app.foundation import text as text_tools
router = ResponseAPIRouter()
@router.get(
"/qrcode/{name}",
summary="生成二维码内容",
response_model=schemas.Response[schemas.StorageQrCodeData],
@router.post(
"/manage", summary="网盘存储统一管理", response_model=schemas.Response[Dict[str, Any]]
)
def qrcode(name: str, _: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
生成二维码
"""
qrcode_data, errmsg = StorageChain().generate_qrcode(name)
if qrcode_data:
return schemas.Response(success=True, data=qrcode_data, message=errmsg)
return schemas.Response(success=False, message=errmsg)
@router.get(
"/auth_url/{name}",
summary="获取 OAuth2 授权 URL",
response_model=schemas.Response[schemas.StorageAuthUrlData],
)
def auth_url(name: str, _: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
获取 OAuth2 授权 URL
"""
auth_data, errmsg = StorageChain().generate_auth_url(name)
if auth_data:
return schemas.Response(success=True, data=auth_data)
return schemas.Response(success=False, message=errmsg)
@router.get(
"/check/{name}",
summary="二维码登录确认",
response_model=schemas.Response[schemas.StorageLoginStatusData],
)
def check(
name: str,
ck: Optional[str] = None,
t: Optional[str] = None,
_: schemas.TokenPayload = Depends(verify_token),
def manage(
request: schemas.ManageRequest, _: User = Depends(get_current_active_superuser)
) -> Any:
"""
二维码登录确认
"""
if ck or t:
data, errmsg = StorageChain().check_login(name, ck=ck, t=t)
else:
data, errmsg = StorageChain().check_login(name)
if data:
return schemas.Response(success=True, data=data)
return schemas.Response(success=False, message=errmsg)
网盘存储统一管理入口
@router.post("/save/{name}", summary="保存存储配置", response_model=schemas.Response[None])
def save(name: str, conf: dict, _: User = Depends(get_current_active_superuser)) -> Any:
端点层不定义任何存储特定的名称与参数,
存储标识、管理动作与表单参数由前端上送并原样透传给存储模块
"""
保存存储配置
"""
StorageChain().save_config(name, conf)
return schemas.Response(success=True)
@router.get("/reset/{name}", summary="重置存储配置", response_model=schemas.Response[None])
def reset(name: str, _: User = Depends(get_current_active_superuser)) -> Any:
"""
重置存储配置
"""
StorageChain().reset_config(name)
return schemas.Response(success=True)
result = StorageChain().manage_storage(
storage=request.target,
action=request.action,
**request.params,
)
return schemas.Response(
success=bool(result.get("success")),
message=result.get("message"),
data=result.get("data"),
)
@router.post("/list", summary="所有目录和文件", response_model=List[schemas.FileItem])
@@ -293,33 +244,3 @@ def rename(
if result:
return schemas.Response(success=True)
return schemas.Response(success=False)
@router.get(
"/usage/{name}", summary="存储空间信息", response_model=schemas.StorageUsage
)
def usage(name: str, _: User = Depends(get_current_active_superuser)) -> Any:
"""
查询存储空间
"""
ret = StorageChain().storage_usage(name)
if ret:
return ret
return schemas.StorageUsage()
@router.get(
"/transtype/{name}",
summary="支持的整理方式获取",
response_model=schemas.StorageTransType,
)
async def transtype(
name: str, _: User = Depends(get_current_active_superuser_async)
) -> Any:
"""
查询支持的整理方式
"""
ret = StorageChain().support_transtype(name)
if ret:
return schemas.StorageTransType(transtype=ret)
return schemas.StorageTransType()