diff --git a/api/app_factory.py b/api/app_factory.py index 124b1c9a35..e056ebb389 100644 --- a/api/app_factory.py +++ b/api/app_factory.py @@ -44,14 +44,15 @@ def initialize_extensions(app: DifyApp): ext_login, ext_mail, ext_migrate, + ext_phone_sms, ext_proxy_fix, ext_redis, ext_sentry, ext_set_secretkey, ext_storage, + ext_swagger, ext_timezone, ext_warnings, - ext_swagger ) extensions = [ @@ -70,12 +71,13 @@ def initialize_extensions(app: DifyApp): ext_celery, ext_login, ext_mail, + ext_phone_sms, ext_hosting_provider, ext_sentry, ext_proxy_fix, ext_blueprints, ext_commands, - ext_swagger + ext_swagger, ] for ext in extensions: short_name = ext.__name__.split(".")[-1] diff --git a/api/configs/deploy/__init__.py b/api/configs/deploy/__init__.py index 377d9ad9b7..b321f8e425 100644 --- a/api/configs/deploy/__init__.py +++ b/api/configs/deploy/__init__.py @@ -1,3 +1,5 @@ +from typing import Optional + from pydantic import Field from pydantic_settings import BaseSettings @@ -17,9 +19,9 @@ class DeploymentConfig(BaseSettings): default=False, ) - DEBUG_CODE_FOR_LOGIN: str = Field( + DEBUG_CODE_FOR_LOGIN: Optional[str] = Field( description="Default code for login", - default="", + default=None, ) EDITION: str = Field( diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index 0bea0620f4..ad690b3d41 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -586,6 +586,32 @@ class MailConfig(BaseSettings): ) +class PhoneSmsConfig(BaseSettings): + """ + Configuration for phone SMS services + """ + + ALIYUN_ACCESS_KEY_ID: Optional[str] = Field( + description="Aliyun access key id", + default=None, + ) + + ALIYUN_ACCESS_KEY_SECRET: Optional[str] = Field( + description="Aliyun access key secret", + default=None, + ) + + ALIYUN_SIGN_NAME: Optional[str] = Field( + description="Aliyun sign name", + default=None, + ) + + ALIYUN_TEMPLATE_CODE: Optional[str] = Field( + description="Aliyun template code", + default=None, + ) + + class RagEtlConfig(BaseSettings): """ Configuration for RAG ETL processes @@ -809,6 +835,7 @@ class FeatureConfig( ModelLoadBalanceConfig, ModerationConfig, MultiModalTransferConfig, + PhoneSmsConfig, PositionConfig, RagEtlConfig, SecurityConfig, diff --git a/api/extensions/ext_phone_sms.py b/api/extensions/ext_phone_sms.py new file mode 100644 index 0000000000..c85591ba81 --- /dev/null +++ b/api/extensions/ext_phone_sms.py @@ -0,0 +1,75 @@ +import json +import logging +from typing import Optional + +from alibabacloud_dysmsapi20170525 import models as dysmsapi_20170525_models +from alibabacloud_dysmsapi20170525.client import Client as Dysmsapi20170525Client +from alibabacloud_tea_openapi import models as open_api_models +from alibabacloud_tea_util import models as util_models +from alibabacloud_tea_util.client import Client as UtilClient +from configs import dify_config +from dify_app import DifyApp +from flask import Flask + + +class PhoneSms: + def __init__(self): + self._client: Optional[Dysmsapi20170525Client] = None + + def init_app(self, app: Flask): + if not dify_config.ALIYUN_ACCESS_KEY_ID or not dify_config.ALIYUN_ACCESS_KEY_SECRET: + logging.warning("ALIYUN_ACCESS_KEY_ID and ALIYUN_ACCESS_KEY_SECRET must be set") + return + + if not dify_config.ALIYUN_SIGN_NAME or not dify_config.ALIYUN_TEMPLATE_CODE: + logging.warning("ALIYUN_SIGN_NAME and ALIYUN_TEMPLATE_CODE must be set") + return + + self._client = self._create_client(dify_config.ALIYUN_ACCESS_KEY_ID, dify_config.ALIYUN_ACCESS_KEY_SECRET) + self._sign_name = dify_config.ALIYUN_SIGN_NAME + self._template_code = dify_config.ALIYUN_TEMPLATE_CODE + + def is_inited(self) -> bool: + return self._client is not None + + def _create_client(self, id: str, secret: str) -> Dysmsapi20170525Client: + """ + 使用AK&SK初始化账号Client + @return: Client + @throws Exception + """ + # 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。 + # 建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378659.html。 + config = open_api_models.Config( + # 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。, + access_key_id=id, + # 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。, + access_key_secret=secret, + ) + # Endpoint 请参考 https://api.aliyun.com/product/Dysmsapi + config.endpoint = f'dysmsapi.aliyuncs.com' + return Dysmsapi20170525Client(config) + + def send_sms(self, phone_numbers: str, code: str) -> None: + + if not self._client: + raise ValueError("PhoneSms client is not initialized") + + send_sms_request = dysmsapi_20170525_models.SendSmsRequest( + phone_numbers=phone_numbers, + sign_name=self._sign_name, + template_code=self._template_code, + template_param=json.dumps({"code": code}), + ) + + response = self._client.send_sms_with_options(send_sms_request, util_models.RuntimeOptions()) + if response.body.code != 'OK': + raise Exception(response.body.message) + + +def init_app(app: DifyApp): + phone_sms.init_app(app) + app.extensions["phone_sms"] = phone_sms + + +phone_sms = PhoneSms() diff --git a/api/poetry.lock b/api/poetry.lock index c6dbd925af..d8b9e046b2 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -197,7 +197,7 @@ version = "0.3.6" description = "The alibabacloud credentials module of alibabaCloud Python SDK." optional = false python-versions = ">=3.6" -groups = ["vdb"] +groups = ["main", "vdb"] files = [ {file = "alibabacloud_credentials-0.3.6.tar.gz", hash = "sha256:caa82cf258648dcbe1ca14aeba50ba21845567d6ac3cd48d318e0a445fff7f96"}, ] @@ -205,13 +205,31 @@ files = [ [package.dependencies] alibabacloud-tea = ">=0.3.9" +[[package]] +name = "alibabacloud-dysmsapi20170525" +version = "3.1.1" +description = "Alibaba Cloud Dysmsapi (20170525) SDK Library for Python" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "alibabacloud_dysmsapi20170525-3.1.1-py3-none-any.whl", hash = "sha256:a6f02476f4475e66fab680ce96281d73dc5ae506e5197190c54232eda6afd191"}, + {file = "alibabacloud_dysmsapi20170525-3.1.1.tar.gz", hash = "sha256:5b0440dab0195afa1e42f97ce3ba9ce445f2afca2d0de873688248abc9592854"}, +] + +[package.dependencies] +alibabacloud-endpoint-util = ">=0.0.3,<1.0.0" +alibabacloud-openapi-util = ">=0.2.2,<1.0.0" +alibabacloud-tea-openapi = ">=0.3.12,<1.0.0" +alibabacloud-tea-util = ">=0.3.13,<1.0.0" + [[package]] name = "alibabacloud-endpoint-util" version = "0.0.3" description = "The endpoint-util module of alibabaCloud Python SDK." optional = false python-versions = "*" -groups = ["vdb"] +groups = ["main", "vdb"] files = [ {file = "alibabacloud_endpoint_util-0.0.3.tar.gz", hash = "sha256:8c0efb76fdcc3af4ca716ef24bbce770201a3f83f98c0afcf81655f684b9c7d2"}, ] @@ -225,7 +243,7 @@ version = "0.0.2" description = "Alibaba Cloud Gateway SPI SDK Library for Python" optional = false python-versions = ">=3.6" -groups = ["vdb"] +groups = ["main", "vdb"] files = [ {file = "alibabacloud_gateway_spi-0.0.2.tar.gz", hash = "sha256:f932c8ba67291531dfbee6ca521dcf3523eb4ff93512bf0aaf135f2d4fc4704d"}, ] @@ -261,7 +279,7 @@ version = "0.2.2" description = "Aliyun Tea OpenApi Library for Python" optional = false python-versions = "*" -groups = ["vdb"] +groups = ["main", "vdb"] files = [ {file = "alibabacloud_openapi_util-0.2.2.tar.gz", hash = "sha256:ebbc3906f554cb4bf8f513e43e8a33e8b6a3d4a0ef13617a0e14c3dda8ef52a8"}, ] @@ -326,7 +344,7 @@ version = "0.4.0" description = "The tea module of alibabaCloud Python SDK." optional = false python-versions = ">=3.7" -groups = ["vdb"] +groups = ["main", "vdb"] files = [ {file = "alibabacloud-tea-0.4.0.tar.gz", hash = "sha256:bdf72d747723bab190331b3c8593109fe2807504469bc0147f78c8c4945ed396"}, {file = "alibabacloud_tea-0.4.0-py3-none-any.whl", hash = "sha256:59fae5765e6654f884e130233df6fb61ca0fbe01a29ed0755a1cf099a3d4d863"}, @@ -356,7 +374,7 @@ version = "0.3.12" description = "Alibaba Cloud openapi SDK Library for Python" optional = false python-versions = ">=3.6" -groups = ["vdb"] +groups = ["main", "vdb"] files = [ {file = "alibabacloud_tea_openapi-0.3.12.tar.gz", hash = "sha256:2e14809f357438e62c1ef4976a7655110dd54a75bbfa7d905fa3798355cfd974"}, ] @@ -374,7 +392,7 @@ version = "0.3.13" description = "The tea-util module of alibabaCloud Python SDK." optional = false python-versions = ">=3.6" -groups = ["vdb"] +groups = ["main", "vdb"] files = [ {file = "alibabacloud_tea_util-0.3.13.tar.gz", hash = "sha256:8cbdfd2a03fbbf622f901439fa08643898290dd40e1d928347f6346e43f63c90"}, ] @@ -388,7 +406,7 @@ version = "0.0.2" description = "The tea-xml module of alibabaCloud Python SDK." optional = false python-versions = "*" -groups = ["vdb"] +groups = ["main", "vdb"] files = [ {file = "alibabacloud_tea_xml-0.0.2.tar.gz", hash = "sha256:f0135e8148fd7d9c1f029db161863f37f144f837c280cba16c2edeb2f9c549d8"}, ] @@ -11828,4 +11846,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.13" -content-hash = "b26521378e2b5423ccebc7b1af0890275ddbc3dbcea748de74b83efd1c06e877" +content-hash = "97dd07fa06f04b556689dace7e0821f44aa7b6ad2fa4fba3da24ad0941bba63c" diff --git a/api/pyproject.toml b/api/pyproject.toml index f125517267..445e8e2ff1 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -112,6 +112,7 @@ zhipuai = "~2.1.5" # required by main implementations ############################################################ flasgger = "^0.9.7.1" +alibabacloud-dysmsapi20170525 = "^3.1.1" [tool.poetry.group.indirect.dependencies] kaleido = "0.2.1" rank-bm25 = "~0.2.2" diff --git a/api/services/account_service.py b/api/services/account_service.py index b3d7ef4d37..f2615ae1cb 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -55,6 +55,7 @@ from tasks.mail_account_deletion_task import send_account_deletion_verification_ from tasks.mail_email_code_login import send_email_code_login_mail_task from tasks.mail_invite_member_task import send_invite_member_mail_task from tasks.mail_reset_password_task import send_reset_password_mail_task +from tasks.phone_sms_code_login import send_phone_sms_code_login_task from werkzeug.exceptions import Unauthorized @@ -676,7 +677,7 @@ class AccountService: if cls.phone_code_login_rate_limiter.is_rate_limited(phone) and not DeploymentConfig().DEBUG: raise Exception("Phone verification code rate limit exceeded") - if dify_config.DEBUG_CODE_FOR_LOGIN and dify_config.DEBUG_CODE_FOR_LOGIN != "": + if dify_config.DEBUG_CODE_FOR_LOGIN: code = dify_config.DEBUG_CODE_FOR_LOGIN else: code = "".join([str(random.randint(0, 9)) for _ in range(6)]) @@ -689,12 +690,11 @@ class AccountService: additional_data={"code": code, "phone": phone}, ) - # Here you would typically send an SMS with the code - # For now we'll just assume the SMS sending service exists - # send_phone_code_login_sms_task.delay(to=phone, code=code) - - # Log SMS sending in production environment - logging.info(f"Phone verification code sent to {phone}") + if dify_config.DEBUG_CODE_FOR_LOGIN: + logging.info(f"Mock Code, Skip sending phone verification code to {phone}") + else: + send_phone_sms_code_login_task.delay(phone=phone, code=code) + logging.info(f"Phone verification code sent to {phone}") cls.phone_code_login_rate_limiter.increment_rate_limit(phone) return token diff --git a/api/tasks/phone_sms_code_login.py b/api/tasks/phone_sms_code_login.py new file mode 100644 index 0000000000..c57ba81388 --- /dev/null +++ b/api/tasks/phone_sms_code_login.py @@ -0,0 +1,36 @@ +import logging +import time + +import click +from celery import shared_task # type: ignore +from extensions.ext_phone_sms import phone_sms +from flask import render_template + + +@shared_task(queue="phone_sms") +def send_phone_sms_code_login_task(phone: str, code: str): + """ + Async Send email code login mail + :param language: Language in which the email should be sent (e.g., 'en', 'zh') + :param to: Recipient email address + :param code: Email code to be included in the email + """ + if not phone_sms.is_inited(): + return + + logging.info(click.style(f"Start phone sms code login mail to {phone}", fg="green")) + start_at = time.perf_counter() + + # send email code login mail using different languages + try: + phone_sms.send_sms(phone, code) + + end_at = time.perf_counter() + logging.info( + click.style( + f"Send phone sms code login mail to {phone} succeeded: latency: {end_at - start_at}", + fg="green", + ) + ) + except Exception as e: + logging.exception(f"Send phone sms code login mail to {phone} failed: {e}") diff --git a/docker/.env.example b/docker/.env.example index 5a735c9c99..203ce37a84 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -950,3 +950,9 @@ NEED_USER_PROFILE_GENERATION_APP_IDS= USER_PROFILE_GENERATE_TASK_INTERVAL=5 USER_MEMORY_GENERATION_APP_ID= USER_HEALTH_SUMMARY_GENERATION_APP_ID= + +# Phone SMS Code Login +ALIYUN_ACCESS_KEY_ID= +ALIYUN_ACCESS_KEY_SECRET= +ALIYUN_SIGN_NAME= +ALIYUN_TEMPLATE_CODE= diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index e08d895472..6b4e0bb86c 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -401,6 +401,10 @@ x-shared-env: &shared-api-worker-env USER_PROFILE_GENERATE_TASK_INTERVAL: ${USER_PROFILE_GENERATE_TASK_INTERVAL:-5} USER_MEMORY_GENERATION_APP_ID: ${USER_MEMORY_GENERATION_APP_ID:-} USER_HEALTH_SUMMARY_GENERATION_APP_ID: ${USER_HEALTH_SUMMARY_GENERATION_APP_ID:-} + ALIYUN_ACCESS_KEY_ID: ${ALIYUN_ACCESS_KEY_ID:-} + ALIYUN_ACCESS_KEY_SECRET: ${ALIYUN_ACCESS_KEY_SECRET:-} + ALIYUN_SIGN_NAME: ${ALIYUN_SIGN_NAME:-} + ALIYUN_TEMPLATE_CODE: ${ALIYUN_TEMPLATE_CODE:-} services: # API service