Files
2026-07-27 19:01:19 +08:00

197 lines
6.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""知识文档的安全上传、解析、分块和索引状态编排。"""
from django.db import transaction
from django.utils import timezone
from apps.audit.models import OperationAuditLog
from apps.knowledge.models import KnowledgeDocument
from apps.knowledge.services import rebuild_document_chunks
from apps.tickets.materials import (
MaterialParseError,
MaterialScanError,
MaterialValidationError,
PreparedMaterial,
parse_material,
prepare_material,
scan_bytes,
)
from apps.tickets.storage import delete_object, get_object, put_object
def prepare_document(raw: bytes, file_name: str, content_type: str) -> PreparedMaterial:
prepared = prepare_material(raw, file_name, content_type)
if prepared.material_type == "image":
raise MaterialValidationError(415, "知识文档暂不支持图片,请上传可提取文本的文档")
return prepared
def create_uploaded_document(
*,
knowledge_base_id: int,
prepared: PreparedMaterial,
operator=None,
) -> KnowledgeDocument:
"""先保存原件再创建文档记录,提交后异步执行扫描、解析和索引。"""
document = KnowledgeDocument(
knowledge_base_id=knowledge_base_id,
file_name=prepared.file_name,
file_type=prepared.material_type,
extension=prepared.extension,
mime_type=prepared.mime_type,
file_size=len(prepared.data),
sha256=prepared.sha256,
scan_status="pending",
parse_status="pending",
created_by=operator,
)
document.storage_path = (
f"knowledge-documents/{knowledge_base_id}/{document.public_id}."
f"{prepared.extension}"
)
put_object(document.storage_path, prepared.data, prepared.mime_type)
try:
with transaction.atomic():
document.save()
OperationAuditLog.objects.create(
user=operator,
module="knowledge",
action="document_uploaded",
target_type="knowledge_document",
target_id=str(document.public_id),
request_snapshot={
"knowledge_base_id": knowledge_base_id,
"file_name": document.file_name,
"file_size": document.file_size,
"sha256": document.sha256,
},
result="success",
)
except Exception:
try:
delete_object(document.storage_path)
except Exception:
pass
raise
return document
def _mark_failed(
document: KnowledgeDocument,
*,
scan_status: str | None = None,
scan_result: dict | None = None,
message: str,
) -> None:
if scan_status is not None:
document.scan_status = scan_status
if scan_result is not None:
document.scan_result = scan_result
document.parse_status = "failed"
document.parse_error = message
document.indexed_at = None
document.save()
OperationAuditLog.objects.create(
user=document.created_by,
module="knowledge",
action="document_processing_failed",
target_type="knowledge_document",
target_id=str(document.public_id),
request_snapshot={
"scan_status": document.scan_status,
"error_type": message.split(":", 1)[0],
},
result="failed",
)
def process_knowledge_document(document_id: int) -> KnowledgeDocument:
"""处理单个知识文档;失败保留原件和明确状态供人工重试。"""
with transaction.atomic():
document = (
KnowledgeDocument.objects.select_for_update()
.select_related("knowledge_base", "created_by")
.get(id=document_id)
)
if document.parse_status != "pending" or document.scan_status != "pending":
return document
document.processing_attempts += 1
document.scan_status = "scanning"
document.scan_result = {}
document.parse_error = ""
document.indexed_at = None
document.save()
try:
raw = get_object(document.storage_path)
except Exception:
_mark_failed(document, message="原始文件读取失败:对象存储暂时不可用")
return document
try:
scan_result = scan_bytes(raw)
except MaterialScanError as error:
_mark_failed(
document,
scan_status="failed",
scan_result={"status": "failed", "message": str(error)},
message=f"恶意文件扫描失败:{error}",
)
return document
except Exception:
_mark_failed(
document,
scan_status="failed",
scan_result={"status": "failed"},
message="恶意文件扫描失败:扫描器发生未预期错误",
)
return document
document.scan_status = scan_result["status"]
document.scan_result = scan_result
if document.scan_status != "clean":
_mark_failed(
document,
scan_status=document.scan_status,
scan_result=scan_result,
message="恶意文件扫描未通过:原件已隔离保留",
)
return document
document.parse_status = "parsing"
document.save(update_fields=["scan_status", "scan_result", "parse_status", "updated_at"])
try:
content = parse_material(raw, document.extension)
if not content.strip():
raise MaterialParseError("文档中未提取到可索引文本")
except (MaterialParseError, MaterialValidationError) as error:
_mark_failed(document, message=f"文档解析失败:{error}")
return document
document.content_text = content
document.parse_status = "indexing"
document.parse_error = ""
document.save(update_fields=["content_text", "parse_status", "parse_error", "updated_at"])
try:
rebuild_document_chunks(document)
except Exception as error:
_mark_failed(
document,
message=f"文档索引失败:{type(error).__name__}",
)
return document
document.parse_status = "success"
document.parse_error = ""
document.indexed_at = timezone.now()
document.save(update_fields=["parse_status", "parse_error", "indexed_at", "updated_at"])
OperationAuditLog.objects.create(
user=document.created_by,
module="knowledge",
action="document_indexed",
target_type="knowledge_document",
target_id=str(document.public_id),
request_snapshot={
"knowledge_base_id": document.knowledge_base_id,
"chunk_count": document.chunks.count(),
"processing_attempts": document.processing_attempts,
},
result="success",
)
return document