feat: persist photos and enrich conversation history

This commit is contained in:
Xin Wang
2026-08-05 16:14:03 +08:00
parent 545536bdf3
commit 317a0600bc
19 changed files with 1146 additions and 119 deletions
+71 -2
View File
@@ -1,15 +1,20 @@
"""对话历史查询 API。"""
from db.models import ConversationMessage, ConversationSession
import asyncio
from db.models import ConversationArtifact, ConversationMessage, ConversationSession
from db.session import get_session
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi import APIRouter, Depends, HTTPException, Query, Response
from loguru import logger
from schemas import (
ConversationArtifactOut,
ConversationDetailOut,
ConversationListOut,
ConversationMessageOut,
ConversationOut,
)
from services.auth import require_admin
from services.object_storage import delete_object, get_object, key_from_storage_uri
from sqlalchemy import func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -100,6 +105,17 @@ async def get_conversation(
.order_by(ConversationMessage.sequence)
)
).scalars().all()
artifacts = (
await session.execute(
select(ConversationArtifact)
.where(ConversationArtifact.session_id == conversation_id)
.order_by(ConversationArtifact.created_at)
)
).scalars().all()
artifacts_by_message: dict[str, list[ConversationArtifact]] = {}
for artifact in artifacts:
if artifact.message_id:
artifacts_by_message.setdefault(artifact.message_id, []).append(artifact)
return ConversationDetailOut(
**_session_out(conversation).model_dump(),
extra=conversation.extra or {},
@@ -112,12 +128,52 @@ async def get_conversation(
content=message.content,
occurred_at=message.occurred_at,
extra=message.extra or {},
artifacts=[
ConversationArtifactOut(
id=artifact.id,
kind=artifact.kind,
content_url=(
f"/api/conversations/{conversation_id}/artifacts/"
f"{artifact.id}/content"
),
mime_type=artifact.mime_type,
size_bytes=artifact.size_bytes,
duration_ms=artifact.duration_ms,
extra=artifact.extra or {},
)
for artifact in artifacts_by_message.get(message.id, [])
],
)
for message in messages
],
)
@router.get("/{conversation_id}/artifacts/{artifact_id}/content")
async def get_conversation_artifact(
conversation_id: str,
artifact_id: str,
session: AsyncSession = Depends(get_session),
):
artifact = await session.get(ConversationArtifact, artifact_id)
if not artifact or artifact.session_id != conversation_id:
raise HTTPException(404, "会话附件不存在")
try:
key = key_from_storage_uri(artifact.storage_uri)
data = await asyncio.to_thread(get_object, key)
except Exception as exc:
logger.warning(f"读取会话附件失败: {artifact_id}: {exc}")
raise HTTPException(404, "会话附件不可用") from exc
return Response(
content=data,
media_type=artifact.mime_type or "application/octet-stream",
headers={
"Cache-Control": "private, max-age=300",
"Content-Disposition": f'inline; filename="{artifact.id}"',
},
)
@router.delete("/{conversation_id}")
async def delete_conversation(
conversation_id: str,
@@ -126,6 +182,19 @@ async def delete_conversation(
conversation = await session.get(ConversationSession, conversation_id)
if not conversation:
raise HTTPException(404, "对话记录不存在")
storage_uris = (
await session.execute(
select(ConversationArtifact.storage_uri).where(
ConversationArtifact.session_id == conversation_id
)
)
).scalars().all()
await session.delete(conversation)
await session.commit()
for storage_uri in storage_uris:
try:
key = key_from_storage_uri(storage_uri)
await asyncio.to_thread(delete_object, key)
except Exception as exc:
logger.warning(f"清理已删除会话的附件失败: {storage_uri}: {exc}")
return {"ok": True}