From b6ee6c949bfa35a035af0522d5896f714c5c2ac8 Mon Sep 17 00:00:00 2001 From: MT-Fire <798521692@qq.com> Date: Mon, 9 Mar 2026 11:10:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=A0=E9=99=A4=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E6=97=B6=E8=A6=81oss=E5=88=A0=E9=99=A4=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/adapters/base.py | 2 +- server/api/conversation_routes.py | 73 ++++++++++++++++++++++- server/utils/oss_uploader.py | 98 ++++++++++++++++++++++++++++++- src/components/chat/ChatMain.vue | 2 +- 4 files changed, 170 insertions(+), 5 deletions(-) diff --git a/server/adapters/base.py b/server/adapters/base.py index a7ea570..13f2b6b 100644 --- a/server/adapters/base.py +++ b/server/adapters/base.py @@ -30,7 +30,7 @@ class ModelInfo: "maxTokens": self.max_tokens, "provider": self.provider, "supports_thinking": self.supports_thinking, - "supports_web_Search": self.supports_web_search, + "supports_web_search": self.supports_web_search, "supports_vision": self.supports_vision, "supports_files": self.supports_files, } diff --git a/server/api/conversation_routes.py b/server/api/conversation_routes.py index c087f5d..88f56ca 100644 --- a/server/api/conversation_routes.py +++ b/server/api/conversation_routes.py @@ -65,8 +65,38 @@ async def save_conversation_handler(data: dict): 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": "删除成功"} @@ -74,6 +104,47 @@ async def delete_conversation_handler(conversation_id: str): 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() diff --git a/server/utils/oss_uploader.py b/server/utils/oss_uploader.py index e47706f..e8e552f 100644 --- a/server/utils/oss_uploader.py +++ b/server/utils/oss_uploader.py @@ -57,11 +57,12 @@ def _get_client() -> oss.Client: return oss.Client(cfg) -def _generate_object_key(filename: str, prefix: str = "uploads") -> str: +def _generate_object_key(filename: str, prefix: str = "chat-ui") -> str: """ 根据文件名生成唯一的 OSS 对象 Key 格式: {prefix}/{日期}/{uuid}_{原始文件名} """ + # TODO: 需要按用户ID分目录 date_str = datetime.now().strftime("%Y%m%d") unique_id = uuid.uuid4().hex[:8] safe_name = Path(filename).name # 只取文件名,去掉路径 @@ -80,7 +81,7 @@ def _build_url(object_key: str) -> str: def upload_file( file_path: str, object_key: Optional[str] = None, - prefix: str = "uploads", + prefix: str = "chat-ui", ) -> dict: """ 上传本地文件到 OSS @@ -204,6 +205,99 @@ def upload_fileobj( ) +def delete_file(object_key: str) -> bool: + """ + 删除 OSS 上的单个文件 + + 参数: + object_key: OSS 对象路径(如 "uploads/20240301/abc123_file.jpg") + + 返回: + True 表示删除成功,False 表示失败 + """ + try: + client = _get_client() + result = client.delete_object( + oss.DeleteObjectRequest( + bucket=OSS_BUCKET_NAME, + key=object_key, + ) + ) + return result.status_code == 204 + except Exception as e: + print(f"[OSS] 删除文件失败: {object_key}, 错误: {e}") + return False + + +def delete_files(object_keys: list) -> dict: + """ + 批量删除 OSS 上的文件 + + 参数: + object_keys: OSS 对象路径列表 + + 返回: + { + "deleted": ["成功删除的 object_key 列表"], + "failed": ["删除失败的 object_key 列表"], + } + """ + deleted = [] + failed = [] + + for key in object_keys: + if delete_file(key): + deleted.append(key) + else: + failed.append(key) + + return {"deleted": deleted, "failed": failed} + + +def extract_object_key_from_url(url: str) -> Optional[str]: + """ + 从 OSS URL 中提取 object_key + + 参数: + url: OSS 文件的完整 URL + + 返回: + object_key 或 None(如果不是有效的 OSS URL) + """ + if not url: + return None + + # 支持两种 URL 格式: + # 1. 自定义域名: OSS_URL_PREFIX/object_key + # 2. 默认域名: https://bucket.endpoint/object_key + + try: + # 移除查询参数 + url_path = url.split("?")[0] + + if OSS_URL_PREFIX: + # 自定义域名格式 + prefix = OSS_URL_PREFIX.rstrip("/") + if url_path.startswith(prefix): + return url_path[len(prefix) + 1:] # +1 去掉开头的 / + + # 默认域名格式: https://bucket.endpoint/object_key + endpoint = OSS_ENDPOINT.replace("https://", "").replace("http://", "") + default_prefix = f"https://{OSS_BUCKET_NAME}.{endpoint}/" + + if url_path.startswith(default_prefix): + return url_path[len(default_prefix):] + + # 也尝试匹配 http 版本 + http_prefix = f"http://{OSS_BUCKET_NAME}.{endpoint}/" + if url_path.startswith(http_prefix): + return url_path[len(http_prefix):] + + return None + except Exception: + return None + + # ──────────────────────────────────────────────────────────────── # 命令行入口:python -m utils.oss_uploader --file <路径> # ──────────────────────────────────────────────────────────────── diff --git a/src/components/chat/ChatMain.vue b/src/components/chat/ChatMain.vue index 1bb55ab..2b26ea8 100644 --- a/src/components/chat/ChatMain.vue +++ b/src/components/chat/ChatMain.vue @@ -196,7 +196,7 @@ async function handleSend( // 如果没有当前对话,创建新对话 if (!currentConversation.value) { - chatStore.createConversation(); + await chatStore.createConversation(); } // 从当前会话中提取历史消息(用于上下文记忆),在添加新消息之前提取