From 21a836a56c5fd4d6216268512fdfa588b88d1a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Thu, 23 Jan 2025 19:49:07 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E5=AE=9A=E4=B9=89=E4=B8=AD=E7=9A=84=E5=86=B2?= =?UTF-8?q?=E7=AA=81=E9=A1=B9=EF=BC=9B=E4=BF=AE=E5=A4=8D=20cleancode=20?= =?UTF-8?q?=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- apps/entities/flow.py | 10 ++++------ apps/entities/pool.py | 23 ++++++----------------- apps/entities/request_data.py | 5 +++-- apps/entities/response_data.py | 13 ++++++++++++- 4 files changed, 25 insertions(+), 26 deletions(-) diff --git a/apps/entities/flow.py b/apps/entities/flow.py index 2fe35f87..ad6caa1e 100644 --- a/apps/entities/flow.py +++ b/apps/entities/flow.py @@ -7,9 +7,9 @@ from typing import Any, Optional from pydantic import BaseModel, Field, HttpUrl from apps.entities.enum_var import ( - PermissionType, EdgeType, MetadataType, + PermissionType, ) @@ -125,11 +125,10 @@ class AppLink(BaseModel): url: HttpUrl = Field(..., description="链接地址") -class AppPermission(BaseModel): +class Permission(BaseModel): """App的权限配置""" - type: PermissionType = Field( - description="权限类型", default=PermissionType.PRIVATE) + type: PermissionType = Field(description="权限类型", default=PermissionType.PRIVATE) users: list[str] = Field(description="可访问的用户列表", default=[]) @@ -140,8 +139,7 @@ class AppMetadata(MetadataBase): links: list[AppLink] = Field(description="相关链接", default=[]) first_questions: list[str] = Field(description="首次提问", default=[]) history_len: int = Field(description="对话轮次", default=3, le=10) - permissions: Optional[AppPermission] = Field( - description="应用权限配置", default=None) + permission: Optional[Permission] = Field(description="应用权限配置", default=None) class ServiceApiSpec(BaseModel): diff --git a/apps/entities/pool.py b/apps/entities/pool.py index 851c7c13..d5f2c07d 100644 --- a/apps/entities/pool.py +++ b/apps/entities/pool.py @@ -7,15 +7,15 @@ from typing import Any, Optional from pydantic import BaseModel, Field -from apps.entities.enum_var import CallType, PermissionType +from apps.entities.enum_var import CallType +from apps.entities.flow import AppLink, Permission from apps.entities.flow_topology import PositionItem -from apps.entities.flow import AppLink class PoolBase(BaseModel): """Pool的基础信息""" - id: str = Field(alias='_id') + id: str = Field(alias="_id") name: str description: str created_at: float = Field(default_factory=lambda: round(datetime.now(tz=timezone.utc).timestamp(), 3)) @@ -29,14 +29,6 @@ class ServiceApiInfo(BaseModel): path: str = Field(description="OpenAPI文件路径") -class Permission(BaseModel): - """App的权限配置""" - - type: PermissionType = Field( - description="权限类型", default=PermissionType.PRIVATE) - users: list[str] = Field(description="可访问的用户列表", default=[]) - - class ServicePool(PoolBase): """外部服务信息 @@ -45,11 +37,9 @@ class ServicePool(PoolBase): author: str api: list[ServiceApiInfo] = Field(description="API信息列表", default=[]) - permissions: Optional[Permission] = Field( - description="用户与服务的权限关系", default=[]) + permission: Permission = Field(description="用户与服务的权限关系", default=Permission()) favorites: list[str] = Field(description="收藏此应用的用户列表", default=[]) - hashes: dict[str, str] = Field( - description="关联文件的hash值;Service作为整体更新或删除", default={}) + hashes: dict[str, str] = Field(description="关联文件的hash值;Service作为整体更新或删除", default={}) class NodePool(PoolBase): @@ -97,8 +87,7 @@ class AppPool(PoolBase): links: list[AppLink] = Field(description="相关链接", default=[]) first_questions: list[str] = Field(description="推荐问题", default=[]) history_len: int = Field(3, ge=1, le=10, description="对话轮次(1~10)") - permission: Permission = Field( - description="应用权限配置", default=Permission(type=PermissionType.PRIVATE.value,users=[])) + permission: Permission = Field(description="应用权限配置", default=Permission()) flows: list[AppFlow] = Field(description="Flow列表", default=[]) favorites: list[str] = Field(description="收藏此应用的用户列表", default=[]) hashes: dict[str, str] = Field(description="关联文件的hash值", default={}) diff --git a/apps/entities/request_data.py b/apps/entities/request_data.py index 08540509..9061c4b6 100644 --- a/apps/entities/request_data.py +++ b/apps/entities/request_data.py @@ -7,7 +7,7 @@ from typing import Optional from pydantic import BaseModel, Field from apps.entities.appcenter import AppData -from apps.entities.flow import EdgeItem, FlowItem, NodeItem, PositionItem +from apps.entities.flow_topology import EdgeItem, FlowItem, NodeItem, PositionItem from apps.entities.task import RequestDataApp @@ -114,10 +114,11 @@ class PostKnowledgeIDData(BaseModel): kb_id: str + class PutFlowReq(BaseModel): """创建/修改流拓扑结构""" + flow: FlowItem nodes: list[NodeItem] edges: list[EdgeItem] focus_point: PositionItem = Field(alias="focusPoint") - \ No newline at end of file diff --git a/apps/entities/response_data.py b/apps/entities/response_data.py index 5e938722..e2c55800 100644 --- a/apps/entities/response_data.py +++ b/apps/entities/response_data.py @@ -9,8 +9,8 @@ from pydantic import BaseModel, Field from apps.entities.appcenter import AppCenterCardItem, AppData from apps.entities.collection import Blacklist, Document, NodeMetaData from apps.entities.enum_var import DocumentStatus +from apps.entities.flow_topology import FlowItem, NodeMetaDataItem, PositionItem, ServiceItem from apps.entities.record import RecordData -from apps.entities.flow_topology import ServiceItem, NodeMetaDataItem, PositionItem, FlowItem class ResponseData(BaseModel): @@ -315,23 +315,27 @@ class GetRecentAppListRsp(ResponseData): class NodeServiceListMsg(BaseModel): """GET /api/flow/service result""" + total: int services: list[ServiceItem] class NodeServiceListRsp(ResponseData): """GET /api/flow/service 返回数据结构""" + result: NodeServiceListMsg class NodeMetaDataListMsg(BaseModel): """GET /api/flow/service/node result""" + service_id: str = Field(alias="serviceId") node_meta_datas: list[NodeMetaDataItem] class ServiceNodeMetaDatasMsg(BaseModel): """GET /api/flow/node/metadata 服务与服务下节点元数据结构""" + service_id: str = Field(alias="serviceId") name: str type: str @@ -346,35 +350,42 @@ class NodeMetaDataListMsg(ResponseData): class NodeMetaDataListRsp(ResponseData): """GET /api/flow/service/node 返回数据结构""" + result: NodeMetaDataListMsg class FlowStructureGetMsg(BaseModel): """GET /api/flow result""" + flow: FlowItem focus_point: PositionItem class FlowStructureGetRsp(ResponseData): """GET /api/flow 返回数据结构""" + result: FlowStructureGetMsg class FlowStructurePutMsg(BaseModel): """PUT /api/flow result""" + flow_id: str = Field(alias="flowId") class FlowStructurePutRsp(ResponseData): """PUT /api/flow 返回数据结构""" + result: FlowStructurePutMsg class FlowStructureDeleteMsg(BaseModel): """DELETE /api/flow/{flowId} result""" + flow_id: str = Field(alias="flowId") class FlowStructureDeleteRsp(ResponseData): """DELETE /api/flow/{flowId} 返回数据结构""" + result: FlowStructureDeleteMsg -- Gitee From 9245fdaecc9252edb9f64c6b1eb90acdd8f76e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Thu, 23 Jan 2025 20:02:55 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E5=BA=94=E7=94=A8=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=20API=20&=20Manager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- apps/manager/appcenter.py | 348 ++++++++++++++++++++++++++++++++++++++ apps/routers/appcenter.py | 286 +++++++++++++++++++++++++++++++ 2 files changed, 634 insertions(+) create mode 100644 apps/manager/appcenter.py create mode 100644 apps/routers/appcenter.py diff --git a/apps/manager/appcenter.py b/apps/manager/appcenter.py new file mode 100644 index 00000000..df693ff5 --- /dev/null +++ b/apps/manager/appcenter.py @@ -0,0 +1,348 @@ +"""应用中心 Manager + +Copyright (c) Huawei Technologies Co., Ltd. 2024-2025. All rights reserved. +""" +import uuid +from enum import Enum +from typing import Any, Optional + +from apps.constants import LOGGER +from apps.entities.appcenter import AppCenterCardItem, AppData +from apps.entities.enum_var import SearchType +from apps.entities.flow import Permission +from apps.entities.pool import AppPool +from apps.entities.response_data import RecentAppList, RecentAppListItem +from apps.models.mongo import MongoDB + + +class AppCenterManager: + """应用中心管理器""" + + class ModFavAppFlag(Enum): + """收藏应用标志""" + + SUCCESS = 0 + NOT_FOUND = 1 + BAD_REQUEST = 2 + INTERNAL_ERROR = 3 + + @staticmethod + async def fetch_all_apps( + user_sub: str, + search_type: SearchType, + keyword: Optional[str], + page: int, + page_size: int, + ) -> tuple[list[AppCenterCardItem], int]: + """获取所有应用列表""" + try: + # 搜索条件 + filters: dict[str, Any] = AppCenterManager._build_filters( + {"published": True}, + search_type, + keyword, + ) if keyword and search_type != SearchType.AUTHOR else {} + apps, total_pages = await AppCenterManager._search_apps_by_filter(filters, page, page_size) + return [ + AppCenterCardItem( + appId=app.id, + icon=app.icon, + name=app.name, + description=app.description, + author=app.author, + favorited=(user_sub in app.favorites), + published=app.published, + ) + for app in apps + ], total_pages + except Exception as e: + LOGGER.error(f"[AppCenterManager] Get app list failed: {e}") + return [], -1 + + @staticmethod + async def fetch_user_apps( + user_sub: str, + search_type: SearchType, + keyword: Optional[str], + page: int, + page_size: int, + ) -> tuple[list[AppCenterCardItem], int]: + """获取用户应用列表""" + try: + # 搜索条件 + base_filter = {"author": user_sub} + filters: dict[str, Any] = AppCenterManager._build_filters( + base_filter, + search_type, + keyword, + ) if keyword and search_type != SearchType.AUTHOR else base_filter + apps, total_pages = await AppCenterManager._search_apps_by_filter(filters, page, page_size) + return [ + AppCenterCardItem( + appId=app.id, + icon=app.icon, + name=app.name, + description=app.description, + author=app.author, + favorited=(user_sub in app.favorites), + published=app.published, + ) + for app in apps + ], total_pages + except Exception as e: + LOGGER.info(f"[AppCenterManager] Get app list by user failed: {e}") + return [], -1 + + @staticmethod + async def fetch_favorite_apps( + user_sub: str, + search_type: SearchType, + keyword: Optional[str], + page: int, + page_size: int, + ) -> tuple[list[AppCenterCardItem], int]: + """获取用户收藏的应用列表""" + try: + fav_app = await AppCenterManager._get_favorite_app_ids_by_user(user_sub) + # 搜索条件 + base_filter = { + "_id": {"$in": fav_app}, + "published": True, + } + filters: dict[str, Any] = AppCenterManager._build_filters( + base_filter, + search_type, + keyword, + ) if keyword else base_filter + apps, total_pages = await AppCenterManager._search_apps_by_filter(filters, page, page_size) + return [ + AppCenterCardItem( + appId=app.id, + icon=app.icon, + name=app.name, + description=app.description, + author=app.author, + favorited=True, + published=app.published, + ) + for app in apps + ], total_pages + except Exception as e: + LOGGER.info(f"[AppCenterManager] Get favorite app list failed: {e}") + return [], -1 + + @staticmethod + async def fetch_app_data_by_id(app_id: str) -> Optional[AppPool]: + """根据应用ID获取应用元数据""" + try: + app_collection = MongoDB.get_collection("app") + db_data = await app_collection.find_one({"_id": app_id}) + if not db_data: + return None + return AppPool.model_validate(db_data) + except Exception as e: + LOGGER.info(f"[AppCenterManager] Get app metadata by app_id failed: {e}") + return None + + @staticmethod + async def create_app(user_sub: str, data: AppData) -> Optional[str]: + """创建应用""" + app_id = str(uuid.uuid4()) + app = AppPool( + _id=app_id, + name=data.name, + description=data.description, + author=user_sub, + icon=data.icon, + links=data.links, + first_questions=data.first_questions, + history_len=data.history_len, + permission=Permission( + type=data.permission.type, + users=data.permission.users or [], + ), + ) + try: + app_collection = MongoDB.get_collection("app") + await app_collection.insert_one(app.model_dump(by_alias=True)) + return app_id + except Exception as e: + LOGGER.error(f"[AppCenterManager] Create app failed: {e}") + return None + + @staticmethod + async def update_app(app_id: str, data: AppData) -> bool: + """更新应用""" + try: + app_collection = MongoDB.get_collection("app") + app_data = AppPool.model_validate(await app_collection.find_one({"_id": app_id})) + if not app_data: + return False + # 如果工作流ID列表不一致,则需要取消发布状态 + published_false_needed = {flow.id for flow in app_data.flows} != set(data.workflows) + update_data = { + "name": data.name, + "description": data.description, + "icon": data.icon, + "links": data.links, + "first_questions": data.first_questions, + "history_len": data.history_len, + "permission": Permission( + type=data.permission.type, + users=data.permission.users or [], + ), + } + if published_false_needed: + update_data["published"] = False + await app_collection.update_one({"_id": app_id}, {"$set": update_data}) + return True + except Exception as e: + LOGGER.error(f"[AppCenterManager] Update app failed: {e}") + return False + + @staticmethod + async def publish_app(app_id: str) -> bool: + """发布应用""" + try: + app_collection = MongoDB.get_collection("app") + await app_collection.update_one( + {"_id": app_id}, + {"$set": {"published": True}}, + ) + return True + except Exception as e: + LOGGER.error(f"[AppCenterManager] Publish app failed: {e}") + return False + + @staticmethod + async def modify_favorite_app(app_id: str, user_sub: str, *, favorited: bool) -> ModFavAppFlag: + """修改收藏状态""" + try: + app_collection = MongoDB.get_collection("app") + db_data = await app_collection.find_one({"_id": app_id}) + if not db_data: + return AppCenterManager.ModFavAppFlag.NOT_FOUND + + 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: + return AppCenterManager.ModFavAppFlag.BAD_REQUEST + + if favorited: + await app_collection.update_one( + {"_id": app_id}, + {"$addToSet": {"favorites": user_sub}}, + upsert=True, + ) + else: + await app_collection.update_one( + {"_id": app_id}, + {"$pull": {"favorites": user_sub}}, + ) + return AppCenterManager.ModFavAppFlag.SUCCESS + except Exception as e: + LOGGER.error(f"[AppCenterManager] Modify favorite app failed: {e}") + return AppCenterManager.ModFavAppFlag.INTERNAL_ERROR + + @staticmethod + async def get_recently_used_apps(count: int, user_sub: str) -> Optional[RecentAppList]: + """获取用户最近使用的应用列表""" + try: + user_collection = MongoDB.get_collection("user") + app_collection = MongoDB.get_collection("app") + user_data = await user_collection.find_one({"_id": user_sub}, {"_id": 0, "app_usage": 1}) + if user_data and "app_usage" in user_data and user_data["app_usage"]: + usage_list = sorted( + user_data["app_usage"].items(), + key=lambda x: x[1]["last_used"], + reverse=True, + )[:count] + app_ids = [t[0] for t in usage_list] + apps = await app_collection.find( + {"_id": {"$in": app_ids}}, {"name": 1}).to_list(len(app_ids)) + app_map = {str(a["_id"]): a.get("name", "") for a in apps} + return RecentAppList(applications=[ + RecentAppListItem(appId=app_id, name=app_map.get(app_id, "")) + for app_id in app_ids + ]) + except Exception as e: + LOGGER.info(f"[AppCenterManager] Get recently used apps failed: {e}") + return None + + @staticmethod + async def delete_app(app_id: str, user_sub: str) -> bool: + """删除应用""" + try: + async with MongoDB.get_session() as session, await session.start_transaction(): + app_collection = MongoDB.get_collection("app") + await app_collection.delete_one({"_id": app_id}, session=session) + user_collection = MongoDB.get_collection("user") + await user_collection.update_one( + {"_id": user_sub}, + {"$unset": {f"app_usage.{app_id}": ""}}, + session=session, + ) + await session.commit_transaction() + return True + except Exception as e: + LOGGER.error(f"[AppCenterManager] Delete app failed: {e}") + return False + + @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 + + @staticmethod + async def _search_apps_by_filter( + search_conditions: dict[str, Any], + page: int, + page_size: int, + ) -> tuple[list[AppPool], int]: + """根据过滤条件搜索应用并计算总页数""" + try: + app_collection = MongoDB.get_collection("app") + total_apps = await app_collection.count_documents(search_conditions) + total_pages = (total_apps + page_size - 1) // page_size + db_data = await app_collection.find(search_conditions) \ + .sort("created_at", -1) \ + .skip((page - 1) * page_size) \ + .limit(page_size) \ + .to_list(length=page_size) + apps = [AppPool.model_validate(doc) for doc in db_data] + return apps, total_pages + except Exception as e: + LOGGER.info(f"[AppCenterManager] Search apps by filter failed: {e}") + return [], -1 + + @staticmethod + async def _get_favorite_app_ids_by_user(user_sub: str) -> list[str]: + """获取用户收藏的应用ID""" + try: + app_collection = MongoDB.get_collection("app") + cursor = app_collection.find({"favorites": user_sub}) + return [AppPool.model_validate(doc).id async for doc in cursor] + except Exception as e: + LOGGER.info(f"[AppCenterManager] Get favorite app ids by user_sub failed: {e}") + return [] diff --git a/apps/routers/appcenter.py b/apps/routers/appcenter.py new file mode 100644 index 00000000..9d25b740 --- /dev/null +++ b/apps/routers/appcenter.py @@ -0,0 +1,286 @@ +"""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.dependency.csrf import verify_csrf_token +from apps.dependency.user import get_user, verify_user +from apps.entities.appcenter import AppPermissionData +from apps.entities.enum_var import SearchType +from apps.entities.request_data import CreateAppRequest, ModFavAppRequest +from apps.entities.response_data import ( + BaseAppOperationMsg, + BaseAppOperationRsp, + GetAppListMsg, + GetAppListRsp, + GetAppPropertyMsg, + GetAppPropertyRsp, + GetRecentAppListRsp, + ModFavAppMsg, + ModFavAppRsp, + ResponseData, +) +from apps.manager.appcenter import AppCenterManager +from apps.manager.flow import FlowManager + +router = APIRouter( + prefix="/api/app", + tags=["appcenter"], + dependencies=[Depends(verify_user)], +) + + +@router.get("", response_model=Union[GetAppListRsp, ResponseData]) +async def get_applications( # noqa: ANN201, PLR0913 + user_sub: Annotated[str, Depends(get_user)], + my_app: Annotated[Optional[bool], Query(None, alias="createdByMe", description="筛选我创建的")], + my_fav: Annotated[Optional[bool], Query(None, alias="favorited", description="筛选我收藏的")], + search_type: Annotated[SearchType, Query(SearchType.ALL, alias="searchType", description="搜索类型")], + keyword: Annotated[Optional[str], Query(None, alias="keyword", description="搜索关键字")], + page: Annotated[int, Query(1, alias="page", ge=1, description="页码")], + page_size: Annotated[int, Query(20, alias="pageSize", ge=1, le=100, description="每页条数")], +): + """获取应用列表""" + if my_app and my_fav: # 只能同时使用一个过滤条件 + return ResponseData( + code=status.HTTP_400_BAD_REQUEST, + message="createdByMe 和 favorited 不能同时生效", + result={}, + ) + + app_cards, total_pages = [], -1 + if my_app: # 筛选我创建的 + app_cards, total_pages = await AppCenterManager.fetch_user_apps( + user_sub, search_type, keyword, page, page_size) + elif my_fav: # 筛选已收藏的 + app_cards, total_pages = await AppCenterManager.fetch_favorite_apps( + user_sub, search_type, keyword, page, page_size) + else: # 获取所有应用 + app_cards, total_pages = await AppCenterManager.fetch_all_apps( + user_sub, search_type, keyword, page, page_size) + if total_pages == -1: + return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="查询失败", + result={}, + ).model_dump(exclude_none=True, by_alias=True)) + return JSONResponse(status_code=status.HTTP_200_OK, content=GetAppListRsp( + code=status.HTTP_200_OK, + message="查询成功", + result=GetAppListMsg( + currentPage=page, + totalPages=total_pages, + applications=app_cards, + ), + ).model_dump(exclude_none=True, by_alias=True)) + + +@router.post("", dependencies=[Depends(verify_csrf_token)], response_model=Union[BaseAppOperationRsp, ResponseData]) +async def create_or_update_application( # noqa: ANN201 + request: Annotated[CreateAppRequest, Body(...)], + user_sub: Annotated[str, Depends(get_user)], +): + """创建或更新应用""" + app_id = request.app_id + if app_id: + # 更新应用 + confirm = await AppCenterManager.update_app(app_id, request) + if not confirm: + return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="更新失败", + result={}, + ).model_dump(exclude_none=True, by_alias=True)) + return JSONResponse(status_code=status.HTTP_200_OK, content=BaseAppOperationRsp( + code=status.HTTP_200_OK, + message="更新成功", + result=BaseAppOperationMsg(appId=app_id), + ).model_dump(exclude_none=True, by_alias=True)) + # 创建应用 + app_id = await AppCenterManager.create_app(user_sub, request) + if not app_id: + return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="创建失败", + result={}, + ).model_dump(exclude_none=True, by_alias=True)) + return JSONResponse(status_code=status.HTTP_200_OK, content=BaseAppOperationRsp( + code=status.HTTP_200_OK, + message="创建成功", + result=BaseAppOperationMsg(appId=app_id), + ).model_dump(exclude_none=True, by_alias=True)) + + +@router.get("/recent", response_model=Union[GetRecentAppListRsp, ResponseData]) +async def get_recently_used_applications( # noqa: ANN201 + count: Annotated[int, Query(5, ge=1, le=10)], + user_sub: Annotated[str, Depends(get_user)], +): + """获取最近使用的应用""" + recent_apps = await AppCenterManager.get_recently_used_apps( + count, user_sub) + if not recent_apps: + return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="查询失败", + result={}, + ).model_dump(exclude_none=True, by_alias=True)) + return JSONResponse(status_code=status.HTTP_200_OK, content=GetRecentAppListRsp( + code=status.HTTP_200_OK, + message="查询成功", + result=recent_apps, + ).model_dump(exclude_none=True, by_alias=True)) + + +@router.get("/{appId}", response_model=Union[GetAppPropertyRsp, ResponseData]) +async def get_application( # noqa: ANN201 + app_id: Annotated[str, Path(..., alias="appId", description="应用ID")], +): + """获取应用详情""" + app_data = await AppCenterManager.fetch_app_data_by_id(app_id) + if not app_data: + return JSONResponse(status_code=status.HTTP_404_NOT_FOUND, content=ResponseData( + code=status.HTTP_404_NOT_FOUND, + message="找不到应用", + result={}, + ).model_dump(exclude_none=True, by_alias=True)) + workflows = [flow.id for flow in app_data.flows] + return JSONResponse(status_code=status.HTTP_200_OK, content=GetAppPropertyRsp( + code=status.HTTP_200_OK, + message="查询成功", + result=GetAppPropertyMsg( + appId=app_data.id, + published=app_data.published, + name=app_data.name, + description=app_data.description, + icon=app_data.icon, + links=app_data.links, + recommendedQuestions=app_data.first_questions, + dialogRounds=app_data.history_len, + permission=AppPermissionData( + visibility=app_data.permission.type, + authorizedUsers=app_data.permission.users, + ), + workflows=workflows, + ), + ).model_dump(exclude_none=True, by_alias=True)) + + +@router.delete( + "/{appId}", + dependencies=[Depends(verify_csrf_token)], + response_model=Union[BaseAppOperationRsp, ResponseData], +) +async def delete_application( # noqa: ANN201 + app_id: Annotated[str, Path(..., alias="appId", description="应用ID")], + user_sub: Annotated[str, Depends(get_user)], +): + """删除应用""" + app_data = await AppCenterManager.fetch_app_data_by_id(app_id) + if not app_data: + return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, content=ResponseData( + code=status.HTTP_400_BAD_REQUEST, + message="查询失败", + result={}, + ).model_dump(exclude_none=True, by_alias=True)) + # 校验应用作者是否为当前用户 + if app_data.author != user_sub: + return JSONResponse(status_code=status.HTTP_403_FORBIDDEN, content=ResponseData( + code=status.HTTP_403_FORBIDDEN, + message="无权删除他人创建的应用", + result={}, + ).model_dump(exclude_none=True, by_alias=True)) + # 删除应用相关的工作流 + for flow in app_data.flows: + if not await FlowManager.delete_flow_by_app_and_flow_id(app_id, flow.id): + return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message=f"删除应用下属工作流 {flow.id} 失败", + result={}, + ).model_dump(exclude_none=True, by_alias=True)) + # 删除应用 + if not await AppCenterManager.delete_app(app_id, user_sub): + return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="删除失败", + result={}, + ).model_dump(exclude_none=True, by_alias=True)) + return JSONResponse(status_code=status.HTTP_200_OK, content=BaseAppOperationRsp( + code=status.HTTP_200_OK, + message="删除成功", + result=BaseAppOperationMsg(appId=app_id), + ).model_dump(exclude_none=True, by_alias=True)) + + +@router.post("/{appId}", dependencies=[Depends(verify_csrf_token)], response_model=BaseAppOperationRsp) +async def publish_application( # noqa: ANN201 + app_id: Annotated[str, Path(..., alias="appId", description="应用ID")], + user_sub: Annotated[str, Depends(get_user)], +): + """发布应用""" + app_data = await AppCenterManager.fetch_app_data_by_id(app_id) + if not app_data: + return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, content=ResponseData( + code=status.HTTP_400_BAD_REQUEST, + message="查询失败", + result={}, + ).model_dump(exclude_none=True, by_alias=True)) + # 验证用户权限 + if app_data.author != user_sub: + return JSONResponse(status_code=status.HTTP_403_FORBIDDEN, content=ResponseData( + code=status.HTTP_403_FORBIDDEN, + message="无权发布他人创建的应用", + result={}, + ).model_dump(exclude_none=True, by_alias=True)) + # 发布应用 + if not await AppCenterManager.publish_app(app_id): + return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="发布失败", + result={}, + ).model_dump(exclude_none=True, by_alias=True)) + return JSONResponse(status_code=status.HTTP_200_OK, content=BaseAppOperationRsp( + code=status.HTTP_200_OK, + message="发布成功", + result=BaseAppOperationMsg(appId=app_id), + ).model_dump(exclude_none=True, by_alias=True)) + + +@router.put("/{appId}", dependencies=[Depends(verify_csrf_token)], response_model=Union[ModFavAppRsp, ResponseData]) +async def modify_favorite_application( # noqa: ANN201 + app_id: Annotated[str, Path(..., alias="appId", description="应用ID")], + request: Annotated[ModFavAppRequest, Body(...)], + user_sub: Annotated[str, Depends(get_user)], +): + """更改应用收藏状态""" + flag = await AppCenterManager.modify_favorite_app(app_id, user_sub, favorited=request.favorited) + if flag == AppCenterManager.ModFavAppFlag.NOT_FOUND: + return JSONResponse(status_code=status.HTTP_404_NOT_FOUND, content=ResponseData( + code=status.HTTP_404_NOT_FOUND, + message="找不到应用", + result={}, + ).model_dump(exclude_none=True, by_alias=True)) + if flag == AppCenterManager.ModFavAppFlag.BAD_REQUEST: + return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, content=ResponseData( + code=status.HTTP_400_BAD_REQUEST, + message="不可重复操作", + result={}, + ).model_dump(exclude_none=True, by_alias=True)) + if flag == AppCenterManager.ModFavAppFlag.INTERNAL_ERROR: + return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + message="操作失败", + result={}, + ).model_dump(exclude_none=True, by_alias=True)) + return JSONResponse(status_code=status.HTTP_200_OK, content=ModFavAppRsp( + code=status.HTTP_200_OK, + message="操作成功", + result=ModFavAppMsg( + appId=app_id, + favorited=request.favorited, + ), + ).model_dump(exclude_none=True, by_alias=True)) -- Gitee