feat: fix i18n missing keys and merge upstream/main (#24615)

Signed-off-by: -LAN- <laipz8200@outlook.com>
Signed-off-by: kenwoodjw <blackxin55+@gmail.com>
Signed-off-by: Yongtao Huang <yongtaoh2022@gmail.com>
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
Signed-off-by: zhanluxianshen <zhanluxianshen@163.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: GuanMu <ballmanjq@gmail.com>
Co-authored-by: Davide Delbianco <davide.delbianco@outlook.com>
Co-authored-by: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com>
Co-authored-by: kenwoodjw <blackxin55+@gmail.com>
Co-authored-by: Yongtao Huang <yongtaoh2022@gmail.com>
Co-authored-by: Yongtao Huang <99629139+hyongtao-db@users.noreply.github.com>
Co-authored-by: Qiang Lee <18018968632@163.com>
Co-authored-by: 李强04 <liqiang04@gaotu.cn>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Asuka Minato <i@asukaminato.eu.org>
Co-authored-by: Matri Qi <matrixdom@126.com>
Co-authored-by: huayaoyue6 <huayaoyue@163.com>
Co-authored-by: Bowen Liang <liangbowen@gf.com.cn>
Co-authored-by: znn <jubinkumarsoni@gmail.com>
Co-authored-by: crazywoola <427733928@qq.com>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: yihong <zouzou0208@gmail.com>
Co-authored-by: Muke Wang <shaodwaaron@gmail.com>
Co-authored-by: wangmuke <wangmuke@kingsware.cn>
Co-authored-by: Wu Tianwei <30284043+WTW0313@users.noreply.github.com>
Co-authored-by: quicksand <quicksandzn@gmail.com>
Co-authored-by: 非法操作 <hjlarry@163.com>
Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
Co-authored-by: Eric Guo <eric.guocz@gmail.com>
Co-authored-by: Zhedong Cen <cenzhedong2@126.com>
Co-authored-by: jiangbo721 <jiangbo721@163.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: hjlarry <25834719+hjlarry@users.noreply.github.com>
Co-authored-by: lxsummer <35754229+lxjustdoit@users.noreply.github.com>
Co-authored-by: 湛露先生 <zhanluxianshen@163.com>
Co-authored-by: Guangdong Liu <liugddx@gmail.com>
Co-authored-by: QuantumGhost <obelisk.reg+git@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Yessenia-d <yessenia.contact@gmail.com>
Co-authored-by: huangzhuo1949 <167434202+huangzhuo1949@users.noreply.github.com>
Co-authored-by: huangzhuo <huangzhuo1@xiaomi.com>
Co-authored-by: 17hz <0x149527@gmail.com>
Co-authored-by: Amy <1530140574@qq.com>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: Nite Knite <nkCoding@gmail.com>
Co-authored-by: Yeuoly <45712896+Yeuoly@users.noreply.github.com>
Co-authored-by: Petrus Han <petrus.hanks@gmail.com>
Co-authored-by: iamjoel <2120155+iamjoel@users.noreply.github.com>
Co-authored-by: Kalo Chin <frog.beepers.0n@icloud.com>
Co-authored-by: Ujjwal Maurya <ujjwalsbx@gmail.com>
Co-authored-by: Maries <xh001x@hotmail.com>
This commit is contained in:
lyzno1
2025-08-27 15:07:28 +08:00
committed by GitHub
parent a63d1e87b1
commit 5bbf685035
625 changed files with 23778 additions and 10693 deletions

View File

@ -1,119 +1,128 @@
import re
import sys
from collections.abc import Mapping
from typing import Any
from flask import current_app, got_request_exception
from flask_restful import Api, http_status_message
from werkzeug.datastructures import Headers
from flask import Blueprint, Flask, current_app, got_request_exception
from flask_restx import Api
from werkzeug.exceptions import HTTPException
from werkzeug.http import HTTP_STATUS_CODES
from configs import dify_config
from core.errors.error import AppInvokeQuotaExceededError
class ExternalApi(Api):
def handle_error(self, e):
"""Error handler for the API transforms a raised exception into a Flask
response, with the appropriate HTTP status code and body.
def http_status_message(code):
return HTTP_STATUS_CODES.get(code, "")
:param e: the raised Exception object
:type e: Exception
"""
def register_external_error_handlers(api: Api) -> None:
@api.errorhandler(HTTPException)
def handle_http_exception(e: HTTPException):
got_request_exception.send(current_app, exception=e)
headers = Headers()
if isinstance(e, HTTPException):
if e.response is not None:
resp = e.get_response()
return resp
# If Werkzeug already prepared a Response, just use it.
if getattr(e, "response", None) is not None:
return e.response
status_code = e.code
default_data = {
"code": re.sub(r"(?<!^)(?=[A-Z])", "_", type(e).__name__).lower(),
"message": getattr(e, "description", http_status_message(status_code)),
"status": status_code,
}
status_code = getattr(e, "code", 500) or 500
if (
default_data["message"]
and default_data["message"] == "Failed to decode JSON object: Expecting value: line 1 column 1 (char 0)"
):
default_data["message"] = "Invalid JSON payload received or JSON payload is empty."
# Build a safe, dict-like payload
default_data = {
"code": re.sub(r"(?<!^)(?=[A-Z])", "_", type(e).__name__).lower(),
"message": getattr(e, "description", http_status_message(status_code)),
"status": status_code,
}
if default_data["message"] == "Failed to decode JSON object: Expecting value: line 1 column 1 (char 0)":
default_data["message"] = "Invalid JSON payload received or JSON payload is empty."
headers = e.get_response().headers
elif isinstance(e, ValueError):
status_code = 400
default_data = {
"code": "invalid_param",
"message": str(e),
"status": status_code,
}
elif isinstance(e, AppInvokeQuotaExceededError):
status_code = 429
default_data = {
"code": "too_many_requests",
"message": str(e),
"status": status_code,
}
else:
status_code = 500
default_data = {
"message": http_status_message(status_code),
}
# Use headers on the exception if present; otherwise none.
headers = {}
exc_headers = getattr(e, "headers", None)
if exc_headers:
headers.update(exc_headers)
# Werkzeug exceptions generate a content-length header which is added
# to the response in addition to the actual content-length header
# https://github.com/flask-restful/flask-restful/issues/534
remove_headers = ("Content-Length",)
for header in remove_headers:
headers.pop(header, None)
data = getattr(e, "data", default_data)
error_cls_name = type(e).__name__
if error_cls_name in self.errors:
custom_data = self.errors.get(error_cls_name, {})
custom_data = custom_data.copy()
status_code = custom_data.get("status", 500)
if "message" in custom_data:
custom_data["message"] = custom_data["message"].format(
message=str(e.description if hasattr(e, "description") else e)
)
data.update(custom_data)
# record the exception in the logs when we have a server error of status code: 500
if status_code and status_code >= 500:
exc_info: Any = sys.exc_info()
if exc_info[1] is None:
exc_info = None
current_app.log_exception(exc_info)
if status_code == 406 and self.default_mediatype is None:
# if we are handling NotAcceptable (406), make sure that
# make_response uses a representation we support as the
# default mediatype (so that make_response doesn't throw
# another NotAcceptable error).
supported_mediatypes = list(self.representations.keys()) # only supported application/json
fallback_mediatype = supported_mediatypes[0] if supported_mediatypes else "text/plain"
data = {"code": "not_acceptable", "message": data.get("message")}
resp = self.make_response(data, status_code, headers, fallback_mediatype=fallback_mediatype)
# Payload per status
if status_code == 406 and api.default_mediatype is None:
data = {"code": "not_acceptable", "message": default_data["message"], "status": status_code}
return data, status_code, headers
elif status_code == 400:
if isinstance(data.get("message"), dict):
param_key, param_value = list(data.get("message", {}).items())[0]
data = {"code": "invalid_param", "message": param_value, "params": param_key}
msg = default_data["message"]
if isinstance(msg, Mapping) and msg:
# Convert param errors like {"field": "reason"} into a friendly shape
param_key, param_value = next(iter(msg.items()))
data = {
"code": "invalid_param",
"message": str(param_value),
"params": param_key,
"status": status_code,
}
else:
if "code" not in data:
data["code"] = "unknown"
resp = self.make_response(data, status_code, headers)
data = {**default_data}
data.setdefault("code", "unknown")
return data, status_code, headers
else:
if "code" not in data:
data["code"] = "unknown"
data = {**default_data}
data.setdefault("code", "unknown")
# If you need WWW-Authenticate for 401, add it to headers
if status_code == 401:
headers["WWW-Authenticate"] = 'Bearer realm="api"'
return data, status_code, headers
resp = self.make_response(data, status_code, headers)
@api.errorhandler(ValueError)
def handle_value_error(e: ValueError):
got_request_exception.send(current_app, exception=e)
status_code = 400
data = {"code": "invalid_param", "message": str(e), "status": status_code}
return data, status_code
if status_code == 401:
resp = self.unauthorized(resp)
return resp
@api.errorhandler(AppInvokeQuotaExceededError)
def handle_quota_exceeded(e: AppInvokeQuotaExceededError):
got_request_exception.send(current_app, exception=e)
status_code = 429
data = {"code": "too_many_requests", "message": str(e), "status": status_code}
return data, status_code
@api.errorhandler(Exception)
def handle_general_exception(e: Exception):
got_request_exception.send(current_app, exception=e)
status_code = 500
data: dict[str, Any] = getattr(e, "data", {"message": http_status_message(status_code)})
# 🔒 Normalize non-mapping data (e.g., if someone set e.data = Response)
if not isinstance(data, Mapping):
data = {"message": str(e)}
data.setdefault("code", "unknown")
data.setdefault("status", status_code)
# Log stack
exc_info: Any = sys.exc_info()
if exc_info[1] is None:
exc_info = None
current_app.log_exception(exc_info)
return data, status_code
class ExternalApi(Api):
_authorizations = {
"Bearer": {
"type": "apiKey",
"in": "header",
"name": "Authorization",
"description": "Type: Bearer {your-api-key}",
}
}
def __init__(self, app: Blueprint | Flask, *args, **kwargs):
kwargs.setdefault("authorizations", self._authorizations)
kwargs.setdefault("security", "Bearer")
kwargs["add_specs"] = dify_config.SWAGGER_UI_ENABLED
kwargs["doc"] = dify_config.SWAGGER_UI_PATH if dify_config.SWAGGER_UI_ENABLED else False
# manual separate call on construction and init_app to ensure configs in kwargs effective
super().__init__(app=None, *args, **kwargs) # type: ignore
self.init_app(app, **kwargs)
register_external_error_handlers(self)

View File

@ -14,7 +14,7 @@ from typing import TYPE_CHECKING, Any, Optional, Union, cast
from zoneinfo import available_timezones
from flask import Response, stream_with_context
from flask_restful import fields
from flask_restx import fields
from pydantic import BaseModel
from configs import dify_config
@ -27,6 +27,8 @@ if TYPE_CHECKING:
from models.account import Account
from models.model import EndUser
logger = logging.getLogger(__name__)
def extract_tenant_id(user: Union["Account", "EndUser"]) -> str | None:
"""
@ -57,7 +59,7 @@ def run(script):
class AppIconUrlField(fields.Raw):
def output(self, key, obj):
def output(self, key, obj, **kwargs):
if obj is None:
return None
@ -72,7 +74,7 @@ class AppIconUrlField(fields.Raw):
class AvatarUrlField(fields.Raw):
def output(self, key, obj):
def output(self, key, obj, **kwargs):
if obj is None:
return None
@ -321,7 +323,7 @@ class TokenManager:
key = cls._get_token_key(token, token_type)
token_data_json = redis_client.get(key)
if token_data_json is None:
logging.warning("%s token %s not found with key %s", token_type, token, key)
logger.warning("%s token %s not found with key %s", token_type, token, key)
return None
token_data: Optional[dict[str, Any]] = json.loads(token_data_json)
return token_data

View File

@ -1,5 +1,5 @@
from functools import wraps
from typing import Any
from typing import Union, cast
from flask import current_app, g, has_request_context, request
from flask_login.config import EXEMPT_METHODS # type: ignore
@ -11,7 +11,7 @@ from models.model import EndUser
#: A proxy for the current user. If no user is logged in, this will be an
#: anonymous user
current_user: Any = LocalProxy(lambda: _get_user())
current_user = cast(Union[Account, EndUser, None], LocalProxy(lambda: _get_user()))
def login_required(func):
@ -52,7 +52,7 @@ def login_required(func):
def decorated_view(*args, **kwargs):
if request.method in EXEMPT_METHODS or dify_config.LOGIN_DISABLED:
pass
elif not current_user.is_authenticated:
elif current_user is not None and not current_user.is_authenticated:
return current_app.login_manager.unauthorized() # type: ignore
# flask 1.x compatibility

View File

@ -0,0 +1,55 @@
"""
Module loading utilities similar to Django's module_loading.
Reference implementation from Django:
https://github.com/django/django/blob/main/django/utils/module_loading.py
"""
import sys
from importlib import import_module
from typing import Any
def cached_import(module_path: str, class_name: str) -> Any:
"""
Import a module and return the named attribute/class from it, with caching.
Args:
module_path: The module path to import from
class_name: The attribute/class name to retrieve
Returns:
The imported attribute/class
"""
if not (
(module := sys.modules.get(module_path))
and (spec := getattr(module, "__spec__", None))
and getattr(spec, "_initializing", False) is False
):
module = import_module(module_path)
return getattr(module, class_name)
def import_string(dotted_path: str) -> Any:
"""
Import a dotted module path and return the attribute/class designated by
the last name in the path. Raise ImportError if the import failed.
Args:
dotted_path: Full module path to the class (e.g., 'module.submodule.ClassName')
Returns:
The imported class or attribute
Raises:
ImportError: If the module or attribute cannot be imported
"""
try:
module_path, class_name = dotted_path.rsplit(".", 1)
except ValueError as err:
raise ImportError(f"{dotted_path} doesn't look like a module path") from err
try:
return cached_import(module_path, class_name)
except AttributeError as err:
raise ImportError(f'Module "{module_path}" does not define a "{class_name}" attribute/class') from err

View File

@ -4,6 +4,8 @@ import sendgrid # type: ignore
from python_http_client.exceptions import ForbiddenError, UnauthorizedError
from sendgrid.helpers.mail import Content, Email, Mail, To # type: ignore
logger = logging.getLogger(__name__)
class SendGridClient:
def __init__(self, sendgrid_api_key: str, _from: str):
@ -11,7 +13,7 @@ class SendGridClient:
self._from = _from
def send(self, mail: dict):
logging.debug("Sending email with SendGrid")
logger.debug("Sending email with SendGrid")
try:
_to = mail["to"]
@ -27,19 +29,19 @@ class SendGridClient:
mail = Mail(from_email, to_email, subject, content)
mail_json = mail.get() # type: ignore
response = sg.client.mail.send.post(request_body=mail_json)
logging.debug(response.status_code)
logging.debug(response.body)
logging.debug(response.headers)
logger.debug(response.status_code)
logger.debug(response.body)
logger.debug(response.headers)
except TimeoutError as e:
logging.exception("SendGridClient Timeout occurred while sending email")
logger.exception("SendGridClient Timeout occurred while sending email")
raise
except (UnauthorizedError, ForbiddenError) as e:
logging.exception(
logger.exception(
"SendGridClient Authentication failed. "
"Verify that your credentials and the 'from' email address are correct"
)
raise
except Exception as e:
logging.exception("SendGridClient Unexpected error occurred while sending email to %s", _to)
logger.exception("SendGridClient Unexpected error occurred while sending email to %s", _to)
raise

View File

@ -3,6 +3,8 @@ import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
logger = logging.getLogger(__name__)
class SMTPClient:
def __init__(
@ -44,13 +46,13 @@ class SMTPClient:
smtp.sendmail(self._from, mail["to"], msg.as_string())
except smtplib.SMTPException as e:
logging.exception("SMTP error occurred")
logger.exception("SMTP error occurred")
raise
except TimeoutError as e:
logging.exception("Timeout occurred while sending email")
logger.exception("Timeout occurred while sending email")
raise
except Exception as e:
logging.exception("Unexpected error occurred while sending email to %s", mail["to"])
logger.exception("Unexpected error occurred while sending email to %s", mail["to"])
raise
finally:
if smtp: