feat: upgrade客服多店铺分流、批量报价与稳定性防护

This commit is contained in:
2026-02-28 18:52:31 +08:00
parent c39840fe15
commit 46143be86c
16 changed files with 1329 additions and 37 deletions

View File

@@ -14,11 +14,19 @@ import time
from datetime import datetime
from pathlib import Path
import logging
from dotenv import load_dotenv
from utils.metrics_tracker import emit as metrics_emit
from utils.service_base import BaseService
logger = logging.getLogger(__name__)
load_dotenv()
GEMINI_IMAGE_MODEL = os.getenv("GEMINI_IMAGE_MODEL", "gemini-3.1-flash-image-preview")
GEMINI_IMAGE_FALLBACK_MODEL = os.getenv("GEMINI_IMAGE_FALLBACK_MODEL", "gemini-2.5-flash-image")
GEMINI_IMAGE_SIZE = os.getenv("GEMINI_IMAGE_SIZE", "1K")
GEMINI_THINKING_LEVEL = os.getenv("GEMINI_THINKING_LEVEL", "MINIMAL")
GEMINI_PERSON_GENERATION = os.getenv("GEMINI_PERSON_GENERATION", "")
class GeminiExtractV2Service(BaseService):
@@ -46,17 +54,26 @@ class GeminiExtractV2Service(BaseService):
"name": "西风接口$0.014",
"api_key": "sk-uRuvzLfIHsc3BiHZ2cyebk0cYsZ8NR9rLL326QqXCKIy9EpK",
"api_url": "https://api.apiqik.online/v1beta/models",
"api_model": "gemini-2.5-flash-image", # 更稳定的模型
"max_retries": 2, # 贵接口少重试
"api_model": GEMINI_IMAGE_MODEL,
"max_retries": 2,
"cost": "",
"use_gemini_format": True # 使用Gemini原生API格式
},
{
"name": "西风接口Fallback",
"api_key": "sk-uRuvzLfIHsc3BiHZ2cyebk0cYsZ8NR9rLL326QqXCKIy9EpK",
"api_url": "https://api.apiqik.online/v1beta/models",
"api_model": GEMINI_IMAGE_FALLBACK_MODEL,
"max_retries": 1,
"cost": "",
"use_gemini_format": True
},
{
"name": "最贵的",
"api_key": "sk-8i7uYE0RtnQwDImV8a5f7014DcAb46F6BcEb72Df92218aC8",
"api_url": "https://api.laozhang.ai/v1/chat/completions",
"api_model": "gemini-2.5-flash-image-preview",
"api_model": GEMINI_IMAGE_MODEL,
"max_retries": 1,
"cost": ""
}
@@ -90,6 +107,9 @@ class GeminiExtractV2Service(BaseService):
output_path: str,
custom_prompt: str = None,
aspect_ratio: str = "1:1",
image_size: str = "",
person_generation: str = "",
thinking_level: str = "",
) -> tuple[bool, str, dict]:
"""
使用多API配置进行印花图案提取
@@ -113,6 +133,7 @@ class GeminiExtractV2Service(BaseService):
# 按优先级逐个尝试API配置
for config_index, config in enumerate(self.API_CONFIGS):
logger.info(f"尝试使用API: {config['name']} (成本: {config['cost']})")
metrics_emit("gemini_request", model=config.get("api_model", ""), provider=config.get("name", ""))
# 对每个API配置进行重试
for attempt in range(config['max_retries']):
@@ -126,12 +147,25 @@ class GeminiExtractV2Service(BaseService):
headers = {
"Content-Type": "application/json"
}
# 有效比例列表Auto 不传 aspectRatio
valid_ratios = {"1:1", "9:16", "16:9", "3:4", "4:3", "3:2", "2:3", "5:4", "4:5"}
valid_sizes = {"1K", "2K", "4K"}
valid_thinking = {"MINIMAL", "LOW", "MEDIUM", "HIGH"}
image_config = {}
if aspect_ratio in valid_ratios:
image_config["aspectRatio"] = aspect_ratio
size_val = (image_size or GEMINI_IMAGE_SIZE or "").upper().strip()
if size_val in valid_sizes:
image_config["imageSize"] = size_val
person_val = (person_generation or GEMINI_PERSON_GENERATION or "").strip()
if person_val:
# 中转接口若支持该字段会生效;不设置时不发送,保证兼容
image_config["personGeneration"] = person_val
thinking_val = (thinking_level or GEMINI_THINKING_LEVEL or "").upper().strip()
thinking_config = {}
if thinking_val in valid_thinking:
thinking_config["thinkingLevel"] = thinking_val
data = {
"contents": [
@@ -153,9 +187,13 @@ class GeminiExtractV2Service(BaseService):
"generationConfig": {
"responseModalities": ["IMAGE"],
**({"imageConfig": image_config} if image_config else {}),
**({"thinkingConfig": thinking_config} if thinking_config else {}),
}
}
logger.info(f"Gemini 生成配置: 比例={aspect_ratio} 格式=JPEG")
logger.info(
f"Gemini 生成配置: 比例={aspect_ratio} 尺寸={image_config.get('imageSize', '默认')} "
f"person={image_config.get('personGeneration', '默认')} thinking={thinking_config.get('thinkingLevel', '默认')}"
)
else:
# OpenAI兼容格式
api_url = config['api_url']
@@ -212,6 +250,25 @@ class GeminiExtractV2Service(BaseService):
continue
result = await response.json()
# Gemini 偶发只返回文本不返回图片NO_IMAGE 时快速重试/降级
if config.get('use_gemini_format', False):
finish_reason = ""
try:
finish_reason = (
(result.get("candidates") or [{}])[0].get("finishReason", "")
)
except Exception:
finish_reason = ""
if finish_reason == "NO_IMAGE":
logger.warning(
f"{config['name']} 返回 NO_IMAGE (模型={config.get('api_model')}),第{attempt + 1}"
)
metrics_emit("gemini_no_image", model=config.get("api_model", ""), provider=config.get("name", ""))
if attempt == config['max_retries'] - 1:
logger.warning(f"{config['name']} NO_IMAGE 重试已用完,切换下一个配置")
break
await asyncio.sleep(1 + attempt)
continue
except (aiohttp.ClientError, asyncio.TimeoutError, AssertionError) as e:
logger.error(f"{config['name']} 网络连接错误 (第{attempt + 1}次): {str(e)}")
@@ -235,6 +292,7 @@ class GeminiExtractV2Service(BaseService):
if success:
logger.info(f"使用 {config['name']} 成功完成印花提取")
metrics_emit("gemini_success", model=config.get("api_model", ""), provider=config.get("name", ""))
try:
from utils.api_cost_tracker import record
record("gemini_extract", count=1)
@@ -506,4 +564,4 @@ if __name__ == "__main__":
await service.cleanup()
asyncio.run(test())
asyncio.run(test())