import base64 import httpx import asyncio import logging from pathlib import Path from typing import Optional from config import config logger = logging.getLogger(__name__) def image_to_base64(image_path: str) -> str: """将图片转为 base64""" with open(image_path, "rb") as f: return base64.b64encode(f.read()).decode("utf-8") def get_mime_type(image_path: str) -> str: """获取图片 MIME 类型""" suffix = Path(image_path).suffix.lower() mime_map = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".gif": "image/gif", ".webp": "image/webp", ".bmp": "image/bmp", } return mime_map.get(suffix, "image/jpeg") class BaseClient: """AI API 客户端基类""" async def analyze_image(self, image_path: str, custom_prompt: Optional[str] = None) -> str: raise NotImplementedError class OpenAIClient(BaseClient): """OpenAI / OpenAI 兼容 API 客户端""" def __init__(self, api_key: str, base_url: str, model: str): self.api_key = api_key self.base_url = base_url.rstrip("/") self.model = model async def analyze_image(self, image_path: str, custom_prompt: Optional[str] = None) -> str: b64 = image_to_base64(image_path) mime = get_mime_type(image_path) prompt = custom_prompt or config.SYSTEM_PROMPT payload = { "model": self.model, "messages": [ { "role": "user", "content": [ {"type": "text", "text": prompt}, { "type": "image_url", "image_url": { "url": f"data:{mime};base64,{b64}", "detail": "high" } } ] } ], "max_tokens": 1024, "temperature": 0.3, } headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", } async with httpx.AsyncClient(timeout=120) as client: resp = await client.post( f"{self.base_url}/chat/completions", json=payload, headers=headers ) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"].strip() class GeminiClient(BaseClient): """Google Gemini API 客户端""" def __init__(self, api_key: str, model: str): self.api_key = api_key self.model = model async def analyze_image(self, image_path: str, custom_prompt: Optional[str] = None) -> str: b64 = image_to_base64(image_path) mime = get_mime_type(image_path) prompt = custom_prompt or config.SYSTEM_PROMPT url = f"https://generativelanguage.googleapis.com/v1beta/models/{self.model}:generateContent" payload = { "contents": [ { "parts": [ {"text": prompt}, { "inline_data": { "mime_type": mime, "data": b64 } } ] } ], "generationConfig": { "temperature": 0.3, "maxOutputTokens": 1024, } } async with httpx.AsyncClient(timeout=120) as client: resp = await client.post( url, json=payload, params={"key": self.api_key} ) resp.raise_for_status() data = resp.json() return data["candidates"][0]["content"]["parts"][0]["text"].strip() class ClaudeClient(BaseClient): """Anthropic Claude API 客户端""" def __init__(self, api_key: str, model: str): self.api_key = api_key self.model = model async def analyze_image(self, image_path: str, custom_prompt: Optional[str] = None) -> str: b64 = image_to_base64(image_path) mime = get_mime_type(image_path) prompt = custom_prompt or config.SYSTEM_PROMPT payload = { "model": self.model, "max_tokens": 1024, "messages": [ { "role": "user", "content": [ { "type": "image", "source": { "type": "base64", "media_type": mime, "data": b64 } }, {"type": "text", "text": prompt} ] } ] } headers = { "x-api-key": self.api_key, "anthropic-version": "2023-06-01", "Content-Type": "application/json", } async with httpx.AsyncClient(timeout=120) as client: resp = await client.post( "https://api.anthropic.com/v1/messages", json=payload, headers=headers ) resp.raise_for_status() data = resp.json() return data["content"][0]["text"].strip() def get_ai_client( provider: Optional[str] = None, api_key: Optional[str] = None, base_url: Optional[str] = None, model: Optional[str] = None, ) -> BaseClient: """工厂方法:获取 AI 客户端""" provider = (provider or config.AI_PROVIDER).lower() if provider == "openai": return OpenAIClient( api_key=api_key or config.OPENAI_API_KEY, base_url=base_url or config.OPENAI_BASE_URL, model=model or config.OPENAI_MODEL, ) elif provider == "gemini": return GeminiClient( api_key=api_key or config.GEMINI_API_KEY, model=model or config.GEMINI_MODEL, ) elif provider == "claude": return ClaudeClient( api_key=api_key or config.CLAUDE_API_KEY, model=model or config.CLAUDE_MODEL, ) elif provider == "qwen": return OpenAIClient( api_key=api_key or config.QWEN_API_KEY, base_url=base_url or config.QWEN_BASE_URL, model=model or config.QWEN_MODEL, ) elif provider == "custom": return OpenAIClient( api_key=api_key or config.CUSTOM_API_KEY, base_url=base_url or config.CUSTOM_BASE_URL, model=model or config.CUSTOM_MODEL, ) else: raise ValueError(f"不支持的 AI 提供商: {provider}")