mirror of
https://github.com/langgenius/dify.git
synced 2026-05-02 16:38:04 +08:00
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:
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
55
api/libs/module_loading.py
Normal file
55
api/libs/module_loading.py
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
Reference in New Issue
Block a user