2025-09-07 21:48:21 +08:00
|
|
|
"""腾讯云客户端"""
|
|
|
|
|
|
|
|
|
|
import base64
|
|
|
|
|
import io
|
|
|
|
|
import json
|
|
|
|
|
import logging
|
|
|
|
|
import os
|
|
|
|
|
import random
|
|
|
|
|
import re
|
|
|
|
|
import requests
|
|
|
|
|
import time
|
|
|
|
|
import uuid
|
|
|
|
|
from typing import Dict, Any, List, Optional, Union, Callable
|
|
|
|
|
|
|
|
|
|
from tencentcloud.common import credential
|
|
|
|
|
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
|
|
|
|
from tencentcloud.iai.v20200303 import iai_client, models
|
|
|
|
|
|
|
|
|
|
from .const import (
|
|
|
|
|
CONF_SECRET_ID,
|
|
|
|
|
CONF_SECRET_KEY,
|
|
|
|
|
CONF_REGION,
|
|
|
|
|
)
|
|
|
|
|
from .errors import (
|
|
|
|
|
handle_api_error,
|
|
|
|
|
ImageNotFoundError,
|
|
|
|
|
FaceNotDetectedError,
|
|
|
|
|
log_and_raise_error,
|
|
|
|
|
validate_config,
|
|
|
|
|
NetworkError,
|
|
|
|
|
AuthenticationError,
|
|
|
|
|
RateLimitError,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
2026-05-01 23:14:39 +08:00
|
|
|
_LOGGER_POLLING = _LOGGER.getChild("polling")
|
|
|
|
|
_LOGGER_POLLING.setLevel(logging.DEBUG)
|
2025-09-07 21:48:21 +08:00
|
|
|
|
2026-05-01 23:14:39 +08:00
|
|
|
ENABLE_REQUEST_LOGGING = False
|
|
|
|
|
ENABLE_RESPONSE_LOGGING = False
|
|
|
|
|
ENABLE_PERFORMANCE_LOGGING = True
|
|
|
|
|
LOG_REQUEST_PAYLOAD_MAX_SIZE = 1024
|
|
|
|
|
LOG_RESPONSE_PAYLOAD_MAX_SIZE = 1024
|
2025-09-07 21:48:21 +08:00
|
|
|
|
2026-05-01 23:14:39 +08:00
|
|
|
MAX_ATTEMPTS = 3
|
|
|
|
|
INITIAL_RETRY_DELAY = 1
|
|
|
|
|
MAX_RETRY_DELAY = 30
|
|
|
|
|
RETRY_BACKOFF_FACTOR = 2
|
|
|
|
|
RETRY_JITTER = 0.1
|
2025-09-07 21:48:21 +08:00
|
|
|
|
|
|
|
|
NON_RETRYABLE_ERRORS = {
|
2026-05-01 23:14:39 +08:00
|
|
|
"AuthFailure",
|
|
|
|
|
"InvalidParameter",
|
|
|
|
|
"ResourceNotFound",
|
|
|
|
|
"LimitExceeded",
|
|
|
|
|
"RequestLimitExceeded",
|
|
|
|
|
"RequestQuotaExceeded",
|
2025-09-07 21:48:21 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RATE_LIMIT_ERRORS = {
|
|
|
|
|
"RequestLimitExceeded",
|
|
|
|
|
"RequestQuotaExceeded",
|
|
|
|
|
"LimitExceeded",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
NETWORK_ERRORS = {
|
|
|
|
|
"ResourceUnavailable.NetworkError",
|
|
|
|
|
"ResourceUnavailable.ServiceTimeout",
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 23:14:39 +08:00
|
|
|
MAX_IMAGE_SIZE = 10 * 1024 * 1024
|
|
|
|
|
MAX_IMAGE_DIMENSION = 4096
|
2025-09-07 21:48:21 +08:00
|
|
|
SUPPORTED_IMAGE_FORMATS = [".jpg", ".jpeg", ".png", ".bmp", ".gif"]
|
2026-05-01 23:14:39 +08:00
|
|
|
IMAGE_COMPRESSION_QUALITY = 85
|
|
|
|
|
MIN_FACE_SIZE = 34
|
2025-09-07 21:48:21 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class RetryConfig:
|
|
|
|
|
"""重试配置类"""
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
def __init__(
|
|
|
|
|
self,
|
2026-05-01 23:14:39 +08:00
|
|
|
max_attempts: int = MAX_ATTEMPTS,
|
2025-09-07 21:48:21 +08:00
|
|
|
initial_delay: float = INITIAL_RETRY_DELAY,
|
|
|
|
|
max_delay: float = MAX_RETRY_DELAY,
|
|
|
|
|
backoff_factor: float = RETRY_BACKOFF_FACTOR,
|
|
|
|
|
jitter: float = RETRY_JITTER
|
|
|
|
|
):
|
2026-05-01 23:14:39 +08:00
|
|
|
self.max_attempts = max_attempts
|
2025-09-07 21:48:21 +08:00
|
|
|
self.initial_delay = initial_delay
|
|
|
|
|
self.max_delay = max_delay
|
|
|
|
|
self.backoff_factor = backoff_factor
|
|
|
|
|
self.jitter = jitter
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TencentCloudClient:
|
|
|
|
|
"""腾讯云客户端"""
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
secret_id: str,
|
|
|
|
|
secret_key: str,
|
|
|
|
|
region: str = "ap-shanghai",
|
|
|
|
|
retry_config: Optional[RetryConfig] = None
|
|
|
|
|
):
|
|
|
|
|
config = {
|
|
|
|
|
"secret_id": secret_id,
|
|
|
|
|
"secret_key": secret_key,
|
|
|
|
|
"region": region
|
|
|
|
|
}
|
|
|
|
|
validate_config(config)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
self.secret_id = secret_id
|
|
|
|
|
self.secret_key = secret_key
|
|
|
|
|
self.region = region
|
|
|
|
|
self.retry_config = retry_config or RetryConfig()
|
|
|
|
|
self.client = self._create_client()
|
|
|
|
|
|
|
|
|
|
def _create_client(self) -> iai_client.IaiClient:
|
|
|
|
|
"""创建腾讯云人脸识别客户端"""
|
|
|
|
|
try:
|
|
|
|
|
_LOGGER.debug("创建腾讯云客户端,区域: %s", self.region)
|
|
|
|
|
cred = credential.Credential(self.secret_id, self.secret_key)
|
|
|
|
|
client = iai_client.IaiClient(cred, self.region)
|
|
|
|
|
_LOGGER.debug("腾讯云客户端创建成功")
|
|
|
|
|
return client
|
|
|
|
|
except Exception as ex:
|
2026-05-01 23:14:39 +08:00
|
|
|
_LOGGER.error("创建腾讯云客户端失败: %s", self._sanitize_error(str(ex)))
|
2025-09-07 21:48:21 +08:00
|
|
|
log_and_raise_error(
|
|
|
|
|
AuthenticationError,
|
2026-05-01 23:14:39 +08:00
|
|
|
"创建腾讯云客户端失败",
|
2025-09-07 21:48:21 +08:00
|
|
|
{"region": self.region, "error_type": type(ex).__name__}
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-01 23:14:39 +08:00
|
|
|
def verify_credentials(self) -> bool:
|
|
|
|
|
"""验证凭据是否有效,通过调用 GetGroupList 测试"""
|
|
|
|
|
try:
|
|
|
|
|
req = models.GetGroupListRequest()
|
|
|
|
|
params = {"Limit": 1, "Offset": 0}
|
|
|
|
|
req.from_json_string(json.dumps(params))
|
|
|
|
|
self.client.GetGroupList(req)
|
|
|
|
|
_LOGGER.info("凭据验证成功")
|
|
|
|
|
return True
|
|
|
|
|
except TencentCloudSDKException as ex:
|
|
|
|
|
_LOGGER.error("凭据验证失败: %s", ex.code)
|
|
|
|
|
handle_api_error(ex.code, ex.message)
|
|
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
def _handle_error(self, ex: TencentCloudSDKException) -> None:
|
|
|
|
|
"""处理API错误"""
|
|
|
|
|
_LOGGER.error(
|
2026-05-01 23:14:39 +08:00
|
|
|
"腾讯云API调用失败: 错误码=%s, 请求ID=%s",
|
|
|
|
|
ex.code, getattr(ex, 'request_id', '未知')
|
2025-09-07 21:48:21 +08:00
|
|
|
)
|
|
|
|
|
handle_api_error(ex.code, ex.message)
|
|
|
|
|
|
2026-05-01 23:14:39 +08:00
|
|
|
def _sanitize_error(self, error_msg: str) -> str:
|
|
|
|
|
"""脱敏错误信息中的凭据"""
|
|
|
|
|
if self.secret_id and self.secret_id in error_msg:
|
|
|
|
|
error_msg = error_msg.replace(self.secret_id, f"{self.secret_id[:8]}***")
|
|
|
|
|
if self.secret_key and self.secret_key in error_msg:
|
|
|
|
|
error_msg = error_msg.replace(self.secret_key, "***")
|
|
|
|
|
return error_msg
|
|
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
def _get_image_base64(self, image_url: str = None, image_path: str = None, image_file: str = None) -> str:
|
|
|
|
|
"""获取图片的base64编码"""
|
|
|
|
|
_LOGGER.debug(
|
|
|
|
|
"获取图片 base64: image_url=%s, image_path=%s, image_file is not None: %s",
|
|
|
|
|
image_url, image_path, image_file is not None
|
|
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
if not any([image_url, image_path, image_file]):
|
|
|
|
|
log_and_raise_error(
|
|
|
|
|
ValueError,
|
|
|
|
|
"必须提供image_url、image_path或image_file"
|
|
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
try:
|
|
|
|
|
if image_url:
|
|
|
|
|
_LOGGER.debug("使用图片URL: %s", image_url)
|
|
|
|
|
return self._download_image_as_base64(image_url)
|
|
|
|
|
elif image_path:
|
|
|
|
|
_LOGGER.debug("使用本地图片路径: %s", image_path)
|
|
|
|
|
return self._read_local_image_as_base64(image_path)
|
|
|
|
|
elif image_file:
|
|
|
|
|
_LOGGER.debug("使用上传的图片文件")
|
|
|
|
|
return self._process_uploaded_image_as_base64(image_file)
|
|
|
|
|
except Exception as ex:
|
2026-05-01 23:14:39 +08:00
|
|
|
_LOGGER.error("处理图片失败: %s", ex)
|
2025-09-07 21:48:21 +08:00
|
|
|
log_and_raise_error(
|
|
|
|
|
ImageNotFoundError,
|
|
|
|
|
f"处理图片失败: {str(ex)}",
|
|
|
|
|
{"image_url": image_url, "image_path": image_path, "error_type": type(ex).__name__}
|
|
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
def _download_image_as_base64(self, image_url: str) -> str:
|
|
|
|
|
"""下载图片并转换为base64编码"""
|
|
|
|
|
try:
|
|
|
|
|
_LOGGER.debug("开始下载图片: %s", image_url)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
if not self._is_valid_url(image_url):
|
|
|
|
|
log_and_raise_error(
|
|
|
|
|
ValueError,
|
|
|
|
|
f"无效的图片URL: {image_url}",
|
|
|
|
|
{"url": image_url}
|
|
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
session = requests.Session()
|
|
|
|
|
session.mount('http://', requests.adapters.HTTPAdapter(max_retries=2))
|
|
|
|
|
session.mount('https://', requests.adapters.HTTPAdapter(max_retries=2))
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
headers = {
|
2026-05-01 23:14:39 +08:00
|
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
2025-09-07 21:48:21 +08:00
|
|
|
}
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
response = session.get(image_url, timeout=10, headers=headers)
|
|
|
|
|
response.raise_for_status()
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
content_type = response.headers.get('content-type', '').lower()
|
|
|
|
|
if not content_type.startswith('image/'):
|
|
|
|
|
log_and_raise_error(
|
|
|
|
|
ValueError,
|
|
|
|
|
f"URL不是有效的图片: {image_url}, 内容类型: {content_type}",
|
|
|
|
|
{"url": image_url, "content_type": content_type}
|
|
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
image_data = self._process_image_data(response.content)
|
|
|
|
|
encoded_image = base64.b64encode(image_data).decode("utf-8")
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
_LOGGER.debug("成功下载并编码图片,大小: %d 字节", len(encoded_image))
|
|
|
|
|
return encoded_image
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
except requests.exceptions.Timeout:
|
|
|
|
|
_LOGGER.error("下载图片超时: %s", image_url)
|
|
|
|
|
log_and_raise_error(
|
|
|
|
|
NetworkError,
|
|
|
|
|
f"下载图片超时: {image_url}",
|
|
|
|
|
{"url": image_url, "error": "timeout"}
|
|
|
|
|
)
|
|
|
|
|
except requests.exceptions.ConnectionError:
|
|
|
|
|
_LOGGER.error("下载图片连接错误: %s", image_url)
|
|
|
|
|
log_and_raise_error(
|
|
|
|
|
NetworkError,
|
|
|
|
|
f"下载图片连接错误: {image_url}",
|
|
|
|
|
{"url": image_url, "error": "connection_error"}
|
|
|
|
|
)
|
|
|
|
|
except requests.exceptions.RequestException as ex:
|
2026-05-01 23:14:39 +08:00
|
|
|
_LOGGER.error("下载图片失败: %s", image_url)
|
2025-09-07 21:48:21 +08:00
|
|
|
log_and_raise_error(
|
|
|
|
|
NetworkError,
|
|
|
|
|
f"下载图片失败: {image_url}",
|
|
|
|
|
{"url": image_url, "error": str(ex)}
|
|
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
def _read_local_image_as_base64(self, image_path: str) -> str:
|
|
|
|
|
"""读取本地图片并转换为base64编码"""
|
|
|
|
|
try:
|
|
|
|
|
if not os.path.exists(image_path):
|
|
|
|
|
_LOGGER.error("图片文件不存在: %s", image_path)
|
|
|
|
|
log_and_raise_error(
|
|
|
|
|
ImageNotFoundError,
|
|
|
|
|
f"图片文件不存在: {image_path}",
|
|
|
|
|
{"path": image_path}
|
|
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
if not os.path.isfile(image_path):
|
|
|
|
|
_LOGGER.error("路径不是文件: %s", image_path)
|
|
|
|
|
log_and_raise_error(
|
|
|
|
|
ImageNotFoundError,
|
|
|
|
|
f"路径不是文件: {image_path}",
|
|
|
|
|
{"path": image_path}
|
|
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
file_ext = os.path.splitext(image_path)[1].lower()
|
|
|
|
|
if file_ext not in SUPPORTED_IMAGE_FORMATS:
|
|
|
|
|
_LOGGER.error("不支持的图片格式: %s", file_ext)
|
|
|
|
|
log_and_raise_error(
|
|
|
|
|
ImageNotFoundError,
|
|
|
|
|
f"不支持的图片格式: {file_ext}",
|
|
|
|
|
{"path": image_path, "supported_formats": SUPPORTED_IMAGE_FORMATS}
|
|
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
file_size = os.path.getsize(image_path)
|
|
|
|
|
if file_size > MAX_IMAGE_SIZE:
|
|
|
|
|
_LOGGER.error("图片文件过大: %s, 大小: %d 字节", image_path, file_size)
|
|
|
|
|
log_and_raise_error(
|
|
|
|
|
ImageNotFoundError,
|
|
|
|
|
f"图片文件过大: {image_path}",
|
|
|
|
|
{"path": image_path, "size": file_size, "max_size": f"{MAX_IMAGE_SIZE // (1024*1024)}MB"}
|
|
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
with open(image_path, "rb") as img_file:
|
|
|
|
|
image_data = img_file.read()
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
processed_image = self._process_image_data(image_data)
|
|
|
|
|
encoded_image = base64.b64encode(processed_image).decode("utf-8")
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
_LOGGER.debug("成功读取图片文件,大小: %d 字节", len(encoded_image))
|
|
|
|
|
return encoded_image
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
except PermissionError:
|
|
|
|
|
_LOGGER.error("没有权限读取图片文件: %s", image_path)
|
|
|
|
|
log_and_raise_error(
|
|
|
|
|
ImageNotFoundError,
|
|
|
|
|
f"没有权限读取图片文件: {image_path}",
|
|
|
|
|
{"path": image_path, "error": "permission_denied"}
|
|
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
except ImageNotFoundError:
|
|
|
|
|
raise
|
2025-09-07 21:48:21 +08:00
|
|
|
except Exception as ex:
|
2026-05-01 23:14:39 +08:00
|
|
|
_LOGGER.error("读取图片文件失败: %s", image_path)
|
2025-09-07 21:48:21 +08:00
|
|
|
log_and_raise_error(
|
|
|
|
|
ImageNotFoundError,
|
|
|
|
|
f"读取图片文件失败: {image_path}",
|
|
|
|
|
{"path": image_path, "error": str(ex)}
|
|
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
def _process_uploaded_image_as_base64(self, image_file: str) -> str:
|
|
|
|
|
"""处理上传的图片文件并转换为base64编码"""
|
|
|
|
|
try:
|
|
|
|
|
if isinstance(image_file, str):
|
|
|
|
|
if image_file.startswith('data:image'):
|
|
|
|
|
_LOGGER.debug("处理 data URL 格式图片")
|
|
|
|
|
try:
|
|
|
|
|
header, encoded = image_file.split(',', 1)
|
|
|
|
|
image_data = base64.b64decode(encoded)
|
|
|
|
|
processed_image = self._process_image_data(image_data)
|
|
|
|
|
return base64.b64encode(processed_image).decode("utf-8")
|
|
|
|
|
except ValueError:
|
|
|
|
|
log_and_raise_error(
|
|
|
|
|
ImageNotFoundError,
|
|
|
|
|
"无效的 data URL 格式",
|
|
|
|
|
{"data_url_length": len(image_file)}
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
_LOGGER.debug("使用 base64 编码图片,长度: %d", len(image_file))
|
|
|
|
|
try:
|
|
|
|
|
image_data = base64.b64decode(image_file)
|
2026-05-01 23:14:39 +08:00
|
|
|
if len(image_data) <= MAX_IMAGE_SIZE:
|
|
|
|
|
try:
|
|
|
|
|
from PIL import Image
|
|
|
|
|
img = Image.open(io.BytesIO(image_data))
|
|
|
|
|
if (img.size[0] <= MAX_IMAGE_DIMENSION
|
|
|
|
|
and img.size[1] <= MAX_IMAGE_DIMENSION
|
|
|
|
|
and img.format == 'JPEG'):
|
|
|
|
|
return image_file
|
|
|
|
|
except ImportError:
|
|
|
|
|
pass
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
processed_image = self._process_image_data(image_data)
|
|
|
|
|
return base64.b64encode(processed_image).decode("utf-8")
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
_LOGGER.error("base64解码失败: %s", ex)
|
|
|
|
|
log_and_raise_error(
|
|
|
|
|
ImageNotFoundError,
|
|
|
|
|
"base64解码失败",
|
|
|
|
|
{"error": str(ex), "data_length": len(image_file)}
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
_LOGGER.debug("处理文件对象")
|
|
|
|
|
image_data = image_file.read()
|
|
|
|
|
processed_image = self._process_image_data(image_data)
|
|
|
|
|
encoded_image = base64.b64encode(processed_image).decode("utf-8")
|
|
|
|
|
_LOGGER.debug("成功读取上传的图片文件,大小: %d 字节", len(encoded_image))
|
|
|
|
|
return encoded_image
|
2026-05-01 23:14:39 +08:00
|
|
|
|
|
|
|
|
except ImageNotFoundError:
|
|
|
|
|
raise
|
2025-09-07 21:48:21 +08:00
|
|
|
except Exception as ex:
|
|
|
|
|
_LOGGER.error("处理上传的图片文件失败: %s", ex)
|
|
|
|
|
log_and_raise_error(
|
|
|
|
|
ImageNotFoundError,
|
2026-05-01 23:14:39 +08:00
|
|
|
"处理上传的图片文件失败",
|
2025-09-07 21:48:21 +08:00
|
|
|
{"error": str(ex), "error_type": type(ex).__name__}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def search_faces(
|
|
|
|
|
self,
|
|
|
|
|
image_url: str = None,
|
|
|
|
|
image_path: str = None,
|
|
|
|
|
image_file: str = None,
|
2026-05-01 23:14:39 +08:00
|
|
|
image_base64: str = None,
|
2025-09-07 21:48:21 +08:00
|
|
|
group_ids: List[str] = None,
|
|
|
|
|
max_face_num: int = 1,
|
|
|
|
|
min_face_size: int = 34,
|
|
|
|
|
max_user_num: int = 5,
|
|
|
|
|
quality_control: int = 1,
|
|
|
|
|
need_rotate_check: int = 1,
|
|
|
|
|
face_match_threshold: float = 60.0
|
2025-09-07 23:21:24 +08:00
|
|
|
) -> Dict[str, Any]:
|
2025-09-07 21:48:21 +08:00
|
|
|
"""搜索人脸"""
|
|
|
|
|
_LOGGER.info(
|
2026-05-01 23:14:39 +08:00
|
|
|
"开始搜索人脸: max_face_num=%d, min_face_size=%d, max_user_num=%d, quality_control=%d, need_rotate_check=%d, face_match_threshold=%.2f",
|
|
|
|
|
max_face_num, min_face_size, max_user_num, quality_control, need_rotate_check, face_match_threshold
|
2025-09-07 21:48:21 +08:00
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
if not group_ids:
|
|
|
|
|
_LOGGER.warning("未提供人员库ID,搜索可能失败")
|
2026-05-01 23:14:39 +08:00
|
|
|
|
|
|
|
|
if image_base64 is None:
|
|
|
|
|
image_base64 = self._get_image_base64(image_url, image_path, image_file)
|
|
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
def api_call():
|
|
|
|
|
request_id = str(uuid.uuid4())
|
|
|
|
|
start_time = time.time()
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
try:
|
|
|
|
|
req = models.SearchFacesRequest()
|
|
|
|
|
params = {
|
|
|
|
|
"NeedPersonInfo": 1,
|
2026-05-01 23:14:39 +08:00
|
|
|
"Image": image_base64,
|
2025-09-07 21:48:21 +08:00
|
|
|
"MaxFaceNum": max_face_num,
|
|
|
|
|
"MinFaceSize": min_face_size,
|
|
|
|
|
"MaxPersonNum": max_user_num,
|
|
|
|
|
"QualityControl": quality_control,
|
|
|
|
|
"NeedRotateCheck": need_rotate_check,
|
|
|
|
|
"FaceMatchThreshold": face_match_threshold,
|
|
|
|
|
"GroupIds": group_ids
|
|
|
|
|
}
|
2026-05-01 23:14:39 +08:00
|
|
|
|
|
|
|
|
self._log_request("人脸搜索", params, request_id)
|
|
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
req.from_json_string(json.dumps(params))
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
_LOGGER.debug("发送人脸搜索请求")
|
|
|
|
|
resp = self.client.SearchFaces(req)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
duration = time.time() - start_time
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
self._log_response("人脸搜索", request_id, resp, duration)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
results = []
|
|
|
|
|
if getattr(resp, "Results", []):
|
|
|
|
|
_LOGGER.info("检测到 %d 个人脸", len(resp.Results))
|
|
|
|
|
for result in resp.Results:
|
|
|
|
|
face_result = {
|
|
|
|
|
"face_id": getattr(result, "FaceId", None),
|
|
|
|
|
"candidates": []
|
|
|
|
|
}
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
if result.Candidates:
|
|
|
|
|
for candidate in result.Candidates:
|
|
|
|
|
candidate_info = {
|
|
|
|
|
"person_id": candidate.PersonId,
|
|
|
|
|
"person_name": candidate.PersonName,
|
|
|
|
|
"score": candidate.Score,
|
|
|
|
|
"person_tag": getattr(candidate, "PersonTag", None)
|
|
|
|
|
}
|
|
|
|
|
face_result["candidates"].append(candidate_info)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
results.append(face_result)
|
|
|
|
|
else:
|
|
|
|
|
_LOGGER.info("未检测到人脸")
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 23:21:24 +08:00
|
|
|
return {
|
|
|
|
|
"success": True,
|
|
|
|
|
"faces": results,
|
|
|
|
|
"error": None,
|
|
|
|
|
"error_code": None,
|
|
|
|
|
"error_message": None
|
|
|
|
|
}
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
except Exception as ex:
|
|
|
|
|
duration = time.time() - start_time
|
|
|
|
|
self._log_error("人脸搜索", request_id, ex, duration)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 23:21:24 +08:00
|
|
|
error_code = None
|
|
|
|
|
error_message = str(ex)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 23:21:24 +08:00
|
|
|
if isinstance(ex, TencentCloudSDKException):
|
|
|
|
|
error_code = getattr(ex, "code", None)
|
|
|
|
|
error_message = getattr(ex, "message", str(ex))
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 23:21:24 +08:00
|
|
|
return {
|
|
|
|
|
"success": False,
|
|
|
|
|
"faces": [],
|
|
|
|
|
"error": str(ex),
|
|
|
|
|
"error_code": error_code,
|
|
|
|
|
"error_message": error_message
|
|
|
|
|
}
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
return self._execute_with_retry(api_call, "人脸搜索")
|
|
|
|
|
|
|
|
|
|
def detect_faces(
|
|
|
|
|
self,
|
|
|
|
|
image_url: str = None,
|
|
|
|
|
image_path: str = None,
|
|
|
|
|
image_file: str = None,
|
2026-05-01 23:14:39 +08:00
|
|
|
image_base64: str = None,
|
2025-09-07 21:48:21 +08:00
|
|
|
max_face_num: int = 1,
|
|
|
|
|
min_face_size: int = 34,
|
|
|
|
|
need_rotate_check: int = 1
|
2025-09-07 23:21:24 +08:00
|
|
|
) -> Dict[str, Any]:
|
2025-09-07 21:48:21 +08:00
|
|
|
"""检测人脸"""
|
|
|
|
|
_LOGGER.info(
|
|
|
|
|
"开始检测人脸: max_face_num=%d, min_face_size=%d, need_rotate_check=%d",
|
|
|
|
|
max_face_num, min_face_size, need_rotate_check
|
|
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
|
|
|
|
if image_base64 is None:
|
|
|
|
|
image_base64 = self._get_image_base64(image_url, image_path, image_file)
|
|
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
def api_call():
|
|
|
|
|
request_id = str(uuid.uuid4())
|
|
|
|
|
start_time = time.time()
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
try:
|
|
|
|
|
req = models.DetectFaceRequest()
|
|
|
|
|
params = {
|
2026-05-01 23:14:39 +08:00
|
|
|
"Image": image_base64,
|
2025-09-07 21:48:21 +08:00
|
|
|
"MaxFaceNum": max_face_num,
|
|
|
|
|
"MinFaceSize": min_face_size,
|
|
|
|
|
"NeedRotateCheck": need_rotate_check
|
|
|
|
|
}
|
2026-05-01 23:14:39 +08:00
|
|
|
|
|
|
|
|
self._log_request("人脸检测", params, request_id)
|
|
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
req.from_json_string(json.dumps(params))
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
_LOGGER.debug("发送人脸检测请求")
|
|
|
|
|
resp = self.client.DetectFace(req)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
duration = time.time() - start_time
|
|
|
|
|
self._log_response("人脸检测", request_id, resp, duration)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
results = []
|
|
|
|
|
if getattr(resp, "FaceInfos", []):
|
|
|
|
|
_LOGGER.info("检测到 %d 个人脸", len(resp.FaceInfos))
|
|
|
|
|
for face_info in resp.FaceInfos:
|
|
|
|
|
face_result = {
|
|
|
|
|
"face_id": getattr(face_info, "FaceId", ""),
|
|
|
|
|
"x": getattr(face_info, "X", 0),
|
|
|
|
|
"y": getattr(face_info, "Y", 0),
|
|
|
|
|
"width": getattr(face_info, "Width", 0),
|
|
|
|
|
"height": getattr(face_info, "Height", 0),
|
|
|
|
|
"face_rect": getattr(face_info, "FaceRect", None).__dict__ if getattr(face_info, "FaceRect", None) else None
|
|
|
|
|
}
|
|
|
|
|
results.append(face_result)
|
|
|
|
|
else:
|
|
|
|
|
_LOGGER.info("未检测到人脸")
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 23:21:24 +08:00
|
|
|
return {
|
|
|
|
|
"success": True,
|
|
|
|
|
"faces": results,
|
|
|
|
|
"error": None,
|
|
|
|
|
"error_code": None,
|
|
|
|
|
"error_message": None
|
|
|
|
|
}
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
except Exception as ex:
|
|
|
|
|
duration = time.time() - start_time
|
|
|
|
|
self._log_error("人脸检测", request_id, ex, duration)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 23:21:24 +08:00
|
|
|
error_code = None
|
|
|
|
|
error_message = str(ex)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 23:21:24 +08:00
|
|
|
if isinstance(ex, TencentCloudSDKException):
|
|
|
|
|
error_code = getattr(ex, "code", None)
|
|
|
|
|
error_message = getattr(ex, "message", str(ex))
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 23:21:24 +08:00
|
|
|
return {
|
|
|
|
|
"success": False,
|
|
|
|
|
"faces": [],
|
|
|
|
|
"error": str(ex),
|
|
|
|
|
"error_code": error_code,
|
|
|
|
|
"error_message": error_message
|
|
|
|
|
}
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
return self._execute_with_retry(api_call, "人脸检测")
|
|
|
|
|
|
|
|
|
|
def get_face_attributes(
|
|
|
|
|
self,
|
|
|
|
|
image_url: str = None,
|
|
|
|
|
image_path: str = None,
|
|
|
|
|
image_file: str = None,
|
2026-05-01 23:14:39 +08:00
|
|
|
image_base64: str = None,
|
2025-09-07 21:48:21 +08:00
|
|
|
max_face_num: int = 1,
|
|
|
|
|
need_rotate_check: int = 1
|
2025-09-07 23:21:24 +08:00
|
|
|
) -> Dict[str, Any]:
|
2025-09-07 21:48:21 +08:00
|
|
|
"""获取人脸属性"""
|
|
|
|
|
_LOGGER.info(
|
|
|
|
|
"开始获取人脸属性: max_face_num=%d, need_rotate_check=%d",
|
|
|
|
|
max_face_num, need_rotate_check
|
|
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
|
|
|
|
if image_base64 is None:
|
|
|
|
|
image_base64 = self._get_image_base64(image_url, image_path, image_file)
|
|
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
def api_call():
|
|
|
|
|
request_id = str(uuid.uuid4())
|
|
|
|
|
start_time = time.time()
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
try:
|
|
|
|
|
req = models.DetectFaceRequest()
|
|
|
|
|
params = {
|
2026-05-01 23:14:39 +08:00
|
|
|
"Image": image_base64,
|
2025-09-07 21:48:21 +08:00
|
|
|
"MaxFaceNum": max_face_num,
|
2026-05-01 23:14:39 +08:00
|
|
|
"NeedRotateCheck": need_rotate_check,
|
|
|
|
|
"NeedFaceAttributes": 1,
|
|
|
|
|
"NeedQualityDetection": 1,
|
2025-09-07 21:48:21 +08:00
|
|
|
}
|
2026-05-01 23:14:39 +08:00
|
|
|
|
|
|
|
|
self._log_request("获取人脸属性", params, request_id)
|
|
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
req.from_json_string(json.dumps(params))
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
_LOGGER.debug("发送获取人脸属性请求")
|
|
|
|
|
resp = self.client.DetectFace(req)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
duration = time.time() - start_time
|
|
|
|
|
self._log_response("获取人脸属性", request_id, resp, duration)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
results = []
|
|
|
|
|
if getattr(resp, "FaceInfos", []):
|
|
|
|
|
_LOGGER.info("检测到 %d 个人脸", len(resp.FaceInfos))
|
|
|
|
|
for face_info in resp.FaceInfos:
|
|
|
|
|
face_result = {
|
|
|
|
|
"face_id": getattr(face_info, "FaceId", ""),
|
|
|
|
|
"x": getattr(face_info, "X", 0),
|
|
|
|
|
"y": getattr(face_info, "Y", 0),
|
|
|
|
|
"width": getattr(face_info, "Width", 0),
|
|
|
|
|
"height": getattr(face_info, "Height", 0),
|
|
|
|
|
"gender": getattr(face_info, "Gender", None),
|
|
|
|
|
"age": getattr(face_info, "Age", None),
|
|
|
|
|
"expression": getattr(face_info, "Expression", None),
|
|
|
|
|
"beauty": getattr(face_info, "Beauty", None),
|
|
|
|
|
"face_rect": getattr(face_info, "FaceRect", None).__dict__ if getattr(face_info, "FaceRect", None) else None
|
|
|
|
|
}
|
|
|
|
|
results.append(face_result)
|
|
|
|
|
else:
|
|
|
|
|
_LOGGER.info("未检测到人脸")
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 23:21:24 +08:00
|
|
|
return {
|
|
|
|
|
"success": True,
|
|
|
|
|
"faces": results,
|
|
|
|
|
"error": None,
|
|
|
|
|
"error_code": None,
|
|
|
|
|
"error_message": None
|
|
|
|
|
}
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
except Exception as ex:
|
|
|
|
|
duration = time.time() - start_time
|
|
|
|
|
self._log_error("获取人脸属性", request_id, ex, duration)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 23:21:24 +08:00
|
|
|
error_code = None
|
|
|
|
|
error_message = str(ex)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 23:21:24 +08:00
|
|
|
if isinstance(ex, TencentCloudSDKException):
|
|
|
|
|
error_code = getattr(ex, "code", None)
|
|
|
|
|
error_message = getattr(ex, "message", str(ex))
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 23:21:24 +08:00
|
|
|
return {
|
|
|
|
|
"success": False,
|
|
|
|
|
"faces": [],
|
|
|
|
|
"error": str(ex),
|
|
|
|
|
"error_code": error_code,
|
|
|
|
|
"error_message": error_message
|
|
|
|
|
}
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
return self._execute_with_retry(api_call, "获取人脸属性")
|
2026-05-01 23:14:39 +08:00
|
|
|
|
|
|
|
|
def create_person(
|
|
|
|
|
self,
|
|
|
|
|
person_id: str,
|
|
|
|
|
person_name: str,
|
|
|
|
|
group_id: str,
|
|
|
|
|
image_url: str = None,
|
|
|
|
|
image_path: str = None,
|
|
|
|
|
image_file: str = None,
|
|
|
|
|
image_base64: str = None,
|
|
|
|
|
gender: int = None,
|
|
|
|
|
person_tag: str = None,
|
|
|
|
|
quality_control: int = 1,
|
|
|
|
|
need_rotate_check: int = 1,
|
|
|
|
|
) -> Dict[str, Any]:
|
|
|
|
|
"""创建人员"""
|
|
|
|
|
_LOGGER.info("开始创建人员: person_id=%s, group_id=%s", person_id, group_id)
|
|
|
|
|
|
|
|
|
|
if image_base64 is None and any([image_url, image_path, image_file]):
|
|
|
|
|
image_base64 = self._get_image_base64(image_url, image_path, image_file)
|
|
|
|
|
|
|
|
|
|
def api_call():
|
|
|
|
|
request_id = str(uuid.uuid4())
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
try:
|
|
|
|
|
req = models.CreatePersonRequest()
|
|
|
|
|
params = {
|
|
|
|
|
"PersonId": person_id,
|
|
|
|
|
"PersonName": person_name,
|
|
|
|
|
"GroupId": group_id,
|
|
|
|
|
"QualityControl": quality_control,
|
|
|
|
|
"NeedRotateCheck": need_rotate_check,
|
|
|
|
|
}
|
|
|
|
|
if image_base64:
|
|
|
|
|
params["Image"] = image_base64
|
|
|
|
|
if gender is not None:
|
|
|
|
|
params["Gender"] = gender
|
|
|
|
|
if person_tag is not None:
|
|
|
|
|
params["PersonTag"] = person_tag
|
|
|
|
|
|
|
|
|
|
self._log_request("创建人员", params, request_id)
|
|
|
|
|
req.from_json_string(json.dumps(params))
|
|
|
|
|
|
|
|
|
|
_LOGGER.debug("发送创建人员请求")
|
|
|
|
|
resp = self.client.CreatePerson(req)
|
|
|
|
|
|
|
|
|
|
duration = time.time() - start_time
|
|
|
|
|
self._log_response("创建人员", request_id, resp, duration)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"success": True,
|
|
|
|
|
"person_id": getattr(resp, "PersonId", person_id),
|
|
|
|
|
"face_id": getattr(resp, "FaceId", ""),
|
|
|
|
|
"face_rect": str(getattr(resp, "FaceRect", "")),
|
|
|
|
|
}
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
duration = time.time() - start_time
|
|
|
|
|
self._log_error("创建人员", request_id, ex, duration)
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
return self._execute_with_retry(api_call, "创建人员")
|
|
|
|
|
|
|
|
|
|
def delete_person(self, person_id: str) -> Dict[str, Any]:
|
|
|
|
|
"""删除人员"""
|
|
|
|
|
_LOGGER.info("开始删除人员: person_id=%s", person_id)
|
|
|
|
|
|
|
|
|
|
def api_call():
|
|
|
|
|
request_id = str(uuid.uuid4())
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
try:
|
|
|
|
|
req = models.DeletePersonRequest()
|
|
|
|
|
params = {"PersonId": person_id}
|
|
|
|
|
self._log_request("删除人员", params, request_id)
|
|
|
|
|
req.from_json_string(json.dumps(params))
|
|
|
|
|
|
|
|
|
|
_LOGGER.debug("发送删除人员请求")
|
|
|
|
|
resp = self.client.DeletePerson(req)
|
|
|
|
|
|
|
|
|
|
duration = time.time() - start_time
|
|
|
|
|
self._log_response("删除人员", request_id, resp, duration)
|
|
|
|
|
|
|
|
|
|
return {"success": True, "person_id": person_id}
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
duration = time.time() - start_time
|
|
|
|
|
self._log_error("删除人员", request_id, ex, duration)
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
return self._execute_with_retry(api_call, "删除人员")
|
|
|
|
|
|
|
|
|
|
def create_face(
|
|
|
|
|
self,
|
|
|
|
|
person_id: str,
|
|
|
|
|
image_url: str = None,
|
|
|
|
|
image_path: str = None,
|
|
|
|
|
image_file: str = None,
|
|
|
|
|
image_base64: str = None,
|
|
|
|
|
quality_control: int = 1,
|
|
|
|
|
need_rotate_check: int = 1,
|
|
|
|
|
) -> Dict[str, Any]:
|
|
|
|
|
"""为人员注册人脸"""
|
|
|
|
|
_LOGGER.info("开始注册人脸: person_id=%s", person_id)
|
|
|
|
|
|
|
|
|
|
if image_base64 is None:
|
|
|
|
|
image_base64 = self._get_image_base64(image_url, image_path, image_file)
|
|
|
|
|
|
|
|
|
|
def api_call():
|
|
|
|
|
request_id = str(uuid.uuid4())
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
try:
|
|
|
|
|
req = models.CreateFaceRequest()
|
|
|
|
|
params = {
|
|
|
|
|
"PersonId": person_id,
|
|
|
|
|
"Image": image_base64,
|
|
|
|
|
"QualityControl": quality_control,
|
|
|
|
|
"NeedRotateCheck": need_rotate_check,
|
|
|
|
|
}
|
|
|
|
|
self._log_request("注册人脸", params, request_id)
|
|
|
|
|
req.from_json_string(json.dumps(params))
|
|
|
|
|
|
|
|
|
|
_LOGGER.debug("发送注册人脸请求")
|
|
|
|
|
resp = self.client.CreateFace(req)
|
|
|
|
|
|
|
|
|
|
duration = time.time() - start_time
|
|
|
|
|
self._log_response("注册人脸", request_id, resp, duration)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"success": True,
|
|
|
|
|
"person_id": person_id,
|
|
|
|
|
"face_ids": getattr(resp, "FaceIds", []),
|
|
|
|
|
"face_rects": [str(r) for r in getattr(resp, "FaceRects", [])],
|
|
|
|
|
}
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
duration = time.time() - start_time
|
|
|
|
|
self._log_error("注册人脸", request_id, ex, duration)
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
return self._execute_with_retry(api_call, "注册人脸")
|
|
|
|
|
|
|
|
|
|
def delete_face(self, person_id: str, face_id: str) -> Dict[str, Any]:
|
|
|
|
|
"""删除人脸"""
|
|
|
|
|
_LOGGER.info("开始删除人脸: person_id=%s, face_id=%s", person_id, face_id)
|
|
|
|
|
|
|
|
|
|
def api_call():
|
|
|
|
|
request_id = str(uuid.uuid4())
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
try:
|
|
|
|
|
req = models.DeleteFaceRequest()
|
|
|
|
|
params = {
|
|
|
|
|
"PersonId": person_id,
|
|
|
|
|
"FaceId": face_id,
|
|
|
|
|
}
|
|
|
|
|
self._log_request("删除人脸", params, request_id)
|
|
|
|
|
req.from_json_string(json.dumps(params))
|
|
|
|
|
|
|
|
|
|
_LOGGER.debug("发送删除人脸请求")
|
|
|
|
|
resp = self.client.DeleteFace(req)
|
|
|
|
|
|
|
|
|
|
duration = time.time() - start_time
|
|
|
|
|
self._log_response("删除人脸", request_id, resp, duration)
|
|
|
|
|
|
|
|
|
|
return {"success": True, "person_id": person_id, "face_id": face_id}
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
duration = time.time() - start_time
|
|
|
|
|
self._log_error("删除人脸", request_id, ex, duration)
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
return self._execute_with_retry(api_call, "删除人脸")
|
|
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
def get_group_info(self, group_id: str) -> Dict[str, Any]:
|
|
|
|
|
"""获取人员库信息"""
|
|
|
|
|
_LOGGER.info("开始获取人员库信息: group_id=%s", group_id)
|
|
|
|
|
|
|
|
|
|
def api_call():
|
|
|
|
|
request_id = str(uuid.uuid4())
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
try:
|
|
|
|
|
req = models.GetGroupInfoRequest()
|
|
|
|
|
params = {"GroupId": group_id}
|
2026-05-01 23:14:39 +08:00
|
|
|
self._log_request("获取人员库信息", params, request_id)
|
2025-09-07 21:48:21 +08:00
|
|
|
req.from_json_string(json.dumps(params))
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
_LOGGER.debug("发送获取人员库信息请求")
|
|
|
|
|
resp = self.client.GetGroupInfo(req)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
duration = time.time() - start_time
|
|
|
|
|
self._log_response("获取人员库信息", request_id, resp, duration)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
return {
|
|
|
|
|
"group_name": getattr(resp, "GroupName", ""),
|
|
|
|
|
"group_tag": getattr(resp, "GroupTag", ""),
|
|
|
|
|
"face_model_version": getattr(resp, "FaceModelVersion", ""),
|
|
|
|
|
"creation_timestamp": getattr(resp, "CreationTimestamp", 0),
|
|
|
|
|
}
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
duration = time.time() - start_time
|
|
|
|
|
self._log_error("获取人员库信息", request_id, ex, duration)
|
|
|
|
|
raise
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
return self._execute_with_retry(api_call, "获取人员库信息")
|
|
|
|
|
|
2026-05-01 23:14:39 +08:00
|
|
|
def verify_credentials_lightweight(self) -> bool:
|
|
|
|
|
"""测试API连接,使用 GetGroupList 轻量验证凭据"""
|
|
|
|
|
try:
|
|
|
|
|
_LOGGER.info("开始测试API连接(轻量验证)")
|
|
|
|
|
req = models.GetGroupListRequest()
|
|
|
|
|
params = {"Limit": 1, "Offset": 0}
|
|
|
|
|
req.from_json_string(json.dumps(params))
|
|
|
|
|
self.client.GetGroupList(req)
|
|
|
|
|
_LOGGER.info("API连接测试成功")
|
|
|
|
|
return True
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
_LOGGER.error("API连接测试失败: %s", self._sanitize_error(str(ex)))
|
|
|
|
|
return False
|
|
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
def _test_api_connection(self, group_id: str) -> bool:
|
|
|
|
|
"""测试API连接"""
|
|
|
|
|
try:
|
|
|
|
|
_LOGGER.info("开始测试API连接: group_id=%s", group_id)
|
|
|
|
|
self.get_group_info(group_id)
|
|
|
|
|
_LOGGER.info("API连接测试成功")
|
|
|
|
|
return True
|
|
|
|
|
except Exception as ex:
|
2026-05-01 23:14:39 +08:00
|
|
|
_LOGGER.error("API连接测试失败: %s", self._sanitize_error(str(ex)))
|
2025-09-07 21:48:21 +08:00
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def get_person_list(self, group_id: str, limit: int = 100, offset: int = 0) -> List[Dict[str, Any]]:
|
|
|
|
|
"""获取人员列表"""
|
|
|
|
|
_LOGGER.info("开始获取人员列表: group_id=%s, limit=%d, offset=%d", group_id, limit, offset)
|
|
|
|
|
|
|
|
|
|
def api_call():
|
|
|
|
|
request_id = str(uuid.uuid4())
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
try:
|
|
|
|
|
req = models.GetPersonListRequest()
|
|
|
|
|
params = {
|
|
|
|
|
"GroupId": group_id,
|
|
|
|
|
"Limit": limit,
|
|
|
|
|
"Offset": offset
|
|
|
|
|
}
|
2026-05-01 23:14:39 +08:00
|
|
|
self._log_request("获取人员列表", params, request_id)
|
2025-09-07 21:48:21 +08:00
|
|
|
req.from_json_string(json.dumps(params))
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
_LOGGER.debug("发送获取人员列表请求")
|
|
|
|
|
resp = self.client.GetPersonList(req)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
duration = time.time() - start_time
|
|
|
|
|
self._log_response("获取人员列表", request_id, resp, duration)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
persons = []
|
|
|
|
|
if getattr(resp, "PersonInfos", []):
|
|
|
|
|
for person_info in resp.PersonInfos:
|
|
|
|
|
persons.append({
|
|
|
|
|
"person_name": getattr(person_info, "PersonName", ""),
|
|
|
|
|
"person_id": getattr(person_info, "PersonId", ""),
|
|
|
|
|
"gender": getattr(person_info, "Gender", 0),
|
|
|
|
|
"face_ids": getattr(person_info, "FaceIds", []),
|
|
|
|
|
})
|
|
|
|
|
return persons
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
duration = time.time() - start_time
|
|
|
|
|
self._log_error("获取人员列表", request_id, ex, duration)
|
|
|
|
|
raise
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
return self._execute_with_retry(api_call, "获取人员列表")
|
|
|
|
|
|
2026-05-01 23:14:39 +08:00
|
|
|
def get_person_list_all(self, group_id: str) -> List[Dict[str, Any]]:
|
|
|
|
|
"""获取人员列表(自动分页获取全部)"""
|
|
|
|
|
all_persons = []
|
|
|
|
|
offset = 0
|
|
|
|
|
limit = 100
|
|
|
|
|
while True:
|
|
|
|
|
persons = self.get_person_list(group_id, limit=limit, offset=offset)
|
|
|
|
|
all_persons.extend(persons)
|
|
|
|
|
if len(persons) < limit:
|
|
|
|
|
break
|
|
|
|
|
offset += limit
|
|
|
|
|
return all_persons
|
|
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
def _execute_with_retry(self, api_call: Callable, operation_name: str) -> Any:
|
2026-05-01 23:14:39 +08:00
|
|
|
"""执行带重试的API调用,max_attempts 为总尝试次数(1次初始 + N次重试)"""
|
2025-09-07 21:48:21 +08:00
|
|
|
last_exception = None
|
2026-05-01 23:14:39 +08:00
|
|
|
|
|
|
|
|
for attempt in range(self.retry_config.max_attempts):
|
2025-09-07 21:48:21 +08:00
|
|
|
try:
|
|
|
|
|
if attempt > 0:
|
|
|
|
|
delay = self._calculate_retry_delay(attempt)
|
2026-05-01 23:14:39 +08:00
|
|
|
_LOGGER.info("重试第 %d 次%s,延迟 %.2f 秒", attempt, operation_name, delay)
|
2025-09-07 21:48:21 +08:00
|
|
|
time.sleep(delay)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
return api_call()
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
except TencentCloudSDKException as ex:
|
|
|
|
|
last_exception = ex
|
2026-05-01 23:14:39 +08:00
|
|
|
_LOGGER.warning("%s失败 (尝试 %d/%d): %s", operation_name, attempt + 1, self.retry_config.max_attempts, ex.code)
|
|
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
if not self._should_retry(ex):
|
|
|
|
|
_LOGGER.error("遇到不可重试的错误: %s", ex.code)
|
|
|
|
|
self._handle_error(ex)
|
|
|
|
|
break
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
except Exception as ex:
|
|
|
|
|
last_exception = ex
|
2026-05-01 23:14:39 +08:00
|
|
|
_LOGGER.warning("%s失败 (尝试 %d/%d): %s", operation_name, attempt + 1, self.retry_config.max_attempts, ex)
|
|
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
if last_exception:
|
|
|
|
|
if isinstance(last_exception, TencentCloudSDKException):
|
|
|
|
|
self._handle_error(last_exception)
|
|
|
|
|
else:
|
2026-05-01 23:14:39 +08:00
|
|
|
_LOGGER.error("%s最终失败: %s", operation_name, last_exception)
|
2025-09-07 21:48:21 +08:00
|
|
|
log_and_raise_error(
|
|
|
|
|
RuntimeError,
|
|
|
|
|
f"{operation_name}失败: {str(last_exception)}",
|
|
|
|
|
{"error": str(last_exception), "error_type": type(last_exception).__name__}
|
|
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
def _calculate_retry_delay(self, attempt: int) -> float:
|
|
|
|
|
"""计算重试延迟时间"""
|
|
|
|
|
delay = self.retry_config.initial_delay * (
|
|
|
|
|
self.retry_config.backoff_factor ** (attempt - 1)
|
|
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
delay = min(delay, self.retry_config.max_delay)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
jitter = delay * self.retry_config.jitter
|
|
|
|
|
delay = delay + random.uniform(-jitter, jitter)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
return max(0, delay)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
def _should_retry(self, ex: TencentCloudSDKException) -> bool:
|
|
|
|
|
"""判断是否应该重试"""
|
|
|
|
|
if ex.code in NON_RETRYABLE_ERRORS:
|
|
|
|
|
return False
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
if ex.code in RATE_LIMIT_ERRORS:
|
|
|
|
|
return True
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
if ex.code in NETWORK_ERRORS:
|
|
|
|
|
return True
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
return True
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
def _is_valid_url(self, url: str) -> bool:
|
|
|
|
|
"""验证URL格式是否有效"""
|
|
|
|
|
url_pattern = re.compile(
|
2026-05-01 23:14:39 +08:00
|
|
|
r'^https?://'
|
|
|
|
|
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|'
|
|
|
|
|
r'localhost|'
|
|
|
|
|
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
|
|
|
|
|
r'(?::\d+)?'
|
2025-09-07 21:48:21 +08:00
|
|
|
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
|
|
|
|
|
return url_pattern.match(url) is not None
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
def _process_image_data(self, image_data: bytes) -> bytes:
|
|
|
|
|
"""处理图片数据,包括压缩和格式转换"""
|
|
|
|
|
try:
|
|
|
|
|
if len(image_data) > MAX_IMAGE_SIZE:
|
|
|
|
|
_LOGGER.warning("图片大小超过限制,尝试压缩: %d 字节", len(image_data))
|
|
|
|
|
image_data = self._compress_image(image_data)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
try:
|
|
|
|
|
from PIL import Image
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
img = Image.open(io.BytesIO(image_data))
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
width, height = img.size
|
|
|
|
|
if width > MAX_IMAGE_DIMENSION or height > MAX_IMAGE_DIMENSION:
|
|
|
|
|
_LOGGER.warning("图片尺寸过大,尝试缩放: %dx%d", width, height)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
scale = min(MAX_IMAGE_DIMENSION / width, MAX_IMAGE_DIMENSION / height)
|
|
|
|
|
new_width = int(width * scale)
|
|
|
|
|
new_height = int(height * scale)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
img = img.resize((new_width, new_height), Image.LANCZOS)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
output = io.BytesIO()
|
|
|
|
|
img.save(output, format='JPEG', quality=IMAGE_COMPRESSION_QUALITY)
|
|
|
|
|
image_data = output.getvalue()
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
_LOGGER.info("图片缩放完成: %dx%d -> %dx%d", width, height, new_width, new_height)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
if img.format != 'JPEG':
|
|
|
|
|
_LOGGER.debug("转换图片格式为JPEG: %s", img.format)
|
|
|
|
|
output = io.BytesIO()
|
|
|
|
|
if img.mode == 'RGBA':
|
|
|
|
|
img = img.convert('RGB')
|
|
|
|
|
img.save(output, format='JPEG', quality=IMAGE_COMPRESSION_QUALITY)
|
|
|
|
|
image_data = output.getvalue()
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
except ImportError:
|
2026-05-01 23:14:39 +08:00
|
|
|
_LOGGER.warning("PIL库(Pillow)未安装,无法进行图片处理。建议安装: pip install Pillow>=10.0.0")
|
2025-09-07 21:48:21 +08:00
|
|
|
except Exception as ex:
|
|
|
|
|
_LOGGER.warning("图片处理失败: %s", ex)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
return image_data
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
except Exception as ex:
|
2026-05-01 23:14:39 +08:00
|
|
|
_LOGGER.error("处理图片数据失败: %s", ex)
|
2025-09-07 21:48:21 +08:00
|
|
|
return image_data
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
def _compress_image(self, image_data: bytes) -> bytes:
|
|
|
|
|
"""压缩图片数据"""
|
|
|
|
|
try:
|
|
|
|
|
from PIL import Image
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
img = Image.open(io.BytesIO(image_data))
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
original_size = len(image_data)
|
2026-05-01 23:14:39 +08:00
|
|
|
target_size = MAX_IMAGE_SIZE * 0.8
|
2025-09-07 21:48:21 +08:00
|
|
|
quality = IMAGE_COMPRESSION_QUALITY
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
while quality > 30 and original_size > target_size:
|
|
|
|
|
output = io.BytesIO()
|
|
|
|
|
img.save(output, format='JPEG', quality=quality)
|
|
|
|
|
compressed_data = output.getvalue()
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
if len(compressed_data) <= target_size:
|
|
|
|
|
break
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
quality -= 5
|
|
|
|
|
original_size = len(compressed_data)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
_LOGGER.info("图片压缩完成: %d -> %d 字节 (质量: %d)",
|
|
|
|
|
len(image_data), len(compressed_data), quality)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
return compressed_data
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
except ImportError:
|
2026-05-01 23:14:39 +08:00
|
|
|
_LOGGER.warning("PIL库(Pillow)未安装,无法进行图片压缩。建议安装: pip install Pillow>=10.0.0")
|
2025-09-07 21:48:21 +08:00
|
|
|
return image_data
|
|
|
|
|
except Exception as ex:
|
2026-05-01 23:14:39 +08:00
|
|
|
_LOGGER.error("图片压缩失败: %s", ex)
|
2025-09-07 21:48:21 +08:00
|
|
|
return image_data
|
2026-05-01 23:14:39 +08:00
|
|
|
|
|
|
|
|
def _log_request(self, operation: str, params: Dict[str, Any], request_id: str) -> None:
|
2025-09-07 21:48:21 +08:00
|
|
|
"""记录API请求日志"""
|
|
|
|
|
if not ENABLE_REQUEST_LOGGING:
|
|
|
|
|
return
|
2026-05-01 23:14:39 +08:00
|
|
|
|
|
|
|
|
_LOGGER.debug(
|
2025-09-07 21:48:21 +08:00
|
|
|
"API请求: operation=%s, request_id=%s, params_count=%d",
|
|
|
|
|
operation, request_id, len(params)
|
|
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
if ENABLE_PERFORMANCE_LOGGING:
|
|
|
|
|
safe_params = {}
|
|
|
|
|
for key, value in params.items():
|
|
|
|
|
if key == "Image":
|
|
|
|
|
safe_params[key] = f"<base64_image_{len(str(value))}_chars>"
|
|
|
|
|
else:
|
|
|
|
|
safe_params[key] = value
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
params_str = json.dumps(safe_params, ensure_ascii=False)
|
|
|
|
|
if len(params_str) > LOG_REQUEST_PAYLOAD_MAX_SIZE:
|
|
|
|
|
params_str = params_str[:LOG_REQUEST_PAYLOAD_MAX_SIZE] + "...[truncated]"
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
_LOGGER.debug(
|
|
|
|
|
"API请求详情: operation=%s, request_id=%s, params=%s",
|
|
|
|
|
operation, request_id, params_str
|
|
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
def _log_response(self, operation: str, request_id: str, response: Any, duration: float) -> None:
|
|
|
|
|
"""记录API响应日志"""
|
|
|
|
|
if not ENABLE_RESPONSE_LOGGING:
|
|
|
|
|
return
|
2026-05-01 23:14:39 +08:00
|
|
|
|
|
|
|
|
_LOGGER.debug(
|
2025-09-07 21:48:21 +08:00
|
|
|
"API响应: operation=%s, request_id=%s, duration=%.3fs, response_type=%s",
|
|
|
|
|
operation, request_id, duration, type(response).__name__
|
|
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
if ENABLE_PERFORMANCE_LOGGING:
|
|
|
|
|
if duration > 5.0:
|
|
|
|
|
_LOGGER.warning(
|
|
|
|
|
"API响应缓慢: operation=%s, request_id=%s, duration=%.3fs",
|
|
|
|
|
operation, request_id, duration
|
|
|
|
|
)
|
|
|
|
|
elif duration > 2.0:
|
|
|
|
|
_LOGGER.info(
|
|
|
|
|
"API响应较慢: operation=%s, request_id=%s, duration=%.3fs",
|
|
|
|
|
operation, request_id, duration
|
|
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
try:
|
|
|
|
|
if hasattr(response, "__dict__"):
|
|
|
|
|
response_dict = {}
|
|
|
|
|
for attr in dir(response):
|
|
|
|
|
if not attr.startswith("_") and not callable(getattr(response, attr)):
|
|
|
|
|
response_dict[attr] = getattr(response, attr)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
response_str = json.dumps(response_dict, ensure_ascii=False, default=str)
|
|
|
|
|
else:
|
|
|
|
|
response_str = str(response)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
if len(response_str) > LOG_RESPONSE_PAYLOAD_MAX_SIZE:
|
|
|
|
|
response_str = response_str[:LOG_RESPONSE_PAYLOAD_MAX_SIZE] + "...[truncated]"
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
_LOGGER.debug(
|
|
|
|
|
"API响应详情: operation=%s, request_id=%s, response=%s",
|
|
|
|
|
operation, request_id, response_str
|
|
|
|
|
)
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
_LOGGER.debug(
|
|
|
|
|
"无法记录API响应详情: operation=%s, request_id=%s, error=%s",
|
|
|
|
|
operation, request_id, ex
|
|
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
def _log_error(self, operation: str, request_id: str, error: Exception, duration: float) -> None:
|
|
|
|
|
"""记录API错误日志"""
|
|
|
|
|
error_type = type(error).__name__
|
2026-05-01 23:14:39 +08:00
|
|
|
error_msg = self._sanitize_error(str(error))
|
|
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
_LOGGER.error(
|
2026-05-01 23:14:39 +08:00
|
|
|
"API错误: operation=%s, request_id=%s, duration=%.3fs, error_type=%s",
|
|
|
|
|
operation, request_id, duration, error_type
|
2025-09-07 21:48:21 +08:00
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
if isinstance(error, TencentCloudSDKException):
|
|
|
|
|
_LOGGER.error(
|
2026-05-01 23:14:39 +08:00
|
|
|
"腾讯云API错误详情: operation=%s, request_id=%s, code=%s",
|
|
|
|
|
operation, request_id, getattr(error, "code", "未知")
|
2025-09-07 21:48:21 +08:00
|
|
|
)
|
2026-05-01 23:14:39 +08:00
|
|
|
|
2025-09-07 21:48:21 +08:00
|
|
|
if _LOGGER.isEnabledFor(logging.DEBUG):
|
|
|
|
|
_LOGGER.debug(
|
|
|
|
|
"API错误堆栈跟踪: operation=%s, request_id=%s",
|
|
|
|
|
operation, request_id, exc_info=True
|
2026-05-01 23:14:39 +08:00
|
|
|
)
|