""" 提供站点配置、认证、数据查询和状态管理接口。 """ from typing import Annotated, Any, List, Literal, Optional from fastapi import Depends, HTTPException, Query, Response from app.adapters.web.security.access import verify_token from app.api.context import get_background_task_registry, resolve_background_task_registry from app.api.dependencies.auth import ( get_current_active_manage_user, get_current_active_manage_user_async, get_current_active_superuser, get_current_active_superuser_async, get_current_active_user_async, ) from app.api.dependencies.site import ( get_site_mutation_command, get_site_query_service, get_site_sync_query_service, ) from app.api.principal import ApiPrincipal from app.api.response import ( COLLECTION_TOTAL_HEADER, COLLECTION_TOTAL_OPENAPI_KEY, CompatibleCountParam, CompatiblePageParam, ResponseAPIRouter, resolve_compatible_pagination, ) from app.application.commands import init_commands from app.application.configuration import get_configured_system_config from app.application.plugin.routes import register_plugin_api from app.application.plugin.runtime import get_plugin_manager from app.application.scheduling import get_scheduler from app.application.site.auth import normalize_site_auth_params from app.application.site.mutation import SiteMutationCommand from app.application.site.query import SiteQueryService # 站点目录由外部资源加载,静态检查无法解析其动态导出。 from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module from app.chain.site import SiteChain from app.chain.torrents import TorrentsChain from app.domain import site as site_rules from app.runtime.log import logger from app.runtime.tasks import TaskRegistry from app.schemas.common import JsonData from app.schemas.common import JsonObject as _SchemaJsonObject from app.schemas.context import TorrentInfo as _SchemaTorrentInfo from app.schemas.response import Response as _SchemaResponse from app.schemas.site import Site as _SchemaSite from app.schemas.site import SiteAuth as _SchemaSiteAuth from app.schemas.site import SiteCategory as _SchemaSiteCategory from app.schemas.site import SiteCookieSet as _SchemaSiteCookieSet from app.schemas.site import SiteCookieUpdate as _SchemaSiteCookieUpdate from app.schemas.site import SiteIconData as _SchemaSiteIconData from app.schemas.site import SiteMappingData as _SchemaSiteMappingData from app.schemas.site import SitePriorityUpdate as _SchemaSitePriorityUpdate from app.schemas.site import SiteStatistic as _SchemaSiteStatistic from app.schemas.site import SiteUserData as _SchemaSiteUserData from app.schemas.token import TokenPayload as _SchemaTokenPayload from app.schemas.types import MediaType, SystemConfigKey router = ResponseAPIRouter() def _project_agent_site(site: Any, *, include_secrets: bool) -> dict[str, JsonData]: """ 构造旧 Agent 查询语义的站点安全投影,普通用户不返回认证凭据。 """ projected: dict[str, JsonData] = { "id": site.id, "name": site.name, "domain": site.domain, "url": site.url, "pri": site.pri, "is_active": site.is_active, "downloader": site.downloader, "ua": site.ua, "proxy": site.proxy, "filter": site.filter, "render": site.render, "public": site.public, "note": site.note, "limit_interval": site.limit_interval, "limit_count": site.limit_count, "limit_seconds": site.limit_seconds, "timeout": site.timeout, } if include_secrets: projected.update( { "rss": site.rss, "cookie": site.cookie, "apikey": site.apikey, "token": site.token, } ) return projected def _project_readable_site( site: _SchemaSite, token_payload: _SchemaTokenPayload ) -> _SchemaSite | dict[str, JsonData]: """ 普通登录用户可读的站点信息不返回认证凭据,超级管理员保持完整字段。 """ if token_payload.super_user: return site return _project_agent_site(site, include_secrets=False) def _indexer_supports_media_type(indexer: dict[str, Any], media_type: MediaType) -> bool: """ 判断站点索引器是否支持指定媒体类型。 :param indexer: 站点索引器配置 :param media_type: 待搜索的媒体类型 :return: 是否应在该媒体类型的站点选择列表中显示 """ declared_media_type = indexer.get("media_type") site_media_type: Optional[MediaType] if isinstance(declared_media_type, MediaType): site_media_type = declared_media_type elif isinstance(declared_media_type, str): site_media_type = MediaType.from_agent(declared_media_type) else: site_media_type = None if site_media_type: return site_media_type == media_type categories = indexer.get("category") or {} if not isinstance(categories, dict): return media_type != MediaType.MUSIC category_key = media_type.to_agent() if media_type == MediaType.MUSIC: return bool(categories.get(category_key)) declared_category_keys = { item.to_agent() for item in (MediaType.MOVIE, MediaType.TV, MediaType.MUSIC) if categories.get(item.to_agent()) } if declared_category_keys: return category_key in declared_category_keys return True def _normalize_site_ids(values: Any) -> list[int]: """ 把配置或索引器中的站点标识归一为可查询的整数主键。 """ normalized: list[int] = [] for value in values or []: try: normalized.append(int(value)) except (TypeError, ValueError): continue return list(dict.fromkeys(normalized)) @router.get( "/", summary="所有站点", response_model=List[_SchemaSite], openapi_extra={COLLECTION_TOTAL_OPENAPI_KEY: True}, ) async def read_sites( response: Response = None, # type: ignore[assignment] query: SiteQueryService = Depends(get_site_query_service), _: ApiPrincipal = Depends(get_current_active_manage_user_async), page: CompatiblePageParam = None, count: CompatibleCountParam = None, ) -> List[_SchemaSite]: """ 获取站点列表 """ page, count = resolve_compatible_pagination(page, count) if response is not None: response.headers[COLLECTION_TOTAL_HEADER] = str( await query.count_ordered() ) return await query.list_ordered(page=page, count=count) @router.get( "/agent", summary="查询 Agent 可用站点", response_model=List[_SchemaJsonObject], openapi_extra={COLLECTION_TOTAL_OPENAPI_KEY: True}, ) async def read_agent_sites( response: Response = None, # type: ignore[assignment] status: Literal["active", "inactive", "all"] = "all", name: Optional[str] = None, query: SiteQueryService = Depends(get_site_query_service), current_user: Any = Depends(get_current_active_user_async), page: CompatiblePageParam = None, count: CompatibleCountParam = None, ) -> List[dict[str, JsonData]]: """ 按旧 Agent 过滤语义返回站点,非超级管理员自动剔除认证字段。 """ active_filter = None if status == "active": active_filter = True elif status == "inactive": active_filter = False page, count = resolve_compatible_pagination(page, count) if response is not None: response.headers[COLLECTION_TOTAL_HEADER] = str( await query.count_ordered(is_active=active_filter, name=name) ) sites = await query.list_ordered( is_active=active_filter, name=name, page=page, count=count, ) results = [] for site in sites: results.append( _project_agent_site( site, include_secrets=bool(current_user.is_superuser), ) ) return results @router.get( "/media/{media_type}", summary="按媒体类型获取可搜索站点", response_model=List[_SchemaSite], openapi_extra={COLLECTION_TOTAL_OPENAPI_KEY: True}, ) async def read_sites_by_media_type( media_type: str, response: Response = None, # type: ignore[assignment] query: SiteQueryService = Depends(get_site_query_service), _: ApiPrincipal = Depends(get_current_active_manage_user_async), page: CompatiblePageParam = None, count: CompatibleCountParam = None, ) -> List[_SchemaSite]: """ 获取支持指定媒体类型的已配置启用站点。 :param media_type: Agent 媒体类型名称或中文媒体类型 :param query: 站点查询服务 :return: 按优先级排序的可搜索站点 """ target_media_type = MediaType.from_agent(media_type) if not target_media_type: try: target_media_type = MediaType(media_type) except ValueError as error: raise HTTPException(status_code=400, detail="不支持的媒体类型") from error if target_media_type not in (MediaType.MOVIE, MediaType.TV, MediaType.MUSIC): raise HTTPException(status_code=400, detail="不支持的媒体类型") supported_ids: set[int] = set() supported_domains: set[str] = set() for indexer in await SitesHelper().async_get_indexers() or []: if not _indexer_supports_media_type(indexer, target_media_type): continue if indexer.get("id") is not None: supported_ids.update(_normalize_site_ids([indexer.get("id")])) domain = site_rules.extract_domain(indexer.get("domain")) if domain: supported_domains.add(domain) site_ids = sorted(supported_ids) domains = sorted(supported_domains) page, count = resolve_compatible_pagination(page, count) if response is not None: response.headers[COLLECTION_TOTAL_HEADER] = str( await query.count_ordered( is_active=True, site_ids=site_ids, domains=domains, ) ) return await query.list_ordered( is_active=True, site_ids=site_ids, domains=domains, page=page, count=count, ) @router.post("/", summary="新增站点", response_model=_SchemaResponse[None]) async def add_site( *, site_in: _SchemaSite, command: SiteMutationCommand = Depends(get_site_mutation_command), _: ApiPrincipal = Depends(get_current_active_manage_user_async), ) -> Any: """ 新增站点 """ result = await command.create(site_in.model_dump()) return _SchemaResponse(success=result.success, message=result.message) @router.put("/", summary="更新站点", response_model=_SchemaResponse[None]) async def update_site( *, site_in: _SchemaSite, command: SiteMutationCommand = Depends(get_site_mutation_command), _: ApiPrincipal = Depends(get_current_active_manage_user_async), ) -> Any: """ 更新站点信息 """ result = await command.update(site_in.model_dump()) return _SchemaResponse(success=result.success, message=result.message) @router.get( "/cookiecloud", summary="CookieCloud同步(兼容入口)", response_model=_SchemaResponse[None], include_in_schema=False, deprecated=True, ) @router.post( "/cookiecloud", summary="CookieCloud同步", response_model=_SchemaResponse[None], ) async def cookie_cloud_sync( task_registry: Annotated[TaskRegistry, Depends(get_background_task_registry)], _: ApiPrincipal = Depends(get_current_active_superuser_async), ) -> Any: """ 运行CookieCloud同步站点信息 """ resolve_background_task_registry(task_registry).create_sync( get_scheduler().start, job_id="cookiecloud", owner="api.site.cookiecloud_sync" ) return _SchemaResponse(success=True, message="CookieCloud同步任务已启动!") @router.get( "/reset", summary="重置站点(兼容入口)", response_model=_SchemaResponse[None], include_in_schema=False, deprecated=True, ) @router.post( "/reset", summary="重置站点", response_model=_SchemaResponse[None], ) async def reset( task_registry: Annotated[TaskRegistry, Depends(get_background_task_registry)], command: SiteMutationCommand = Depends(get_site_mutation_command), _: ApiPrincipal = Depends(get_current_active_superuser_async), ) -> Any: """ 清空所有站点数据并重新同步CookieCloud站点信息 """ result = await command.reset() await get_configured_system_config().async_set(SystemConfigKey.IndexerSites, []) await get_configured_system_config().async_set(SystemConfigKey.RssSites, []) resolve_background_task_registry(task_registry).create_sync( get_scheduler().start, job_id="cookiecloud", owner="api.site.reset", manual=True, ) return _SchemaResponse(success=result.success, message="站点已重置!") @router.post( "/priorities", summary="批量更新站点优先级", response_model=_SchemaResponse[None] ) async def update_sites_priority( priorities: List[_SchemaSitePriorityUpdate], command: SiteMutationCommand = Depends(get_site_mutation_command), _: ApiPrincipal = Depends(get_current_active_manage_user_async), ) -> Any: """ 批量更新站点优先级 """ result = await command.update_priorities([priority.model_dump() for priority in priorities]) return _SchemaResponse(success=result.success, message=result.message) def _update_site_cookie( site_id: int, username: str, password: str, code: Optional[str], query: SiteQueryService, ) -> _SchemaResponse[None]: """ 执行站点 Cookie 与 UA 更新。 :param site_id: 站点编号 :param username: 站点登录用户名 :param password: 站点登录密码 :param code: 二步验证码或密钥 :param query: 站点查询服务 :return: 更新结果 """ site_info = query.get_sync(site_id) if not site_info: raise HTTPException( status_code=404, detail=f"站点 {site_id} 不存在!", ) logger.info(f"开始更新站点【{site_info.name}】Cookie&UA") state, message = SiteChain().update_cookie( site_info=site_info, username=username, password=password, two_step_code=code ) if state: logger.info(f"站点【{site_info.name}】Cookie&UA更新成功") else: logger.error(f"站点【{site_info.name}】Cookie&UA更新失败:{message}") return _SchemaResponse(success=state, message=message) @router.post( "/cookie/{site_id}", summary="更新站点Cookie&UA", response_model=_SchemaResponse[None] ) def update_cookie_by_body( site_id: int, site_cookie_update: _SchemaSiteCookieUpdate, query: SiteQueryService = Depends(get_site_sync_query_service), _: ApiPrincipal = Depends(get_current_active_manage_user), ) -> Any: """ 使用请求体中的用户密码更新站点Cookie """ return _update_site_cookie( site_id=site_id, username=site_cookie_update.username, password=site_cookie_update.password, code=site_cookie_update.code, query=query, ) @router.post( "/cookie/{site_id}/set", summary="直接保存站点Cookie&UA", response_model=_SchemaResponse[None], ) async def set_cookie_by_body( site_id: int, site_cookie_set: _SchemaSiteCookieSet, command: SiteMutationCommand = Depends(get_site_mutation_command), _: ApiPrincipal = Depends(get_current_active_manage_user_async), ) -> Any: """ 保存受信任浏览器会话取得的 Cookie,不改写站点其他配置。 """ result = await command.set_cookie( site_id=site_id, cookie=site_cookie_set.cookie, ua=site_cookie_set.ua, ) return _SchemaResponse(success=result.success, message=result.message) @router.get( "/cookie/{site_id}", summary="更新站点Cookie&UA", response_model=_SchemaResponse[None] ) def update_cookie( site_id: int, username: str, password: str, code: Optional[str] = None, query: SiteQueryService = Depends(get_site_sync_query_service), _: ApiPrincipal = Depends(get_current_active_manage_user), ) -> Any: """ 使用用户密码更新站点Cookie """ return _update_site_cookie( site_id=site_id, username=username, password=password, code=code, query=query, ) @router.post( "/userdata/{site_id}", summary="更新站点用户数据", response_model=_SchemaResponse[_SchemaSiteUserData], ) def refresh_userdata( site_id: int, query: SiteQueryService = Depends(get_site_sync_query_service), _: ApiPrincipal = Depends(get_current_active_manage_user), ) -> Any: """ 刷新站点用户数据 """ site = query.get_sync(site_id) if not site: raise HTTPException( status_code=404, detail=f"站点 {site_id} 不存在", ) if not site.domain: return _SchemaResponse(success=False, message="站点域名未配置") indexer = SitesHelper().get_indexer(site.domain) if not indexer: return _SchemaResponse( success=False, message="站点不支持索引或未通过用户认证!" ) user_data = SiteChain().refresh_userdata(site=indexer) if not user_data or not user_data.userid: message = ( user_data.err_msg if user_data and user_data.err_msg else "未获取到站点用户数据,请检查 Cookie 是否有效!" ) return _SchemaResponse(success=False, message=message) return _SchemaResponse(success=True, data=user_data) @router.get( "/userdata/latest", summary="查询所有站点最新用户数据", response_model=List[_SchemaSiteUserData], openapi_extra={COLLECTION_TOTAL_OPENAPI_KEY: True}, ) async def read_userdata_latest( response: Response = None, # type: ignore[assignment] query: SiteQueryService = Depends(get_site_query_service), _: ApiPrincipal = Depends(get_current_active_manage_user_async), page: CompatiblePageParam = None, count: CompatibleCountParam = None, ) -> Any: """ 查询所有站点最新用户数据 """ page, count = resolve_compatible_pagination(page, count) if response is not None: response.headers[COLLECTION_TOTAL_HEADER] = str( await query.count_userdata_latest() ) return await query.userdata_latest(page=page, count=count) @router.get( "/userdata/{site_id}", summary="查询某站点用户数据", response_model=_SchemaResponse[list[_SchemaSiteUserData]], openapi_extra={COLLECTION_TOTAL_OPENAPI_KEY: True}, ) async def read_userdata( site_id: int, response: Response, workdate: Optional[str] = None, query: SiteQueryService = Depends(get_site_query_service), _: ApiPrincipal = Depends(get_current_active_manage_user_async), page: CompatiblePageParam = None, count: CompatibleCountParam = None, latest_seeding_only: Annotated[bool, Query(description="仅窗口内最新记录返回做种明细,历史统计不变")] = False, ) -> Any: """ 查询站点用户数据,可仅保留窗口内最新记录的做种明细。 :param site_id: 站点标识 :param response: 由 FastAPI 注入的响应,用于写入集合总数 :param workdate: 可选采集日期 :param query: 注入的站点查询服务 :param _: 已验证的管理用户 :param page: 可选页码 :param count: 可选每页数量 :param latest_seeding_only: 为真时历史记录不重复返回做种明细;默认保留旧合同 :return: 按请求窗口返回的数据,不修改持久化记录 """ site = await query.get(site_id) if not site: raise HTTPException( status_code=404, detail=f"站点 {site_id} 不存在", ) if not site.domain: raise HTTPException( status_code=409, detail=f"站点 {site_id} 未配置域名", ) page, count = resolve_compatible_pagination(page, count) response.headers[COLLECTION_TOTAL_HEADER] = str( await query.count_userdata(site.domain, workdate) ) user_datas = await query.userdata( site.domain, workdate, page=page, count=count, ) if not user_datas: return _SchemaResponse(success=False, data=[]) if latest_seeding_only: latest = max(user_datas, key=lambda item: (item.updated_day or "", item.updated_time or "")) user_datas = [ item if item is latest else item.model_copy(update={"seeding_info": []}) for item in user_datas ] return _SchemaResponse(success=True, data=user_datas) @router.get("/test/{site_id}", summary="连接测试", response_model=_SchemaResponse[None]) def test_site( site_id: int, query: SiteQueryService = Depends(get_site_sync_query_service), _: _SchemaTokenPayload = Depends(verify_token), ) -> Any: """ 测试站点是否可用 """ site = query.get_sync(site_id) if not site: raise HTTPException( status_code=404, detail=f"站点 {site_id} 不存在", ) if not site.domain: return _SchemaResponse(success=False, message="站点域名未配置") status, message = SiteChain().test(site.domain) return _SchemaResponse(success=status, message=message) @router.get( "/icon/{site_id}", summary="站点图标", response_model=_SchemaResponse[_SchemaSiteIconData], ) async def site_icon( site_id: int, query: SiteQueryService = Depends(get_site_query_service), _: _SchemaTokenPayload = Depends(verify_token), ) -> Any: """ 获取站点图标:base64或者url """ site = await query.get(site_id) if not site: raise HTTPException( status_code=404, detail=f"站点 {site_id} 不存在", ) if not site.domain: return _SchemaResponse(success=False, message="站点域名未配置") icon = await query.icon(site.domain) if not icon: return _SchemaResponse(success=False, message="站点图标不存在!") return _SchemaResponse(success=True, data=icon.model_dump()) @router.get( "/category/{site_id}", summary="站点分类", response_model=List[_SchemaSiteCategory] ) async def site_category( site_id: int, query: SiteQueryService = Depends(get_site_query_service), _: _SchemaTokenPayload = Depends(verify_token), page: CompatiblePageParam = None, count: CompatibleCountParam = None, ) -> Any: """ 获取站点分类,排除搜索参数并按配置顺序合并去重。 :param site_id: 已保存的站点标识 :return: 站点分类映射,分类标识支持数字和文本 """ site = await query.get(site_id) if not site: raise HTTPException( status_code=404, detail=f"站点 {site_id} 不存在", ) if not site.domain: raise HTTPException(status_code=400, detail="站点域名未配置") indexer = await SitesHelper().async_get_indexer(site.domain) if not indexer: raise HTTPException( status_code=404, detail=f"站点 {site.domain} 不支持", ) category_value = indexer.get("category") if not isinstance(category_value, dict): return [] # field、delimiter、param 等是搜索请求参数,只有列表值才是分类项 result: list[dict[str, Any]] = [] for cats in category_value.values(): if not isinstance(cats, list): continue for cat in cats: if isinstance(cat, dict) and cat not in result: result.append(cat) return result @router.get( "/resource/{site_id}", summary="站点资源", response_model=List[_SchemaTorrentInfo] ) async def site_resource( site_id: int, keyword: Optional[str] = None, mtype: Optional[str] = None, cat: Optional[str] = None, page: Optional[int] = 0, query: SiteQueryService = Depends(get_site_query_service), _: ApiPrincipal = Depends(get_current_active_manage_user_async), ) -> Any: """ 浏览站点资源,沿用前端和索引模块从 0 开始的分页约定。 :param cat: 逗号分隔的站点分类 ID,未指定时不限制分类 :param page: 从 0 开始的页码,默认查询第一页 """ site = await query.get(site_id) if not site: raise HTTPException( status_code=404, detail=f"站点 {site_id} 不存在", ) if not site.domain: raise HTTPException(status_code=400, detail="站点域名未配置") torrents = await TorrentsChain().async_browse( domain=site.domain, keyword=keyword, cat=cat, page=page, mtype=MediaType.from_agent(mtype) or MediaType(mtype) if mtype else None, ) if not torrents: return [] return [torrent.to_dict() for torrent in torrents] @router.get("/domain/{site_url}", summary="站点详情", response_model=_SchemaSite) async def read_site_by_domain( site_url: str, query: SiteQueryService = Depends(get_site_query_service), token_payload: _SchemaTokenPayload = Depends(verify_token), ) -> Any: """ 通过域名获取站点信息 """ domain = site_rules.extract_domain(site_url) site = await query.get_by_domain(domain) if not site: raise HTTPException( status_code=404, detail=f"站点 {domain} 不存在", ) return _project_readable_site(site, token_payload) @router.get( "/statistic/{site_url}", summary="特定站点统计信息", response_model=_SchemaSiteStatistic, ) async def read_statistic_by_domain( site_url: str, query: SiteQueryService = Depends(get_site_query_service), _: _SchemaTokenPayload = Depends(verify_token), ) -> Any: """ 通过域名获取站点统计信息 """ domain = site_rules.extract_domain(site_url) return await query.statistic(domain) @router.get( "/statistic", summary="所有站点统计信息", response_model=List[_SchemaSiteStatistic], openapi_extra={COLLECTION_TOTAL_OPENAPI_KEY: True}, ) async def read_statistics( response: Response = None, # type: ignore[assignment] query: SiteQueryService = Depends(get_site_query_service), _: _SchemaTokenPayload = Depends(verify_token), page: CompatiblePageParam = None, count: CompatibleCountParam = None, ) -> Any: """ 获取所有站点统计信息 """ page, count = resolve_compatible_pagination(page, count) if response is not None: response.headers[COLLECTION_TOTAL_HEADER] = str( await query.count_statistics() ) return await query.statistics(page=page, count=count) @router.get( "/rss", summary="所有订阅站点", response_model=List[_SchemaSite], openapi_extra={COLLECTION_TOTAL_OPENAPI_KEY: True}, ) async def read_rss_sites( response: Response = None, # type: ignore[assignment] query: SiteQueryService = Depends(get_site_query_service), token_payload: _SchemaTokenPayload = Depends(verify_token), page: CompatiblePageParam = None, count: CompatibleCountParam = None, ) -> List[_SchemaSite | dict[str, JsonData]]: """ 获取站点列表 """ # 选中的rss站点 selected_sites = get_configured_system_config().get(SystemConfigKey.RssSites) or [] site_ids = _normalize_site_ids(selected_sites) if selected_sites else None page, count = resolve_compatible_pagination(page, count) if response is not None: response.headers[COLLECTION_TOTAL_HEADER] = str( await query.count_ordered(site_ids=site_ids) ) sites = await query.list_ordered( site_ids=site_ids, page=page, count=count, ) return [_project_readable_site(site, token_payload) for site in sites] @router.get("/auth", summary="查询认证站点", response_model=_SchemaJsonObject) async def read_auth_sites( _: _SchemaTokenPayload = Depends(verify_token), ) -> dict[str, Any]: """ 获取可认证站点列表 """ return SitesHelper().get_authsites() @router.post("/auth", summary="用户站点认证", response_model=_SchemaResponse[None]) def auth_site( auth_info: _SchemaSiteAuth, _: ApiPrincipal = Depends(get_current_active_superuser) ) -> Any: """ 用户站点认证 """ if not auth_info or not auth_info.site or not auth_info.params: return _SchemaResponse(success=False, message="请输入认证站点和认证参数") sites_helper = SitesHelper() auth_params = normalize_site_auth_params( auth_info.site, auth_info.params, sites_helper.get_authsites(), ) status, msg = sites_helper.check_user(auth_info.site, auth_params) if status: get_configured_system_config().set( SystemConfigKey.UserSiteAuthParams, {"site": auth_info.site, "params": auth_params}, ) # 认证成功后,重新初始化插件 get_plugin_manager().init_config() get_scheduler().init_plugin_jobs() init_commands() register_plugin_api() return _SchemaResponse(success=status, message=msg) @router.get( "/mapping", summary="获取站点域名到名称的映射", response_model=_SchemaResponse[_SchemaSiteMappingData], ) async def site_mapping( query: SiteQueryService = Depends(get_site_query_service), _: ApiPrincipal = Depends(get_current_active_superuser_async), ) -> _SchemaResponse[dict[str, str]]: """ 获取站点域名到名称的映射关系 """ try: sites = await query.list_ordered() mapping: dict[str, str] = {} for site in sites: if site.domain and site.name: mapping[site.domain] = site.name return _SchemaResponse(success=True, data=mapping) except Exception as e: return _SchemaResponse(success=False, message=f"获取映射失败:{str(e)}") @router.get( "/supporting", summary="获取支持的站点列表", response_model=_SchemaJsonObject, ) async def support_sites( _: ApiPrincipal = Depends(get_current_active_superuser_async), ) -> dict[str, Any]: """ 获取支持的站点列表 """ return SitesHelper().get_indexsites() @router.get("/{site_id}", summary="站点详情", response_model=_SchemaSite) async def read_site( site_id: int, query: SiteQueryService = Depends(get_site_query_service), _: ApiPrincipal = Depends(get_current_active_manage_user_async), ) -> Any: """ 通过ID获取站点信息 """ site = await query.get(site_id) if not site: raise HTTPException( status_code=404, detail=f"站点 {site_id} 不存在", ) return site @router.delete("/{site_id}", summary="删除站点", response_model=_SchemaResponse[None]) async def delete_site( site_id: int, command: SiteMutationCommand = Depends(get_site_mutation_command), _: ApiPrincipal = Depends(get_current_active_manage_user_async), ) -> Any: """ 删除站点 """ result = await command.delete(site_id) return _SchemaResponse(success=result.success, message=result.message)