feat: enhance metadata schema API with scope filter and delete endpoint [AC-IDSMETA-13]

- Add scope filter and include_deprecated parameter to list endpoint
- Add delete metadata schema endpoint
- Fix JSONB contains query for PostgreSQL
- Add metadata config entry to dashboard help section
This commit is contained in:
MerCry 2026-03-03 00:13:57 +08:00
parent 6b6b7fb5e7
commit ee220b0b10
4 changed files with 124 additions and 8 deletions

View File

@ -401,6 +401,15 @@
<p>配置敏感词过滤和内容审核规则保障输出安全</p> <p>配置敏感词过滤和内容审核规则保障输出安全</p>
</div> </div>
</div> </div>
<div class="help-item" @click="navigateTo('/admin/metadata-schemas')">
<div class="help-icon primary">
<el-icon><Setting /></el-icon>
</div>
<div class="help-text">
<h4>元数据配置</h4>
<p>配置知识库意图规则等的动态元数据字段定义</p>
</div>
</div>
</div> </div>
</el-card> </el-card>
</el-col> </el-col>
@ -411,7 +420,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue' import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { FolderOpened, Document, ChatDotSquare, Monitor, Cpu, InfoFilled, Connection, Timer, DataLine, Aim, DocumentCopy, Share, Warning } from '@element-plus/icons-vue' import { FolderOpened, Document, ChatDotSquare, Monitor, Cpu, InfoFilled, Connection, Timer, DataLine, Aim, DocumentCopy, Share, Warning, Setting } from '@element-plus/icons-vue'
import { getDashboardStats, type DashboardStats } from '@/api/dashboard' import { getDashboardStats, type DashboardStats } from '@/api/dashboard'
const router = useRouter() const router = useRouter()

View File

@ -38,7 +38,7 @@ def get_current_tenant_id() -> str:
"", "",
operation_id="listMetadataSchemas", operation_id="listMetadataSchemas",
summary="List metadata schemas", summary="List metadata schemas",
description="[AC-IDSMETA-13] 获取元数据字段定义列表,支持按状态过滤", description="[AC-IDSMETA-13] 获取元数据字段定义列表,支持按状态和范围过滤",
) )
async def list_schemas( async def list_schemas(
tenant_id: Annotated[str, Depends(get_current_tenant_id)], tenant_id: Annotated[str, Depends(get_current_tenant_id)],
@ -46,13 +46,24 @@ async def list_schemas(
status: Annotated[str | None, Query( status: Annotated[str | None, Query(
description="按状态过滤: draft/active/deprecated" description="按状态过滤: draft/active/deprecated"
)] = None, )] = None,
scope: Annotated[str | None, Query(
description="按适用范围过滤: kb_document/intent_rule/script_flow/prompt_template"
)] = None,
include_deprecated: Annotated[bool, Query(
description="是否包含已废弃的字段"
)] = False,
) -> JSONResponse: ) -> JSONResponse:
""" """
[AC-IDSMETA-13] 列出元数据字段定义 [AC-IDSMETA-13] 列出元数据字段定义
Args:
status: 按状态过滤
scope: 按适用范围过滤
include_deprecated: 是否包含已废弃的字段 status 未指定时生效
""" """
logger.info( logger.info(
f"[AC-IDSMETA-13] Listing metadata field definitions: " f"[AC-IDSMETA-13] Listing metadata field definitions: "
f"tenant={tenant_id}, status={status}" f"tenant={tenant_id}, status={status}, scope={scope}, include_deprecated={include_deprecated}"
) )
if status and status not in [s.value for s in MetadataFieldStatus]: if status and status not in [s.value for s in MetadataFieldStatus]:
@ -68,7 +79,11 @@ async def list_schemas(
) )
service = MetadataFieldDefinitionService(session) service = MetadataFieldDefinitionService(session)
fields = await service.list_field_definitions(tenant_id, status)
if include_deprecated and not status:
fields = await service.get_field_definitions_for_read(tenant_id, scope)
else:
fields = await service.list_field_definitions(tenant_id, status, scope)
return JSONResponse( return JSONResponse(
content={ content={
@ -358,3 +373,42 @@ async def validate_metadata_for_create(
"errors": errors, "errors": errors,
} }
) )
@router.delete(
"/{field_id}",
operation_id="deleteMetadataSchema",
summary="Delete metadata schema",
description="[AC-IDSMETA-13] 删除元数据字段定义",
)
async def delete_schema(
field_id: str,
tenant_id: Annotated[str, Depends(get_current_tenant_id)],
session: Annotated[AsyncSession, Depends(get_session)],
) -> JSONResponse:
"""
[AC-IDSMETA-13] 删除元数据字段定义
"""
logger.info(
f"[AC-IDSMETA-13] Deleting metadata field definition: "
f"tenant={tenant_id}, field_id={field_id}"
)
service = MetadataFieldDefinitionService(session)
success = await service.delete_field_definition(tenant_id, field_id)
if not success:
return JSONResponse(
status_code=404,
content={
"code": "NOT_FOUND",
"message": f"Field definition not found: {field_id}",
}
)
return JSONResponse(
content={
"success": True,
"message": "Field definition deleted successfully",
}
)

View File

@ -87,8 +87,14 @@ class KBService:
""" """
[AC-ASA-01] Upload document and create indexing job. [AC-ASA-01] Upload document and create indexing job.
""" """
import urllib.parse
doc_id = uuid.uuid4() doc_id = uuid.uuid4()
file_path = os.path.join(self._upload_dir, f"{tenant_id}_{doc_id}_{file_name}")
# 安全处理文件名:使用 UUID 作为存储文件名,保留原始文件名在数据库中
file_ext = os.path.splitext(file_name)[1] if file_name else ""
safe_file_name = f"{tenant_id}_{doc_id}{file_ext}"
file_path = os.path.join(self._upload_dir, safe_file_name)
with open(file_path, "wb") as f: with open(file_path, "wb") as f:
f.write(file_content) f.write(file_content)

View File

@ -9,7 +9,8 @@ import uuid
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
from sqlalchemy import select from sqlalchemy import select, func, cast, text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import col from sqlmodel import col
@ -61,7 +62,12 @@ class MetadataFieldDefinitionService:
stmt = stmt.where(MetadataFieldDefinition.status == status) stmt = stmt.where(MetadataFieldDefinition.status == status)
if scope: if scope:
stmt = stmt.where(MetadataFieldDefinition.scope.contains([scope])) stmt = stmt.where(
func.jsonb_contains(
cast(MetadataFieldDefinition.scope, JSONB),
func.cast(f'["{scope}"]', JSONB)
)
)
stmt = stmt.order_by(col(MetadataFieldDefinition.created_at).desc()) stmt = stmt.order_by(col(MetadataFieldDefinition.created_at).desc())
@ -272,7 +278,12 @@ class MetadataFieldDefinitionService:
) )
if scope: if scope:
stmt = stmt.where(MetadataFieldDefinition.scope.contains([scope])) stmt = stmt.where(
func.jsonb_contains(
cast(MetadataFieldDefinition.scope, JSONB),
func.cast(f'["{scope}"]', JSONB)
)
)
stmt = stmt.order_by(col(MetadataFieldDefinition.created_at).desc()) stmt = stmt.order_by(col(MetadataFieldDefinition.created_at).desc())
@ -470,3 +481,39 @@ class MetadataFieldDefinitionService:
} }
for f in fields for f in fields
} }
async def delete_field_definition(
self,
tenant_id: str,
field_id: str,
) -> bool:
"""
删除元数据字段定义
Args:
tenant_id: 租户 ID
field_id: 字段定义 ID
Returns:
是否删除成功
"""
try:
import uuid
field_uuid = uuid.UUID(field_id)
except ValueError:
return False
stmt = select(MetadataFieldDefinition).where(
MetadataFieldDefinition.tenant_id == tenant_id,
MetadataFieldDefinition.id == field_uuid,
)
result = await self._session.execute(stmt)
field = result.scalar_one_or_none()
if not field:
return False
await self._session.delete(field)
await self._session.commit()
return True