Files
aerospaceboy ebe33392ca fix: improve import, evidence indexing and material-scoped grading
Preserve document revisions and evidence, harden indexing and export behavior, add explicit material selection and direct-folder import, validate model JSON schemas with safe error messages, and retain student selection while loading scoring data.
2026-10-14 13:52:56 +08:00

86 lines
3.6 KiB
Python

"""Document preparation shared by scoring and indexing."""
from contextlib import contextmanager
import errno
import os
import time
from sqlalchemy import select
from .config import get_settings
from .models import KnowledgeDocument, Project, SubmissionFile
from .parsing import parse_document, text_quality
from .rag import INDEX_VERSION, ingest
from .storage import secure_path, sha256
@contextmanager
def file_index_lock(file_id: int):
root = get_settings().processed_path
root.mkdir(parents=True, exist_ok=True)
with (root / f"file-{file_id}.lock").open("a+b") as handle:
if os.name == "nt":
import msvcrt
if not handle.tell():
handle.write(b"0")
handle.flush()
while True:
handle.seek(0)
try:
msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1)
break
except OSError as exc:
if exc.errno not in (errno.EACCES, errno.EAGAIN, errno.EDEADLK):
raise
time.sleep(.05)
try:
yield
finally:
handle.seek(0)
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
else:
import fcntl
fcntl.flock(handle, fcntl.LOCK_EX)
try:
yield
finally:
fcntl.flock(handle, fcntl.LOCK_UN)
def parse_submission(db, file: SubmissionFile, *, embedder=None):
with file_index_lock(file.id):
db.refresh(file)
if file.recognition_status != "recognized":
raise ValueError("材料已取消选择,请重新预览")
path = secure_path(file.relative_path)
if sha256(path) != file.sha256:
file.parse_status, file.parse_error = "changed", "源文件已变化,请重新扫描并确认导入"
db.commit()
raise ValueError(file.parse_error)
try:
# An indexing failure does not invalidate already saved parsing.
parsed = file.parsed if file.parsed and file.parsed.get("pages") else parse_document(
path, file.category, get_settings().processed_path / f"file-{file.id}" / file.sha256[:16])
parsed = {**parsed, "pages": [{**page, "text_quality": text_quality(page.get("text", ""))}
for page in parsed["pages"]]}
file.parsed = parsed
doc = db.scalar(select(KnowledgeDocument).where(KnowledgeDocument.submission_file_id == file.id,
KnowledgeDocument.source_hash == file.sha256, KnowledgeDocument.status != "superseded"))
if not doc or doc.source_hash != file.sha256 or doc.index_version != INDEX_VERSION or doc.status != "ready":
project = db.get(Project, file.project_id)
kwargs = {"embedder": embedder} if embedder else {}
ingest(db, course_version_id=project.course_version_id, project_id=project.id,
submission_file_id=file.id, title=path.name, category=file.category,
source_hash=file.sha256, pages=parsed["pages"], **kwargs)
file.parse_status, file.parse_error = "ready", ""
db.commit()
return parsed
except Exception as exc:
db.rollback()
file = db.get(SubmissionFile, file.id)
# Preserve parse output on model/index errors for a later retry.
if "parsed" in locals():
file.parsed = parsed
file.parse_status, file.parse_error = "failed", str(exc)[:1000]
db.commit()
raise