""" 会话管理路由 - 对话 CRUD 和文件上传 与平台无关的通用功能,不涉及任何 LLM 平台细节。 """ import json import os from datetime import datetime from pathlib import Path from typing import Dict from fastapi import File, HTTPException, UploadFile from fastapi.responses import FileResponse import sys sys.path.append(str(Path(__file__).parent.parent)) from database import get_db from utils.helpers import generate_unique_id from core import log_error, log_exception, log_info # 配置上传目录 upload_dir = Path(__file__).parent.parent / "uploads" upload_dir.mkdir(exist_ok=True) # ── 会话管理 ───────────────────────────────────────────────────── async def get_conversations_handler(user_id: str = "default"): """获取所有对话处理器""" db = get_db() return db.list_conversations(user_id) async def get_conversation_handler(conversation_id: str): """获取特定对话处理器""" db = get_db() conversation = db.get_conversation(conversation_id) if not conversation: raise HTTPException(status_code=404, detail="对话不存在") return conversation async def save_conversation_handler(data: dict): """保存或更新对话处理器""" try: db = get_db() conversation_id = data.get("id") # 检查是否已存在 existing = db.get_conversation(conversation_id) if conversation_id else None if existing: # 更新现有会话 return db.update_conversation(conversation_id, data) else: # 创建新会话 return db.create_conversation(data) except Exception as e: log_error(f"Error saving conversation: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) async def delete_conversation_handler(conversation_id: str): """删除对话处理器(同时删除关联的 OSS 文件)""" db = get_db() # 先获取会话数据,提取 OSS 文件 URL conversation = db.get_conversation(conversation_id) if not conversation: raise HTTPException(status_code=404, detail="对话不存在") # 提取所有 OSS 文件 URL oss_urls = _extract_oss_urls_from_conversation(conversation) # 删除 OSS 文件 if oss_urls: try: from utils.oss_uploader import delete_files, extract_object_key_from_url object_keys = [] for url in oss_urls: key = extract_object_key_from_url(url) if key: object_keys.append(key) if object_keys: result = delete_files(object_keys) log_info(f"[删除会话] OSS 文件清理结果: 删除 {len(result['deleted'])} 个, 失败 {len(result['failed'])} 个") if result['failed']: log_error(f"[删除会话] OSS 文件删除失败: {result['failed']}") except Exception as e: log_error(f"[删除会话] OSS 文件删除异常: {e}") # 继续删除会话,即使 OSS 删除失败 # 删除数据库记录 success = db.delete_conversation(conversation_id) if success: return {"success": True, "message": "删除成功"} else: raise HTTPException(status_code=404, detail="对话不存在") def _extract_oss_urls_from_conversation(conversation: dict) -> list: """ 从会话消息中提取所有 OSS 文件 URL 消息结构: - content.images: 图片附件列表 - content.files: 文件附件列表 每个附件包含 url 字段 """ urls = [] messages = conversation.get("messages", []) for message in messages: content = message.get("content") if not content: continue # content 可能是字符串(需要解析)或已解析的字典 if isinstance(content, str): try: content = json.loads(content) except json.JSONDecodeError: continue # 提取图片附件 images = content.get("images", []) for img in images: url = img.get("url") if url and url not in urls: urls.append(url) # 提取文件附件 files = content.get("files", []) for f in files: url = f.get("url") if url and url not in urls: urls.append(url) return urls async def update_conversation_handler(conversation_id: str, data: dict): """部分更新对话处理器""" db = get_db() result = db.update_conversation(conversation_id, data) if result: return result else: raise HTTPException(status_code=404, detail="对话不存在") # ── 消息管理 ───────────────────────────────────────────────────── async def add_message_handler(conversation_id: str, message: dict): """添加消息到对话处理器""" db = get_db() # 检查对话是否存在 existing = db.get_conversation(conversation_id) if not existing: raise HTTPException(status_code=404, detail="对话不存在") return db.add_message(conversation_id, message) async def update_message_handler(conversation_id: str, message_id: str, data: dict): """更新消息处理器""" db = get_db() # 检查对话是否存在 existing = db.get_conversation(conversation_id) if not existing: raise HTTPException(status_code=404, detail="对话不存在") result = db.update_message(message_id, data) if result: return result else: raise HTTPException(status_code=404, detail="消息不存在") # ── 文件上传 ───────────────────────────────────────────────────── async def upload_file_handler(file: UploadFile = File(...)): """文件上传处理器""" try: # 允许的 MIME 类型(宽松策略) allowed_types = { # 图片 "image/jpeg", "image/png", "image/gif", "image/webp", "image/bmp", "image/svg+xml", # 文本类 "text/plain", "text/csv", "text/markdown", "text/html", "text/xml", "application/json", "application/xml", # PDF "application/pdf", # Office 文档 "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation", } # 允许的扩展名(兜底:MIME 类型可能被浏览器误判) allowed_extensions = { ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".txt", ".md", ".csv", ".json", ".xml", ".yaml", ".yml", ".log", ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".py", ".js", ".ts", ".html", ".css", } file_extension = Path(file.filename).suffix.lower() if ( file.content_type not in allowed_types and file_extension not in allowed_extensions ): raise HTTPException( status_code=400, detail=f"不支持的文件类型: {file.content_type}({file_extension})", ) # 生成唯一文件名 unique_filename = f"{int(datetime.utcnow().timestamp())}_{generate_unique_id()}{file_extension}" file_path = upload_dir / unique_filename # 保存文件到本地(临时缓存) content = await file.read() with open(file_path, "wb") as f: f.write(content) # 文件关闭后再上传到 OSS from utils.oss_uploader import upload_file as oss_upload oss_result = oss_upload(str(file_path)) file_url = oss_result["url"] # 返回文件信息 result = { "url": file_url, "name": file.filename, "size": len(content), "mimeType": file.content_type, } log_info(f"File uploaded: {result}") return result except Exception as e: log_error(f"Upload error: {str(e)}") raise HTTPException(status_code=500, detail=f"上传失败: {str(e)}") def serve_upload_handler(filename: str): """提供上传文件访问处理器""" file_path = upload_dir / filename if not file_path.exists(): raise HTTPException(status_code=404, detail="文件不存在") return FileResponse(str(file_path)) # ── 停止生成 ───────────────────────────────────────────────────── async def stop_generation_handler(message_id: str = None): """停止生成处理器""" message = ( f"已发出停止指令,消息ID: {message_id}" if message_id else "已发出停止指令" ) return {"success": True, "message": message} async def delete_attachment_handler(url: str): """删除附件处理器 - 从 OSS 删除文件""" try: from utils.oss_uploader import delete_file, extract_object_key_from_url object_key = extract_object_key_from_url(url) if not object_key: raise HTTPException(status_code=400, detail="无效的文件 URL") success = delete_file(object_key) if success: return {"success": True, "message": "文件删除成功"} else: raise HTTPException(status_code=500, detail="文件删除失败") except HTTPException: raise except Exception as e: log_error(f"删除附件失败: {str(e)}") raise HTTPException(status_code=500, detail=f"删除失败: {str(e)}")