Files
life-echo/api/app/core/cos_url_keys.py
Kevin eabda2c6a9 chore: resolve WIP after merging internal/development
- .gitignore: keep api/uploads ignore and copyright_source_listing.pdf path

- auth: keep COS avatar upload URL; delete prior COS object when applying preset

- i18n: regenerate resources.ts (includes profile tapAwayToClose)

- Avatar/COS tests and personal-info remain from prior local work

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 15:34:50 +08:00

172 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""从 URL 解析当前环境腾讯云 COS object key仅当 host 与配置一致时)。"""
from __future__ import annotations
from typing import Any
from urllib.parse import urlparse
from app.core.config import settings
from app.core.logging import get_logger
from app.ports.storage import ObjectStorage
logger = get_logger(__name__)
# 客户端再读 TTS / 拉取音频:预签名有效期(秒),与移动端会话长度匹配
TTS_PRESIGNED_EXPIRES_SEC = 86_400
# 用户头像 API 下发(与 TTS 一致)
AVATAR_PRESIGNED_EXPIRES_SEC = TTS_PRESIGNED_EXPIRES_SEC
def extract_cos_object_key_if_owned(url: str | None) -> str | None:
"""
若 url 指向 settings 中配置的 COS 域名,返回去掉前导 / 的 object key。
非 http(s)、或 host 不匹配时返回 None。
"""
if not url:
return None
s = str(url).strip()
if not s.startswith(("http://", "https://")):
return None
parsed = urlparse(s)
host = (parsed.netloc or "").lower()
if not host:
return None
candidates: list[str] = []
bucket = (settings.tencent_cos_bucket or "").strip().lower()
region = (settings.tencent_cos_region or "").strip().lower()
if bucket and region:
candidates.append(f"{bucket}.cos.{region}.myqcloud.com")
base = (settings.tencent_cos_base_url or "").strip()
if base:
base_parsed = urlparse(base if "://" in base else f"https://{base}")
bh = (base_parsed.netloc or "").lower()
if bh:
candidates.append(bh)
if not candidates:
return None
matched = any(host == c for c in candidates if c)
if not matched:
return None
key = (parsed.path or "").lstrip("/")
return key or None
def collect_cos_keys_from_conversation_history(
history: list[dict[str, Any]],
) -> set[str]:
"""从 Redis 会话历史中收集 AI 消息附带的 TTS 音频 COS object key。"""
keys: set[str] = set()
for msg in history:
if msg.get("role") != "ai":
continue
raw = msg.get("ttsAudioUrls")
if not isinstance(raw, list):
continue
for u in raw:
if isinstance(u, str):
k = extract_cos_object_key_if_owned(u)
if k:
keys.add(k)
return keys
def collect_cos_keys_from_tts_url_list(urls: list[str] | None) -> set[str]:
if not urls:
return set()
keys: set[str] = set()
for u in urls:
if isinstance(u, str):
k = extract_cos_object_key_if_owned(u)
if k:
keys.add(k)
return keys
def presign_tts_urls_for_playback(
urls: list[str],
storage: ObjectStorage | None,
*,
expires: int = TTS_PRESIGNED_EXPIRES_SEC,
) -> list[str]:
"""
将本环境 COS 直链替换为预签名下载 URL私有桶下匿名 GET 会 AccessDenied
目的与回忆录 `normalize_image_assets_for_api` 中对 `get_download_url` 的用法一致。
非本环境 URL 或无法解析 key 时原样返回。
"""
if not storage or not urls:
return list(urls)
out: list[str] = []
for u in urls:
if not isinstance(u, str):
continue
s = u.strip()
if not s:
continue
key = extract_cos_object_key_if_owned(s)
if key:
try:
out.append(storage.get_url(key, expires=expires))
except Exception as exc:
logger.warning(
"presign tts url failed, keeping original url: key={} err={}",
key,
exc,
)
out.append(s)
else:
out.append(s)
return out
def avatar_url_for_api_response(stored_url: str | None) -> str | None:
"""DB 中的头像 URL本环境 COS 直链改为预签名下载 URL。"""
if stored_url is None:
return None
s = str(stored_url).strip()
if not s:
return None
key = extract_cos_object_key_if_owned(s)
if not key:
return s
if not (
(settings.tencent_cos_secret_id or "").strip()
and (settings.tencent_cos_bucket or "").strip()
):
return s
from app.core.dependencies import get_object_storage
storage = get_object_storage()
try:
return storage.get_url(key, expires=AVATAR_PRESIGNED_EXPIRES_SEC)
except Exception as exc:
logger.warning(
"presign avatar url failed, keeping original: key={} err={}",
key,
exc,
)
return s
def best_effort_delete_cos_object_for_url(url: str | None) -> None:
"""本环境 COS 对象则尽力 delete换头像 / 预设时清理)。"""
key = extract_cos_object_key_if_owned(url)
if not key:
return
if not (
(settings.tencent_cos_secret_id or "").strip()
and (settings.tencent_cos_bucket or "").strip()
):
return
from app.core.dependencies import get_object_storage
try:
get_object_storage().delete(key)
except Exception as exc:
logger.warning("delete cos avatar object failed: key={} err={}", key, exc)