# app/services/image_proxy_service.py import logging import base64 import httpx import asyncio # 导入 asyncio 模块 from fastapi import Request, HTTPException from fastapi.responses import StreamingResponse, JSONResponse, Response # 导入 Response 类 from typing import Optional, Tuple # 导入自定义模块 from app.core.gemini_client_manager import get_next_gemini_cookie_tuple # 从 core 模块导入获取 cookie 的函数 logger = logging.getLogger(__name__) # 定义需要跳过后端下载的域名,这些域名的图片将通过代理加载 SKIP_DOWNLOAD_DOMAINS = ["googleusercontent.com", "blogger.googleusercontent.com", "lh3.googleusercontent.com"] async def proxy_image_request(image_url: str, request: Request): """ 代理图片下载,解决前端跨域或防盗链问题。 """ logger.info(f"接收到图片代理请求: {image_url}") # 获取下一组Gemini Cookie cookie_tuple: Optional[Tuple[str, str]] = get_next_gemini_cookie_tuple() cookies = {} if cookie_tuple: sid, sidts = cookie_tuple cookies = { "__Secure-1PSID": sid, "__Secure-1PSIDTS": sidts } logger.info(f"使用Cookie代理图片下载: sid={sid[:5]}..., sidts={sidts[:5]}...") # 设置更安全的请求头 headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36", "Accept": "image/webp,image/apng,image/*,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.9", "Referer": "https://gemini.google.com/", "Sec-Fetch-Dest": "image", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "same-origin", } max_retries = 3 retry_delay = 1.0 # 重试间隔(秒) for attempt in range(max_retries): try: async with httpx.AsyncClient( follow_redirects=True, trust_env=False, max_redirects=5, cookies=cookies, http2=True ) as client: # 添加详细日志 logger.debug(f"开始代理下载图片 (尝试 {attempt+1}/{max_retries}): {image_url}") response = await client.get(image_url, timeout=30, headers=headers) response.raise_for_status() # 记录响应详情 logger.info(f"图片代理成功: URL={image_url}, 状态码={response.status_code}, 类型={response.headers.get('Content-Type')}") # 返回流式响应 return StreamingResponse( content=response.iter_bytes(), media_type=response.headers.get("Content-Type", "image/jpeg"), headers={ "Cache-Control": "public, max-age=86400", # 缓存一天 "Access-Control-Allow-Origin": "*" } ) except httpx.HTTPStatusError as e: if attempt == max_retries - 1: logger.error(f"代理下载图片失败: URL: {image_url}, 状态码: {e.response.status_code}") # 返回一个透明的 1x1 像素 GIF 图片作为占位符 transparent_gif_base64 = "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" transparent_gif_bytes = base64.b64decode(transparent_gif_base64) return Response( content=transparent_gif_bytes, media_type="image/gif", headers={ "Cache-Control": "public, max-age=60", # 短时间缓存 "Access-Control-Allow-Origin": "*" # 允许跨域 } ) logger.warning(f"尝试 {attempt+1}/{max_retries} 失败,状态码: {e.response.status_code}") except httpx.TooManyRedirects: if attempt == max_retries - 1: logger.error(f"代理下载图片重定向过多: URL: {image_url}") # 返回一个透明的 1x1 像素 GIF 图片作为占位符 transparent_gif_base64 = "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" transparent_gif_bytes = base64.b64decode(transparent_gif_base64) return Response( content=transparent_gif_bytes, media_type="image/gif", headers={ "Cache-Control": "public, max-age=60", # 短时间缓存 "Access-Control-Allow-Origin": "*" # 允许跨域 } ) logger.warning(f"尝试 {attempt+1}/{max_retries} 失败: 重定向过多") except (httpx.RequestError, Exception) as e: if attempt == max_retries - 1: logger.error(f"代理下载图片失败: URL: {image_url}, 错误: {type(e).__name__} - {e}") # 返回一个透明的 1x1 像素 GIF 图片作为占位符 transparent_gif_base64 = "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" transparent_gif_bytes = base64.b64decode(transparent_gif_base64) return Response( content=transparent_gif_bytes, media_type="image/gif", headers={ "Cache-Control": "public, max-age=60", # 短时间缓存 "Access-Control-Allow-Origin": "*" # 允许跨域 } ) logger.warning(f"尝试 {attempt+1}/{max_retries} 失败: {type(e).__name__} - {e}") # 等待一段时间后重试 await asyncio.sleep(retry_delay * (attempt + 1))