| | """Google API交互模块
|
| |
|
| | 负责与Google Gemini Business API的所有交互操作
|
| | """
|
| | import asyncio
|
| | import json
|
| | import logging
|
| | import os
|
| | import time
|
| | import uuid
|
| | from typing import TYPE_CHECKING, List
|
| |
|
| | import httpx
|
| | from fastapi import HTTPException
|
| |
|
| | if TYPE_CHECKING:
|
| | from main import AccountManager
|
| |
|
| | logger = logging.getLogger(__name__)
|
| |
|
| |
|
| | GEMINI_API_BASE = "https://biz-discoveryengine.googleapis.com/v1alpha"
|
| |
|
| |
|
| | def get_common_headers(jwt: str, user_agent: str) -> dict:
|
| | """生成通用请求头"""
|
| | return {
|
| | "accept": "*/*",
|
| | "accept-encoding": "gzip, deflate, br, zstd",
|
| | "accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
|
| | "authorization": f"Bearer {jwt}",
|
| | "content-type": "application/json",
|
| | "origin": "https://business.gemini.google",
|
| | "referer": "https://business.gemini.google/",
|
| | "user-agent": user_agent,
|
| | "x-server-timeout": "1800",
|
| | "sec-ch-ua": '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"',
|
| | "sec-ch-ua-mobile": "?0",
|
| | "sec-ch-ua-platform": '"Windows"',
|
| | "sec-fetch-dest": "empty",
|
| | "sec-fetch-mode": "cors",
|
| | "sec-fetch-site": "cross-site",
|
| | }
|
| |
|
| |
|
| | async def make_request_with_jwt_retry(
|
| | account_mgr: "AccountManager",
|
| | method: str,
|
| | url: str,
|
| | http_client: httpx.AsyncClient,
|
| | user_agent: str,
|
| | request_id: str = "",
|
| | **kwargs
|
| | ) -> httpx.Response:
|
| | """通用HTTP请求,自动处理JWT过期重试
|
| |
|
| | Args:
|
| | account_mgr: AccountManager实例
|
| | method: HTTP方法 (GET/POST)
|
| | url: 请求URL
|
| | http_client: httpx客户端
|
| | user_agent: User-Agent字符串
|
| | request_id: 请求ID(用于日志)
|
| | **kwargs: 传递给httpx的其他参数(如json, headers等)
|
| |
|
| | Returns:
|
| | httpx.Response对象
|
| | """
|
| | jwt = await account_mgr.get_jwt(request_id)
|
| | headers = get_common_headers(jwt, user_agent)
|
| |
|
| |
|
| | extra_headers = kwargs.pop("headers", None)
|
| | if extra_headers:
|
| | headers.update(extra_headers)
|
| |
|
| |
|
| | if method.upper() == "GET":
|
| | resp = await http_client.get(url, headers=headers, **kwargs)
|
| | elif method.upper() == "POST":
|
| | resp = await http_client.post(url, headers=headers, **kwargs)
|
| | else:
|
| | raise ValueError(f"Unsupported HTTP method: {method}")
|
| |
|
| |
|
| | if resp.status_code == 401:
|
| | jwt = await account_mgr.get_jwt(request_id)
|
| | headers = get_common_headers(jwt, user_agent)
|
| | if extra_headers:
|
| | headers.update(extra_headers)
|
| |
|
| | if method.upper() == "GET":
|
| | resp = await http_client.get(url, headers=headers, **kwargs)
|
| | elif method.upper() == "POST":
|
| | resp = await http_client.post(url, headers=headers, **kwargs)
|
| |
|
| | return resp
|
| |
|
| |
|
| | async def create_google_session(
|
| | account_manager: "AccountManager",
|
| | http_client: httpx.AsyncClient,
|
| | user_agent: str,
|
| | request_id: str = ""
|
| | ) -> str:
|
| | """创建Google Session"""
|
| | jwt = await account_manager.get_jwt(request_id)
|
| | headers = get_common_headers(jwt, user_agent)
|
| | body = {
|
| | "configId": account_manager.config.config_id,
|
| | "additionalParams": {"token": "-"},
|
| | "createSessionRequest": {
|
| | "session": {"name": "", "displayName": ""}
|
| | }
|
| | }
|
| |
|
| | req_tag = f"[req_{request_id}] " if request_id else ""
|
| | r = await http_client.post(
|
| | f"{GEMINI_API_BASE}/locations/global/widgetCreateSession",
|
| | headers=headers,
|
| | json=body,
|
| | )
|
| | if r.status_code != 200:
|
| | logger.error(f"[SESSION] [{account_manager.config.account_id}] {req_tag}Session 创建失败: {r.status_code}")
|
| | raise HTTPException(r.status_code, "createSession failed")
|
| | sess_name = r.json()["session"]["name"]
|
| | logger.info(f"[SESSION] [{account_manager.config.account_id}] {req_tag}创建成功: {sess_name[-12:]}")
|
| | return sess_name
|
| |
|
| |
|
| | async def upload_context_file(
|
| | session_name: str,
|
| | mime_type: str,
|
| | base64_content: str,
|
| | account_manager: "AccountManager",
|
| | http_client: httpx.AsyncClient,
|
| | user_agent: str,
|
| | request_id: str = ""
|
| | ) -> str:
|
| | """上传文件到指定 Session,返回 fileId"""
|
| | jwt = await account_manager.get_jwt(request_id)
|
| | headers = get_common_headers(jwt, user_agent)
|
| |
|
| |
|
| | ext = mime_type.split('/')[-1] if '/' in mime_type else "bin"
|
| | file_name = f"upload_{int(time.time())}_{uuid.uuid4().hex[:6]}.{ext}"
|
| |
|
| | body = {
|
| | "configId": account_manager.config.config_id,
|
| | "additionalParams": {"token": "-"},
|
| | "addContextFileRequest": {
|
| | "name": session_name,
|
| | "fileName": file_name,
|
| | "mimeType": mime_type,
|
| | "fileContents": base64_content
|
| | }
|
| | }
|
| |
|
| | r = await http_client.post(
|
| | f"{GEMINI_API_BASE}/locations/global/widgetAddContextFile",
|
| | headers=headers,
|
| | json=body,
|
| | )
|
| |
|
| | req_tag = f"[req_{request_id}] " if request_id else ""
|
| | if r.status_code != 200:
|
| | logger.error(f"[FILE] [{account_manager.config.account_id}] {req_tag}文件上传失败: {r.status_code}")
|
| | error_text = r.text
|
| | if r.status_code == 400:
|
| | try:
|
| | payload = json.loads(r.text or "{}")
|
| | message = payload.get("error", {}).get("message", "")
|
| | except Exception:
|
| | message = ""
|
| | if "Unsupported file type" in message:
|
| | mime_type = message.split("Unsupported file type:", 1)[-1].strip()
|
| | hint = f"不支持的文件类型: {mime_type}。请转换为 PDF、图片或纯文本后再上传。"
|
| | raise HTTPException(400, hint)
|
| | raise HTTPException(r.status_code, f"Upload failed: {error_text}")
|
| |
|
| | data = r.json()
|
| | file_id = data.get("addContextFileResponse", {}).get("fileId")
|
| | logger.info(f"[FILE] [{account_manager.config.account_id}] {req_tag}文件上传成功: {mime_type}")
|
| | return file_id
|
| |
|
| |
|
| | async def get_session_file_metadata(
|
| | account_mgr: "AccountManager",
|
| | session_name: str,
|
| | http_client: httpx.AsyncClient,
|
| | user_agent: str,
|
| | request_id: str = ""
|
| | ) -> dict:
|
| | """获取session中的文件元数据,包括正确的session路径"""
|
| | body = {
|
| | "configId": account_mgr.config.config_id,
|
| | "additionalParams": {"token": "-"},
|
| | "listSessionFileMetadataRequest": {
|
| | "name": session_name,
|
| | "filter": "file_origin_type = AI_GENERATED"
|
| | }
|
| | }
|
| |
|
| | resp = await make_request_with_jwt_retry(
|
| | account_mgr,
|
| | "POST",
|
| | f"{GEMINI_API_BASE}/locations/global/widgetListSessionFileMetadata",
|
| | http_client,
|
| | user_agent,
|
| | request_id,
|
| | json=body
|
| | )
|
| |
|
| | if resp.status_code != 200:
|
| | logger.warning(f"[IMAGE] [{account_mgr.config.account_id}] [req_{request_id}] 获取文件元数据失败: {resp.status_code}")
|
| | return {}
|
| |
|
| | data = resp.json()
|
| | result = {}
|
| | file_metadata_list = data.get("listSessionFileMetadataResponse", {}).get("fileMetadata", [])
|
| |
|
| | for fm in file_metadata_list:
|
| | fid = fm.get("fileId")
|
| | if fid:
|
| | result[fid] = fm
|
| |
|
| | return result
|
| |
|
| |
|
| | def build_image_download_url(session_name: str, file_id: str) -> str:
|
| | """构造图片下载URL"""
|
| | return f"{GEMINI_API_BASE}/{session_name}:downloadFile?fileId={file_id}&alt=media"
|
| |
|
| |
|
| | async def download_image_with_jwt(
|
| | account_mgr: "AccountManager",
|
| | session_name: str,
|
| | file_id: str,
|
| | http_client: httpx.AsyncClient,
|
| | user_agent: str,
|
| | request_id: str = "",
|
| | max_retries: int = 3
|
| | ) -> bytes:
|
| | """
|
| | 使用JWT认证下载图片(带超时和重试机制)
|
| |
|
| | Args:
|
| | account_mgr: 账户管理器
|
| | session_name: Session名称
|
| | file_id: 文件ID
|
| | http_client: httpx客户端
|
| | user_agent: User-Agent字符串
|
| | request_id: 请求ID
|
| | max_retries: 最大重试次数(默认3次)
|
| |
|
| | Returns:
|
| | 图片字节数据
|
| |
|
| | Raises:
|
| | HTTPException: 下载失败
|
| | asyncio.TimeoutError: 超时
|
| | """
|
| | url = build_image_download_url(session_name, file_id)
|
| | logger.info(f"[IMAGE] [{account_mgr.config.account_id}] [req_{request_id}] 开始下载图片: {file_id[:8]}...")
|
| |
|
| | for attempt in range(max_retries):
|
| | try:
|
| |
|
| | resp = await asyncio.wait_for(
|
| | make_request_with_jwt_retry(
|
| | account_mgr,
|
| | "GET",
|
| | url,
|
| | http_client,
|
| | user_agent,
|
| | request_id,
|
| | follow_redirects=True
|
| | ),
|
| | timeout=180
|
| | )
|
| |
|
| | resp.raise_for_status()
|
| | logger.info(f"[IMAGE] [{account_mgr.config.account_id}] [req_{request_id}] 图片下载成功: {file_id[:8]}... ({len(resp.content)} bytes)")
|
| | return resp.content
|
| |
|
| | except asyncio.TimeoutError:
|
| | logger.warning(f"[IMAGE] [{account_mgr.config.account_id}] [req_{request_id}] 图片下载超时 (尝试 {attempt + 1}/{max_retries}): {file_id[:8]}...")
|
| | if attempt == max_retries - 1:
|
| | raise HTTPException(504, f"Image download timeout after {max_retries} attempts")
|
| | await asyncio.sleep(2 ** attempt)
|
| |
|
| | except httpx.HTTPError as e:
|
| | logger.warning(f"[IMAGE] [{account_mgr.config.account_id}] [req_{request_id}] 图片下载失败 (尝试 {attempt + 1}/{max_retries}): {type(e).__name__}")
|
| | if attempt == max_retries - 1:
|
| | raise HTTPException(500, f"Image download failed: {str(e)[:100]}")
|
| | await asyncio.sleep(2 ** attempt)
|
| |
|
| | except Exception as e:
|
| | logger.error(f"[IMAGE] [{account_mgr.config.account_id}] [req_{request_id}] 图片下载异常: {type(e).__name__}: {str(e)[:100]}")
|
| | raise
|
| |
|
| |
|
| | raise HTTPException(500, "Image download failed unexpectedly")
|
| |
|
| |
|
| | def save_image_to_hf(image_data: bytes, chat_id: str, file_id: str, mime_type: str, base_url: str, image_dir: str) -> str:
|
| | """保存图片到持久化存储,返回完整的公开URL"""
|
| | ext_map = {"image/png": ".png", "image/jpeg": ".jpg", "image/gif": ".gif", "image/webp": ".webp"}
|
| | ext = ext_map.get(mime_type, ".png")
|
| |
|
| | filename = f"{chat_id}_{file_id}{ext}"
|
| | save_path = os.path.join(image_dir, filename)
|
| |
|
| |
|
| | with open(save_path, "wb") as f:
|
| | f.write(image_data)
|
| |
|
| | return f"{base_url}/images/{filename}"
|
| |
|