diff --git a/apps/entities/flow.py b/apps/entities/flow.py index 21592ff9dd3f3a6779dcbfcf0c1820ccf2eaa114..42e2665529ee75df77acf86011483036338f568c 100644 --- a/apps/entities/flow.py +++ b/apps/entities/flow.py @@ -2,7 +2,7 @@ Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. """ -from typing import Any, Optional +from typing import Any, ClassVar, Optional from pydantic import BaseModel, Field, HttpUrl @@ -124,6 +124,11 @@ class AppLink(BaseModel): title: str = Field(description="链接标题") url: HttpUrl = Field(..., description="链接地址") + class Config: + """自定义配置: 将 HttpUrl 转换为 str""" + + json_encoders: ClassVar[dict] = {HttpUrl: str} + class Permission(BaseModel): """App的权限配置""" @@ -151,8 +156,10 @@ class ServiceApiSpec(BaseModel): path: str = Field(description="OpenAPI文件路径") hash: str = Field(description="OpenAPI文件的hash值") + class FlowConfig(BaseModel): - """Flow的配置信息 用于前期调试使用""" - app_id: str - flow_id: str - flow_config: Flow \ No newline at end of file + """Flow的配置信息 用于前期调试使用""" + + app_id: str + flow_id: str + flow_config: Flow diff --git a/apps/entities/response_data.py b/apps/entities/response_data.py index b3d26bf1ab99fc248a18231bd6bc6902b3271b29..cc685823ab0222f763ef872853a7c2ff6ad88c41 100644 --- a/apps/entities/response_data.py +++ b/apps/entities/response_data.py @@ -289,7 +289,7 @@ class GetAppListMsg(BaseModel): """GET /api/app Result数据结构""" page_number: int = Field(..., alias="currentPage", description="当前页码") - app_count: int = Field(..., alias="total", description="总页数") + app_count: int = Field(..., alias="totalApps", description="总应用数") applications: list[AppCenterCardItem] = Field(..., description="应用列表") diff --git a/apps/manager/appcenter.py b/apps/manager/appcenter.py index bd0a341152cc65ba04060e00ed8ae8cf9958cd80..46d9707b4f8fc42b44b8d7340a71408f387a631f 100644 --- a/apps/manager/appcenter.py +++ b/apps/manager/appcenter.py @@ -38,7 +38,15 @@ class AppCenterManager: page: int, page_size: int, ) -> tuple[list[AppCenterCardItem], int]: - """获取所有应用列表""" + """获取所有应用列表 + + :param user_sub: 用户唯一标识 + :param search_type: 搜索类型 + :param keyword: 搜索关键字 + :param page: 页码 + :param page_size: 每页条数 + :return: 应用列表, 总应用数 + """ try: # 搜索条件 filters: dict[str, Any] = AppCenterManager._build_filters( @@ -71,7 +79,15 @@ class AppCenterManager: page: int, page_size: int, ) -> tuple[list[AppCenterCardItem], int]: - """获取用户应用列表""" + """获取用户应用列表 + + :param user_sub: 用户唯一标识 + :param search_type: 搜索类型 + :param keyword: 搜索关键词 + :param page: 页码 + :param page_size: 每页数量 + :return: 应用列表, 总应用数 + """ try: # 搜索条件 base_filter = {"author": user_sub} @@ -105,7 +121,15 @@ class AppCenterManager: page: int, page_size: int, ) -> tuple[list[AppCenterCardItem], int]: - """获取用户收藏的应用列表""" + """获取用户收藏的应用列表 + + :param user_sub: 用户唯一标识 + :param search_type: 搜索类型 + :param keyword: 搜索关键词 + :param page: 页码 + :param page_size: 每页数量 + :return: 应用列表,总应用数 + """ try: fav_app = await AppCenterManager._get_favorite_app_ids_by_user(user_sub) # 搜索条件 @@ -137,11 +161,16 @@ class AppCenterManager: @staticmethod async def fetch_app_data_by_id(app_id: str) -> Optional[AppPool]: - """根据应用ID获取应用元数据""" + """根据应用ID获取应用元数据 + + :param app_id: 应用ID + :return: 应用元数据 + """ try: app_collection = MongoDB.get_collection("app") db_data = await app_collection.find_one({"_id": app_id}) if not db_data: + LOGGER.warning(f"[AppCenterManager] No data found for app_id: {app_id}") return None return AppPool.model_validate(db_data) except Exception as e: @@ -150,7 +179,12 @@ class AppCenterManager: @staticmethod async def create_app(user_sub: str, data: AppData) -> Optional[str]: - """创建应用""" + """创建应用 + + :param user_sub: 用户唯一标识 + :param data: 应用数据 + :return: 应用ID + """ app_id = str(uuid.uuid4()) app = AppPool( _id=app_id, @@ -176,7 +210,12 @@ class AppCenterManager: @staticmethod async def update_app(app_id: str, data: AppData) -> bool: - """更新应用""" + """更新应用 + + :param app_id: 应用唯一标识 + :param data: 应用数据 + :return: 是否成功 + """ try: app_collection = MongoDB.get_collection("app") app_data = AppPool.model_validate(await app_collection.find_one({"_id": app_id})) @@ -206,7 +245,11 @@ class AppCenterManager: @staticmethod async def publish_app(app_id: str) -> bool: - """发布应用""" + """发布应用 + + :param app_id: 应用唯一标识 + :return: 是否成功 + """ try: app_collection = MongoDB.get_collection("app") await app_collection.update_one( @@ -220,7 +263,13 @@ class AppCenterManager: @staticmethod async def modify_favorite_app(app_id: str, user_sub: str, *, favorited: bool) -> ModFavAppFlag: - """修改收藏状态""" + """修改收藏状态 + + :param app_id: 应用唯一标识 + :param user_sub: 用户唯一标识 + :param favorited: 是否收藏 + :return: 修改结果 + """ try: app_collection = MongoDB.get_collection("app") db_data = await app_collection.find_one({"_id": app_id}) @@ -255,7 +304,12 @@ class AppCenterManager: @staticmethod async def get_recently_used_apps(count: int, user_sub: str) -> Optional[RecentAppList]: - """获取用户最近使用的应用列表""" + """获取用户最近使用的应用列表 + + :param count: 应用数量 + :param user_sub: 用户唯一标识 + :return: 最近使用的应用列表 + """ try: user_collection = MongoDB.get_collection("user") app_collection = MongoDB.get_collection("app") @@ -282,7 +336,12 @@ class AppCenterManager: @staticmethod async def delete_app(app_id: str, user_sub: str) -> bool: - """删除应用""" + """删除应用 + + :param app_id: 应用唯一标识 + :param user_sub: 用户唯一标识 + :return: 删除是否成功 + """ try: async with MongoDB.get_session() as session, await session.start_transaction(): app_collection = MongoDB.get_collection("app") @@ -299,6 +358,38 @@ class AppCenterManager: LOGGER.error(f"[AppCenterManager] Delete app failed: {e}") return False + @staticmethod + async def update_recent_app(user_sub: str, app_id: str) -> bool: + """更新用户的最近使用应用列表 + + :param user_sub: 用户唯一标识 + :param app_id: 应用唯一标识 + :return: 更新是否成功 + """ + try: + user_collection = MongoDB.get_collection("user") + current_time = round(datetime.now(tz=timezone.utc).timestamp(), 3) + result = await user_collection.update_one( + {"_id": user_sub}, # 查询条件 + { + "$set": { + f"app_usage.{app_id}.last_used": current_time, # 更新最后使用时间 + }, + "$inc": { + f"app_usage.{app_id}.count": 1, # 增加使用次数 + }, + }, + upsert=True, # 如果 app_usage 字段或 app_id 不存在,则创建 + ) + if result.modified_count > 0 or result.upserted_id is not None: + LOGGER.info(f"[AppCenterManager] Updated recent app for user {user_sub}: {app_id}") + return True + LOGGER.warning(f"[AppCenterManager] No changes made for user {user_sub}") + return False + except Exception as e: + LOGGER.error(f"[AppCenterManager] Failed to update recent app: {e}") + return False + @staticmethod def _build_filters( base_filters: dict[str, Any], @@ -346,49 +437,8 @@ class AppCenterManager: """获取用户收藏的应用ID""" try: app_collection = MongoDB.get_collection("app") - cursor = app_collection.find({"favorites": user_sub}) + cursor = app_collection.find({"favorites": {"$in": [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 [] - - @staticmethod - async def update_recent_app(user_sub: str, app_id: str) -> bool: - """更新用户的最近使用应用列表 - - :param user_sub: 用户唯一标识 - :param app_id: 应用唯一标识 - :return: 更新是否成功 - """ - try: - # 获取 user 集合 - user_collection = MongoDB.get_collection("user") - - # 获取当前时间戳 - current_time = round(datetime.now(tz=timezone.utc).timestamp(), 3) - - # 更新用户的 app_usage 字段 - result = await user_collection.update_one( - {"_id": user_sub}, # 查询条件 - { - "$set": { - f"app_usage.{app_id}.last_used": current_time, # 更新最后使用时间 - }, - "$inc": { - f"app_usage.{app_id}.count": 1, # 增加使用次数 - }, - }, - upsert=True, # 如果 app_usage 字段或 app_id 不存在,则创建 - ) - - # 检查更新是否成功 - if result.modified_count > 0 or result.upserted_id is not None: - LOGGER.info(f"[AppCenterManager] Updated recent app for user {user_sub}: {app_id}") - return True - else: - LOGGER.warning(f"[AppCenterManager] No changes made for user {user_sub}") - return False - - except Exception as e: - LOGGER.error(f"[AppCenterManager] Failed to update recent app: {e}") - return False \ No newline at end of file diff --git a/apps/manager/flow.py b/apps/manager/flow.py index 935110fdcf52d8bc37ca52f23f78450912b40dc6..e78ce6d20d29e4bb87611bdb2a9b251030a5d9d7 100644 --- a/apps/manager/flow.py +++ b/apps/manager/flow.py @@ -2,15 +2,16 @@ Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. """ -from typing import Tuple, List +from typing import Optional + from pymongo import ASCENDING from apps.constants import LOGGER -from apps.entities.flow import StepPos, Edge, Step, Flow, FlowConfig +from apps.entities.enum_var import PermissionType +from apps.entities.flow import Edge, Flow, FlowConfig, Step, StepPos +from apps.entities.flow_topology import EdgeItem, FlowItem, NodeItem, NodeMetaDataItem, NodeServiceItem, PositionItem from apps.entities.pool import AppFlow -from apps.entities.flow_topology import NodeServiceItem, NodeMetaDataItem, FlowItem, NodeItem, EdgeItem, PositionItem from apps.models.mongo import MongoDB -from apps.entities.enum_var import NodeType, PermissionType class FlowManager: @@ -23,7 +24,7 @@ class FlowManager: :param service_id: 服务id :return: 如果用户具有所需权限则返回True,否则返回False """ - node_pool_collection = MongoDB.get_collection("node") + node_pool_collection = MongoDB.get_collection("node") service_collection = MongoDB.get_collection("service") try: @@ -34,21 +35,23 @@ class FlowManager: { "$and": [ {"permissions.type": PermissionType.PROTECTED.value}, - {"permissions.users": user_sub} - ] - } + {"permissions.users": user_sub}, + ], + }, ] - query = {"$and": [{"_id": node_pool_record["service_id"]}, - {"$or": match_conditions} - ]} + query = {"$and": [ + {"_id": node_pool_record["service_id"]}, + {"$or": match_conditions}, + ]} result = await service_collection.count_documents(query) return (result > 0) except Exception as e: LOGGER.error(f"Validate user node meta data access failed due to: {e}") return False + @staticmethod - async def get_node_meta_datas_by_service_id(service_id: str) -> List[NodeMetaDataItem]: + async def get_node_meta_datas_by_service_id(service_id: str) -> Optional[list[NodeMetaDataItem]]: """serviceId获取service的接口数据,并将接口转换为节点元数据 :param service_id: 服务id @@ -63,7 +66,7 @@ class FlowManager: async for node_pool_record in cursor: parameters = { "input_parameters": node_pool_record["params_schema"], - "output_parameters": node_pool_record["output_schema"] + "output_parameters": node_pool_record["output_schema"], } node_meta_data_item = NodeMetaDataItem( nodeMetaDataId=node_pool_record["_id"], @@ -75,15 +78,14 @@ class FlowManager: parameters=parameters, # 添加 parametersTemplate 参数 ) nodes_meta_data_items.append(node_meta_data_item) - - return nodes_meta_data_items - except Exception as e: LOGGER.error( f"Get node metadatas by service_id failed due to: {e}") return None - async def get_service_by_user_id(user_sub: str) -> List[NodeServiceItem]: + + @staticmethod + async def get_service_by_user_id(user_sub: str) -> Optional[list[NodeServiceItem]]: """通过user_id获取用户自己上传的、其他人公开的且收藏的、受保护且有权限访问并收藏的service :user_sub: 用户的唯一标识符 @@ -97,22 +99,22 @@ class FlowManager: { "$and": [ {"permissions.type": PermissionType.PUBLIC.value}, - {"favorites": user_sub} - ] + {"favorites": user_sub}, + ], }, { "$and": [ {"permissions.type": PermissionType.PROTECTED.value}, {"permissions.users": user_sub}, - {"favorites": user_sub} - ] - } + {"favorites": user_sub}, + ], + }, ] query = {"$or": match_conditions} service_records_cursor = service_collection.find( query, - sort=[("created_at", ASCENDING)] + sort=[("created_at", ASCENDING)], ) service_records = await service_records_cursor.to_list(length=None) service_items = [ @@ -121,7 +123,7 @@ class FlowManager: name=record["name"], type="default", nodeMetaDatas=[], - createdAt=str(record["created_at"]) + createdAt=str(record["created_at"]), ) for record in service_records ] @@ -135,8 +137,9 @@ class FlowManager: except Exception as e: LOGGER.error(f"Get service by user id failed due to: {e}") return None + @staticmethod - async def get_node_meta_data_by_node_meta_data_id(node_meta_data_id: str) -> NodeMetaDataItem: + async def get_node_meta_data_by_node_meta_data_id(node_meta_data_id: str) -> Optional[NodeMetaDataItem]: """通过node_meta_data_id获取对应的节点源数据信息 :param node_meta_data_id: node_meta_data的id @@ -146,24 +149,24 @@ class FlowManager: try: node_pool_record = await node_pool_collection.find_one({"_id": node_meta_data_id}) parameters = { - "input_parameters": node_pool_record['params_schema'], - "output_parameters": node_pool_record['output_schema'] - } - node_meta_data=NodeMetaDataItem( + "input_parameters": node_pool_record["params_schema"], + "output_parameters": node_pool_record["output_schema"], + } + return NodeMetaDataItem( nodeMetaDataId=node_pool_record["_id"], type=node_pool_record["call_id"], - name=node_pool_record['name'], - description=node_pool_record['description'], + name=node_pool_record["name"], + description=node_pool_record["description"], editable=True, parameters=parameters, - createdAt=node_pool_record['created_at'], + createdAt=node_pool_record["created_at"], ) - return node_meta_data except Exception as e: LOGGER.error(f"获取节点元数据失败: {e}") return None + @staticmethod - async def get_flow_by_app_and_flow_id(app_id: str, flow_id: str) -> Tuple[FlowItem, PositionItem]: + async def get_flow_by_app_and_flow_id(app_id: str, flow_id: str) -> Optional[tuple[FlowItem, PositionItem]]: """通过appId flowId获取flow config的路径和focus,并通过flow config的路径获取flow config,并将其转换为flow item。 :param app_id: 应用的id @@ -178,7 +181,7 @@ class FlowManager: return None cursor = app_collection.find( {"_id": app_id, "flows._id": flow_id}, - {"flows.$": 1} # 只返回 flows 数组中符合条件的第一个元素 + {"flows.$": 1}, # 只返回 flows 数组中符合条件的第一个元素 ) # 获取结果列表,并限制长度为1,因为我们只期待一个结果 @@ -186,7 +189,7 @@ class FlowManager: if len(app_records) == 0: return None app_record = app_records[0] - if "flows" not in app_record.keys() or len(app_record["flows"]) == 0: + if "flows" not in app_record or len(app_record["flows"]) == 0: return None flow_record = app_record["flows"][0] except Exception as e: @@ -199,7 +202,7 @@ class FlowManager: flow_config_record = await flow_config_collection.find_one({"app_id": app_id, "flow_id": flow_id}) if flow_config_record is None or not flow_config_record.get("flow_config"): return None - flow_config = flow_config_record['flow_config'] + flow_config = flow_config_record["flow_config"] if not flow_config: LOGGER.error( "Get flow config by app_id and flow_id failed") @@ -207,44 +210,44 @@ class FlowManager: focus_point = flow_record["focus_point"] flow_item = FlowItem( flowId=flow_id, - name=flow_config['name'], - description=flow_config['description'], + name=flow_config["name"], + description=flow_config["description"], enable=True, editable=True, nodes=[], edges=[], - createdAt=flow_record["created_at"] + createdAt=flow_record["created_at"], ) - for node_config in flow_config['steps']: + for node_config in flow_config["steps"]: node_item = NodeItem( - nodeId=node_config['id'], - nodeMetaDataId=node_config['node'], - name=node_config['name'], - description=node_config['description'], + nodeId=node_config["id"], + nodeMetaDataId=node_config["node"], + name=node_config["name"], + description=node_config["description"], enable=True, editable=True, - type=node_config['type'], - parameters=node_config['params'], + type=node_config["type"], + parameters=node_config["params"], position=PositionItem( - x=node_config['pos']['x'], y=node_config['pos']['y']) + x=node_config["pos"]["x"], y=node_config["pos"]["y"]), ) flow_item.nodes.append(node_item) - for edge_config in flow_config['edges']: - edge_from = edge_config['edge_from'] + for edge_config in flow_config["edges"]: + edge_from = edge_config["edge_from"] branch_id = "" - tmp_list = edge_config['edge_from'].split('.') - if len(tmp_list)==0 or len(tmp_list)>2: + tmp_list = edge_config["edge_from"].split(".") + if len(tmp_list) == 0 or len(tmp_list) > 2: LOGGER.error("edge from format error") continue if len(tmp_list) == 2: edge_from = tmp_list[0] branch_id = tmp_list[1] flow_item.edges.append(EdgeItem( - edgeId=edge_config['id'], + edgeId=edge_config["id"], sourceNode=edge_from, - targetNode=edge_config['edge_to'], - type=edge_config['edge_type'], + targetNode=edge_config["edge_to"], + type=edge_config["edge_type"], branchId=branch_id, )) return (flow_item, focus_point) @@ -256,7 +259,7 @@ class FlowManager: @staticmethod async def put_flow_by_app_and_flow_id( - app_id: str, flow_id: str, flow_item: FlowItem, focus_point: PositionItem) -> str: + app_id: str, flow_id: str, flow_item: FlowItem, focus_point: PositionItem) -> Optional[str]: """存储/更新flow的数据库数据和配置文件 :param app_id: 应用的id @@ -272,13 +275,13 @@ class FlowManager: return None cursor = app_collection.find( {"_id": app_id, "flows._id": flow_id}, - {"flows.$": 1} + {"flows.$": 1}, ) app_records = await cursor.to_list(length=1) flow_record = None if len(app_records) != 0: app_record = app_records[0] - if "flows" in app_record.keys() and len(app_record["flows"]) != 0: + if "flows" in app_record and len(app_record["flows"]) != 0: flow_record = app_record["flows"][0] except Exception as e: LOGGER.error( @@ -320,7 +323,7 @@ class FlowManager: await flow_config_collection.update_one( {"app_id": app_id, "flow_id": flow_id}, {"$set": flow_config.dict()}, - upsert=True # 如果没有找到匹配的文档,则插入新文档 + upsert=True, # 如果没有找到匹配的文档,则插入新文档 ) except Exception as e: LOGGER.error(f"Error updating flow config due to: {e}") @@ -328,53 +331,52 @@ class FlowManager: if flow_record: app_collection = MongoDB.get_collection("app") result = await app_collection.find_one_and_update( - {'_id': app_id}, + {"_id": app_id}, { - '$set': { - 'flows.$[element].focus_point': focus_point.model_dump(by_alias=True) - } + "$set": { + "flows.$[element].focus_point": focus_point.model_dump(by_alias=True), + }, }, - array_filters=[{'element._id': flow_id}], - return_document=True # 返回更新后的文档 + array_filters=[{"element._id": flow_id}], + return_document=True, # 返回更新后的文档 ) if result is None: LOGGER.error("Update flow failed") return None return result - else: - new_flow = AppFlow( - _id=flow_id, - name=flow_item.name, - description=flow_item.description, - path="", - focus_point=PositionItem(x=focus_point.x, y=focus_point.y), - ) - app_collection = MongoDB.get_collection("app") - result = await app_collection.find_one_and_update( - {'_id': app_id}, - { - '$push': { - 'flows': new_flow.model_dump(by_alias=True) - } - } - ) - if result is None: - LOGGER.error("Add flow failed") - return None - return flow_item + new_flow = AppFlow( + _id=flow_id, + name=flow_item.name, + description=flow_item.description, + path="", + focus_point=PositionItem(x=focus_point.x, y=focus_point.y), + ) + app_collection = MongoDB.get_collection("app") + result = await app_collection.find_one_and_update( + {"_id": app_id}, + { + "$push": { + "flows": new_flow.model_dump(by_alias=True), + }, + }, + ) + if result is None: + LOGGER.error("Add flow failed") + return None + return flow_item except Exception as e: LOGGER.error( f"Put flow by app_id and flow_id failed due to: {e}") return None - async def delete_flow_by_app_and_flow_id(app_id: str, flow_id: str) -> str: + @staticmethod + async def delete_flow_by_app_and_flow_id(app_id: str, flow_id: str) -> Optional[str]: """删除flow的数据库数据和配置文件 :param app_id: 应用的id :param flow_id: 流的id :return: 流的id """ - try: flow_config_collection = MongoDB.get_collection("flow_config") result = await flow_config_collection.delete_one({"app_id": app_id, "flow_id": flow_id}) @@ -384,12 +386,12 @@ class FlowManager: app_pool_collection = MongoDB.get_collection("app") # 获取集合 result = await app_pool_collection.find_one_and_update( - {'_id': app_id}, + {"_id": app_id}, { - '$pull': { - 'flows': {'_id': flow_id} - } - } + "$pull": { + "flows": {"_id": flow_id}, + }, + }, ) if result is None: LOGGER.error("Delete flow from app pool failed") diff --git a/apps/routers/appcenter.py b/apps/routers/appcenter.py index d32c75597d94cfd0a1abb319c67fa69a7805a046..630daf03c72412fda5b9f36b80a62c3309e1d669 100644 --- a/apps/routers/appcenter.py +++ b/apps/routers/appcenter.py @@ -5,7 +5,6 @@ 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.requests import HTTPConnection from fastapi.responses import JSONResponse from apps.dependency.csrf import verify_csrf_token @@ -75,7 +74,7 @@ async def get_applications( # noqa: ANN201, PLR0913 message="查询成功", result=GetAppListMsg( currentPage=page, - total=total_apps, + totalApps=total_apps, applications=app_cards, ), ).model_dump(exclude_none=True, by_alias=True)) diff --git a/docs/design/appcenter.md b/docs/design/appcenter.md new file mode 100644 index 0000000000000000000000000000000000000000..c18e8d3f43a6bde2585dc9a4f7a1d600d42c7d10 --- /dev/null +++ b/docs/design/appcenter.md @@ -0,0 +1,100 @@ +# openEuler Copilot Framework AppCenter 设计文档 + +## 1. 整体设计 + +AppCenter 是 openEuler Copilot Framework 的应用中心模块, 主要提供以下功能: + +- 应用的 CRUD 操作 (创建、读取、更新、删除) +- 应用收藏管理 +- 最近使用应用记录 +- 应用搜索与过滤 +- 应用发布管理 + +### 1.1 核心组件 + +- AppCenter Manager: 应用中心管理器, 提供应用相关的核心业务逻辑 +- AppCenter Router: 提供 HTTP API 接口 +- MongoDB: 存储应用数据 + +## 2. 数据结构 + +### 2.1 App 数据模型 + +![App 数据模型](./uml/appcenter/DB%20AppPool%20数据模型.png) + +### 2.2 User 数据模型 + +![User 数据模型](./uml/appcenter/DB%20User%20数据模型.png) + +## 3. 接口设计 + +### 3.1 HTTP API + +| 路径 | 方法 | 描述 | +|------|------|------| +| `/api/app` | `GET` | 获取应用列表 | +| `/api/app` | `POST` | 创建/更新应用 | +| `/api/app/recent` | `GET` | 获取最近使用应用 | +| `/api/app/{appId}` | `GET` | 获取应用详情 | +| `/api/app/{appId}` | `DELETE` | 删除应用 | +| `/api/app/{appId}` | `POST` | 发布应用 | +| `/api/app/{appId}` | `PUT` | 修改应用收藏状态 | + +## 4. 核心流程 + +### 4.1 创建应用流程 + +![创建应用流程](./uml/appcenter/API%20创建应用.png) + +### 4.2 应用搜索流程 + +![应用搜索流程](./uml/appcenter/API%20搜索应用.png) + +### 4.3 获取最近使用的应用流程 + +![获取最近使用的应用流程](./uml/appcenter/API%20获取最近使用的应用.png) + +### 4.4 获取应用详情流程 + +![获取应用详情流程](./uml/appcenter/API%20获取应用详情.png) + +### 4.5 删除应用流程 + +![删除应用流程](./uml/appcenter/API%20删除应用.png) + +### 4.6 发布应用流程 + +![发布应用流程](./uml/appcenter/API%20发布应用.png) + +### 4.7 更改应用收藏状态流程 + +![更改应用收藏状态流程](./uml/appcenter/API%20收藏应用.png) + +## 5. 主要功能实现 + +### 5.1 应用搜索与过滤 + +支持以下搜索类型: + +- 全文搜索 (名称、描述、作者) +- 按名称搜索 +- 按描述搜索 +- 按作者搜索 + +### 5.2 应用收藏管理 + +- 支持收藏/取消收藏应用 +- 记录用户收藏列表 +- 支持获取收藏应用列表 + +### 5.3 最近使用记录 + +- 记录应用使用次数和最后使用时间 +- 支持获取最近使用应用列表 +- 按最后使用时间排序 + +## 6. 安全性设计 + +- 接口鉴权: 依赖 `verify_user` 中间件进行用户认证 +- CSRF 防护: 使用 `verify_csrf_token` 中间件 +- 权限控制: 应用删除、发布等操作需验证用户身份 diff --git "a/docs/design/uml/appcenter/API \345\210\233\345\273\272\345\272\224\347\224\250.png" "b/docs/design/uml/appcenter/API \345\210\233\345\273\272\345\272\224\347\224\250.png" new file mode 100644 index 0000000000000000000000000000000000000000..0f2bc13dd63a57ffc118b2dc5c894bc080303202 Binary files /dev/null and "b/docs/design/uml/appcenter/API \345\210\233\345\273\272\345\272\224\347\224\250.png" differ diff --git "a/docs/design/uml/appcenter/API \345\210\240\351\231\244\345\272\224\347\224\250.png" "b/docs/design/uml/appcenter/API \345\210\240\351\231\244\345\272\224\347\224\250.png" new file mode 100644 index 0000000000000000000000000000000000000000..a813a87b989b3bec2f2ebcdc94d118c691506ae4 Binary files /dev/null and "b/docs/design/uml/appcenter/API \345\210\240\351\231\244\345\272\224\347\224\250.png" differ diff --git "a/docs/design/uml/appcenter/API \345\217\221\345\270\203\345\272\224\347\224\250.png" "b/docs/design/uml/appcenter/API \345\217\221\345\270\203\345\272\224\347\224\250.png" new file mode 100644 index 0000000000000000000000000000000000000000..f59eec536921dda7b6509db887d0855bc0eb67ed Binary files /dev/null and "b/docs/design/uml/appcenter/API \345\217\221\345\270\203\345\272\224\347\224\250.png" differ diff --git "a/docs/design/uml/appcenter/API \346\220\234\347\264\242\345\272\224\347\224\250.png" "b/docs/design/uml/appcenter/API \346\220\234\347\264\242\345\272\224\347\224\250.png" new file mode 100644 index 0000000000000000000000000000000000000000..f69ddbdece13fac13547f68bf224b522390b2eea Binary files /dev/null and "b/docs/design/uml/appcenter/API \346\220\234\347\264\242\345\272\224\347\224\250.png" differ diff --git "a/docs/design/uml/appcenter/API \346\224\266\350\227\217\345\272\224\347\224\250.png" "b/docs/design/uml/appcenter/API \346\224\266\350\227\217\345\272\224\347\224\250.png" new file mode 100644 index 0000000000000000000000000000000000000000..143d0f43d8ee8e95e9a3f7d89c4e87468372aee5 Binary files /dev/null and "b/docs/design/uml/appcenter/API \346\224\266\350\227\217\345\272\224\347\224\250.png" differ diff --git "a/docs/design/uml/appcenter/API \350\216\267\345\217\226\345\272\224\347\224\250\350\257\246\346\203\205.png" "b/docs/design/uml/appcenter/API \350\216\267\345\217\226\345\272\224\347\224\250\350\257\246\346\203\205.png" new file mode 100644 index 0000000000000000000000000000000000000000..f6c9ab68c1d3fa4493ec1aea1ea6eaf05a655e1a Binary files /dev/null and "b/docs/design/uml/appcenter/API \350\216\267\345\217\226\345\272\224\347\224\250\350\257\246\346\203\205.png" differ diff --git "a/docs/design/uml/appcenter/API \350\216\267\345\217\226\346\234\200\350\277\221\344\275\277\347\224\250\347\232\204\345\272\224\347\224\250.png" "b/docs/design/uml/appcenter/API \350\216\267\345\217\226\346\234\200\350\277\221\344\275\277\347\224\250\347\232\204\345\272\224\347\224\250.png" new file mode 100644 index 0000000000000000000000000000000000000000..9bb87e59b66bedc4aa03cde4ccb83d65143f3a1f Binary files /dev/null and "b/docs/design/uml/appcenter/API \350\216\267\345\217\226\346\234\200\350\277\221\344\275\277\347\224\250\347\232\204\345\272\224\347\224\250.png" differ diff --git "a/docs/design/uml/appcenter/DB AppPool \346\225\260\346\215\256\346\250\241\345\236\213.png" "b/docs/design/uml/appcenter/DB AppPool \346\225\260\346\215\256\346\250\241\345\236\213.png" new file mode 100644 index 0000000000000000000000000000000000000000..02a4abcc583c4068bb03089bfc6113e153671115 Binary files /dev/null and "b/docs/design/uml/appcenter/DB AppPool \346\225\260\346\215\256\346\250\241\345\236\213.png" differ diff --git "a/docs/design/uml/appcenter/DB User \346\225\260\346\215\256\346\250\241\345\236\213.png" "b/docs/design/uml/appcenter/DB User \346\225\260\346\215\256\346\250\241\345\236\213.png" new file mode 100644 index 0000000000000000000000000000000000000000..ee3000ed1734cf56c98805ebe3f93c0cdb018a49 Binary files /dev/null and "b/docs/design/uml/appcenter/DB User \346\225\260\346\215\256\346\250\241\345\236\213.png" differ diff --git a/docs/design/uml/appcenter/api_create_app.puml b/docs/design/uml/appcenter/api_create_app.puml new file mode 100644 index 0000000000000000000000000000000000000000..1d973a2c80fff7115c5095a5de57735df89ebc6b --- /dev/null +++ b/docs/design/uml/appcenter/api_create_app.puml @@ -0,0 +1,13 @@ +@startuml API 创建应用 +actor User +participant "AppCenter Router" as Router +participant "AppCenter Manager" as Manager +database MongoDB + +User -> Router: POST /api/app +Router -> Manager: create_app() +Manager -> MongoDB: insert app data +MongoDB --> Manager: return app_id +Manager --> Router: return app_id +Router --> User: return success response +@enduml \ No newline at end of file diff --git a/docs/design/uml/appcenter/api_delete_app.puml b/docs/design/uml/appcenter/api_delete_app.puml new file mode 100644 index 0000000000000000000000000000000000000000..2f5ef442fbc2ddd884305d29acdc2d165bb7e20b --- /dev/null +++ b/docs/design/uml/appcenter/api_delete_app.puml @@ -0,0 +1,21 @@ +@startuml API 删除应用 +actor User +participant "AppCenter Router" as Router +participant "AppCenter Manager" as Manager +participant "FlowManager" as FlowManager +database MongoDB + +User -> Router: DELETE /api/app/{appId} +Router -> Manager: fetch_app_data_by_id() +MongoDB --> Manager: return app data +Router -> Router: verify author +Router -> FlowManager: delete_flow_by_app_and_flow_id() +FlowManager -> MongoDB: delete flows +Router -> Manager: delete_app() +Manager -> MongoDB: start transaction +Manager -> MongoDB: delete app +Manager -> MongoDB: update user app_usage +MongoDB -> MongoDB: commit transaction +Manager --> Router: return result +Router --> User: return response +@enduml \ No newline at end of file diff --git a/docs/design/uml/appcenter/api_fav_app.puml b/docs/design/uml/appcenter/api_fav_app.puml new file mode 100644 index 0000000000000000000000000000000000000000..f526c6821a0b42d3c69271f79a005a645b7a3be6 --- /dev/null +++ b/docs/design/uml/appcenter/api_fav_app.puml @@ -0,0 +1,15 @@ +@startuml API 收藏应用 +actor User +participant "AppCenter Router" as Router +participant "AppCenter Manager" as Manager +database MongoDB + +User -> Router: PUT /api/app/{appId} +Router -> Manager: modify_favorite_app() +Manager -> MongoDB: find app +MongoDB --> Manager: return app data +Manager -> Manager: verify favorite status +Manager -> MongoDB: update favorites array +Manager --> Router: return ModFavAppFlag +Router --> User: return response +@enduml \ No newline at end of file diff --git a/docs/design/uml/appcenter/api_fetch_app_data.puml b/docs/design/uml/appcenter/api_fetch_app_data.puml new file mode 100644 index 0000000000000000000000000000000000000000..5169aaeafff11f0b2b4eb48b59ed09106e86a7e8 --- /dev/null +++ b/docs/design/uml/appcenter/api_fetch_app_data.puml @@ -0,0 +1,14 @@ +@startuml API 获取应用详情 +actor User +participant "AppCenter Router" as Router +participant "AppCenter Manager" as Manager +database MongoDB + +User -> Router: GET /api/app/{appId} +Router -> Manager: fetch_app_data_by_id() +Manager -> MongoDB: find_one app +MongoDB --> Manager: return app data +Manager -> Manager: build app property +Manager --> Router: return app details +Router --> User: return response +@enduml \ No newline at end of file diff --git a/docs/design/uml/appcenter/api_fetch_recent_app.puml b/docs/design/uml/appcenter/api_fetch_recent_app.puml new file mode 100644 index 0000000000000000000000000000000000000000..da7a0628648f24ebf29b42fce895bd3e4120c227 --- /dev/null +++ b/docs/design/uml/appcenter/api_fetch_recent_app.puml @@ -0,0 +1,16 @@ +@startuml API 获取最近使用的应用 +actor User +participant "AppCenter Router" as Router +participant "AppCenter Manager" as Manager +database MongoDB + +User -> Router: GET /api/app/recent +Router -> Manager: get_recently_used_apps() +Manager -> MongoDB: find user data +MongoDB --> Manager: return user data +Manager -> MongoDB: find app data +MongoDB --> Manager: return app data +Manager -> Manager: sort by last_used time +Manager --> Router: return recent apps +Router --> User: return response +@enduml \ No newline at end of file diff --git a/docs/design/uml/appcenter/api_publish_app.puml b/docs/design/uml/appcenter/api_publish_app.puml new file mode 100644 index 0000000000000000000000000000000000000000..6f2dace22de4a624a9f678353d4878e460972635 --- /dev/null +++ b/docs/design/uml/appcenter/api_publish_app.puml @@ -0,0 +1,15 @@ +@startuml API 发布应用 +actor User +participant "AppCenter Router" as Router +participant "AppCenter Manager" as Manager +database MongoDB + +User -> Router: POST /api/app/{appId} +Router -> Manager: fetch_app_data_by_id() +MongoDB --> Manager: return app data +Router -> Router: verify author +Router -> Manager: publish_app() +Manager -> MongoDB: update published status +Manager --> Router: return result +Router --> User: return response +@enduml \ No newline at end of file diff --git a/docs/design/uml/appcenter/api_serach_app.puml b/docs/design/uml/appcenter/api_serach_app.puml new file mode 100644 index 0000000000000000000000000000000000000000..2e6f3e0f0676cbb2ce2bea5f5010e2fb78958e8d --- /dev/null +++ b/docs/design/uml/appcenter/api_serach_app.puml @@ -0,0 +1,15 @@ +@startuml API 搜索应用 +actor User +participant "AppCenter Router" as Router +participant "AppCenter Manager" as Manager +database MongoDB + +User -> Router: GET /api/app with filters +Router -> Manager: fetch_all_apps() +Manager -> Manager: build search filters +Manager -> MongoDB: find with filters +MongoDB --> Manager: return apps +Manager -> Manager: build response items +Manager --> Router: return app list +Router --> User: return apps response +@enduml \ No newline at end of file diff --git a/docs/design/uml/appcenter/db_app_pool.puml b/docs/design/uml/appcenter/db_app_pool.puml new file mode 100644 index 0000000000000000000000000000000000000000..9000bfbcc40cf8e936005a0f28ac1b6c2180cad8 --- /dev/null +++ b/docs/design/uml/appcenter/db_app_pool.puml @@ -0,0 +1,47 @@ +@startuml DB AppPool 数据模型 +class AppPool { + - id: str + - name: str + - description: str + - author: str + - icon: str + - links: List[str] + - first_questions: List[str] + - history_len: int + - permission: Permission + - flows: List[Flow] + - favorites: List[str] + - published: bool + - created_at: datetime +} + +class Permission { + - type: str + - users: List[str] +} + +class AppData { + - app_id: str + - name: str + - description: str + - icon: str + - links: List[str] + - first_questions: List[str] + - history_len: int + - permission: Permission + - workflows: List[str] +} + +class AppCenterCardItem { + - appId: str + - icon: str + - name: str + - description: str + - author: str + - favorited: bool + - published: bool +} + +AppPool --> Permission +AppData --> Permission +@enduml \ No newline at end of file diff --git a/docs/design/uml/appcenter/db_user.puml b/docs/design/uml/appcenter/db_user.puml new file mode 100644 index 0000000000000000000000000000000000000000..f2e5b15881f66175846a2f3fb40ecd2090319e9e --- /dev/null +++ b/docs/design/uml/appcenter/db_user.puml @@ -0,0 +1,27 @@ +@startuml DB User 数据模型 +class User { + _id: str + last_login: float + is_active: bool + is_whitelisted: bool + credit: int + api_key: Optional[str] + kb_id: Optional[str] + conversations: List[str] + domains: List[UserDomainData] + app_usage: Dict[str, AppUsageData] +} + +class AppUsageData { + count: int + last_used: float +} + +class UserDomainData { + name: str + count: int +} + +User --> AppUsageData +User --> UserDomainData +@enduml \ No newline at end of file