feat(multimodal): 完成阶段F运行管理与收尾验收

This commit is contained in:
2026-09-05 02:02:45 +08:00
parent 510936431a
commit 6ee6cd7d73
33 changed files with 1180 additions and 86 deletions
+38 -4
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio
import json
import hashlib
from contextlib import closing
from pathlib import Path
from uuid import uuid4
@@ -22,14 +23,17 @@ MEDIA_SUFFIXES = {".wav", ".mp3", ".flac", ".ogg", ".m4a", ".mp4", ".webm", ".tx
@router.post("/attachments", status_code=201)
async def upload_attachment(request: Request, filename: str = Query(min_length=1, max_length=255)):
async def upload_attachment(request: Request, filename: str = Query(min_length=1, max_length=255),
idempotency_key: str | None = Header(None, min_length=16, max_length=100, pattern=r"^[a-zA-Z0-9_-]+$")):
suffix = Path(filename).suffix.lower()
if suffix not in MEDIA_SUFFIXES:
raise ApiError(422, "UNSUPPORTED_MEDIA", "Unsupported attachment extension.")
attachment_id = f"media_{uuid4().hex}{suffix}"
identity = hashlib.sha256(idempotency_key.encode()).hexdigest() if idempotency_key else uuid4().hex
attachment_id = f"media_{identity}{suffix}"
destination = attachment_path(attachment_id)
destination.parent.mkdir(parents=True, exist_ok=True)
temporary = destination.with_suffix(destination.suffix + ".upload")
temporary = destination.with_suffix(destination.suffix + f".{uuid4().hex}.upload")
digest = hashlib.sha256()
size = 0
try:
with temporary.open("xb") as stream:
@@ -37,10 +41,40 @@ async def upload_attachment(request: Request, filename: str = Query(min_length=1
size += len(chunk)
if size > MAX_UPLOAD_BYTES:
raise ApiError(413, "ATTACHMENT_TOO_LARGE", "Attachment exceeds 25 MiB.")
digest.update(chunk)
stream.write(chunk)
if not size:
raise ApiError(422, "EMPTY_ATTACHMENT", "Attachment is empty.")
temporary.replace(destination)
content_hash = digest.hexdigest()
if idempotency_key:
with closing(connect()) as conn:
conn.execute("CREATE TABLE IF NOT EXISTS media_upload_idempotency (idempotency_key TEXT PRIMARY KEY, attachment_id TEXT NOT NULL, filename TEXT NOT NULL, content_hash TEXT NOT NULL)")
conn.execute("BEGIN IMMEDIATE")
try:
row = conn.execute("SELECT attachment_id,filename,content_hash FROM media_upload_idempotency WHERE idempotency_key=?", (idempotency_key,)).fetchone()
if row:
if row["filename"] != Path(filename).name or row["content_hash"] != content_hash:
raise ApiError(409, "IDEMPOTENCY_CONFLICT", "同一上传标识不能用于不同附件。")
existing = attachment_path(row["attachment_id"])
if not existing.is_file() or hashlib.sha256(existing.read_bytes()).hexdigest() != content_hash:
raise ApiError(409, "IDEMPOTENCY_EXPIRED", "该上传标识对应的附件已不存在,请开始一次新提交。")
attachment_id = row["attachment_id"]
else:
if destination.exists() and hashlib.sha256(destination.read_bytes()).hexdigest() != content_hash:
raise ApiError(409, "IDEMPOTENCY_CONFLICT", "同一上传标识不能用于不同附件。")
if not destination.exists():
temporary.replace(destination)
conn.execute("INSERT INTO media_upload_idempotency VALUES (?,?,?,?)",
(idempotency_key, attachment_id, Path(filename).name, content_hash))
conn.execute("COMMIT")
except BaseException:
conn.execute("ROLLBACK")
raise
elif destination.exists():
if hashlib.sha256(destination.read_bytes()).digest() != digest.digest():
raise ApiError(409, "IDEMPOTENCY_CONFLICT", "同一上传标识不能用于不同附件。")
else:
temporary.replace(destination)
finally:
temporary.unlink(missing_ok=True)
return {"attachment_id": attachment_id, "filename": Path(filename).name, "size": size}