113 lines
4.6 KiB
Python
113 lines
4.6 KiB
Python
"""查询前端组件目录:目录只来自启动时从 MongoDB 载入的进程内存缓存,没有文件回退"""
|
|
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from app.schemas.component_resource import (
|
|
ComponentCatalog,
|
|
ComponentQuery,
|
|
ComponentResult,
|
|
)
|
|
|
|
# 进程内共享的组件目录,由启动流程载入;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
|
|
|
|
|
|
# 手写的物料定义目录,只供导入脚本读取后写入 MongoDB,运行时不读取
|
|
MATERIALS_DIR = Path(__file__).resolve().parent / "materials"
|
|
FIELD_GROUPS_FILE = "field_groups.json"
|
|
|
|
|
|
def load_material_definitions(directory: str | Path = MATERIALS_DIR) -> ComponentCatalog:
|
|
"""读取物料定义目录:共享字段分组一个文件,每个物料一个文件;不合法直接报错
|
|
|
|
version 为全部文件内容的摘要,定义有任何改动都会得到新版本
|
|
"""
|
|
target = Path(directory)
|
|
if not target.is_absolute():
|
|
target = Path(__file__).resolve().parents[2] / target
|
|
files = sorted(target.glob("*.json"))
|
|
if not any(path.name == FIELD_GROUPS_FILE for path in files):
|
|
raise FileNotFoundError(f"物料定义目录缺少 {FIELD_GROUPS_FILE}: {target}")
|
|
digest = hashlib.sha256()
|
|
components, field_groups = [], []
|
|
for path in files:
|
|
# utf-8-sig 同时兼容带 BOM 和不带 BOM 的 UTF-8 文件
|
|
text = path.read_text(encoding="utf-8-sig")
|
|
digest.update(path.name.encode() + b"\0" + text.encode())
|
|
if path.name == FIELD_GROUPS_FILE:
|
|
field_groups = json.loads(text)
|
|
else:
|
|
components.append(json.loads(text))
|
|
# 文件名排序与展示顺序无关,物料按定义中的 order 排列,未写时排在最后
|
|
components.sort(key=lambda item: item.get("order", len(files)))
|
|
return ComponentCatalog.model_validate(
|
|
{
|
|
"version": f"sha256:{digest.hexdigest()}",
|
|
"components": [
|
|
{key: value for key, value in item.items() if key != "order"}
|
|
for item in components
|
|
],
|
|
"field_groups": field_groups,
|
|
}
|
|
)
|
|
|
|
|
|
class ComponentProvider:
|
|
def query(self, query: ComponentQuery) -> ComponentResult:
|
|
catalog = _cached_catalog
|
|
if catalog is None:
|
|
# 启动时未载入目录,返回未支持状态和原因,由调用方决定失败处理方式
|
|
return ComponentResult(
|
|
status="unsupported",
|
|
message="物料目录未载入,请确认已配置 MongoDB 并导入物料",
|
|
)
|
|
|
|
# 如果提供关键词,去除首尾空白并统一大小写,未提供时不启用关键词筛选
|
|
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 "已查询组件目录,未找到匹配的组件",
|
|
)
|