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:
parent
6b6b7fb5e7
commit
ee220b0b10
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue