feat(image): 支持多模态模型输入base64格式图片

- 在消息转换中,增加对 `data:image/png;base64,...` 格式图片的支持,允许用户直接在输入中提供base64编码的图片。
- 调整图片处理逻辑,使其能够根据模型名称判断是否启用多模态能力,避免非多模态模型错误处理图片链接。
- 当未配置图床时,模型输出的图片将回退为base64格式,确保图片内容始终可用。
- 优化了相关函数的参数传递和代码格式,提高了代码的可读性和健壮性。
This commit is contained in:
snaily
2025-08-31 21:39:12 +08:00
parent b0127e6fc2
commit 611559d298
6 changed files with 181 additions and 110 deletions
+23 -9
View File
@@ -27,7 +27,7 @@ class MessageConverter(ABC):
@abstractmethod @abstractmethod
def convert( def convert(
self, messages: List[Dict[str, Any]] self, messages: List[Dict[str, Any]], model: str
) -> tuple[List[Dict[str, Any]], Optional[Dict[str, Any]]]: ) -> tuple[List[Dict[str, Any]], Optional[Dict[str, Any]]]:
pass pass
@@ -84,7 +84,7 @@ def _convert_image_to_base64(url: str) -> str:
raise Exception(f"Failed to fetch image: {response.status_code}") raise Exception(f"Failed to fetch image: {response.status_code}")
def _process_text_with_image(text: str) -> List[Dict[str, Any]]: def _process_text_with_image(text: str, model: str) -> List[Dict[str, Any]]:
""" """
处理可能包含图片URL的文本,提取图片并转换为base64 处理可能包含图片URL的文本,提取图片并转换为base64
@@ -94,17 +94,31 @@ def _process_text_with_image(text: str) -> List[Dict[str, Any]]:
Returns: Returns:
List[Dict[str, Any]]: 包含文本和图片的部分列表 List[Dict[str, Any]]: 包含文本和图片的部分列表
""" """
# 如果模型名中没有包含image,当作普通文本处理
if "image" not in model:
return [{"text": text}]
parts = [] parts = []
img_url_match = re.search(IMAGE_URL_PATTERN, text) img_url_match = re.search(IMAGE_URL_PATTERN, text)
if img_url_match: if img_url_match:
# 提取URL # 提取URL
img_url = img_url_match.group(2) img_url = img_url_match.group(2)
# 将URL对应的图片转换为base64 # 先判断是否是base64url如果是,直接用,不过不是,再将URL对应的图片转换为base64
try: try:
base64_data = _convert_image_to_base64(img_url) base64_url_match = re.search(DATA_URL_PATTERN, img_url)
parts.append( if base64_url_match:
{"inline_data": {"mimeType": "image/png", "data": base64_data}} parts.append(
) {
"inline_data": {
"mimeType": base64_url_match.group(1),
"data": base64_url_match.group(2),
}
}
)
else:
base64_data = _convert_image_to_base64(img_url)
parts.append(
{"inline_data": {"mimeType": "image/png", "data": base64_data}}
)
except Exception: except Exception:
# 如果转换失败,回退到文本模式 # 如果转换失败,回退到文本模式
parts.append({"text": text}) parts.append({"text": text})
@@ -145,7 +159,7 @@ class OpenAIMessageConverter(MessageConverter):
raise raise
def convert( def convert(
self, messages: List[Dict[str, Any]] self, messages: List[Dict[str, Any]], model: str
) -> tuple[List[Dict[str, Any]], Optional[Dict[str, Any]]]: ) -> tuple[List[Dict[str, Any]], Optional[Dict[str, Any]]]:
converted_messages = [] converted_messages = []
system_instruction_parts = [] system_instruction_parts = []
@@ -296,7 +310,7 @@ class OpenAIMessageConverter(MessageConverter):
elif ( elif (
"content" in msg and isinstance(msg["content"], str) and msg["content"] "content" in msg and isinstance(msg["content"], str) and msg["content"]
): ):
parts.extend(_process_text_with_image(msg["content"])) parts.extend(_process_text_with_image(msg["content"], model))
elif "tool_calls" in msg and isinstance(msg["tool_calls"], list): elif "tool_calls" in msg and isinstance(msg["tool_calls"], list):
# Keep existing tool call processing # Keep existing tool call processing
for tool_call in msg["tool_calls"]: for tool_call in msg["tool_calls"]:
+44 -22
View File
@@ -8,9 +8,9 @@ from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from app.config.config import settings from app.config.config import settings
from app.utils.uploader import ImageUploaderFactory
from app.log.logger import get_openai_logger from app.log.logger import get_openai_logger
from app.utils.helpers import is_image_upload_configured from app.utils.helpers import is_image_upload_configured
from app.utils.uploader import ImageUploaderFactory
logger = get_openai_logger() logger = get_openai_logger()
@@ -33,7 +33,11 @@ class GeminiResponseHandler(ResponseHandler):
self.thinking_status = False self.thinking_status = False
def handle_response( def handle_response(
self, response: Dict[str, Any], model: str, stream: bool = False, usage_metadata: Optional[Dict[str, Any]] = None self,
response: Dict[str, Any],
model: str,
stream: bool = False,
usage_metadata: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
if stream: if stream:
return _handle_gemini_stream_response(response, model, stream) return _handle_gemini_stream_response(response, model, stream)
@@ -41,7 +45,10 @@ class GeminiResponseHandler(ResponseHandler):
def _handle_openai_stream_response( def _handle_openai_stream_response(
response: Dict[str, Any], model: str, finish_reason: str, usage_metadata: Optional[Dict[str, Any]] response: Dict[str, Any],
model: str,
finish_reason: str,
usage_metadata: Optional[Dict[str, Any]],
) -> Dict[str, Any]: ) -> Dict[str, Any]:
choices = [] choices = []
candidates = response.get("candidates", []) candidates = response.get("candidates", [])
@@ -55,15 +62,15 @@ def _handle_openai_stream_response(
if not text and not tool_calls and not reasoning_content: if not text and not tool_calls and not reasoning_content:
delta = {} delta = {}
else: else:
delta = {"content": text, "reasoning_content": reasoning_content, "role": "assistant"} delta = {
"content": text,
"reasoning_content": reasoning_content,
"role": "assistant",
}
if tool_calls: if tool_calls:
delta["tool_calls"] = tool_calls delta["tool_calls"] = tool_calls
choice = { choice = {"index": index, "delta": delta, "finish_reason": finish_reason}
"index": index,
"delta": delta,
"finish_reason": finish_reason
}
choices.append(choice) choices.append(choice)
template_chunk = { template_chunk = {
@@ -74,12 +81,19 @@ def _handle_openai_stream_response(
"choices": choices, "choices": choices,
} }
if usage_metadata: if usage_metadata:
template_chunk["usage"] = {"prompt_tokens": usage_metadata.get("promptTokenCount", 0), "completion_tokens": usage_metadata.get("candidatesTokenCount",0), "total_tokens": usage_metadata.get("totalTokenCount", 0)} template_chunk["usage"] = {
"prompt_tokens": usage_metadata.get("promptTokenCount", 0),
"completion_tokens": usage_metadata.get("candidatesTokenCount", 0),
"total_tokens": usage_metadata.get("totalTokenCount", 0),
}
return template_chunk return template_chunk
def _handle_openai_normal_response( def _handle_openai_normal_response(
response: Dict[str, Any], model: str, finish_reason: str, usage_metadata: Optional[Dict[str, Any]] response: Dict[str, Any],
model: str,
finish_reason: str,
usage_metadata: Optional[Dict[str, Any]],
) -> Dict[str, Any]: ) -> Dict[str, Any]:
choices = [] choices = []
candidates = response.get("candidates", []) candidates = response.get("candidates", [])
@@ -106,7 +120,11 @@ def _handle_openai_normal_response(
"created": int(time.time()), "created": int(time.time()),
"model": model, "model": model,
"choices": choices, "choices": choices,
"usage": {"prompt_tokens": usage_metadata.get("promptTokenCount", 0), "completion_tokens": usage_metadata.get("candidatesTokenCount",0), "total_tokens": usage_metadata.get("totalTokenCount", 0)}, "usage": {
"prompt_tokens": usage_metadata.get("promptTokenCount", 0),
"completion_tokens": usage_metadata.get("candidatesTokenCount", 0),
"total_tokens": usage_metadata.get("totalTokenCount", 0),
},
} }
@@ -127,8 +145,12 @@ class OpenAIResponseHandler(ResponseHandler):
usage_metadata: Optional[Dict[str, Any]] = None, usage_metadata: Optional[Dict[str, Any]] = None,
) -> Optional[Dict[str, Any]]: ) -> Optional[Dict[str, Any]]:
if stream: if stream:
return _handle_openai_stream_response(response, model, finish_reason, usage_metadata) return _handle_openai_stream_response(
return _handle_openai_normal_response(response, model, finish_reason, usage_metadata) response, model, finish_reason, usage_metadata
)
return _handle_openai_normal_response(
response, model, finish_reason, usage_metadata
)
def handle_image_chat_response( def handle_image_chat_response(
self, image_str: str, model: str, stream=False, finish_reason="stop" self, image_str: str, model: str, stream=False, finish_reason="stop"
@@ -264,10 +286,6 @@ def _has_inline_image_part(response: Dict[str, Any]) -> bool:
def _extract_image_data(part: dict) -> str: def _extract_image_data(part: dict) -> str:
# Return empty string if no uploader is configured
if not is_image_upload_configured():
return ""
image_uploader = None image_uploader = None
if settings.UPLOAD_PROVIDER == "smms": if settings.UPLOAD_PROVIDER == "smms":
image_uploader = ImageUploaderFactory.create( image_uploader = ImageUploaderFactory.create(
@@ -287,13 +305,17 @@ def _extract_image_data(part: dict) -> str:
current_date = time.strftime("%Y/%m/%d") current_date = time.strftime("%Y/%m/%d")
filename = f"{current_date}/{uuid.uuid4().hex[:8]}.png" filename = f"{current_date}/{uuid.uuid4().hex[:8]}.png"
base64_data = part["inlineData"]["data"] base64_data = part["inlineData"]["data"]
mime_type = part["inlineData"]["mimeType"]
# 将base64_data转成bytes数组 # 将base64_data转成bytes数组
# Return empty string if no uploader is configured
if not is_image_upload_configured(settings):
return f"\n\n![image](data:{mime_type};base64,{base64_data})\n\n"
bytes_data = base64.b64decode(base64_data) bytes_data = base64.b64decode(base64_data)
upload_response = image_uploader.upload(bytes_data, filename) upload_response = image_uploader.upload(bytes_data, filename)
if upload_response.success: if upload_response.success:
text = f"\n\n![image]({upload_response.data.url})\n\n" text = f"\n\n![image]({upload_response.data.url})\n\n"
else: else:
text = "" text = f"\n\n![image](data:{mime_type};base64,{base64_data})\n\n"
return text return text
@@ -339,7 +361,7 @@ def _handle_gemini_stream_response(
response: Dict[str, Any], model: str, stream: bool response: Dict[str, Any], model: str, stream: bool
) -> Dict[str, Any]: ) -> Dict[str, Any]:
# Early return raw Gemini response if no uploader configured and contains inline images # Early return raw Gemini response if no uploader configured and contains inline images
if not is_image_upload_configured() and _has_inline_image_part(response): if not is_image_upload_configured(settings) and _has_inline_image_part(response):
return response return response
text, reasoning_content, tool_calls, thought = _extract_result( text, reasoning_content, tool_calls, thought = _extract_result(
@@ -360,7 +382,7 @@ def _handle_gemini_normal_response(
response: Dict[str, Any], model: str, stream: bool response: Dict[str, Any], model: str, stream: bool
) -> Dict[str, Any]: ) -> Dict[str, Any]:
# Early return raw Gemini response if no uploader configured and contains inline images # Early return raw Gemini response if no uploader configured and contains inline images
if not is_image_upload_configured() and _has_inline_image_part(response): if not is_image_upload_configured(settings) and _has_inline_image_part(response):
return response return response
text, reasoning_content, tool_calls, thought = _extract_result( text, reasoning_content, tool_calls, thought = _extract_result(
@@ -371,7 +393,7 @@ def _handle_gemini_normal_response(
parts = tool_calls parts = tool_calls
else: else:
if thought is not None: if thought is not None:
parts.append({"text": reasoning_content,"thought": thought}) parts.append({"text": reasoning_content, "thought": thought})
part = {"text": text} part = {"text": text}
parts.append(part) parts.append(part)
content = {"parts": parts, "role": "model"} content = {"parts": parts, "role": "model"}
+28 -8
View File
@@ -1,9 +1,8 @@
import logging import logging
import platform import platform
import sys
import re import re
import sys
from typing import Dict, Optional from typing import Dict, Optional
from app.utils.helpers import redact_key_for_logging as _redact_key_for_logging
# ANSI转义序列颜色代码 # ANSI转义序列颜色代码
COLORS = { COLORS = {
@@ -15,7 +14,6 @@ COLORS = {
} }
# Windows系统启用ANSI支持 # Windows系统启用ANSI支持
if platform.system() == "Windows": if platform.system() == "Windows":
import ctypes import ctypes
@@ -46,14 +44,16 @@ class AccessLogFormatter(logging.Formatter):
# API key patterns to match in URLs # API key patterns to match in URLs
API_KEY_PATTERNS = [ API_KEY_PATTERNS = [
r'\bAIza[0-9A-Za-z_-]{35}', # Google API keys (like Gemini) r"\bAIza[0-9A-Za-z_-]{35}", # Google API keys (like Gemini)
r'\bsk-[0-9A-Za-z_-]{20,}', # OpenAI and general sk- prefixed keys r"\bsk-[0-9A-Za-z_-]{20,}", # OpenAI and general sk- prefixed keys
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Compile regex patterns for better performance # Compile regex patterns for better performance
self.compiled_patterns = [re.compile(pattern) for pattern in self.API_KEY_PATTERNS] self.compiled_patterns = [
re.compile(pattern) for pattern in self.API_KEY_PATTERNS
]
def format(self, record): def format(self, record):
# Format the record normally first # Format the record normally first
@@ -68,9 +68,10 @@ class AccessLogFormatter(logging.Formatter):
""" """
try: try:
for pattern in self.compiled_patterns: for pattern in self.compiled_patterns:
def replace_key(match): def replace_key(match):
key = match.group(0) key = match.group(0)
return _redact_key_for_logging(key) return redact_key_for_logging(key)
message = pattern.sub(replace_key, message) message = pattern.sub(replace_key, message)
@@ -78,11 +79,31 @@ class AccessLogFormatter(logging.Formatter):
except Exception as e: except Exception as e:
# Log the error but don't expose the original message in case it contains keys # Log the error but don't expose the original message in case it contains keys
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.error(f"Error redacting API keys in access log: {e}") logger.error(f"Error redacting API keys in access log: {e}")
return "[LOG_REDACTION_ERROR]" return "[LOG_REDACTION_ERROR]"
def redact_key_for_logging(key: str) -> str:
"""
Redacts API key for secure logging by showing only first and last 6 characters.
Args:
key: API key to redact
Returns:
str: Redacted key in format "first6...last6" or descriptive placeholder for edge cases
"""
if not key:
return key
if len(key) <= 12:
return f"{key[:3]}...{key[-3:]}"
else:
return f"{key[:6]}...{key[-6:]}"
# 日志格式 - 使用 fileloc 并设置固定宽度 (例如 30) # 日志格式 - 使用 fileloc 并设置固定宽度 (例如 30)
FORMATTER = ColoredFormatter( FORMATTER = ColoredFormatter(
"%(asctime)s | %(levelname)-17s | %(fileloc)-30s | %(message)s" "%(asctime)s | %(levelname)-17s | %(fileloc)-30s | %(message)s"
@@ -326,4 +347,3 @@ def setup_access_logging():
access_logger.propagate = False access_logger.propagate = False
return access_logger return access_logger
+3 -1
View File
@@ -285,7 +285,9 @@ class OpenAIChatService:
api_key: str, api_key: str,
) -> Union[Dict[str, Any], AsyncGenerator[str, None]]: ) -> Union[Dict[str, Any], AsyncGenerator[str, None]]:
"""创建聊天完成""" """创建聊天完成"""
messages, instruction = self.message_converter.convert(request.messages) messages, instruction = self.message_converter.convert(
request.messages, request.model
)
payload = _build_payload(request, messages, instruction) payload = _build_payload(request, messages, instruction)
+5 -2
View File
@@ -9,8 +9,8 @@ from app.config.config import settings
from app.core.constants import VALID_IMAGE_RATIOS from app.core.constants import VALID_IMAGE_RATIOS
from app.domain.openai_models import ImageGenerationRequest from app.domain.openai_models import ImageGenerationRequest
from app.log.logger import get_image_create_logger from app.log.logger import get_image_create_logger
from app.utils.uploader import ImageUploaderFactory
from app.utils.helpers import is_image_upload_configured from app.utils.helpers import is_image_upload_configured
from app.utils.uploader import ImageUploaderFactory
logger = get_image_create_logger() logger = get_image_create_logger()
@@ -99,7 +99,10 @@ class ImageCreateService:
image_uploader = None image_uploader = None
# Return base64 if explicitly requested or if no uploader is configured # Return base64 if explicitly requested or if no uploader is configured
if request.response_format == "b64_json" or not is_image_upload_configured(): if (
request.response_format == "b64_json"
or not is_image_upload_configured(settings)
):
base64_image = base64.b64encode(image_data).decode("utf-8") base64_image = base64.b64encode(image_data).decode("utf-8")
images_data.append( images_data.append(
{"b64_json": base64_image, "revised_prompt": request.prompt} {"b64_json": base64_image, "revised_prompt": request.prompt}
+42 -32
View File
@@ -1,14 +1,17 @@
""" """
通用工具函数模块 通用工具函数模块
""" """
import json
import re
import base64
import requests
from typing import Dict, Any, List, Optional, Tuple
from pathlib import Path
import logging
import base64
import json
import logging
import re
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
import requests
from app.config.config import Settings
from app.core.constants import DATA_URL_PATTERN, IMAGE_URL_PATTERN, VALID_IMAGE_RATIOS from app.core.constants import DATA_URL_PATTERN, IMAGE_URL_PATTERN, VALID_IMAGE_RATIOS
helper_logger = logging.getLogger("app.utils") helper_logger = logging.getLogger("app.utils")
@@ -28,12 +31,14 @@ def extract_mime_type_and_data(base64_string: str) -> Tuple[Optional[str], str]:
tuple: (mime_type, encoded_data) tuple: (mime_type, encoded_data)
""" """
# 检查字符串是否以 "data:" 格式开始 # 检查字符串是否以 "data:" 格式开始
if base64_string.startswith('data:'): if base64_string.startswith("data:"):
# 提取 MIME 类型和数据 # 提取 MIME 类型和数据
pattern = DATA_URL_PATTERN pattern = DATA_URL_PATTERN
match = re.match(pattern, base64_string) match = re.match(pattern, base64_string)
if match: if match:
mime_type = "image/jpeg" if match.group(1) == "image/jpg" else match.group(1) mime_type = (
"image/jpeg" if match.group(1) == "image/jpg" else match.group(1)
)
encoded_data = match.group(2) encoded_data = match.group(2)
return mime_type, encoded_data return mime_type, encoded_data
@@ -57,7 +62,7 @@ def convert_image_to_base64(url: str) -> str:
response = requests.get(url) response = requests.get(url)
if response.status_code == 200: if response.status_code == 200:
# 将图片内容转换为base64 # 将图片内容转换为base64
img_data = base64.b64encode(response.content).decode('utf-8') img_data = base64.b64encode(response.content).decode("utf-8")
return img_data return img_data
else: else:
raise Exception(f"Failed to fetch image: {response.status_code}") raise Exception(f"Failed to fetch image: {response.status_code}")
@@ -77,7 +82,9 @@ def format_json_response(data: Dict[str, Any], indent: int = 2) -> str:
return json.dumps(data, indent=indent, ensure_ascii=False) return json.dumps(data, indent=indent, ensure_ascii=False)
def parse_prompt_parameters(prompt: str, default_ratio: str = "1:1") -> Tuple[str, int, str]: def parse_prompt_parameters(
prompt: str, default_ratio: str = "1:1"
) -> Tuple[str, int, str]:
""" """
从prompt中解析参数 从prompt中解析参数
@@ -97,22 +104,22 @@ def parse_prompt_parameters(prompt: str, default_ratio: str = "1:1") -> Tuple[st
aspect_ratio = default_ratio aspect_ratio = default_ratio
# 解析n参数 # 解析n参数
n_match = re.search(r'{n:(\d+)}', prompt) n_match = re.search(r"{n:(\d+)}", prompt)
if n_match: if n_match:
n = int(n_match.group(1)) n = int(n_match.group(1))
if n < 1 or n > 4: if n < 1 or n > 4:
raise ValueError(f"Invalid n value: {n}. Must be between 1 and 4.") raise ValueError(f"Invalid n value: {n}. Must be between 1 and 4.")
prompt = prompt.replace(n_match.group(0), '').strip() prompt = prompt.replace(n_match.group(0), "").strip()
# 解析ratio参数 # 解析ratio参数
ratio_match = re.search(r'{ratio:(\d+:\d+)}', prompt) ratio_match = re.search(r"{ratio:(\d+:\d+)}", prompt)
if ratio_match: if ratio_match:
aspect_ratio = ratio_match.group(1) aspect_ratio = ratio_match.group(1)
if aspect_ratio not in VALID_IMAGE_RATIOS: if aspect_ratio not in VALID_IMAGE_RATIOS:
raise ValueError( raise ValueError(
f"Invalid ratio: {aspect_ratio}. Must be one of: {', '.join(VALID_IMAGE_RATIOS)}" f"Invalid ratio: {aspect_ratio}. Must be one of: {', '.join(VALID_IMAGE_RATIOS)}"
) )
prompt = prompt.replace(ratio_match.group(0), '').strip() prompt = prompt.replace(ratio_match.group(0), "").strip()
return prompt, n, aspect_ratio return prompt, n, aspect_ratio
@@ -143,17 +150,16 @@ def is_valid_api_key(key: str) -> bool:
bool: 如果密钥格式有效则返回True bool: 如果密钥格式有效则返回True
""" """
# 检查Gemini API密钥格式 # 检查Gemini API密钥格式
if key.startswith('AIza'): if key.startswith("AIza"):
return len(key) >= 30 return len(key) >= 30
# 检查OpenAI API密钥格式 # 检查OpenAI API密钥格式
if key.startswith('sk-'): if key.startswith("sk-"):
return len(key) >= 30 return len(key) >= 30
return False return False
def redact_key_for_logging(key: str) -> str: def redact_key_for_logging(key: str) -> str:
""" """
Redacts API key for secure logging by showing only first and last 6 characters. Redacts API key for secure logging by showing only first and last 6 characters.
@@ -177,26 +183,28 @@ def get_current_version(default_version: str = "0.0.0") -> str:
"""Reads the current version from the VERSION file.""" """Reads the current version from the VERSION file."""
version_file = VERSION_FILE_PATH version_file = VERSION_FILE_PATH
try: try:
with version_file.open('r', encoding='utf-8') as f: with version_file.open("r", encoding="utf-8") as f:
version = f.read().strip() version = f.read().strip()
if not version: if not version:
helper_logger.warning(f"VERSION file ('{version_file}') is empty. Using default version '{default_version}'.") helper_logger.warning(
f"VERSION file ('{version_file}') is empty. Using default version '{default_version}'."
)
return default_version return default_version
return version return version
except FileNotFoundError: except FileNotFoundError:
helper_logger.warning(f"VERSION file not found at '{version_file}'. Using default version '{default_version}'.") helper_logger.warning(
f"VERSION file not found at '{version_file}'. Using default version '{default_version}'."
)
return default_version return default_version
except IOError as e: except IOError as e:
helper_logger.error(f"Error reading VERSION file ('{version_file}'): {e}. Using default version '{default_version}'.") helper_logger.error(
f"Error reading VERSION file ('{version_file}'): {e}. Using default version '{default_version}'."
)
return default_version return default_version
def is_image_upload_configured() -> bool: def is_image_upload_configured(settings: Settings) -> bool:
"""Return True only if a valid upload provider is selected and all required settings for that provider are present. Uses lazy import to avoid circular imports.""" """Return True only if a valid upload provider is selected and all required settings for that provider are present."""
try:
from app.config.config import settings # local import to avoid circular dependency at module import time
except Exception:
return False
provider = (getattr(settings, "UPLOAD_PROVIDER", "") or "").strip().lower() provider = (getattr(settings, "UPLOAD_PROVIDER", "") or "").strip().lower()
if provider == "smms": if provider == "smms":
@@ -204,8 +212,10 @@ def is_image_upload_configured() -> bool:
if provider == "picgo": if provider == "picgo":
return bool(getattr(settings, "PICGO_API_KEY", "")) return bool(getattr(settings, "PICGO_API_KEY", ""))
if provider == "cloudflare_imgbed": if provider == "cloudflare_imgbed":
return all([ return all(
getattr(settings, "CLOUDFLARE_IMGBED_URL", ""), [
getattr(settings, "CLOUDFLARE_IMGBED_AUTH_CODE", ""), getattr(settings, "CLOUDFLARE_IMGBED_URL", ""),
]) getattr(settings, "CLOUDFLARE_IMGBED_AUTH_CODE", ""),
]
)
return False return False