From 83790e0ab742c4486f07c542c6f8fcb3e46bc107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Thu, 20 Feb 2025 15:59:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AF=AD=E4=B9=89=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E4=B8=AD=E5=BF=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- apps/main.py | 3 + apps/manager/appcenter.py | 12 +- apps/manager/service.py | 360 ++++++++++++++++++++++++++++++++++++++ apps/routers/appcenter.py | 48 ++--- apps/routers/service.py | 307 ++++++++++++++++++++++++++++++++ 5 files changed, 698 insertions(+), 32 deletions(-) create mode 100644 apps/manager/service.py create mode 100644 apps/routers/service.py diff --git a/apps/main.py b/apps/main.py index 541afa3e..37f8309e 100644 --- a/apps/main.py +++ b/apps/main.py @@ -2,6 +2,7 @@ Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. """ + from __future__ import annotations import ray @@ -30,6 +31,7 @@ from apps.routers import ( knowledge, mock, record, + service, ) # from apps.scheduler.pool.loader import Loader @@ -50,6 +52,7 @@ app.include_router(conversation.router) app.include_router(auth.router) app.include_router(api_key.router) app.include_router(appcenter.router) +app.include_router(service.router) app.include_router(comment.router) app.include_router(record.router) app.include_router(health.router) diff --git a/apps/manager/appcenter.py b/apps/manager/appcenter.py index d6d398a4..20822b76 100644 --- a/apps/manager/appcenter.py +++ b/apps/manager/appcenter.py @@ -123,7 +123,7 @@ class AppCenterManager: for app in apps ], total_apps except Exception as e: - LOGGER.info(f"[AppCenterManager] Get app list by user failed: {e}") + LOGGER.error(f"[AppCenterManager] Get app list by user failed: {e}") return [], -1 @staticmethod @@ -173,7 +173,7 @@ class AppCenterManager: for app in apps ], total_apps except Exception as e: - LOGGER.info(f"[AppCenterManager] Get favorite app list failed: {e}") + LOGGER.error(f"[AppCenterManager] Get favorite app list failed: {e}") return [], -1 @staticmethod @@ -191,7 +191,7 @@ class AppCenterManager: return None return AppPool.model_validate(db_data) except Exception as e: - LOGGER.info(f"[AppCenterManager] Get app metadata by app_id failed: {e}") + LOGGER.error(f"[AppCenterManager] Get app metadata by app_id failed: {e}") return None @staticmethod @@ -296,11 +296,7 @@ class AppCenterManager: app_data = AppPool.model_validate(db_data) already_favorited = user_sub in app_data.favorites - # 只能收藏未收藏的 - if favorited and already_favorited: - return AppCenterManager.ModFavAppFlag.BAD_REQUEST - # 只能取消已收藏的 - if not favorited and not already_favorited: + if favorited == already_favorited: return AppCenterManager.ModFavAppFlag.BAD_REQUEST if favorited: diff --git a/apps/manager/service.py b/apps/manager/service.py new file mode 100644 index 00000000..867600dd --- /dev/null +++ b/apps/manager/service.py @@ -0,0 +1,360 @@ +"""语义接口中心 Manager + +Copyright (c) Huawei Technologies Co., Ltd. 2024-2025. All rights reserved. +""" + +import uuid +from typing import Any, Optional + +from jsonschema import ValidationError, validate + +from apps.constants import LOGGER +from apps.entities.enum_var import SearchType +from apps.entities.pool import NodePool, ServicePool +from apps.entities.response_data import ServiceApiData, ServiceCardItem +from apps.models.mongo import MongoDB +from apps.scheduler.openapi import reduce_openapi_spec + + +class ServiceCenterManager: + """语义接口中心管理器""" + + @staticmethod + async def fetch_all_services( + user_sub: str, + search_type: SearchType, + keyword: Optional[str], + page: int, + page_size: int, + ) -> tuple[list[ServiceCardItem], int]: + """获取所有服务列表""" + filters = ServiceCenterManager._build_filters({}, search_type, keyword) if keyword else {} + service_pools, total_count = await ServiceCenterManager._search_service(filters, page, page_size) + services = [ + ServiceCardItem( + serviceId=service_pool.id, + icon="", + name=service_pool.name, + description=service_pool.description, + author=service_pool.author, + favorited=(user_sub in service_pool.favorites), + ) + for service_pool in service_pools + ] + return services, total_count + + @staticmethod + async def fetch_user_services( + user_sub: str, + search_type: SearchType, + keyword: Optional[str], + page: int, + page_size: int, + ) -> tuple[list[ServiceCardItem], int]: + """获取用户创建的服务""" + base_filter = {"author": user_sub} + filters = ServiceCenterManager._build_filters(base_filter, search_type, keyword) if keyword else base_filter + service_pools, total_count = await ServiceCenterManager._search_service(filters, page, page_size) + services = [ + ServiceCardItem( + serviceId=service_pool.id, + icon="", + name=service_pool.name, + description=service_pool.description, + author=service_pool.author, + favorited=(user_sub in service_pool.favorites), + ) + for service_pool in service_pools + ] + return services, total_count + + @staticmethod + async def fetch_favorite_services( + user_sub: str, + search_type: SearchType, + keyword: Optional[str], + page: int, + page_size: int, + ) -> tuple[list[ServiceCardItem], int]: + """获取用户收藏的服务""" + fav_service_ids = await ServiceCenterManager._get_favorite_service_ids_by_user(user_sub) + base_filter = {"_id": {"$in": fav_service_ids}} + filters = ServiceCenterManager._build_filters(base_filter, search_type, keyword) if keyword else base_filter + service_pools, total_count = await ServiceCenterManager._search_service(filters, page, page_size) + services = [ + ServiceCardItem( + serviceId=service_pool.id, + icon="", + name=service_pool.name, + description=service_pool.description, + author=service_pool.author, + favorited=True, + ) + for service_pool in service_pools + ] + return services, total_count + + @staticmethod + async def create_service( + user_sub: str, + data: dict[str, Any], + ) -> str: + """创建服务""" + service_id = str(uuid.uuid4()) + # 校验 OpenAPI 规范的 JSON Schema + ServiceCenterManager._validate_service_data(data) + # 更新 Node 信息 + node_collection = MongoDB.get_collection("node") + await node_collection.delete_many({"service_id": service_id}) + openapi_spec_data = reduce_openapi_spec(data) + for endpoint in openapi_spec_data.endpoints: + await node_collection.insert_one( + NodePool( + _id=str(uuid.uuid4()), + service_id=service_id, + name=endpoint.name, + api_path=f"{endpoint.method} {endpoint.uri}", + description=endpoint.description, + call_id="api", + ).model_dump(), + ) + yaml_hash = "hash" # TODO: 计算 OpenAPI YAML 文件的哈希值 + # 存入数据库 + service_pool = ServicePool( + _id=service_id, + author=user_sub, + name=data["info"]["title"], + description=data["info"]["description"], + openapi_hash=yaml_hash, + openapi_spec=data, + ) + service_collection = MongoDB.get_collection("service") + await service_collection.insert_one(service_pool.model_dump()) + # 返回服务ID + return service_id + + @staticmethod + async def update_service( + user_sub: str, + service_id: str, + data: dict[str, Any], + ) -> str: + """更新服务""" + # 验证用户权限 + service_collection = MongoDB.get_collection("service") + db_service = await service_collection.find_one({"_id": service_id}) + if not db_service: + msg = "Service not found" + raise ValueError(msg) + service_pool_store = ServicePool.model_validate(db_service) + if service_pool_store.author != user_sub: + msg = "Permission denied" + raise ValueError(msg) + # 校验 OpenAPI 规范的 JSON Schema + ServiceCenterManager._validate_service_data(data) + # 更新 Node 信息 + node_collection = MongoDB.get_collection("node") + await node_collection.delete_many({"service_id": service_id}) + openapi_spec_data = reduce_openapi_spec(data) + for endpoint in openapi_spec_data.endpoints: + await node_collection.insert_one( + NodePool( + _id=str(uuid.uuid4()), + service_id=service_id, + name=endpoint.name, + api_path=f"{endpoint.method} {endpoint.uri}", + description=endpoint.description, + call_id="api", + ).model_dump(), + ) + yaml_hash = "hash" # TODO: 计算 OpenAPI YAML 文件的哈希值 + # 更新数据库 + service_pool = ServicePool( + _id=service_id, + author=user_sub, + name=data["info"]["title"], + description=data["info"]["description"], + openapi_hash=yaml_hash, + openapi_spec=data, + ) + await service_collection.update_one( + {"_id": service_id}, + {"$set": service_pool.model_dump()}, + ) + # 返回服务ID + return service_id + + @staticmethod + async def get_service_apis( + service_id: str, + ) -> tuple[str, list[ServiceApiData]]: + """获取服务API列表""" + # 获取服务名称 + service_collection = MongoDB.get_collection("service") + db_service = await service_collection.find_one({"_id": service_id}) + if not db_service: + msg = "Service not found" + raise ValueError(msg) + service_pool_store = ServicePool.model_validate(db_service) + # 根据 service_id 获取 API 列表 + node_collection = MongoDB.get_collection("node") + db_nodes = await node_collection.find({"service_id": service_id}).to_list() + api_list = [] + for db_node in db_nodes: + node = NodePool.model_validate(db_node) + api_list.extend( + ServiceApiData( + name=node.name, + path=node.api_path or "", + description=node.description, + ), + ) + return service_pool_store.name, api_list + + @staticmethod + async def get_service_data( + user_sub: str, + service_id: str, + ) -> tuple[str, dict[str, Any]]: + """获取服务数据""" + # 验证用户权限 + service_collection = MongoDB.get_collection("service") + db_service = await service_collection.find_one({"_id": service_id}) + if not db_service: + msg = "Service not found" + raise ValueError(msg) + service_pool_store = ServicePool.model_validate(db_service) + if service_pool_store.author != user_sub: + msg = "Permission denied" + raise ValueError(msg) + return service_pool_store.name, service_pool_store.openapi_spec + + @staticmethod + async def delete_service( + user_sub: str, + service_id: str, + ) -> bool: + """删除服务""" + # 验证用户权限 + service_collection = MongoDB.get_collection("service") + db_service = await service_collection.find_one({"_id": service_id}) + if not db_service: + msg = "Service not found" + raise ValueError(msg) + service_pool_store = ServicePool.model_validate(db_service) + if service_pool_store.author != user_sub: + msg = "Permission denied" + raise ValueError(msg) + # 删除服务 + await service_collection.delete_one({"_id": service_id}) + # 删除 Node 信息 + node_collection = MongoDB.get_collection("node") + await node_collection.delete_many({"service_id": service_id}) + return True + + @staticmethod + async def modify_favorite_service( + user_sub: str, + service_id: str, + *, + favorited: bool, + ) -> bool: + """修改收藏状态""" + service_collection = MongoDB.get_collection("service") + db_service = await service_collection.find_one({"_id": service_id}) + if not db_service: + msg = "Service not found" + raise ValueError(msg) + service_pool_store = ServicePool.model_validate(db_service) + already_favorited = user_sub in service_pool_store.favorites + if already_favorited == favorited: + return False + if favorited: + service_pool_store.favorites.append(user_sub) + service_pool_store.favorites = list(set(service_pool_store.favorites)) + else: + service_pool_store.favorites.remove(user_sub) + await service_collection.update_one( + {"_id": service_id}, + {"$set": service_pool_store.model_dump()}, + ) + return True + + @staticmethod + async def _search_service( + search_conditions: dict, + page: int, + page_size: int, + ) -> tuple[list[ServicePool], int]: + """基于输入条件获取服务数据""" + service_collection = MongoDB.get_collection("service") + # 获取服务总数 + total = await service_collection.count_documents(search_conditions) + # 分页查询 + skip = (page - 1) * page_size + db_services = await service_collection.find(search_conditions).skip(skip).limit(page_size).to_list() + if not db_services and total > 0: + LOGGER.warning(f"[ServiceCenterManager] No services found for conditions: {search_conditions}") + return [], -1 + service_pools = [ServicePool.model_validate(db_service) for db_service in db_services] + return service_pools, total + + @staticmethod + async def _get_favorite_service_ids_by_user(user_sub: str) -> list[str]: + """获取用户收藏的服务ID""" + service_collection = MongoDB.get_collection("service") + cursor = service_collection.find({"favorites": {"$in": [user_sub]}}) + return [ServicePool.model_validate(doc).id async for doc in cursor] + + @staticmethod + def _validate_service_data(data: dict[str, Any]) -> bool: + """验证服务数据""" + # 验证数据是否为空 + if not data: + msg = "Service data is empty" + raise ValueError(msg) + # 校验 OpenAPI 规范的 JSON Schema + openapi_schema = { + "type": "object", + "properties": { + "openapi": {"type": "string"}, + "info": { + "type": "object", + "properties": { + "title": {"type": "string"}, + "version": {"type": "string"}, + "description": {"type": "string"}, + }, + "required": ["title", "version", "description"], + }, + "paths": {"type": "object"}, + }, + "required": ["openapi", "info", "paths"], + } + try: + validate(instance=data, schema=openapi_schema) + except ValidationError as e: + msg = f"Data does not conform to OpenAPI standard: {e.message}" + raise ValueError(msg) from e + return True + + @staticmethod + def _build_filters( + base_filters: dict[str, Any], + search_type: SearchType, + keyword: str, + ) -> dict[str, Any]: + search_filters = [ + {"name": {"$regex": keyword, "$options": "i"}}, + {"description": {"$regex": keyword, "$options": "i"}}, + {"author": {"$regex": keyword, "$options": "i"}}, + ] + if search_type == SearchType.ALL: + base_filters["$or"] = search_filters + elif search_type == SearchType.NAME: + base_filters["name"] = {"$regex": keyword, "$options": "i"} + elif search_type == SearchType.DESCRIPTION: + base_filters["description"] = {"$regex": keyword, "$options": "i"} + elif search_type == SearchType.AUTHOR: + base_filters["author"] = {"$regex": keyword, "$options": "i"} + return base_filters diff --git a/apps/routers/appcenter.py b/apps/routers/appcenter.py index 990f478d..de134cb4 100644 --- a/apps/routers/appcenter.py +++ b/apps/routers/appcenter.py @@ -52,7 +52,7 @@ async def get_applications( # noqa: PLR0913 status_code=status.HTTP_400_BAD_REQUEST, content=ResponseData( code=status.HTTP_400_BAD_REQUEST, - message="createdByMe 和 favorited 不能同时生效", + message="INVALID_PARAMETER", result={}, ).model_dump(exclude_none=True, by_alias=True), ) @@ -75,7 +75,7 @@ async def get_applications( # noqa: PLR0913 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( code=status.HTTP_500_INTERNAL_SERVER_ERROR, - message="查询失败", + message="ERROR", result={}, ).model_dump(exclude_none=True, by_alias=True), ) @@ -83,7 +83,7 @@ async def get_applications( # noqa: PLR0913 status_code=status.HTTP_200_OK, content=GetAppListRsp( code=status.HTTP_200_OK, - message="查询成功", + message="OK", result=GetAppListMsg( currentPage=page, totalApps=total_apps, @@ -108,7 +108,7 @@ async def create_or_update_application( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( code=status.HTTP_500_INTERNAL_SERVER_ERROR, - message="更新失败", + message="ERROR", result={}, ).model_dump(exclude_none=True, by_alias=True), ) @@ -116,7 +116,7 @@ async def create_or_update_application( status_code=status.HTTP_200_OK, content=BaseAppOperationRsp( code=status.HTTP_200_OK, - message="更新成功", + message="OK", result=BaseAppOperationMsg(appId=app_id), ).model_dump(exclude_none=True, by_alias=True), ) @@ -127,7 +127,7 @@ async def create_or_update_application( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( code=status.HTTP_500_INTERNAL_SERVER_ERROR, - message="创建失败", + message="ERROR", result={}, ).model_dump(exclude_none=True, by_alias=True), ) @@ -135,7 +135,7 @@ async def create_or_update_application( status_code=status.HTTP_200_OK, content=BaseAppOperationRsp( code=status.HTTP_200_OK, - message="创建成功", + message="OK", result=BaseAppOperationMsg(appId=app_id), ).model_dump(exclude_none=True, by_alias=True), ) @@ -153,7 +153,7 @@ async def get_recently_used_applications( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( code=status.HTTP_500_INTERNAL_SERVER_ERROR, - message="查询失败", + message="ERROR", result={}, ).model_dump(exclude_none=True, by_alias=True), ) @@ -161,7 +161,7 @@ async def get_recently_used_applications( status_code=status.HTTP_200_OK, content=GetRecentAppListRsp( code=status.HTTP_200_OK, - message="查询成功", + message="OK", result=recent_apps, ).model_dump(exclude_none=True, by_alias=True), ) @@ -178,7 +178,7 @@ async def get_application( status_code=status.HTTP_404_NOT_FOUND, content=ResponseData( code=status.HTTP_404_NOT_FOUND, - message="找不到应用", + message="INVALID_APP_ID", result={}, ).model_dump(exclude_none=True, by_alias=True), ) @@ -187,7 +187,7 @@ async def get_application( status_code=status.HTTP_200_OK, content=GetAppPropertyRsp( code=status.HTTP_200_OK, - message="查询成功", + message="OK", result=GetAppPropertyMsg( appId=app_data.id, published=app_data.published, @@ -223,7 +223,7 @@ async def delete_application( status_code=status.HTTP_400_BAD_REQUEST, content=ResponseData( code=status.HTTP_400_BAD_REQUEST, - message="查询失败", + message="INVALID_PARAMETER", result={}, ).model_dump(exclude_none=True, by_alias=True), ) @@ -233,7 +233,7 @@ async def delete_application( status_code=status.HTTP_403_FORBIDDEN, content=ResponseData( code=status.HTTP_403_FORBIDDEN, - message="无权删除他人创建的应用", + message="UNAUTHORIZED", result={}, ).model_dump(exclude_none=True, by_alias=True), ) @@ -244,7 +244,7 @@ async def delete_application( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( code=status.HTTP_500_INTERNAL_SERVER_ERROR, - message=f"删除应用下属工作流 {flow.id} 失败", + message="ERROR", result={}, ).model_dump(exclude_none=True, by_alias=True), ) @@ -254,7 +254,7 @@ async def delete_application( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( code=status.HTTP_500_INTERNAL_SERVER_ERROR, - message="删除失败", + message="ERROR", result={}, ).model_dump(exclude_none=True, by_alias=True), ) @@ -262,7 +262,7 @@ async def delete_application( status_code=status.HTTP_200_OK, content=BaseAppOperationRsp( code=status.HTTP_200_OK, - message="删除成功", + message="OK", result=BaseAppOperationMsg(appId=app_id), ).model_dump(exclude_none=True, by_alias=True), ) @@ -280,7 +280,7 @@ async def publish_application( status_code=status.HTTP_400_BAD_REQUEST, content=ResponseData( code=status.HTTP_400_BAD_REQUEST, - message="查询失败", + message="INVALID_PARAMETER", result={}, ).model_dump(exclude_none=True, by_alias=True), ) @@ -290,7 +290,7 @@ async def publish_application( status_code=status.HTTP_403_FORBIDDEN, content=ResponseData( code=status.HTTP_403_FORBIDDEN, - message="无权发布他人创建的应用", + message="UNAUTHORIZED", result={}, ).model_dump(exclude_none=True, by_alias=True), ) @@ -300,7 +300,7 @@ async def publish_application( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( code=status.HTTP_500_INTERNAL_SERVER_ERROR, - message="发布失败", + message="ERROR", result={}, ).model_dump(exclude_none=True, by_alias=True), ) @@ -308,7 +308,7 @@ async def publish_application( status_code=status.HTTP_200_OK, content=BaseAppOperationRsp( code=status.HTTP_200_OK, - message="发布成功", + message="OK", result=BaseAppOperationMsg(appId=app_id), ).model_dump(exclude_none=True, by_alias=True), ) @@ -327,7 +327,7 @@ async def modify_favorite_application( status_code=status.HTTP_404_NOT_FOUND, content=ResponseData( code=status.HTTP_404_NOT_FOUND, - message="找不到应用", + message="INVALID_APP_ID", result={}, ).model_dump(exclude_none=True, by_alias=True), ) @@ -336,7 +336,7 @@ async def modify_favorite_application( status_code=status.HTTP_400_BAD_REQUEST, content=ResponseData( code=status.HTTP_400_BAD_REQUEST, - message="不可重复操作", + message="INVALID_PARAMETER", result={}, ).model_dump(exclude_none=True, by_alias=True), ) @@ -345,7 +345,7 @@ async def modify_favorite_application( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( code=status.HTTP_500_INTERNAL_SERVER_ERROR, - message="操作失败", + message="ERROR", result={}, ).model_dump(exclude_none=True, by_alias=True), ) @@ -353,7 +353,7 @@ async def modify_favorite_application( status_code=status.HTTP_200_OK, content=ModFavAppRsp( code=status.HTTP_200_OK, - message="操作成功", + message="OK", result=ModFavAppMsg( appId=app_id, favorited=request.favorited, diff --git a/apps/routers/service.py b/apps/routers/service.py new file mode 100644 index 00000000..0feae74a --- /dev/null +++ b/apps/routers/service.py @@ -0,0 +1,307 @@ +"""FastAPI 语义接口中心相关路由 + +Copyright (c) Huawei Technologies Co., Ltd. 2024-2025. All rights reserved. +""" + +from typing import Annotated, Optional, Union + +from fastapi import APIRouter, Body, Depends, Path, Query, status +from fastapi.responses import JSONResponse + +from apps.constants import LOGGER +from apps.dependency.csrf import verify_csrf_token +from apps.dependency.user import get_user, verify_user +from apps.entities.enum_var import SearchType +from apps.entities.request_data import ModFavServiceRequest, UpdateServiceRequest +from apps.entities.response_data import ( + BaseServiceOperationMsg, + DeleteServiceRsp, + GetServiceDetailMsg, + GetServiceDetailRsp, + GetServiceListMsg, + GetServiceListRsp, + ModFavServiceMsg, + ModFavServiceRsp, + ResponseData, + UpdateServiceMsg, + UpdateServiceRsp, +) +from apps.manager.service import ServiceCenterManager + +router = APIRouter( + prefix="/api/service", + tags=["service-center"], + dependencies=[Depends(verify_user)], +) + + +@router.get("", response_model=Union[GetServiceListRsp, ResponseData]) +async def get_service_list( # noqa: PLR0913 + user_sub: Annotated[str, Depends(get_user)], + *, + my_service: Annotated[bool, Query(..., alias="createdByMe", description="筛选我创建的")] = False, + my_fav: Annotated[bool, Query(..., alias="favorited", description="筛选我收藏的")] = False, + search_type: Annotated[SearchType, Query(..., alias="searchType", description="搜索类型")] = SearchType.ALL, + keyword: Annotated[Optional[str], Query(..., alias="keyword", description="搜索关键字")] = None, + page: Annotated[int, Query(..., alias="page", ge=1, description="页码")] = 1, + page_size: Annotated[int, Query(..., alias="pageSize", ge=1, le=100, description="每页数量")] = 16, +) -> JSONResponse: + """获取服务列表""" + if my_service and my_fav: # 只能同时选择一个筛选条件 + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=ResponseData( + code=status.HTTP_400_BAD_REQUEST, + message="INVALID_PARAMETER", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + + service_cards, total_count = [], -1 + try: + if my_service: # 筛选我创建的 + service_cards, total_count = await ServiceCenterManager.fetch_user_services( + user_sub, + search_type, + keyword, + page, + page_size, + ) + elif my_fav: # 筛选我收藏的 + service_cards, total_count = await ServiceCenterManager.fetch_favorite_services( + user_sub, + search_type, + keyword, + page, + page_size, + ) + else: # 获取所有服务 + service_cards, total_count = await ServiceCenterManager.fetch_all_services( + user_sub, + search_type, + keyword, + page, + page_size, + ) + except Exception as e: + LOGGER.error(f"[ServiceCenter] Get service list error: {e}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + if total_count == -1: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=ResponseData( + code=status.HTTP_400_BAD_REQUEST, + message="INVALID_PARAMETER", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + return JSONResponse( + status_code=status.HTTP_200_OK, + content=GetServiceListRsp( + code=status.HTTP_200_OK, + message="OK", + result=GetServiceListMsg( + currentPage=page, + totalCount=total_count, + services=service_cards, + ), + ).model_dump(exclude_none=True, by_alias=True), + ) + + +@router.post("", response_model=UpdateServiceRsp, dependencies=[Depends(verify_csrf_token)]) +async def update_service( + user_sub: Annotated[str, Depends(get_user)], + data: Annotated[UpdateServiceRequest, Body(..., description="上传 YAML 文本对应数据对象")], +) -> JSONResponse: + """上传并解析服务""" + if not data.service_id: + try: + service_id = await ServiceCenterManager.create_service(user_sub, data.data) + except Exception as e: + LOGGER.error(f"[ServiceCenter] Create service failed: {e}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + else: + try: + service_id = await ServiceCenterManager.update_service(user_sub, data.service_id, data.data) + except ValueError: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=ResponseData( + code=status.HTTP_400_BAD_REQUEST, + message="INVALID_SERVICE_ID", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except Exception as e: + LOGGER.error(f"[ServiceCenter] Update service failed: {e}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + try: + name, apis = await ServiceCenterManager.get_service_apis(service_id) + except Exception as e: + LOGGER.error(f"[ServiceCenter] Get service apis failed: {e}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + msg = UpdateServiceMsg(serviceId=service_id, name=name, apis=apis) + rsp = UpdateServiceRsp(code=status.HTTP_200_OK, message="OK", result=msg) + return JSONResponse(status_code=status.HTTP_200_OK, content=rsp.model_dump(exclude_none=True, by_alias=True)) + + +@router.get("/{serviceId}", response_model=GetServiceDetailRsp) +async def get_service_detail( + user_sub: Annotated[str, Depends(get_user)], + service_id: Annotated[str, Path(..., alias="serviceId", description="服务ID")], + *, + edit: Annotated[bool, Query(..., description="是否为编辑模式")] = False, +) -> JSONResponse: + """获取服务详情""" + # 示例:返回指定服务的详情 + if edit: + try: + name, data = await ServiceCenterManager.get_service_data(user_sub, service_id) + except ValueError: + return JSONResponse( + status_code=status.HTTP_404_NOT_FOUND, + content=ResponseData( + code=status.HTTP_404_NOT_FOUND, + message="INVALID_SERVICE_ID", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except Exception as e: + LOGGER.error(f"[ServiceCenter] Get service data error: {e}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + detail = GetServiceDetailMsg(serviceId=service_id, name=name, data=data) + else: + try: + name, apis = await ServiceCenterManager.get_service_apis(service_id) + except ValueError: + return JSONResponse( + status_code=status.HTTP_404_NOT_FOUND, + content=ResponseData( + code=status.HTTP_404_NOT_FOUND, + message="INVALID_SERVICE_ID", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except Exception as e: + LOGGER.error(f"[ServiceCenter] Get service apis error: {e}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + detail = GetServiceDetailMsg(serviceId=service_id, name=name, apis=apis) + rsp = GetServiceDetailRsp(code=status.HTTP_200_OK, message="OK", result=detail) + return JSONResponse(status_code=status.HTTP_200_OK, content=rsp.model_dump(exclude_none=True, by_alias=True)) + + +@router.delete("/{serviceId}", response_model=DeleteServiceRsp, dependencies=[Depends(verify_csrf_token)]) +async def delete_service( + user_sub: Annotated[str, Depends(get_user)], + service_id: Annotated[str, Path(..., alias="serviceId", description="服务ID")], +) -> JSONResponse: + """删除服务""" + try: + await ServiceCenterManager.delete_service(user_sub, service_id) + except ValueError: + return JSONResponse( + status_code=status.HTTP_404_NOT_FOUND, + content=ResponseData( + code=status.HTTP_404_NOT_FOUND, + message="INVALID_SERVICE_ID", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except Exception as e: + LOGGER.error(f"[ServiceCenter] Delete service error: {e}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + msg = BaseServiceOperationMsg(serviceId=service_id) + rsp = DeleteServiceRsp(code=status.HTTP_200_OK, message="OK", result=msg) + return JSONResponse(status_code=status.HTTP_200_OK, content=rsp.model_dump(exclude_none=True, by_alias=True)) + + +@router.put("/{serviceId}", response_model=ModFavServiceRsp, dependencies=[Depends(verify_csrf_token)]) +async def modify_favorite_service( + user_sub: Annotated[str, Depends(get_user)], + service_id: Annotated[str, Path(..., alias="serviceId", description="服务ID")], + data: Annotated[ModFavServiceRequest, Body(..., description="更改收藏状态请求对象")], +) -> JSONResponse: + """修改服务收藏状态""" + try: + success = await ServiceCenterManager.modify_favorite_service(user_sub, service_id, favorited=data.favorited) + if not success: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=ResponseData( + code=status.HTTP_400_BAD_REQUEST, + message="INVALID_PARAMETER", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except ValueError: + return JSONResponse( + status_code=status.HTTP_404_NOT_FOUND, + content=ResponseData( + code=status.HTTP_404_NOT_FOUND, + message="INVALID_SERVICE_ID", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + except Exception as e: + LOGGER.error(f"[ServiceCenter] Modify favorite service error: {e}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="ERROR", + result={}, + ).model_dump(exclude_none=True, by_alias=True), + ) + msg = ModFavServiceMsg(serviceId=service_id, favorited=data.favorited) + rsp = ModFavServiceRsp(code=status.HTTP_200_OK, message="OK", result=msg) + return JSONResponse(status_code=status.HTTP_200_OK, content=rsp.model_dump(exclude_none=True, by_alias=True)) -- Gitee