"""读取前端组件目录:启动时载入进程内存缓存,未载入时回退读取本地 JSON 文件""" from pathlib import Path from app.schemas.component_resource import ( ComponentCatalog, ComponentQuery, ComponentResult, ) DEFAULT_CATALOG_PATH = Path(__file__).resolve().parent / "catalog" / "components.json" # 进程内共享的组件目录,由启动流程载入;agent 与前端接口读取同一份 _cached_catalog: ComponentCatalog | None = None def set_catalog_cache(catalog: ComponentCatalog | None) -> None: global _cached_catalog _cached_catalog = catalog def get_catalog_cache() -> ComponentCatalog | None: return _cached_catalog def load_catalog_file(path: str | Path | None = None) -> ComponentCatalog | None: # 如果传入的是相对路径,以项目根目录为基准,避免受启动时工作目录影响 target = Path(path) if path is not None else DEFAULT_CATALOG_PATH if not target.is_absolute(): target = Path(__file__).resolve().parents[2] / target try: # utf-8-sig 同时兼容带 BOM 和不带 BOM 的 UTF-8 文件 content = target.read_text(encoding="utf-8-sig") except FileNotFoundError: return None # 文件损坏或定义不合法直接报错,不能伪装成查无结果 return ComponentCatalog.model_validate_json(content) class ComponentProvider: def __init__(self, catalog_path: str | Path | None = None): # 指定路径时始终读取该文件;未指定时优先使用启动缓存,缓存为空再读默认文件 self.catalog_path = catalog_path def query(self, query: ComponentQuery) -> ComponentResult: catalog = ( _cached_catalog if self.catalog_path is None and _cached_catalog is not None else load_catalog_file(self.catalog_path) ) if catalog is None: # 如果目录文件不存在,返回未支持状态和原因,由调用方决定失败处理方式 return ComponentResult( status="unsupported", message="尚未配置前端组件目录,请先导出真实组件定义", ) # 如果提供关键词,去除首尾空白并统一大小写,未提供时不启用关键词筛选 keyword = ( query.keyword.strip().casefold() if query.keyword is not None else None ) # 收集同时满足所有已提供条件的组件,未设置任何条件时返回全量目录 matches = [] for component in catalog.components: # 如果指定目录编号,则跳过编号不完全匹配的组件 if query.key is not None and component.key != query.key: continue # 如果指定前端组件类型,则跳过类型不完全匹配的组件 if query.type is not None and component.type != query.type: continue # 将编号、类型、名称、说明和标签拼成检索文本,并与关键词统一大小写 searchable = " ".join( [ component.key, component.type, component.name, component.description, *component.tags, ] ).casefold() # 如果设置了关键词,但检索文本中不包含该关键词,则跳过当前组件 if keyword is not None and keyword not in searchable: continue # 深拷贝后返回,调用方修改结果不会污染共享缓存 matches.append(component.model_copy(deep=True)) # 根据是否有匹配项区分查询成功和未找到,同时保留目录版本、来源及结果说明 return ComponentResult( status="resolved" if matches else "not_found", components=matches, catalog_version=catalog.version, message="已找到组件定义" if matches else "已查询组件目录,未找到匹配的组件", ) if __name__ == "__main__": provider = ComponentProvider() result = provider.query(ComponentQuery()) print(result.model_dump_json(indent=2))