"""管理员 API:账号 CRUD、缓存、认证调度、系统配置。""" from fastapi import APIRouter, Depends, Request from fastapi.responses import JSONResponse from typing import Dict, Any from datetime import datetime, UTC import time from database.db import db from core.registry import get_all_driver_info, get_config_schema, validate_driver_config, get_driver_names, driver_registry from core.error_handler import raise_not_found, raise_api_error from core.base import get_driver_capabilities, driver_supports from core.log_manager import get_writer, LogModule from api.deps import require_admin_auth from api.responses import error_response as _error_response, success_response as _success_response from core.account_utils import filter_runtime_config as _filter_driver_config, get_account_or_404 from core.driver_service import get_account_driver_instance from core.security import generate_password_hash, assess_admin_credential_state from core.session_manager import session_manager router = APIRouter() def _mask_sensitive_value(value: Any) -> str: if value is None: return "None" text = str(value) if not text: return "(empty)" if len(text) <= 8: return "*" * len(text) return f"{text[:3]}***{text[-3:]}" from core.dependency_container import get_cache_manager, get_cache_cleaner, get_hit_tracker from core.utils import normalize_bool # ==================== 认证管理器通知函数 ==================== async def _notify_auth_manager_account_added(account_id: int, driver_name: str, config: Dict[str, Any]): try: from core.auth_manager import auth_scheduler logger = get_writer(LogModule.API) driver_instance = await get_account_driver_instance( account_id=account_id, require_active=False, account={ "id": account_id, "driver_type": driver_name, "config": config, "is_active": True }, existing_driver_name=driver_name, config_override=config ) if driver_instance and hasattr(driver_instance.config, 'supports_refresh') and driver_instance.config.supports_refresh: await auth_scheduler.add_account(account_id, driver_instance, driver_instance.config) except Exception as e: logger = get_writer(LogModule.API) logger.warning(f"通知认证管理器失败: {e}") async def _notify_auth_manager_account_updated(account_id: int, old_config: Dict[str, Any], new_config: Dict[str, Any], driver_name: str): try: from core.auth_manager import auth_scheduler from database.db import db from core.dependency_container import get_cache_cleaner import time import json logger = get_writer(LogModule.API) auth_fields = ['client_id', 'client_secret', 'access_token', 'refresh_token', 'username', 'password', 'cookie'] auth_changed = False for field in auth_fields: old_value = old_config.get(field) new_value = new_config.get(field) if old_value != new_value: auth_changed = True break # 下载模式改了需要清掉旧 mode 的链接缓存,否则还会返回旧模式的链接 old_download_mode = old_config.get('download_mode', 'redirect') new_download_mode = new_config.get('download_mode', 'redirect') if old_download_mode != new_download_mode: if logger: logger.debug(f"下载模式发生变化: {old_download_mode} -> {new_download_mode},准备清理缓存") cache_cleaner = get_cache_cleaner() if cache_cleaner: await cache_cleaner.clear_download_mode_cache(str(account_id)) if auth_changed: account = await db.get_account(account_id) if account: config = account['config'] if isinstance(config, str): config = json.loads(config) config.update({ 'last_refresh_time': int(time.time()), 'auth_status': 'active', 'refresh_attempts': 0 }) await db.update_account(account_id, config=config) await auth_scheduler.remove_account(account_id) driver_instance = await get_account_driver_instance( account_id=account_id, require_active=False, account={ "id": account_id, "driver_type": driver_name, "config": new_config, "is_active": True }, existing_driver_name=driver_name, config_override=new_config ) if driver_instance and hasattr(driver_instance.config, 'supports_refresh') and driver_instance.config.supports_refresh: await auth_scheduler.add_account(account_id, driver_instance, driver_instance.config) else: await auth_scheduler._trigger_recalculation("账号配置已更新") except Exception as e: logger = get_writer(LogModule.API) logger.warning(f"通知认证管理器账号更新失败: {e}") async def _notify_auth_manager_account_deleted(account_id: int): try: from core.auth_manager import auth_scheduler await auth_scheduler.remove_account(account_id) except Exception as e: logger = get_writer(LogModule.API) logger.warning(f"通知认证管理器账号删除失败: {e}") async def _notify_auth_manager_account_status_changed(account_id: int, enabled: bool): try: from core.auth_manager import auth_scheduler from database.db import db if enabled: account = await db.get_account(account_id) if account: driver_config = _filter_driver_config(account['config']) driver_instance = await get_account_driver_instance( account_id=account_id, account=account, require_active=False, existing_driver_name=account['driver_type'], config_override=driver_config ) if driver_instance and hasattr(driver_instance.config, 'supports_refresh') and driver_instance.config.supports_refresh: await auth_scheduler.add_account(account_id, driver_instance, driver_instance.config) else: await auth_scheduler.remove_account(account_id) except Exception as e: logger = get_writer(LogModule.API) logger.warning(f"通知认证管理器账号状态变更失败: {e}") async def _pause_account_related_tasks(account_id: int, reason: str, message: str): """账号不可用时联动暂停 cache_retention / strm 任务(reason: account_disabled | auth_failure)。""" logger = get_writer(LogModule.API) try: from core.cache_retention_manager import cache_retention_manager await cache_retention_manager.pause_tasks_by_account(account_id, reason, message) except Exception as e: logger.warning(f"联动暂停缓存保持任务失败(account={account_id} reason={reason}): {e}") try: from core.strm_sync_manager import strm_sync_manager await strm_sync_manager.pause_tasks_by_account(account_id, reason, message) except Exception as e: logger.warning(f"联动暂停 STRM 任务失败(account={account_id} reason={reason}): {e}") async def _resume_account_related_tasks(account_id: int): """账号重新可用时,只恢复因 account_disabled/auth_failure 被暂停的任务。""" logger = get_writer(LogModule.API) try: from core.cache_retention_manager import cache_retention_manager await cache_retention_manager.resume_tasks_by_account(account_id) except Exception as e: logger.warning(f"联动恢复缓存保持任务失败(account={account_id}): {e}") try: from core.strm_sync_manager import strm_sync_manager await strm_sync_manager.resume_tasks_by_account(account_id) except Exception as e: logger.warning(f"联动恢复 STRM 任务失败(account={account_id}): {e}") async def _test_driver_connection(driver_name: str, config: Dict[str, Any], account_id: str = "temp_test"): driver = None try: driver = await driver_registry.get_driver_instance( account_id=account_id, driver_name=driver_name, config=config ) result = await driver.test_connection() # test_connection 可能刷新了 token,回抓驱动里最新的 config import dataclasses updated_config = {} if hasattr(driver, 'config') and dataclasses.is_dataclass(driver.config): updated_config = dataclasses.asdict(driver.config) return result.success, (None if result.success else result.message), updated_config finally: if account_id == "temp_test" and driver is not None: await driver_registry._close_driver_instance(account_id) def _raise_non_litepan_error(error: Exception, message: str): from core.exceptions import LitePanError if isinstance(error, LitePanError): raise error raise_api_error(f"{message}: {str(error)}", "internal_error") # ==================== 驱动管理路由 ==================== @router.get("/drivers") async def get_available_drivers(request: Request, session_data: dict = Depends(require_admin_auth)): try: drivers_info = get_all_driver_info() result_data = {} for name, info in drivers_info.items(): result_data[name] = { "name": info["name"], "display_name": info["display_name"], "version": info["version"], "capabilities": info["capabilities"], "description": info["description"], "author": info["author"], "card_color": info.get("card_color"), "card_name": info.get("card_name"), "card_logo": info.get("card_logo"), "icon": info.get("icon"), "auto_oauth": info.get("auto_oauth", 0), "supports_qr_login": info.get("supports_qr_login", 0) } return _success_response(data=result_data, message="获取驱动列表成功") except Exception as e: return _error_response(message=str(e), data={}) @router.get("/drivers/{driver_name}/config-schema") async def get_driver_config_schema(driver_name: str, session_data: dict = Depends(require_admin_auth)): schema = get_config_schema(driver_name) if not schema: raise_not_found(f"驱动 {driver_name} 配置结构") return _success_response(data=schema, message="获取驱动配置成功") def _resolve_qr_login_driver(driver_name: str): driver_info = driver_registry.get_driver_info(driver_name) if not driver_info or not driver_info.get("supports_qr_login"): raise_api_error(f"驱动 {driver_name} 不支持扫码登录", "unsupported", 400) bundle = driver_registry._drivers.get(driver_name) or {} driver_class = bundle.get("driver_class") if driver_class is None or not hasattr(driver_class, "start_qr_login") or not hasattr(driver_class, "poll_qr_login"): raise_api_error(f"驱动 {driver_name} 未实现扫码登录接口", "unsupported", 400) return driver_class @router.post("/qr-login/start") async def qr_login_start(request: Request, session_data: dict = Depends(require_admin_auth)): try: body = await request.json() except Exception: body = {} driver_name = (body or {}).get("driver_type") if not driver_name: raise_api_error("缺少 driver_type", "validation", 400) driver_class = _resolve_qr_login_driver(driver_name) try: result = await driver_class.start_qr_login() return _success_response(data=result, message="二维码生成成功") except Exception as e: return _error_response(message=f"生成二维码失败: {str(e)}") @router.get("/qr-login/status/{state_id}") async def qr_login_status( state_id: str, driver_type: str, session_data: dict = Depends(require_admin_auth), ): driver_class = _resolve_qr_login_driver(driver_type) try: result = await driver_class.poll_qr_login(state_id) return _success_response(data=result, message="查询扫码状态成功") except Exception as e: return _error_response(message=f"查询扫码状态失败: {str(e)}") @router.get("/accounts/{account_id}/capabilities") async def get_account_capabilities(account_id: int, session_data: dict = Depends(require_admin_auth)): """鸭子类型:根据驱动实例动态探测真正支持的能力,前端据此控制按钮。""" account = await get_account_or_404(account_id) try: driver = await get_account_driver_instance(account_id, account=account) capabilities = get_driver_capabilities(driver) return _success_response( data={ "account_id": account_id, "driver_type": account['driver_type'], "capabilities": capabilities, "supports": { "list": driver_supports(driver, "list"), "info": driver_supports(driver, "info"), "download": driver_supports(driver, "download"), "create_folder": driver_supports(driver, "create_folder"), "delete": driver_supports(driver, "delete"), "batch_delete": driver_supports(driver, "batch_delete"), "rename": driver_supports(driver, "rename"), "move": driver_supports(driver, "move"), "upload": driver_supports(driver, "upload"), "copy": driver_supports(driver, "copy"), "share": driver_supports(driver, "share"), "chunk_download": driver_supports(driver, "chunk_download"), "resume_download": driver_supports(driver, "resume_download"), "batch_share": driver_supports(driver, "batch_share") } }, message="获取驱动能力成功" ) except Exception as e: return _error_response(message=f"获取驱动能力失败: {str(e)}") @router.get("/accounts") async def get_accounts(request: Request, session_data: dict = Depends(require_admin_auth)): """返回所有账号(含禁用),默认账号排在前面,前端自己过滤。""" from database.db import db accounts = await db.list_accounts(include_inactive=True) processed_accounts = [] for account in accounts: config = account['config'] processed_accounts.append({ **account, 'status': config.get('status', 'unknown'), 'error_message': config.get('error_message'), 'last_tested': config.get('last_tested'), 'enabled': account.get('is_active', True) }) return _success_response(data=processed_accounts, message="获取账号列表成功") @router.get("/accounts/{account_id}") async def get_account(account_id: int, session_data: dict = Depends(require_admin_auth)): account = await get_account_or_404(account_id) return _success_response(data=account, message="获取账号详情成功") @router.post("/accounts") async def create_account(account_data: Dict[str, Any], session_data: dict = Depends(require_admin_auth)): try: required_fields = ["name", "driver_type", "config"] for field in required_fields: if field not in account_data: raise_api_error(f"缺少必需字段: {field}", "validation", 400) driver_name = account_data["driver_type"] if driver_name not in get_driver_names(): raise_api_error(f"不支持的驱动类型: {driver_name}", "validation", 400) config = account_data["config"] is_valid, errors = validate_driver_config(driver_name, config) if not is_valid: raise_api_error(f"配置验证失败: {'; '.join(errors)}", "validation", 400) connection_success = False connection_error = None try: connection_success, connection_error, updated_config = await _test_driver_connection( driver_name=driver_name, config=config, account_id="temp_test" ) if updated_config: config.update(updated_config) except Exception as e: connection_success = False connection_error = str(e) if not connection_success: from core.error_handler import raise_connection_test_error raise_connection_test_error(driver_name, connection_error) from database.db import db config_with_auth = config.copy() config_with_auth.update({ 'last_refresh_time': int(time.time()), 'auth_status': 'active', 'refresh_attempts': 0, 'status': 'connected', 'error_message': None }) account_id = await db.add_account( name=account_data["name"], driver_type=driver_name, config=config_with_auth ) account = await db.get_account(account_id) await _notify_auth_manager_account_added(account_id, driver_name, config) return { "success": True, "data": account, "message": "账号创建成功" } except Exception as e: _raise_non_litepan_error(e, "创建账号失败") @router.put("/accounts/{account_id}") async def update_account( account_id: int, account_data: Dict[str, Any], session_data: dict = Depends(require_admin_auth) ): from database.db import db account = await db.get_account(account_id) if not account: raise_not_found("账号") try: update_data = {} if "name" in account_data: update_data["name"] = account_data["name"] if "enabled" in account_data: update_data["is_active"] = account_data["enabled"] if "config" in account_data: new_config = account_data["config"] is_valid, errors = validate_driver_config(account['driver_type'], new_config) if not is_valid: raise_api_error(f"配置验证失败: {'; '.join(errors)}", "validation", 400) # 编辑 config 必须重跑连接测试,防止把坏 token 持久化 connection_success = False connection_error = None try: connection_success, connection_error, updated_config = await _test_driver_connection( driver_name=account['driver_type'], config=new_config, account_id="temp_test" ) if updated_config: new_config.update(updated_config) except Exception as e: connection_success = False connection_error = str(e) if not connection_success: from core.error_handler import raise_connection_test_error raise_connection_test_error(account['driver_type'], connection_error) update_data["config"] = new_config if update_data: success = await db.update_account(account_id, **update_data) if not success: raise_api_error("更新账号失败", "update_account", 500) updated_account = await db.get_account(account_id) if "config" in update_data: await _notify_auth_manager_account_updated(account_id, account['config'], update_data["config"], account['driver_type']) elif "is_active" in update_data: await _notify_auth_manager_account_status_changed(account_id, update_data["is_active"]) final_enabled = bool(updated_account.get("is_active", True)) if updated_account else True old_enabled = bool(account.get("is_active", True)) if "is_active" in update_data and old_enabled != final_enabled: if final_enabled: await _resume_account_related_tasks(account_id) else: await _pause_account_related_tasks( account_id, reason="account_disabled", message="关联的账号已禁用", ) elif "config" in update_data and final_enabled: # 账号已启用且刚通过连接测试更新 config,说明认证已修好,顺手恢复被暂停的任务 await _resume_account_related_tasks(account_id) return { "success": True, "data": updated_account, "message": "账号更新成功" } except Exception as e: _raise_non_litepan_error(e, "更新账号失败") @router.delete("/accounts/{account_id}") async def delete_account(account_id: int, session_data: dict = Depends(require_admin_auth)): from database.db import db account = await db.get_account(account_id) if not account: raise_not_found("账号") try: success = await db.delete_account(account_id) await _notify_auth_manager_account_deleted(account_id) # 账号删了,顺带清掉它的 cache_retention / strm 任务,避免变成孤儿任务 from core.cache_retention_manager import cache_retention_manager await cache_retention_manager.remove_configs_by_account(account_id) from core.strm_sync_manager import strm_sync_manager await strm_sync_manager.remove_tasks_by_account(account_id) return { "success": True, "message": "账号删除成功" } except Exception as e: raise_api_error(f"删除账号失败: {str(e)}", "internal_error") @router.post("/accounts/{account_id}/test") async def test_account_connection(account_id: int, session_data: dict = Depends(require_admin_auth)): from database.db import db account = await db.get_account(account_id) if not account: raise_not_found("账号") try: config = account['config'] driver = await driver_registry.get_driver_instance( account_id=str(account_id), driver_name=account['driver_type'], config=config ) result = await driver.test_connection() success = result.success error_msg = result.message if not success else None config = account['config'] config['last_tested'] = datetime.now(UTC).isoformat() if success: config['status'] = "connected" config['error_message'] = None else: config['status'] = "failed" config['error_message'] = error_msg or "连接测试失败" await db.update_account(account_id, config=config) return { "success": True, "data": { "connected": success, "status": config['status'], "error_message": config.get('error_message'), "tested_at": config['last_tested'] } } except Exception as e: config = account['config'] config['status'] = "error" config['error_message'] = str(e) config['last_tested'] = datetime.now(UTC).isoformat() await db.update_account(account_id, config=config) return { "success": False, "data": { "connected": False, "status": config['status'], "error_message": config['error_message'], "tested_at": config['last_tested'] } } @router.post("/accounts/{account_id}/toggle") async def toggle_account_status(account_id: int, session_data: dict = Depends(require_admin_auth)): from database.db import db account = await db.get_account(account_id) if not account: raise_not_found("账号") try: old_enabled = account['is_active'] new_enabled = not old_enabled await db.update_account(account_id, is_active=new_enabled) if old_enabled != new_enabled: await _notify_auth_manager_account_status_changed(account_id, new_enabled) if new_enabled: await _resume_account_related_tasks(account_id) else: await _pause_account_related_tasks( account_id, reason="account_disabled", message="关联的账号已禁用", ) status_text = "启用" if new_enabled else "禁用" return { "success": True, "data": await db.get_account(account_id), "message": f"账号已{status_text}" } except Exception as e: raise_api_error(f"状态切换失败: {str(e)}", "internal_error") @router.post("/accounts/{account_id}/set-default") async def set_default_account(account_id: int, session_data: dict = Depends(require_admin_auth)): from database.db import db account = await db.get_account(account_id) if not account: raise_not_found("账号") try: success = await db.set_default_account(account_id) if success: return { "success": True, "data": account, "message": f"已将账号 '{account['name']}' 设为默认" } else: raise_api_error("设置默认账号失败", "internal_error") except Exception as e: raise_api_error(f"设置默认账号失败: {str(e)}", "internal_error") @router.get("/cache/info") async def get_cache_info(session_data: dict = Depends(require_admin_auth)): try: cache_manager = get_cache_manager() cache_cleaner = get_cache_cleaner() if not cache_manager: return _error_response(message="缓存系统未初始化") cache_info = cache_manager.get_cache_info() cleaner_stats = cache_cleaner.get_cleanup_stats() if cache_cleaner else {} return _success_response( data={ "cache_info": cache_info, "cleaner_stats": cleaner_stats, "system_status": "running" }, message="获取缓存信息成功" ) except Exception as e: return _error_response(message=f"获取缓存信息失败: {str(e)}") @router.post("/cache/test") async def test_cache(test_data: dict = None, session_data: dict = Depends(require_admin_auth)): try: from cache.cache_types import CacheType cache_manager = get_cache_manager() if not cache_manager: return _error_response(message="缓存系统未初始化") test_key = "test:cache:demo" test_value = test_data or {"message": "这是测试数据", "timestamp": str(datetime.now())} test_ttl = 60 set_result = await cache_manager.set(test_key, test_value, test_ttl, CacheType.DIRECTORY) get_result = await cache_manager.get(test_key) # TTL=0 走不写缓存分支,回归一下这条路径 no_cache_key = "test:no_cache" no_cache_result = await cache_manager.set(no_cache_key, test_value, 0, CacheType.DIRECTORY) return _success_response( data={ "test_results": { "set_cache": set_result, "get_cache": get_result, "cache_match": get_result == test_value, "ttl_0_handling": no_cache_result, }, "cache_stats": cache_manager.get_cache_info() }, message="缓存测试完成" ) except Exception as e: return _error_response(message=f"缓存测试失败: {str(e)}") @router.post("/cache/clear") async def clear_cache_by_type(clear_type: str = "all", session_data: dict = Depends(require_admin_auth)): try: cache_manager = get_cache_manager() cache_cleaner = get_cache_cleaner() if not cache_manager: return _error_response(message="缓存系统未初始化") cleared_count = 0 if clear_type == "all": cleared_count = await cache_manager.clear_all() elif clear_type == "expired": if cache_cleaner: cleared_count = await cache_cleaner.cleanup_expired() else: return _error_response(message="缓存清理器未初始化") else: return _error_response(message=f"不支持的清理类型: {clear_type}") return _success_response( data={ "cleared_count": cleared_count, "clear_type": clear_type }, message="缓存清理完成" ) except Exception as e: return _error_response(message=f"清理缓存失败: {str(e)}") @router.get("/cache/stats/{account_id}") async def get_account_cache_stats(account_id: int, session_data: dict = Depends(require_admin_auth)): try: cache_manager = get_cache_manager() if not cache_manager: return _error_response(message="缓存系统未初始化") account_cache_count = 0 account_cache_size = 0 async with cache_manager.lock: for key, item in cache_manager.cache.items(): from cache.cache_keys import CacheKeyValidator extracted_account_id = CacheKeyValidator.extract_account_id(key) if extracted_account_id == account_id: account_cache_count += 1 account_cache_size += item.size_bytes return _success_response( data={ "account_id": account_id, "cache_count": account_cache_count, "cache_size_bytes": account_cache_size, "cache_size_mb": account_cache_size / (1024 * 1024) }, message="获取账号缓存统计成功" ) except Exception as e: return _error_response(message=f"获取账号缓存统计失败: {str(e)}") @router.get("/cache-config") async def get_cache_config(session_data: dict = Depends(require_admin_auth)): try: from config import config_manager from config import Settings # 数据库存秒、前端展示分钟,在这里做换算 cache_enabled = await config_manager.get_async('cache_enabled') cache_ttl_seconds = await config_manager.get_async('cache_ttl') or Settings.CACHE_TTL cache_ttl_minutes = cache_ttl_seconds // 60 cache_persistence_enabled = await config_manager.get_async('cache_persistence_enabled') cache_persistence_interval_seconds = await config_manager.get_async('cache_persistence_interval_seconds') cache_persistence_interval_minutes = ( (cache_persistence_interval_seconds // 60) if cache_persistence_interval_seconds is not None else Settings.CACHE_PERSISTENCE_INTERVAL_SECONDS // 60 ) cache_max_items = await config_manager.get_async('cache_max_items') if cache_max_items is None: cache_max_items = Settings.CACHE_MAX_ITEMS cache_memory_limit_mb = await config_manager.get_async('cache_memory_limit_mb') if cache_memory_limit_mb is None: cache_memory_limit_mb = Settings.CACHE_MEMORY_LIMIT_MB return _success_response( data={ "cache_enabled": cache_enabled if cache_enabled is not None else True, "cache_ttl": cache_ttl_minutes, "cache_persistence_enabled": ( cache_persistence_enabled if cache_persistence_enabled is not None else Settings.CACHE_PERSISTENCE_ENABLED ), "cache_persistence_interval_minutes": cache_persistence_interval_minutes, "cache_max_items": int(cache_max_items), "cache_memory_limit_mb": int(cache_memory_limit_mb), }, message="获取缓存配置成功" ) except Exception as e: logger = get_writer(LogModule.API) logger.error(f"获取缓存配置失败: {e}") return _error_response(message=f"获取缓存配置失败: {str(e)}") @router.post("/update-cache-config") async def update_cache_config( request: Request, session_data: dict = Depends(require_admin_auth) ): try: from config import Settings cache_data = await request.json() cache_enabled = cache_data.get('cache_enabled', True) cache_ttl = cache_data.get('cache_ttl', Settings.CACHE_TTL // 60) # 前端传的是分钟 cache_persistence_enabled = cache_data.get('cache_persistence_enabled', True) cache_persistence_interval_minutes = cache_data.get( 'cache_persistence_interval_minutes', Settings.CACHE_PERSISTENCE_INTERVAL_SECONDS // 60, ) cache_max_items = cache_data.get('cache_max_items', Settings.CACHE_MAX_ITEMS) cache_memory_limit_mb = cache_data.get('cache_memory_limit_mb', Settings.CACHE_MEMORY_LIMIT_MB) if not isinstance(cache_enabled, bool): return _error_response(message="缓存开关必须是布尔值") if not isinstance(cache_persistence_enabled, bool): return _error_response(message="缓存持久化开关必须是布尔值") if not isinstance(cache_ttl, (int, float)) or cache_ttl < 1 or cache_ttl > 1440: return _error_response(message="缓存过期时间必须在1-1440分钟之间") if ( not isinstance(cache_persistence_interval_minutes, (int, float)) or cache_persistence_interval_minutes < 1 or cache_persistence_interval_minutes > 1440 ): return _error_response(message="缓存持久化快照间隔必须在1-1440分钟之间") if ( not isinstance(cache_max_items, (int, float)) or cache_max_items < 1000 or cache_max_items > 1000000 ): return _error_response(message="缓存条目最大数量必须在1000-1000000之间") if ( not isinstance(cache_memory_limit_mb, (int, float)) or cache_memory_limit_mb < 64 or cache_memory_limit_mb > 16384 ): return _error_response(message="缓存内存上限必须在64-16384MB之间") from config import config_manager # 前端分钟 -> 数据库秒 cache_ttl_seconds = int(cache_ttl * 60) cache_persistence_interval_seconds = int(cache_persistence_interval_minutes * 60) cache_max_items = int(cache_max_items) result1 = await config_manager.set_async('cache_enabled', cache_enabled) result2 = await config_manager.set_async('cache_ttl', cache_ttl_seconds) result3 = await config_manager.set_async('cache_persistence_enabled', cache_persistence_enabled) result4 = await config_manager.set_async('cache_persistence_interval_seconds', cache_persistence_interval_seconds) result5 = await config_manager.set_async('cache_max_items', cache_max_items) result6 = await config_manager.set_async('cache_memory_limit_mb', int(cache_memory_limit_mb)) if result1 and result2 and result3 and result4 and result5 and result6: try: from core.cache_retention_manager import cache_retention_manager await cache_retention_manager.on_global_cache_changed(cache_enabled) except Exception as e: # 通知失败不回滚配置保存,只记日志 logger = get_writer(LogModule.API) logger.warning(f"通知缓存保持管理器失败: {e}") try: from cache import apply_cache_runtime_settings await apply_cache_runtime_settings( cache_enabled=cache_enabled, persistence_enabled=cache_persistence_enabled, persistence_interval_seconds=cache_persistence_interval_seconds, max_items=cache_max_items, memory_limit_mb=int(cache_memory_limit_mb), ) except Exception as e: logger = get_writer(LogModule.API) logger.warning(f"应用缓存持久化配置失败: {e}") return _success_response(message="缓存配置更新成功") else: return _error_response(message="配置保存失败,请检查数据库连接") except Exception as e: return {"success": False, "message": f"更新缓存配置失败: {str(e)}"} @router.get("/hit-rate/stats") async def get_hit_rate_stats(session_data: dict = Depends(require_admin_auth)): try: hit_tracker = get_hit_tracker() if not hit_tracker: return _error_response(message="命中率追踪器未初始化") global_stats = await hit_tracker.get_global_stats() return _success_response(data=global_stats, message="获取命中率统计成功") except Exception as e: return _error_response(message=str(e)) @router.get("/cache/stats") async def get_cache_stats(session_data: dict = Depends(require_admin_auth)): """缓存条目 / 大小 / 命中率 + cache_retention 任务数(只做调试展示,不计入缓存条目)。""" try: cache_manager = get_cache_manager() if not cache_manager: return _error_response(message="缓存管理器未初始化") cache_stats = cache_manager.get_stats() cache_info = cache_manager.get_cache_info() hit_tracker = get_hit_tracker() hit_rate_stats = {} if hit_tracker: hit_rate_stats = await hit_tracker.get_global_stats() cache_retention_stats = {} try: from core.cache_retention_manager import cache_retention_manager cache_retention_stats = { "total_tasks": len(cache_retention_manager._tasks), "running_tasks": len([t for t in cache_retention_manager._tasks.values() if t.status.value == 'running']), "paused_tasks": len([t for t in cache_retention_manager._tasks.values() if t.status.value == 'paused']) } except Exception as e: cache_retention_stats = {"error": str(e)} return _success_response( data={ "total_keys": cache_info['total_items'], "total_size_bytes": cache_stats.total_size_bytes, "hit_rate": hit_rate_stats.get('hit_rate', 0), "cache_info": cache_info, "cache_retention": cache_retention_stats }, message="获取缓存统计成功" ) except Exception as e: return _error_response(message=str(e)) @router.get("/hit-rate/session") async def get_session_hit_rate(session_id: str = "default", session_data: dict = Depends(require_admin_auth)): try: hit_tracker = get_hit_tracker() if not hit_tracker: return _error_response(message="命中率追踪器未初始化") session_stats = await hit_tracker.get_stats(session_id) if session_stats is None: return _error_response(message="会话不存在") return _success_response(data=session_stats, message="获取会话命中率成功") except Exception as e: return _error_response(message=str(e)) @router.post("/clear-cache") async def clear_cache(session_data: dict = Depends(require_admin_auth)): try: cache_cleaner = get_cache_cleaner() if not cache_cleaner: return _error_response(message="缓存清理器未初始化") cleared_count = await cache_cleaner.clear_all_cache() hit_tracker = get_hit_tracker() if hit_tracker: await hit_tracker.reset_stats() return _success_response( data={"cleared_count": cleared_count}, message=f"缓存已清空,清理了{cleared_count}个项目" ) except Exception as e: return _error_response(message=str(e)) # ==================== 认证管理相关API ==================== @router.get("/accounts/{account_id}/auth_status") async def get_auth_status(account_id: int, session_data: dict = Depends(require_admin_auth)): try: account = await db.get_account(account_id) if not account: raise_not_found("账号") from core.auth_manager import auth_scheduler auth_manager = auth_scheduler.auth_managers.get(account_id) if not auth_manager: config = account['config'] return { "success": True, "data": { "account_id": account_id, "auth_status": config.get('auth_status', 'active'), "auth_type": "unknown", "last_refresh_time": config.get('last_refresh_time', 0), "refresh_attempts": config.get('refresh_attempts', 0), "next_refresh_time": 0, "supports_refresh": False, "managed_by_scheduler": False } } next_refresh_time = await auth_scheduler._calculate_account_next_time(account_id, time.time()) config = account['config'] return { "success": True, "data": { "account_id": account_id, "auth_status": config.get('auth_status', 'active'), "auth_type": getattr(auth_manager.config, 'auth_type', 'unknown'), "last_refresh_time": config.get('last_refresh_time', 0), "refresh_attempts": config.get('refresh_attempts', 0), "next_refresh_time": int(next_refresh_time), "supports_refresh": getattr(auth_manager.config, 'supports_refresh', False), "managed_by_scheduler": True, "config_params": { "token_expires_seconds": getattr(auth_manager.config, 'token_expires_seconds', None), "refresh_advance_seconds": getattr(auth_manager.config, 'refresh_advance_seconds', None), "health_check_interval": getattr(auth_manager.config, 'health_check_interval', None), "retry_cooldown_seconds": getattr(auth_manager.config, 'retry_cooldown_seconds', None) } } } except Exception as e: raise_api_error(str(e), "get_auth_status") @router.post("/accounts/{account_id}/refresh_auth") async def manual_refresh_auth(account_id: int, session_data: dict = Depends(require_admin_auth)): try: account = await db.get_account(account_id) if not account: raise_not_found("账号") from core.auth_manager import handle_auth_error success = await handle_auth_error(account_id) if success: return _success_response(message="认证刷新成功") else: return _error_response(message="认证刷新失败") except Exception as e: return _error_response(message=f"刷新异常: {str(e)}") @router.get("/auth/scheduler_status") async def get_scheduler_status(session_data: dict = Depends(require_admin_auth)): try: from core.auth_manager import auth_scheduler import time managed_accounts = len(auth_scheduler.auth_managers) is_running = auth_scheduler._running next_check_time = 0 if is_running and managed_accounts > 0: next_check_time_float = await auth_scheduler._calculate_next_check_time(emit_log=False) next_check_time = int(next_check_time_float) accounts_status = [] for account_id, auth_manager in auth_scheduler.auth_managers.items(): try: next_refresh_time = await auth_scheduler._calculate_account_next_time(account_id, time.time()) accounts_status.append({ "account_id": account_id, "auth_type": auth_manager.auth_type, "next_refresh_time": int(next_refresh_time), "next_refresh_in_seconds": max(0, int(next_refresh_time - time.time())) }) except Exception as e: accounts_status.append({ "account_id": account_id, "auth_type": getattr(auth_manager, 'auth_type', 'unknown'), "error": str(e) }) return _success_response( data={ "is_running": is_running, "managed_accounts": managed_accounts, "next_check_time": next_check_time, "accounts_status": accounts_status }, message="获取调度器状态成功" ) except Exception as e: raise_api_error(str(e), "get_auth_scheduler_status") @router.post("/auth/scheduler/recalculate") async def recalculate_scheduler(session_data: dict = Depends(require_admin_auth)): """兼容老前端的占位接口:调度器每轮自然重算,这里不做事。""" try: from core.auth_manager import auth_scheduler pass return _success_response(message="调度时间已重新计算") except Exception as e: return _error_response(message=f"重新计算失败: {str(e)}") # ==================== 系统设置相关API ==================== @router.get("/test-credentials") async def test_credentials(session_data: dict = Depends(require_admin_auth)): return _success_response(message="路由工作正常") @router.post("/dashboard/ack-errors") async def ack_dashboard_errors(session_data: dict = Depends(require_admin_auth)): """仪表盘"标记错误已读"。 把当前时间戳写入 dashboard_errors_ack_at 配置,logs/stats 接口下次返回的 recent_errors 会自动剔除该时间点之前的错误,仪表盘的红色状态也随之消失。 日志本身**不会被删除**,依旧能在日志页和审计中看到。 """ try: from config import config_manager now_iso = datetime.now().isoformat() await config_manager.set_async('dashboard_errors_ack_at', now_iso) return _success_response(data={"ack_at": now_iso}, message="已确认最近错误日志") except Exception as e: return _error_response(message=f"标记已读失败: {str(e)}") @router.get("/system-config") async def get_system_config(session_data: dict = Depends(require_admin_auth)): try: from config import config_manager # 数据库存秒、前端显示小时 session_timeout_seconds = await config_manager.get_async('session_timeout') or 7200 session_timeout_hours = session_timeout_seconds / 3600 webdav_enabled = await config_manager.get_async('webdav_enabled') webdav_smart_chunk = await config_manager.get_async('webdav_smart_chunk_enabled') webdav_chunk_size = await config_manager.get_async('webdav_chunk_size') webdav_cache_enabled = await config_manager.get_async('webdav_cache_enabled') auth_active_refresh_enabled = await config_manager.get_async('auth_active_refresh_enabled') public_index_enabled = await config_manager.get_async('public_index_enabled') index_account_switch_mode = await config_manager.get_async('index_account_switch_mode') or "dropdown" if index_account_switch_mode not in {"dropdown", "floating"}: index_account_switch_mode = "dropdown" admin_home_return_mode = await config_manager.get_async('admin_home_return_mode') or "top_icon" if admin_home_return_mode not in {"sidebar", "top_icon", "both"}: admin_home_return_mode = "top_icon" theme = await config_manager.get_async('theme') or "light" if theme not in {"light", "dark", "auto"}: theme = "light" # 兼容老数据:>8192 视为 bytes,其它按 KB;并回落到默认 256KB if webdav_chunk_size and webdav_chunk_size > 8192: webdav_chunk_size_kb = webdav_chunk_size // 1024 else: webdav_chunk_size_kb = webdav_chunk_size or 256 admin_username = await config_manager.get_async('admin_username') or "admin" admin_password = await config_manager.get_async('admin_password') or "admin" credential_state = assess_admin_credential_state(admin_username, admin_password) return _success_response( data={ "admin_username": admin_username, "session_timeout": session_timeout_hours, "oauth_server_url": await config_manager.get_async('oauth_server_url') or "https://oauth.litepan.top", "public_index_enabled": normalize_bool(public_index_enabled, True), "index_account_switch_mode": index_account_switch_mode, "admin_home_return_mode": admin_home_return_mode, "theme": theme, "upload_task_concurrency": await config_manager.get_async('upload_task_concurrency') or 3, "log_retention_days": await config_manager.get_async('log_retention_days') or 30, "auth_active_refresh_enabled": normalize_bool(auth_active_refresh_enabled, True), "must_change_password": credential_state["must_change_password"], "password_change_reason": credential_state["password_change_reason"], "webdav_enabled": normalize_bool(webdav_enabled, True), "webdav_smart_chunk_enabled": webdav_smart_chunk if isinstance(webdav_smart_chunk, bool) else str(webdav_smart_chunk).lower() != 'false', "webdav_chunk_size": webdav_chunk_size_kb, "webdav_cache_enabled": normalize_bool(webdav_cache_enabled, True), }, message="获取系统配置成功" ) except Exception as e: return { "success": False, "message": f"获取配置失败: {str(e)}" } @router.post("/theme") async def update_theme( request: Request, session_data: dict = Depends(require_admin_auth) ): try: data = await request.json() theme = data.get('theme') if theme not in {"light", "dark", "auto"}: return _error_response(message="界面主题不正确") from config import config_manager await config_manager.set_async('theme', theme) return _success_response(data={"theme": theme}, message="界面主题已更新") except Exception as e: return _error_response(message=f"更新界面主题失败: {str(e)}") @router.post("/update-credentials") async def update_admin_credentials( request: Request, session_data: dict = Depends(require_admin_auth) ): try: credentials_data = await request.json() username = credentials_data.get('admin_username', '').strip() password = credentials_data.get('admin_password', '').strip() session_timeout = credentials_data.get('session_timeout') oauth_server_url = (credentials_data.get('oauth_server_url') or '').strip() public_index_enabled = credentials_data.get('public_index_enabled') upload_task_concurrency = credentials_data.get('upload_task_concurrency') log_retention_days = credentials_data.get('log_retention_days') auth_active_refresh_enabled = credentials_data.get('auth_active_refresh_enabled') index_account_switch_mode = credentials_data.get('index_account_switch_mode') admin_home_return_mode = credentials_data.get('admin_home_return_mode') theme = credentials_data.get('theme') if not username: return _error_response(message="用户名不能为空") import re if not re.match(r'^[a-zA-Z0-9_]+$', username): return _error_response(message="用户名只能包含字母、数字和下划线") from config import config_manager current_admin_password = await config_manager.get_async('admin_password') or "admin" current_credential_state = assess_admin_credential_state(username, current_admin_password) # 前端传小时,存库按秒 if session_timeout is not None: if not isinstance(session_timeout, (int, float)) or session_timeout < 0.5 or session_timeout > 24: return _error_response(message="会话超时时间必须在0.5-24小时之间") session_timeout_seconds = int(session_timeout * 3600) if oauth_server_url: if not re.match(r'^https?://[^/]+$', oauth_server_url): return _error_response(message="OAuth服务地址格式不正确,示例:https://litepan.top") if upload_task_concurrency is not None: if not isinstance(upload_task_concurrency, int) or upload_task_concurrency < 1 or upload_task_concurrency > 5: return _error_response(message="上传任务并发数必须是 1-5 之间的整数") if log_retention_days is not None: if not isinstance(log_retention_days, int) or log_retention_days < 1 or log_retention_days > 365: return _error_response(message="日志保留天数必须是 1-365 之间的整数") if index_account_switch_mode is not None and index_account_switch_mode not in {"dropdown", "floating"}: return _error_response(message="首页账号切换方式不正确") if admin_home_return_mode is not None and admin_home_return_mode not in {"sidebar", "top_icon", "both"}: return _error_response(message="主页返回方式不正确") if theme is not None and theme not in {"light", "dark", "auto"}: return _error_response(message="界面主题不正确") async def apply_upload_task_concurrency(): if upload_task_concurrency is None: return await config_manager.set_async('upload_task_concurrency', upload_task_concurrency) from core.upload_task_manager import upload_task_manager await upload_task_manager.apply_concurrency_limit(upload_task_concurrency) async def apply_public_index_enabled_setting(): if public_index_enabled is None: return normalized_enabled = normalize_bool(public_index_enabled, True) await config_manager.set_async('public_index_enabled', normalized_enabled) async def apply_auth_active_refresh_setting(): if auth_active_refresh_enabled is None: return normalized_enabled = normalize_bool(auth_active_refresh_enabled, True) await config_manager.set_async('auth_active_refresh_enabled', normalized_enabled) from core.auth_manager import auth_scheduler await auth_scheduler.set_active_refresh_enabled(normalized_enabled) async def apply_index_account_switch_mode(): if index_account_switch_mode is None: return await config_manager.set_async('index_account_switch_mode', index_account_switch_mode) async def apply_admin_home_return_mode(): if admin_home_return_mode is None: return await config_manager.set_async('admin_home_return_mode', admin_home_return_mode) async def apply_theme(): if theme is None: return await config_manager.set_async('theme', theme) async def apply_log_retention_days(): if log_retention_days is None: return await config_manager.set_async('log_retention_days', log_retention_days) from core.log_manager import get_log_manager log_manager = get_log_manager() if log_manager: await log_manager.start_auto_cleanup(log_retention_days) await log_manager.storage.cleanup_old_logs(log_retention_days) # 没传新密码视作不改密码,直接保存其他字段 if not password: if current_credential_state["must_change_password"]: return _error_response(message="当前管理员密码需要升级,请先设置新密码后再保存其他设置") await config_manager.set_async('admin_username', username) if session_timeout is not None: await config_manager.set_async('session_timeout', session_timeout_seconds) if oauth_server_url: await config_manager.set_async('oauth_server_url', oauth_server_url.rstrip('/')) await apply_public_index_enabled_setting() await apply_upload_task_concurrency() await apply_auth_active_refresh_setting() await apply_index_account_switch_mode() await apply_admin_home_return_mode() await apply_theme() await apply_log_retention_days() response = JSONResponse(content=_success_response(message="管理员设置更新成功")) session_manager.create_session( response=response, user_data={ "is_admin": True, "username": username, }, remember=bool(session_data.get("remember")), request=request, ) return response if len(password) < 6: return _error_response(message="密码长度至少6位") password_hash = generate_password_hash(password) await config_manager.set_async('admin_username', username) await config_manager.set_async('admin_password', password_hash) await config_manager.set_async("admin_temp_password_hash", "") await config_manager.set_async("admin_temp_password_expires_at", 0) if session_timeout is not None: await config_manager.set_async('session_timeout', session_timeout_seconds) if oauth_server_url: await config_manager.set_async('oauth_server_url', oauth_server_url.rstrip('/')) await apply_public_index_enabled_setting() await apply_upload_task_concurrency() await apply_auth_active_refresh_setting() await apply_index_account_switch_mode() await apply_admin_home_return_mode() await apply_theme() await apply_log_retention_days() response = JSONResponse(content=_success_response(message="管理员设置更新成功")) session_manager.create_session( response=response, user_data={ "is_admin": True, "username": username, }, remember=bool(session_data.get("remember")), request=request, ) return response except Exception as e: return { "success": False, "message": f"更新失败: {str(e)}" } @router.post("/accounts/{account_id}/clear-download-cache") async def clear_account_download_cache(account_id: int, session_data: dict = Depends(require_admin_auth)): try: from core.dependency_container import get_cache_cleaner cache_cleaner = get_cache_cleaner() if cache_cleaner: await cache_cleaner.clear_download_mode_cache(str(account_id)) return _success_response(message="下载模式缓存已清空") else: return _error_response(message="缓存清理器不可用") except Exception as e: return _error_response(message=f"清空缓存失败: {str(e)}") @router.post("/webdav-config") async def update_webdav_config( request: Request, session_data: dict = Depends(require_admin_auth) ): try: data = await request.json() from config import config_manager webdav_enabled = data.get('webdav_enabled') if webdav_enabled is not None: await config_manager.set_async('webdav_enabled', normalize_bool(webdav_enabled, True)) smart_chunk = data.get('webdav_smart_chunk_enabled') if smart_chunk is not None: await config_manager.set_async('webdav_smart_chunk_enabled', smart_chunk) # 前端传 KB,库内存 bytes chunk_size_kb = data.get('webdav_chunk_size') if chunk_size_kb is not None: chunk_size_bytes = int(chunk_size_kb) * 1024 await config_manager.set_async('webdav_chunk_size', chunk_size_bytes) webdav_cache_enabled = data.get('webdav_cache_enabled') if webdav_cache_enabled is not None: normalized_enabled = normalize_bool(webdav_cache_enabled, True) await config_manager.set_async('webdav_cache_enabled', normalized_enabled) if webdav_cache_enabled is not None: try: from webdav.server import clear_webdav_cache await clear_webdav_cache() except Exception as e: get_writer(LogModule.SYSTEM).warning(f"清除 WebDAV 缓存失败: {e}") return _success_response(message="WebDAV设置更新成功") except Exception as e: return _error_response(message=f"更新WebDAV配置失败: {str(e)}")