feat: 删除对话时要oss删除文件
This commit is contained in:
parent
fe4ee53c38
commit
b6ee6c949b
|
|
@ -30,7 +30,7 @@ class ModelInfo:
|
||||||
"maxTokens": self.max_tokens,
|
"maxTokens": self.max_tokens,
|
||||||
"provider": self.provider,
|
"provider": self.provider,
|
||||||
"supports_thinking": self.supports_thinking,
|
"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_vision": self.supports_vision,
|
||||||
"supports_files": self.supports_files,
|
"supports_files": self.supports_files,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,8 +65,38 @@ async def save_conversation_handler(data: dict):
|
||||||
|
|
||||||
|
|
||||||
async def delete_conversation_handler(conversation_id: str):
|
async def delete_conversation_handler(conversation_id: str):
|
||||||
"""删除对话处理器"""
|
"""删除对话处理器(同时删除关联的 OSS 文件)"""
|
||||||
db = get_db()
|
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)
|
success = db.delete_conversation(conversation_id)
|
||||||
if success:
|
if success:
|
||||||
return {"success": True, "message": "删除成功"}
|
return {"success": True, "message": "删除成功"}
|
||||||
|
|
@ -74,6 +104,47 @@ async def delete_conversation_handler(conversation_id: str):
|
||||||
raise HTTPException(status_code=404, detail="对话不存在")
|
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):
|
async def update_conversation_handler(conversation_id: str, data: dict):
|
||||||
"""部分更新对话处理器"""
|
"""部分更新对话处理器"""
|
||||||
db = get_db()
|
db = get_db()
|
||||||
|
|
|
||||||
|
|
@ -57,11 +57,12 @@ def _get_client() -> oss.Client:
|
||||||
return oss.Client(cfg)
|
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
|
根据文件名生成唯一的 OSS 对象 Key
|
||||||
格式: {prefix}/{日期}/{uuid}_{原始文件名}
|
格式: {prefix}/{日期}/{uuid}_{原始文件名}
|
||||||
"""
|
"""
|
||||||
|
# TODO: 需要按用户ID分目录
|
||||||
date_str = datetime.now().strftime("%Y%m%d")
|
date_str = datetime.now().strftime("%Y%m%d")
|
||||||
unique_id = uuid.uuid4().hex[:8]
|
unique_id = uuid.uuid4().hex[:8]
|
||||||
safe_name = Path(filename).name # 只取文件名,去掉路径
|
safe_name = Path(filename).name # 只取文件名,去掉路径
|
||||||
|
|
@ -80,7 +81,7 @@ def _build_url(object_key: str) -> str:
|
||||||
def upload_file(
|
def upload_file(
|
||||||
file_path: str,
|
file_path: str,
|
||||||
object_key: Optional[str] = None,
|
object_key: Optional[str] = None,
|
||||||
prefix: str = "uploads",
|
prefix: str = "chat-ui",
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
上传本地文件到 OSS
|
上传本地文件到 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 <路径>
|
# 命令行入口:python -m utils.oss_uploader --file <路径>
|
||||||
# ────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,7 @@ async function handleSend(
|
||||||
|
|
||||||
// 如果没有当前对话,创建新对话
|
// 如果没有当前对话,创建新对话
|
||||||
if (!currentConversation.value) {
|
if (!currentConversation.value) {
|
||||||
chatStore.createConversation();
|
await chatStore.createConversation();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从当前会话中提取历史消息(用于上下文记忆),在添加新消息之前提取
|
// 从当前会话中提取历史消息(用于上下文记忆),在添加新消息之前提取
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue