755 lines
35 KiB
Python
755 lines
35 KiB
Python
"""
|
||
美图API服务模块 - 处理与美图API的交互
|
||
"""
|
||
|
||
import os
|
||
import time
|
||
import uuid
|
||
import json
|
||
import asyncio
|
||
import logging
|
||
import aiohttp
|
||
import aiofiles
|
||
from pathlib import Path
|
||
from typing import Optional, Dict, Any, Callable, List, Tuple
|
||
|
||
# 配置日志
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# 统计信息
|
||
class MeituServiceStats:
|
||
def __init__(self):
|
||
self.total_requests = 0
|
||
self.successful_requests = 0
|
||
self.failed_requests = 0
|
||
self.timeout_requests = 0
|
||
self.network_error_requests = 0
|
||
self.last_success_time = None
|
||
self.last_error_time = None
|
||
self.last_error_message = None
|
||
|
||
def record_success(self):
|
||
self.total_requests += 1
|
||
self.successful_requests += 1
|
||
self.last_success_time = time.time()
|
||
|
||
def record_failure(self, error_type="general", message=""):
|
||
self.total_requests += 1
|
||
self.failed_requests += 1
|
||
self.last_error_time = time.time()
|
||
self.last_error_message = message
|
||
|
||
if error_type == "timeout":
|
||
self.timeout_requests += 1
|
||
elif error_type == "network":
|
||
self.network_error_requests += 1
|
||
|
||
def get_success_rate(self):
|
||
if self.total_requests == 0:
|
||
return 0.0
|
||
return self.successful_requests / self.total_requests * 100
|
||
|
||
def get_stats(self):
|
||
return {
|
||
"total_requests": self.total_requests,
|
||
"successful_requests": self.successful_requests,
|
||
"failed_requests": self.failed_requests,
|
||
"timeout_requests": self.timeout_requests,
|
||
"network_error_requests": self.network_error_requests,
|
||
"success_rate": self.get_success_rate(),
|
||
"last_success_time": self.last_success_time,
|
||
"last_error_time": self.last_error_time,
|
||
"last_error_message": self.last_error_message
|
||
}
|
||
|
||
# 全局统计实例
|
||
_service_stats = MeituServiceStats()
|
||
|
||
class MeituServiceError(Exception):
|
||
"""美图服务异常"""
|
||
pass
|
||
|
||
class MeituTimeoutError(MeituServiceError):
|
||
"""美图服务超时异常"""
|
||
pass
|
||
|
||
class MeituNetworkError(MeituServiceError):
|
||
"""美图服务网络异常"""
|
||
pass
|
||
|
||
class MeituAPIService:
|
||
"""美图API服务类,处理与美图API的所有交互"""
|
||
|
||
# 服务状态
|
||
_service_status = {
|
||
"available": False,
|
||
"last_check": 0,
|
||
"error": None
|
||
}
|
||
|
||
# 支持的处理模式
|
||
SUPPORTED_MODES = {
|
||
"crystal": "极速重绘",
|
||
"standard": "标准处理",
|
||
"enhance": "增强处理",
|
||
"hdr": "HDR处理",
|
||
"portrait": "人像优化"
|
||
}
|
||
|
||
def __init__(self, api_url: str = None):
|
||
"""
|
||
初始化美图API服务
|
||
:param api_url: API基础URL,如果为None则使用环境变量或默认值
|
||
"""
|
||
# self.api_url = api_url or os.environ.get('MEITU_API_URL', 'http://89358zi786.goho.co:38226')
|
||
self.api_url = api_url or os.environ.get('MEITU_API_URL', 'https://127.0.0.1:6668') # 本地
|
||
self.stats = _service_stats # 使用全局统计实例
|
||
self._active_tasks = set() # 追踪活跃任务,确保高并发安全
|
||
self._task_cancellation_tokens = {} # 任务取消令牌
|
||
|
||
async def process_image(self,
|
||
image_path: str,
|
||
mode: str,
|
||
output_dir: Path,
|
||
progress_callback: Optional[Callable[[int, str], None]] = None) -> Dict[str, Any]:
|
||
"""
|
||
处理图片
|
||
:param image_path: 图片路径
|
||
:param mode: 处理模式(crystal, standard等)
|
||
:param output_dir: 输出目录
|
||
:param progress_callback: 进度回调函数
|
||
:return: 处理结果,包含任务ID和结果图片路径
|
||
"""
|
||
# 检查模式是否支持
|
||
if mode not in self.SUPPORTED_MODES:
|
||
supported_modes = ", ".join(self.SUPPORTED_MODES.keys())
|
||
raise MeituServiceError(f"不支持的处理模式: {mode},支持的模式: {supported_modes}")
|
||
|
||
# 检查图片文件是否存在
|
||
if not os.path.exists(image_path):
|
||
raise MeituServiceError(f"图片文件不存在: {image_path}")
|
||
|
||
try:
|
||
# 生成唯一文件名
|
||
unique_filename = os.path.basename(image_path)
|
||
file_extension = os.path.splitext(unique_filename)[1]
|
||
|
||
# 上传图片并获取任务ID
|
||
task_id = await self._upload_image(image_path, unique_filename, mode)
|
||
|
||
if not task_id:
|
||
raise MeituServiceError("美图API上传失败,未获取到任务ID")
|
||
|
||
# 记录任务ID类型和值
|
||
logger.info(f"获取到任务ID: {task_id}, 类型: {type(task_id)}")
|
||
|
||
# 注册活跃任务
|
||
self._active_tasks.add(task_id)
|
||
self._task_cancellation_tokens[task_id] = False
|
||
|
||
# 轮询等待处理完成 - 确保异常正确传播
|
||
try:
|
||
await self._wait_for_completion(task_id, progress_callback)
|
||
except (MeituTimeoutError, MeituServiceError) as e:
|
||
logger.error(f"美图处理被终止: {str(e)}")
|
||
# 立即清理任务并重新抛出异常
|
||
await self.cleanup_failed_task(task_id)
|
||
raise
|
||
finally:
|
||
# 确保任务总是从活跃列表中移除
|
||
self._active_tasks.discard(task_id)
|
||
self._task_cancellation_tokens.pop(task_id, None)
|
||
logger.info(f"任务{task_id}已从活跃列表中移除")
|
||
|
||
# 下载处理后的图片
|
||
processed_filename = f"processed_{task_id}_{int(time.time())}{file_extension}"
|
||
processed_path = output_dir / processed_filename
|
||
|
||
# 确保输出目录存在
|
||
output_dir.mkdir(exist_ok=True, parents=True)
|
||
|
||
# 下载结果图片
|
||
try:
|
||
await self._download_result(task_id, processed_path)
|
||
except Exception as download_error:
|
||
logger.error(f"下载处理结果失败: {str(download_error)}")
|
||
raise MeituServiceError(f"下载处理结果失败: {str(download_error)}")
|
||
|
||
# 安全处理processing_time计算
|
||
processing_time = 0
|
||
try:
|
||
if '_' in task_id:
|
||
# 尝试从task_id中提取时间戳
|
||
timestamp_str = task_id.split('_')[-1]
|
||
logger.info(f"从task_id提取时间戳: {timestamp_str}, 类型: {type(timestamp_str)}")
|
||
|
||
# 安全转换为整数
|
||
try:
|
||
timestamp = int(timestamp_str)
|
||
processing_time = time.time() - timestamp
|
||
logger.info(f"计算处理时间: {processing_time}秒")
|
||
except (ValueError, TypeError) as e:
|
||
logger.warning(f"时间戳转换失败: {e}")
|
||
else:
|
||
logger.warning(f"任务ID不包含下划线分隔符: {task_id}")
|
||
except Exception as time_error:
|
||
logger.warning(f"处理时间计算错误: {str(time_error)}")
|
||
|
||
# 记录成功
|
||
self.stats.record_success()
|
||
logger.info(f"美图服务处理成功 - 任务ID: {task_id}, 耗时: {processing_time:.2f}秒")
|
||
|
||
return {
|
||
"task_id": task_id,
|
||
"processed_path": processed_path,
|
||
"processed_filename": processed_filename,
|
||
"mode": mode,
|
||
"mode_name": self.SUPPORTED_MODES.get(mode, "未知模式"),
|
||
"processing_time": processing_time
|
||
}
|
||
|
||
except MeituTimeoutError as e:
|
||
self.stats.record_failure("timeout", str(e))
|
||
logger.error(f"美图服务超时: {str(e)}")
|
||
raise
|
||
except MeituNetworkError as e:
|
||
self.stats.record_failure("network", str(e))
|
||
logger.error(f"美图服务网络错误: {str(e)}")
|
||
raise
|
||
except MeituServiceError as e:
|
||
self.stats.record_failure("general", str(e))
|
||
logger.error(f"美图服务错误: {str(e)}")
|
||
raise
|
||
except Exception as e:
|
||
self.stats.record_failure("unknown", str(e))
|
||
logger.error(f"美图API处理失败: {str(e)}")
|
||
import traceback
|
||
logger.error(f"异常堆栈: {traceback.format_exc()}")
|
||
raise MeituServiceError(f"美图API处理失败: {str(e)}")
|
||
|
||
async def process_batch(self,
|
||
image_paths: List[str],
|
||
mode: str,
|
||
output_dir: Path,
|
||
max_concurrent: int = 3) -> List[Dict[str, Any]]:
|
||
"""
|
||
批量处理图片
|
||
:param image_paths: 图片路径列表
|
||
:param mode: 处理模式
|
||
:param output_dir: 输出目录
|
||
:param max_concurrent: 最大并发数
|
||
:return: 处理结果列表
|
||
"""
|
||
results = []
|
||
semaphore = asyncio.Semaphore(max_concurrent)
|
||
|
||
async def process_single(image_path):
|
||
async with semaphore:
|
||
try:
|
||
result = await self.process_image(image_path, mode, output_dir)
|
||
return {
|
||
"success": True,
|
||
"result": result,
|
||
"image_path": image_path
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"处理图片失败: {image_path}, 错误: {str(e)}")
|
||
return {
|
||
"success": False,
|
||
"error": str(e),
|
||
"image_path": image_path
|
||
}
|
||
|
||
# 创建任务
|
||
tasks = [process_single(path) for path in image_paths]
|
||
|
||
# 等待所有任务完成
|
||
try:
|
||
results = await asyncio.gather(*tasks)
|
||
except Exception as e:
|
||
logger.error(f"批量处理任务异常: {str(e)}")
|
||
# 即使有异常,也返回已完成的结果
|
||
results = [{"success": False, "error": f"批量处理异常: {str(e)}", "image_path": path} for path in image_paths]
|
||
|
||
return results
|
||
|
||
async def _upload_image(self, image_path: str, filename: str, mode: str) -> str:
|
||
"""
|
||
上传图片到美图API
|
||
:param image_path: 图片路径
|
||
:param filename: 文件名
|
||
:param mode: 处理模式
|
||
:return: 任务ID
|
||
"""
|
||
logger.info(f"上传图片到美图API - 文件: {filename}, 模式: {mode}")
|
||
|
||
# 检查文件大小
|
||
file_size = os.path.getsize(image_path)
|
||
if file_size > 10 * 1024 * 1024: # 10MB
|
||
raise MeituServiceError(f"文件太大: {file_size / 1024 / 1024:.2f}MB,最大允许10MB")
|
||
|
||
# 检查文件类型
|
||
content_type = await self._get_content_type(filename)
|
||
if not content_type.startswith('image/'):
|
||
raise MeituServiceError(f"不支持的文件类型: {content_type}")
|
||
|
||
# 重试逻辑
|
||
max_retries = 3
|
||
retry_count = 0
|
||
last_error = None
|
||
|
||
while retry_count < max_retries:
|
||
try:
|
||
# 设置上传专用的超时配置
|
||
upload_timeout = aiohttp.ClientTimeout(total=60, connect=10) # 上传允许更长时间
|
||
async with aiohttp.ClientSession(timeout=upload_timeout) as session:
|
||
with open(image_path, 'rb') as f:
|
||
data = aiohttp.FormData()
|
||
data.add_field(filename,
|
||
f.read(),
|
||
filename=filename,
|
||
content_type=content_type)
|
||
|
||
async with session.post(f'{self.api_url}/add_task?scene={mode}', data=data, ssl=False) as resp:
|
||
if resp.status != 200:
|
||
error_text = await resp.text()
|
||
logger.error(f"美图API上传失败 - 状态码: {resp.status}, 响应: {error_text}")
|
||
raise MeituServiceError(f"美图API上传失败: {error_text}")
|
||
|
||
response_text = await resp.text()
|
||
logger.info(f"美图API上传响应: {response_text}")
|
||
|
||
try:
|
||
result = json.loads(response_text)
|
||
if result.get('code') != 0:
|
||
raise MeituServiceError(result.get('message', '美图API上传失败'))
|
||
|
||
task_id = result.get('id')
|
||
logger.info(f"美图API上传成功 - 任务ID: {task_id}")
|
||
return task_id
|
||
except json.JSONDecodeError:
|
||
logger.error(f"美图API响应解析失败: {response_text}")
|
||
raise MeituServiceError("美图API响应格式错误")
|
||
except Exception as e:
|
||
retry_count += 1
|
||
last_error = e
|
||
wait_time = 2 ** retry_count # 指数退避
|
||
logger.warning(f"上传失败,正在重试 ({retry_count}/{max_retries}),等待 {wait_time} 秒: {str(e)}")
|
||
await asyncio.sleep(wait_time)
|
||
|
||
# 所有重试都失败
|
||
raise last_error or MeituServiceError("上传图片失败,已达到最大重试次数")
|
||
|
||
async def _wait_for_completion(self,
|
||
task_id: str,
|
||
progress_callback: Optional[Callable[[int, str], None]] = None,
|
||
max_wait_time: int = 180, # 最长等待3分钟(缩短超时时间)
|
||
check_interval: int = 2 # 每2秒检查一次
|
||
) -> None:
|
||
"""
|
||
轮询等待处理完成
|
||
:param task_id: 任务ID
|
||
:param progress_callback: 进度回调函数
|
||
:param max_wait_time: 最长等待时间(秒)
|
||
:param check_interval: 检查间隔(秒)
|
||
"""
|
||
logger.info(f"开始等待美图API处理完成 - 任务ID: {task_id}")
|
||
|
||
start_time = time.time()
|
||
progress = 0
|
||
|
||
# 连续失败计数
|
||
consecutive_failures = 0
|
||
max_consecutive_failures = 3 # 减少连续失败次数,更快失败
|
||
|
||
# 早期检测到的致命错误类型
|
||
detected_fatal_errors = set()
|
||
|
||
# 创建当前任务的取消令牌
|
||
is_cancelled = False
|
||
|
||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30)) as session:
|
||
for i in range(max_wait_time // check_interval):
|
||
# 检查是否被取消 - 支持外部取消令牌
|
||
if is_cancelled or self._task_cancellation_tokens.get(task_id, False):
|
||
logger.error(f"美图处理任务被取消: {task_id}")
|
||
await self.cleanup_failed_task(task_id)
|
||
raise MeituTimeoutError("美图处理任务被取消")
|
||
|
||
# 检查任务是否还在活跃列表中
|
||
if task_id not in self._active_tasks:
|
||
logger.warning(f"任务{task_id}不在活跃列表中,可能已被清理")
|
||
raise MeituTimeoutError("任务已被停止")
|
||
|
||
# 计算预估进度
|
||
elapsed = time.time() - start_time
|
||
if elapsed < max_wait_time:
|
||
# 假设进度在90%以内线性增长
|
||
progress = min(90, int(90 * elapsed / max_wait_time))
|
||
|
||
if progress_callback:
|
||
progress_callback(progress, f"美图API处理中 - {progress}%")
|
||
|
||
try:
|
||
# 在每次API调用前再次检查取消状态
|
||
if self._task_cancellation_tokens.get(task_id, False):
|
||
logger.info(f"任务{task_id}在API调用前被取消")
|
||
raise MeituTimeoutError("任务被取消")
|
||
|
||
async with session.get(f'{self.api_url}/try_get?taskid={task_id}', ssl=False) as resp:
|
||
if resp.status != 200:
|
||
consecutive_failures += 1
|
||
error_text = await resp.text()
|
||
logger.error(f"美图API状态检查失败 - 状态码: {resp.status}, 响应: {error_text}")
|
||
|
||
# 如果连续失败次数过多,抛出异常
|
||
if consecutive_failures >= max_consecutive_failures:
|
||
logger.error(f"美图API连续失败{consecutive_failures}次,尝试清理任务")
|
||
await self.cleanup_failed_task(task_id)
|
||
raise MeituTimeoutError(f"美图API连续失败{consecutive_failures}次: {error_text}")
|
||
|
||
# 否则继续等待
|
||
await asyncio.sleep(check_interval * 2) # 失败时等待更长时间
|
||
continue
|
||
|
||
# 重置失败计数
|
||
consecutive_failures = 0
|
||
|
||
# 获取响应内容
|
||
try:
|
||
response_text = await resp.text()
|
||
logger.debug(f"美图API状态检查响应: {response_text}")
|
||
|
||
# 解析JSON响应
|
||
try:
|
||
result = json.loads(response_text)
|
||
logger.debug(f"解析的JSON响应: {result}")
|
||
|
||
# 获取code字段
|
||
if 'code' not in result:
|
||
logger.warning(f"美图API响应中缺少code字段: {result}")
|
||
# 继续等待,不抛出异常
|
||
await asyncio.sleep(check_interval)
|
||
continue
|
||
|
||
# 安全获取code值
|
||
try:
|
||
code = result['code']
|
||
# 确保code是整数
|
||
if not isinstance(code, (int, float)):
|
||
code = int(code) if str(code).isdigit() else -1
|
||
except (ValueError, TypeError) as e:
|
||
logger.warning(f"无法转换code为整数: {e}, 原始值: {result.get('code')}")
|
||
code = -1
|
||
|
||
# 检查处理状态
|
||
if code < 0:
|
||
error_message = str(result.get('message', '美图API处理失败'))
|
||
logger.error(f"美图API处理失败: {error_message}")
|
||
|
||
# 检查是否是超时或严重错误,如果是则立即终止
|
||
timeout_indicators = [
|
||
"TimeoutException",
|
||
"selenium.common.exceptions.TimeoutException",
|
||
"WebDriver",
|
||
"已终止处理",
|
||
"处理超时",
|
||
"连接超时",
|
||
"响应超时"
|
||
]
|
||
|
||
is_timeout = any(indicator in error_message for indicator in timeout_indicators)
|
||
|
||
if is_timeout:
|
||
logger.error(f"检测到严重错误/超时,立即终止美图处理: {error_message}")
|
||
raise MeituTimeoutError("美图处理超时或遇到严重错误,已终止处理")
|
||
|
||
raise MeituServiceError(error_message)
|
||
elif code == 0:
|
||
logger.info(f"美图API处理完成 - 任务ID: {task_id}")
|
||
if progress_callback:
|
||
progress_callback(100, "美图API处理完成")
|
||
return
|
||
else:
|
||
# 其他状态码,继续等待
|
||
logger.info(f"美图API处理中 - 状态码: {code}")
|
||
except json.JSONDecodeError as e:
|
||
logger.warning(f"JSON解析失败: {e}, 响应内容: {response_text[:100]}...")
|
||
# 继续等待,不抛出异常
|
||
except Exception as e:
|
||
error_str = str(e)
|
||
logger.error(f"处理响应内容异常: {error_str}")
|
||
import traceback
|
||
logger.error(f"异常堆栈: {traceback.format_exc()}")
|
||
|
||
# 检查是否是致命错误,如果是则立即终止
|
||
fatal_error_indicators = [
|
||
"MeituTimeoutError",
|
||
"美图处理超时",
|
||
"已终止处理",
|
||
"WebDriver",
|
||
"TimeoutException"
|
||
]
|
||
|
||
is_fatal = any(indicator in error_str for indicator in fatal_error_indicators)
|
||
if is_fatal:
|
||
# 记录检测到的致命错误类型
|
||
for indicator in fatal_error_indicators:
|
||
if indicator in error_str:
|
||
detected_fatal_errors.add(indicator)
|
||
|
||
logger.error(f"检测到致命错误,立即终止处理: {error_str}")
|
||
logger.error(f"已检测到的致命错误类型: {detected_fatal_errors}")
|
||
is_cancelled = True # 设置取消标志
|
||
await self.cleanup_failed_task(task_id) # 立即清理
|
||
raise MeituTimeoutError(f"美图处理遇到致命错误: {error_str}")
|
||
|
||
# 增加连续失败计数
|
||
consecutive_failures += 1
|
||
if consecutive_failures >= max_consecutive_failures:
|
||
logger.error(f"连续失败{consecutive_failures}次,终止处理")
|
||
raise MeituServiceError(f"美图处理连续失败{consecutive_failures}次")
|
||
except asyncio.TimeoutError:
|
||
consecutive_failures += 1
|
||
logger.error(f"美图API请求超时 - 第{consecutive_failures}次")
|
||
|
||
# 如果连续超时次数过多,抛出异常
|
||
if consecutive_failures >= max_consecutive_failures:
|
||
logger.error(f"美图API连续超时{consecutive_failures}次,尝试清理任务")
|
||
await self.cleanup_failed_task(task_id)
|
||
raise MeituTimeoutError(f"美图API连续超时{consecutive_failures}次,任务可能已失败")
|
||
|
||
await asyncio.sleep(check_interval * 2)
|
||
continue
|
||
except aiohttp.ClientError as e:
|
||
consecutive_failures += 1
|
||
logger.error(f"网络请求异常: {str(e)} - 第{consecutive_failures}次")
|
||
|
||
# 如果连续失败次数过多,抛出异常
|
||
if consecutive_failures >= max_consecutive_failures:
|
||
logger.error(f"美图API网络连接连续失败{consecutive_failures}次,尝试清理任务")
|
||
await self.cleanup_failed_task(task_id)
|
||
raise MeituNetworkError(f"美图API网络连接连续失败{consecutive_failures}次")
|
||
|
||
await asyncio.sleep(check_interval * 2)
|
||
continue
|
||
except Exception as e:
|
||
if isinstance(e, MeituServiceError):
|
||
raise
|
||
|
||
consecutive_failures += 1
|
||
logger.error(f"美图API状态检查异常: {str(e)} - 第{consecutive_failures}次")
|
||
import traceback
|
||
logger.error(f"异常堆栈: {traceback.format_exc()}")
|
||
|
||
# 如果连续失败次数过多,抛出异常
|
||
if consecutive_failures >= max_consecutive_failures:
|
||
logger.error(f"美图API状态检查连续异常{consecutive_failures}次,尝试清理任务")
|
||
await self.cleanup_failed_task(task_id)
|
||
raise MeituServiceError(f"美图API状态检查连续异常{consecutive_failures}次: {str(e)}")
|
||
|
||
await asyncio.sleep(check_interval * 2)
|
||
continue
|
||
|
||
await asyncio.sleep(check_interval)
|
||
|
||
# 超时
|
||
logger.error(f"美图API处理超时({max_wait_time}秒),尝试清理任务")
|
||
await self.cleanup_failed_task(task_id)
|
||
raise MeituTimeoutError(f"美图API处理超时({max_wait_time}秒) - 任务ID: {task_id}")
|
||
|
||
async def _download_result(self, task_id: str, output_path: Path) -> None:
|
||
"""
|
||
下载处理结果
|
||
:param task_id: 任务ID
|
||
:param output_path: 输出路径
|
||
"""
|
||
logger.info(f"开始下载美图API处理结果 - 任务ID: {task_id}, 输出路径: {output_path}")
|
||
|
||
# 确保输出目录存在
|
||
output_path.parent.mkdir(exist_ok=True, parents=True)
|
||
|
||
# 重试逻辑
|
||
max_retries = 3
|
||
retry_count = 0
|
||
last_error = None
|
||
|
||
while retry_count < max_retries:
|
||
try:
|
||
# 设置下载专用的超时配置
|
||
download_timeout = aiohttp.ClientTimeout(total=120, connect=10) # 下载允许更长时间
|
||
async with aiohttp.ClientSession(timeout=download_timeout) as session:
|
||
async with session.get(f'{self.api_url}/get_image?taskid={task_id}', ssl=False) as resp:
|
||
if resp.status != 200:
|
||
error_text = await resp.text()
|
||
logger.error(f"美图API下载失败 - 状态码: {resp.status}, 响应: {error_text}")
|
||
raise MeituServiceError(f"美图API下载失败: {error_text}")
|
||
|
||
# 读取图片数据
|
||
image_data = await resp.read()
|
||
if not image_data:
|
||
raise MeituServiceError("美图API返回空图片数据")
|
||
|
||
# 保存图片
|
||
async with aiofiles.open(output_path, 'wb') as f:
|
||
await f.write(image_data)
|
||
|
||
# 安全获取文件大小
|
||
try:
|
||
file_size = len(image_data)
|
||
logger.info(f"美图API结果下载成功 - 任务ID: {task_id}, 文件大小: {file_size} 字节")
|
||
except Exception as size_error:
|
||
logger.warning(f"获取文件大小失败: {str(size_error)}")
|
||
logger.info(f"美图API结果下载成功 - 任务ID: {task_id}")
|
||
|
||
# 验证文件是否成功保存
|
||
if not output_path.exists() or output_path.stat().st_size == 0:
|
||
raise MeituServiceError("图片保存失败或文件大小为0")
|
||
|
||
return
|
||
except MeituServiceError:
|
||
# 直接抛出服务特定错误
|
||
raise
|
||
except Exception as e:
|
||
retry_count += 1
|
||
last_error = e
|
||
wait_time = 2 ** retry_count # 指数退避
|
||
logger.warning(f"下载失败,正在重试 ({retry_count}/{max_retries}),等待 {wait_time} 秒: {str(e)}")
|
||
await asyncio.sleep(wait_time)
|
||
|
||
# 所有重试都失败
|
||
error_msg = str(last_error) if last_error else "未知错误"
|
||
logger.error(f"下载结果失败,已达到最大重试次数: {error_msg}")
|
||
raise MeituServiceError(f"下载结果失败,已达到最大重试次数: {error_msg}")
|
||
|
||
@staticmethod
|
||
async def _get_content_type(filename: str) -> str:
|
||
"""
|
||
根据文件名获取内容类型
|
||
:param filename: 文件名
|
||
:return: 内容类型
|
||
"""
|
||
ext = os.path.splitext(filename)[1].lower()
|
||
content_types = {
|
||
'.jpg': 'image/jpeg',
|
||
'.jpeg': 'image/jpeg',
|
||
'.png': 'image/png',
|
||
'.gif': 'image/gif',
|
||
'.webp': 'image/webp',
|
||
'.bmp': 'image/bmp'
|
||
}
|
||
return content_types.get(ext, 'application/octet-stream')
|
||
|
||
@classmethod
|
||
async def test_connection(cls, api_url: str = None) -> Tuple[bool, str]:
|
||
"""
|
||
测试与美图API的连接
|
||
:param api_url: API URL,如果为None则使用默认值
|
||
:return: (连接是否成功, 状态消息)
|
||
"""
|
||
# 如果最后检查时间在30秒内,直接返回缓存的状态
|
||
if time.time() - cls._service_status["last_check"] < 30:
|
||
return (
|
||
cls._service_status["available"],
|
||
"服务在线" if cls._service_status["available"] else f"服务离线: {cls._service_status['error']}"
|
||
)
|
||
|
||
service = MeituAPIService(api_url)
|
||
try:
|
||
async with aiohttp.ClientSession() as session:
|
||
# 尝试健康检查端点
|
||
try:
|
||
async with session.get(f'{service.api_url}/health', ssl=False, timeout=5) as resp:
|
||
if resp.status == 200:
|
||
cls._service_status = {
|
||
"available": True,
|
||
"last_check": time.time(),
|
||
"error": None
|
||
}
|
||
return True, "服务在线"
|
||
except:
|
||
# 健康检查失败,尝试其他端点
|
||
pass
|
||
|
||
# 尝试调用API的其他端点
|
||
try:
|
||
async with session.get(f'{service.api_url}/', ssl=False, timeout=5) as resp:
|
||
cls._service_status = {
|
||
"available": resp.status < 500,
|
||
"last_check": time.time(),
|
||
"error": f"状态码: {resp.status}" if resp.status >= 400 else None
|
||
}
|
||
return cls._service_status["available"], "服务可能在线,但健康检查失败"
|
||
except Exception as e:
|
||
cls._service_status = {
|
||
"available": False,
|
||
"last_check": time.time(),
|
||
"error": str(e)
|
||
}
|
||
return False, f"服务离线: {str(e)}"
|
||
except Exception as e:
|
||
cls._service_status = {
|
||
"available": False,
|
||
"last_check": time.time(),
|
||
"error": str(e)
|
||
}
|
||
logger.error(f"美图API连接测试失败: {str(e)}")
|
||
return False, f"服务离线: {str(e)}"
|
||
|
||
async def cancel_task(self, task_id: str) -> bool:
|
||
"""取消处理任务"""
|
||
try:
|
||
# 设置取消任务的超时配置
|
||
cancel_timeout = aiohttp.ClientTimeout(total=15, connect=5) # 取消操作超时短一些
|
||
async with aiohttp.ClientSession(timeout=cancel_timeout) as session:
|
||
async with session.post(f'{self.api_url}/cancel_task',
|
||
data={'taskid': task_id},
|
||
ssl=False) as resp:
|
||
if resp.status == 200:
|
||
logger.info(f"任务取消成功: {task_id}")
|
||
return True
|
||
else:
|
||
logger.warning(f"任务取消失败: {task_id}, 状态码: {resp.status}")
|
||
return False
|
||
except Exception as e:
|
||
logger.error(f"取消任务异常: {task_id}, 错误: {str(e)}")
|
||
return False
|
||
|
||
async def cancel_all_tasks(self) -> None:
|
||
"""取消所有活跃任务"""
|
||
logger.warning(f"取消所有活跃任务,共{len(self._active_tasks)}个")
|
||
|
||
for task_id in list(self._active_tasks):
|
||
self._task_cancellation_tokens[task_id] = True
|
||
await self.cleanup_failed_task(task_id)
|
||
|
||
self._active_tasks.clear()
|
||
self._task_cancellation_tokens.clear()
|
||
|
||
async def cleanup_failed_task(self, task_id: str) -> None:
|
||
"""清理失败的任务"""
|
||
try:
|
||
# 尝试取消任务
|
||
await self.cancel_task(task_id)
|
||
|
||
# 清理可能的临时文件
|
||
# 这里可以添加更多清理逻辑
|
||
logger.info(f"失败任务清理完成: {task_id}")
|
||
except Exception as e:
|
||
logger.warning(f"清理失败任务异常: {task_id}, 错误: {str(e)}")
|
||
|
||
@classmethod
|
||
def get_service_stats(cls) -> Dict[str, Any]:
|
||
"""获取服务统计信息"""
|
||
return _service_stats.get_stats()
|
||
|
||
@classmethod
|
||
def reset_stats(cls) -> None:
|
||
"""重置服务统计信息"""
|
||
global _service_stats
|
||
_service_stats = MeituServiceStats()
|
||
|
||
@classmethod
|
||
def get_supported_modes(cls) -> Dict[str, str]:
|
||
"""获取支持的处理模式"""
|
||
return cls.SUPPORTED_MODES |