mirror of
https://github.com/langgenius/dify.git
synced 2026-01-08 07:14:14 +00:00
Compare commits
7 Commits
release/0.
...
0.15.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfdce78ca5 | ||
|
|
00c2258352 | ||
|
|
a1b3d41712 | ||
|
|
b26e20fe34 | ||
|
|
161ff432f1 | ||
|
|
99a9def623 | ||
|
|
fe1846c437 |
18
CHANGELOG.md
Normal file
18
CHANGELOG.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to Dify will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.15.6] - 2025-04-22
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fixed clickjacking vulnerability (#18552)
|
||||||
|
- Fixed reset password security issue (#18366)
|
||||||
|
- Updated reset password token when email code verification succeeds (#18362)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed Vertex AI Gemini 2.0 Flash 001 schema (#18405)
|
||||||
@@ -431,3 +431,6 @@ CREATE_TIDB_SERVICE_JOB_ENABLED=false
|
|||||||
MAX_SUBMIT_COUNT=100
|
MAX_SUBMIT_COUNT=100
|
||||||
# Lockout duration in seconds
|
# Lockout duration in seconds
|
||||||
LOGIN_LOCKOUT_DURATION=86400
|
LOGIN_LOCKOUT_DURATION=86400
|
||||||
|
|
||||||
|
# Prevent Clickjacking
|
||||||
|
ALLOW_EMBED=false
|
||||||
@@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings):
|
|||||||
|
|
||||||
CURRENT_VERSION: str = Field(
|
CURRENT_VERSION: str = Field(
|
||||||
description="Dify version",
|
description="Dify version",
|
||||||
default="0.15.5",
|
default="0.15.6",
|
||||||
)
|
)
|
||||||
|
|
||||||
COMMIT_SHA: str = Field(
|
COMMIT_SHA: str = Field(
|
||||||
|
|||||||
@@ -6,9 +6,13 @@ from flask_restful import Resource, reqparse # type: ignore
|
|||||||
|
|
||||||
from constants.languages import languages
|
from constants.languages import languages
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
from controllers.console.auth.error import EmailCodeError, InvalidEmailError, InvalidTokenError, PasswordMismatchError
|
from controllers.console.auth.error import (EmailCodeError, InvalidEmailError,
|
||||||
from controllers.console.error import AccountInFreezeError, AccountNotFound, EmailSendIpLimitError
|
InvalidTokenError,
|
||||||
from controllers.console.wraps import setup_required
|
PasswordMismatchError)
|
||||||
|
from controllers.console.error import (AccountInFreezeError, AccountNotFound,
|
||||||
|
EmailSendIpLimitError)
|
||||||
|
from controllers.console.wraps import (email_password_login_enabled,
|
||||||
|
setup_required)
|
||||||
from events.tenant_event import tenant_was_created
|
from events.tenant_event import tenant_was_created
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from libs.helper import email, extract_remote_ip
|
from libs.helper import email, extract_remote_ip
|
||||||
@@ -22,6 +26,7 @@ from services.feature_service import FeatureService
|
|||||||
|
|
||||||
class ForgotPasswordSendEmailApi(Resource):
|
class ForgotPasswordSendEmailApi(Resource):
|
||||||
@setup_required
|
@setup_required
|
||||||
|
@email_password_login_enabled
|
||||||
def post(self):
|
def post(self):
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("email", type=email, required=True, location="json")
|
parser.add_argument("email", type=email, required=True, location="json")
|
||||||
@@ -53,6 +58,7 @@ class ForgotPasswordSendEmailApi(Resource):
|
|||||||
|
|
||||||
class ForgotPasswordCheckApi(Resource):
|
class ForgotPasswordCheckApi(Resource):
|
||||||
@setup_required
|
@setup_required
|
||||||
|
@email_password_login_enabled
|
||||||
def post(self):
|
def post(self):
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("email", type=str, required=True, location="json")
|
parser.add_argument("email", type=str, required=True, location="json")
|
||||||
@@ -72,11 +78,20 @@ class ForgotPasswordCheckApi(Resource):
|
|||||||
if args["code"] != token_data.get("code"):
|
if args["code"] != token_data.get("code"):
|
||||||
raise EmailCodeError()
|
raise EmailCodeError()
|
||||||
|
|
||||||
return {"is_valid": True, "email": token_data.get("email")}
|
# Verified, revoke the first token
|
||||||
|
AccountService.revoke_reset_password_token(args["token"])
|
||||||
|
|
||||||
|
# Refresh token data by generating a new token
|
||||||
|
_, new_token = AccountService.generate_reset_password_token(
|
||||||
|
user_email, code=args["code"], additional_data={"phase": "reset"}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"is_valid": True, "email": token_data.get("email"), "token": new_token}
|
||||||
|
|
||||||
|
|
||||||
class ForgotPasswordResetApi(Resource):
|
class ForgotPasswordResetApi(Resource):
|
||||||
@setup_required
|
@setup_required
|
||||||
|
@email_password_login_enabled
|
||||||
def post(self):
|
def post(self):
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("token", type=str, required=True, nullable=False, location="json")
|
parser.add_argument("token", type=str, required=True, nullable=False, location="json")
|
||||||
@@ -95,6 +110,9 @@ class ForgotPasswordResetApi(Resource):
|
|||||||
|
|
||||||
if reset_data is None:
|
if reset_data is None:
|
||||||
raise InvalidTokenError()
|
raise InvalidTokenError()
|
||||||
|
# Must use token in reset phase
|
||||||
|
if reset_data.get("phase", "") != "reset":
|
||||||
|
raise InvalidTokenError()
|
||||||
|
|
||||||
AccountService.revoke_reset_password_token(token)
|
AccountService.revoke_reset_password_token(token)
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from controllers.console.error import (
|
|||||||
EmailSendIpLimitError,
|
EmailSendIpLimitError,
|
||||||
NotAllowedCreateWorkspace,
|
NotAllowedCreateWorkspace,
|
||||||
)
|
)
|
||||||
from controllers.console.wraps import setup_required
|
from controllers.console.wraps import email_password_login_enabled, setup_required
|
||||||
from events.tenant_event import tenant_was_created
|
from events.tenant_event import tenant_was_created
|
||||||
from libs.helper import email, extract_remote_ip
|
from libs.helper import email, extract_remote_ip
|
||||||
from libs.password import valid_password
|
from libs.password import valid_password
|
||||||
@@ -38,6 +38,7 @@ class LoginApi(Resource):
|
|||||||
"""Resource for user login."""
|
"""Resource for user login."""
|
||||||
|
|
||||||
@setup_required
|
@setup_required
|
||||||
|
@email_password_login_enabled
|
||||||
def post(self):
|
def post(self):
|
||||||
"""Authenticate user and login."""
|
"""Authenticate user and login."""
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
@@ -110,6 +111,7 @@ class LogoutApi(Resource):
|
|||||||
|
|
||||||
class ResetPasswordSendEmailApi(Resource):
|
class ResetPasswordSendEmailApi(Resource):
|
||||||
@setup_required
|
@setup_required
|
||||||
|
@email_password_login_enabled
|
||||||
def post(self):
|
def post(self):
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("email", type=email, required=True, location="json")
|
parser.add_argument("email", type=email, required=True, location="json")
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ from models.model import DifySetup
|
|||||||
from services.feature_service import FeatureService, LicenseStatus
|
from services.feature_service import FeatureService, LicenseStatus
|
||||||
from services.operation_service import OperationService
|
from services.operation_service import OperationService
|
||||||
|
|
||||||
from .error import NotInitValidateError, NotSetupError, UnauthorizedAndForceLogout
|
from .error import (NotInitValidateError, NotSetupError,
|
||||||
|
UnauthorizedAndForceLogout)
|
||||||
|
|
||||||
|
|
||||||
def account_initialization_required(view):
|
def account_initialization_required(view):
|
||||||
@@ -154,3 +155,16 @@ def enterprise_license_required(view):
|
|||||||
return view(*args, **kwargs)
|
return view(*args, **kwargs)
|
||||||
|
|
||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
|
def email_password_login_enabled(view):
|
||||||
|
@wraps(view)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
features = FeatureService.get_system_features()
|
||||||
|
if features.enable_email_password_login:
|
||||||
|
return view(*args, **kwargs)
|
||||||
|
|
||||||
|
# otherwise, return 403
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
return decorated
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ class GoogleProvider(ModelProvider):
|
|||||||
try:
|
try:
|
||||||
model_instance = self.get_model_instance(ModelType.LLM)
|
model_instance = self.get_model_instance(ModelType.LLM)
|
||||||
|
|
||||||
# Use `gemini-pro` model for validate,
|
# Use `gemini-2.0-flash` model for validate,
|
||||||
model_instance.validate_credentials(model="gemini-pro", credentials=credentials)
|
model_instance.validate_credentials(model="gemini-2.0-flash", credentials=credentials)
|
||||||
except CredentialsValidateFailedError as ex:
|
except CredentialsValidateFailedError as ex:
|
||||||
raise ex
|
raise ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
|||||||
@@ -19,5 +19,3 @@
|
|||||||
- gemini-exp-1206
|
- gemini-exp-1206
|
||||||
- gemini-exp-1121
|
- gemini-exp-1121
|
||||||
- gemini-exp-1114
|
- gemini-exp-1114
|
||||||
- gemini-pro
|
|
||||||
- gemini-pro-vision
|
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
model: gemini-pro-vision
|
|
||||||
label:
|
|
||||||
en_US: Gemini Pro Vision
|
|
||||||
model_type: llm
|
|
||||||
features:
|
|
||||||
- vision
|
|
||||||
model_properties:
|
|
||||||
mode: chat
|
|
||||||
context_size: 12288
|
|
||||||
parameter_rules:
|
|
||||||
- name: temperature
|
|
||||||
use_template: temperature
|
|
||||||
- name: top_p
|
|
||||||
use_template: top_p
|
|
||||||
- name: top_k
|
|
||||||
label:
|
|
||||||
zh_Hans: 取样数量
|
|
||||||
en_US: Top k
|
|
||||||
type: int
|
|
||||||
help:
|
|
||||||
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
|
|
||||||
en_US: Only sample from the top K options for each subsequent token.
|
|
||||||
required: false
|
|
||||||
- name: max_tokens_to_sample
|
|
||||||
use_template: max_tokens
|
|
||||||
required: true
|
|
||||||
default: 4096
|
|
||||||
min: 1
|
|
||||||
max: 4096
|
|
||||||
pricing:
|
|
||||||
input: '0.00'
|
|
||||||
output: '0.00'
|
|
||||||
unit: '0.000001'
|
|
||||||
currency: USD
|
|
||||||
deprecated: true
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
model: gemini-pro
|
|
||||||
label:
|
|
||||||
en_US: Gemini Pro
|
|
||||||
model_type: llm
|
|
||||||
features:
|
|
||||||
- agent-thought
|
|
||||||
- tool-call
|
|
||||||
- stream-tool-call
|
|
||||||
model_properties:
|
|
||||||
mode: chat
|
|
||||||
context_size: 30720
|
|
||||||
parameter_rules:
|
|
||||||
- name: temperature
|
|
||||||
use_template: temperature
|
|
||||||
- name: top_p
|
|
||||||
use_template: top_p
|
|
||||||
- name: top_k
|
|
||||||
label:
|
|
||||||
zh_Hans: 取样数量
|
|
||||||
en_US: Top k
|
|
||||||
type: int
|
|
||||||
help:
|
|
||||||
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
|
|
||||||
en_US: Only sample from the top K options for each subsequent token.
|
|
||||||
required: false
|
|
||||||
- name: max_tokens_to_sample
|
|
||||||
use_template: max_tokens
|
|
||||||
required: true
|
|
||||||
default: 2048
|
|
||||||
min: 1
|
|
||||||
max: 2048
|
|
||||||
- name: response_format
|
|
||||||
use_template: response_format
|
|
||||||
pricing:
|
|
||||||
input: '0.00'
|
|
||||||
output: '0.00'
|
|
||||||
unit: '0.000001'
|
|
||||||
currency: USD
|
|
||||||
deprecated: true
|
|
||||||
@@ -5,11 +5,6 @@ model_type: llm
|
|||||||
features:
|
features:
|
||||||
- agent-thought
|
- agent-thought
|
||||||
- vision
|
- vision
|
||||||
- tool-call
|
|
||||||
- stream-tool-call
|
|
||||||
- document
|
|
||||||
- video
|
|
||||||
- audio
|
|
||||||
model_properties:
|
model_properties:
|
||||||
mode: chat
|
mode: chat
|
||||||
context_size: 1048576
|
context_size: 1048576
|
||||||
@@ -20,20 +15,21 @@ parameter_rules:
|
|||||||
use_template: top_p
|
use_template: top_p
|
||||||
- name: top_k
|
- name: top_k
|
||||||
label:
|
label:
|
||||||
zh_Hans: 取样数量
|
|
||||||
en_US: Top k
|
en_US: Top k
|
||||||
type: int
|
type: int
|
||||||
help:
|
help:
|
||||||
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
|
|
||||||
en_US: Only sample from the top K options for each subsequent token.
|
en_US: Only sample from the top K options for each subsequent token.
|
||||||
required: false
|
required: false
|
||||||
|
- name: presence_penalty
|
||||||
|
use_template: presence_penalty
|
||||||
|
- name: frequency_penalty
|
||||||
|
use_template: frequency_penalty
|
||||||
- name: max_output_tokens
|
- name: max_output_tokens
|
||||||
use_template: max_tokens
|
use_template: max_tokens
|
||||||
|
required: true
|
||||||
default: 8192
|
default: 8192
|
||||||
min: 1
|
min: 1
|
||||||
max: 8192
|
max: 8192
|
||||||
- name: json_schema
|
|
||||||
use_template: json_schema
|
|
||||||
pricing:
|
pricing:
|
||||||
input: '0.00'
|
input: '0.00'
|
||||||
output: '0.00'
|
output: '0.00'
|
||||||
|
|||||||
@@ -406,10 +406,8 @@ class AccountService:
|
|||||||
|
|
||||||
raise PasswordResetRateLimitExceededError()
|
raise PasswordResetRateLimitExceededError()
|
||||||
|
|
||||||
code = "".join([str(random.randint(0, 9)) for _ in range(6)])
|
code, token = cls.generate_reset_password_token(account_email, account)
|
||||||
token = TokenManager.generate_token(
|
|
||||||
account=account, email=email, token_type="reset_password", additional_data={"code": code}
|
|
||||||
)
|
|
||||||
send_reset_password_mail_task.delay(
|
send_reset_password_mail_task.delay(
|
||||||
language=language,
|
language=language,
|
||||||
to=account_email,
|
to=account_email,
|
||||||
@@ -418,6 +416,22 @@ class AccountService:
|
|||||||
cls.reset_password_rate_limiter.increment_rate_limit(account_email)
|
cls.reset_password_rate_limiter.increment_rate_limit(account_email)
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_reset_password_token(
|
||||||
|
cls,
|
||||||
|
email: str,
|
||||||
|
account: Optional[Account] = None,
|
||||||
|
code: Optional[str] = None,
|
||||||
|
additional_data: dict[str, Any] = {},
|
||||||
|
):
|
||||||
|
if not code:
|
||||||
|
code = "".join([str(random.randint(0, 9)) for _ in range(6)])
|
||||||
|
additional_data["code"] = code
|
||||||
|
token = TokenManager.generate_token(
|
||||||
|
account=account, email=email, token_type="reset_password", additional_data=additional_data
|
||||||
|
)
|
||||||
|
return code, token
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def revoke_reset_password_token(cls, token: str):
|
def revoke_reset_password_token(cls, token: str):
|
||||||
TokenManager.revoke_token(token, "reset_password")
|
TokenManager.revoke_token(token, "reset_password")
|
||||||
|
|||||||
@@ -932,3 +932,6 @@ MAX_SUBMIT_COUNT=100
|
|||||||
|
|
||||||
# The maximum number of top-k value for RAG.
|
# The maximum number of top-k value for RAG.
|
||||||
TOP_K_MAX_VALUE=10
|
TOP_K_MAX_VALUE=10
|
||||||
|
|
||||||
|
# Prevent Clickjacking
|
||||||
|
ALLOW_EMBED=false
|
||||||
@@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env
|
|||||||
services:
|
services:
|
||||||
# API service
|
# API service
|
||||||
api:
|
api:
|
||||||
image: langgenius/dify-api:0.15.5
|
image: langgenius/dify-api:0.15.6
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
# Use the shared environment variables.
|
# Use the shared environment variables.
|
||||||
@@ -25,7 +25,7 @@ services:
|
|||||||
# worker service
|
# worker service
|
||||||
# The Celery worker for processing the queue.
|
# The Celery worker for processing the queue.
|
||||||
worker:
|
worker:
|
||||||
image: langgenius/dify-api:0.15.5
|
image: langgenius/dify-api:0.15.6
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
# Use the shared environment variables.
|
# Use the shared environment variables.
|
||||||
@@ -47,7 +47,7 @@ services:
|
|||||||
|
|
||||||
# Frontend web application.
|
# Frontend web application.
|
||||||
web:
|
web:
|
||||||
image: langgenius/dify-web:0.15.5
|
image: langgenius/dify-web:0.15.6
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
||||||
@@ -56,6 +56,7 @@ services:
|
|||||||
NEXT_TELEMETRY_DISABLED: ${NEXT_TELEMETRY_DISABLED:-0}
|
NEXT_TELEMETRY_DISABLED: ${NEXT_TELEMETRY_DISABLED:-0}
|
||||||
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}
|
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}
|
||||||
CSP_WHITELIST: ${CSP_WHITELIST:-}
|
CSP_WHITELIST: ${CSP_WHITELIST:-}
|
||||||
|
ALLOW_EMBED: ${ALLOW_EMBED:-false}
|
||||||
TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-}
|
TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-}
|
||||||
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-}
|
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-}
|
||||||
|
|
||||||
|
|||||||
@@ -389,11 +389,12 @@ x-shared-env: &shared-api-worker-env
|
|||||||
CREATE_TIDB_SERVICE_JOB_ENABLED: ${CREATE_TIDB_SERVICE_JOB_ENABLED:-false}
|
CREATE_TIDB_SERVICE_JOB_ENABLED: ${CREATE_TIDB_SERVICE_JOB_ENABLED:-false}
|
||||||
MAX_SUBMIT_COUNT: ${MAX_SUBMIT_COUNT:-100}
|
MAX_SUBMIT_COUNT: ${MAX_SUBMIT_COUNT:-100}
|
||||||
TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-10}
|
TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-10}
|
||||||
|
ALLOW_EMBED: ${ALLOW_EMBED:-false}
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# API service
|
# API service
|
||||||
api:
|
api:
|
||||||
image: langgenius/dify-api:0.15.5
|
image: langgenius/dify-api:0.15.6
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
# Use the shared environment variables.
|
# Use the shared environment variables.
|
||||||
@@ -416,7 +417,7 @@ services:
|
|||||||
# worker service
|
# worker service
|
||||||
# The Celery worker for processing the queue.
|
# The Celery worker for processing the queue.
|
||||||
worker:
|
worker:
|
||||||
image: langgenius/dify-api:0.15.5
|
image: langgenius/dify-api:0.15.6
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
# Use the shared environment variables.
|
# Use the shared environment variables.
|
||||||
@@ -438,7 +439,7 @@ services:
|
|||||||
|
|
||||||
# Frontend web application.
|
# Frontend web application.
|
||||||
web:
|
web:
|
||||||
image: langgenius/dify-web:0.15.5
|
image: langgenius/dify-web:0.15.6
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
||||||
@@ -447,6 +448,7 @@ services:
|
|||||||
NEXT_TELEMETRY_DISABLED: ${NEXT_TELEMETRY_DISABLED:-0}
|
NEXT_TELEMETRY_DISABLED: ${NEXT_TELEMETRY_DISABLED:-0}
|
||||||
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}
|
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}
|
||||||
CSP_WHITELIST: ${CSP_WHITELIST:-}
|
CSP_WHITELIST: ${CSP_WHITELIST:-}
|
||||||
|
ALLOW_EMBED: ${ALLOW_EMBED:-false}
|
||||||
TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-}
|
TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-}
|
||||||
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-}
|
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-}
|
||||||
|
|
||||||
|
|||||||
@@ -31,3 +31,6 @@ NEXT_PUBLIC_TOP_K_MAX_VALUE=10
|
|||||||
|
|
||||||
# The maximum number of tokens for segmentation
|
# The maximum number of tokens for segmentation
|
||||||
NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=4000
|
NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=4000
|
||||||
|
|
||||||
|
# Default is not allow to embed into iframe to prevent Clickjacking: https://owasp.org/www-community/attacks/Clickjacking
|
||||||
|
NEXT_PUBLIC_ALLOW_EMBED=
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const OPTION_MAP = {
|
|||||||
iframe: {
|
iframe: {
|
||||||
getContent: (url: string, token: string) =>
|
getContent: (url: string, token: string) =>
|
||||||
`<iframe
|
`<iframe
|
||||||
src="${url}/chatbot/${token}"
|
src="${url}/chat/${token}"
|
||||||
style="width: 100%; height: 100%; min-height: 700px"
|
style="width: 100%; height: 100%; min-height: 700px"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
allow="microphone">
|
allow="microphone">
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { UUID_NIL } from './constants'
|
import { UUID_NIL } from './constants'
|
||||||
import type { IChatItem } from './chat/type'
|
import type { IChatItem } from './chat/type'
|
||||||
import type { ChatItem, ChatItemInTree } from './types'
|
import type { ChatItem, ChatItemInTree } from './types'
|
||||||
|
import { addFileInfos, sortAgentSorts } from '../../tools/utils'
|
||||||
|
import { getProcessedFilesFromResponse } from '../file-uploader/utils'
|
||||||
|
|
||||||
async function decodeBase64AndDecompress(base64String: string) {
|
async function decodeBase64AndDecompress(base64String: string) {
|
||||||
const binaryString = atob(base64String)
|
const binaryString = atob(base64String)
|
||||||
@@ -19,6 +21,60 @@ function getProcessedInputsFromUrlParams(): Record<string, any> {
|
|||||||
return inputs
|
return inputs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendQAToChatList(chatList: ChatItem[], item: any) {
|
||||||
|
// we append answer first and then question since will reverse the whole chatList later
|
||||||
|
const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
|
||||||
|
chatList.push({
|
||||||
|
id: item.id,
|
||||||
|
content: item.answer,
|
||||||
|
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
|
||||||
|
feedback: item.feedback,
|
||||||
|
isAnswer: true,
|
||||||
|
citation: item.retriever_resources,
|
||||||
|
message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))),
|
||||||
|
})
|
||||||
|
const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
|
||||||
|
chatList.push({
|
||||||
|
id: `question-${item.id}`,
|
||||||
|
content: item.query,
|
||||||
|
isAnswer: false,
|
||||||
|
message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the latest thread messages from all messages of the conversation.
|
||||||
|
* Same logic as backend codebase `api/core/prompt/utils/extract_thread_messages.py`
|
||||||
|
*
|
||||||
|
* @param fetchedMessages - The history chat list data from the backend, sorted by created_at in descending order. This includes all flattened history messages of the conversation.
|
||||||
|
* @returns An array of ChatItems representing the latest thread.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
function getPrevChatList(fetchedMessages: any[]) {
|
||||||
|
const ret: ChatItem[] = []
|
||||||
|
let nextMessageId = null
|
||||||
|
|
||||||
|
for (const item of fetchedMessages) {
|
||||||
|
if (!item.parent_message_id) {
|
||||||
|
appendQAToChatList(ret, item)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nextMessageId) {
|
||||||
|
appendQAToChatList(ret, item)
|
||||||
|
nextMessageId = item.parent_message_id
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (item.id === nextMessageId || nextMessageId === UUID_NIL) {
|
||||||
|
appendQAToChatList(ret, item)
|
||||||
|
nextMessageId = item.parent_message_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret.reverse()
|
||||||
|
}
|
||||||
|
|
||||||
function isValidGeneratedAnswer(item?: ChatItem | ChatItemInTree): boolean {
|
function isValidGeneratedAnswer(item?: ChatItem | ChatItemInTree): boolean {
|
||||||
return !!item && item.isAnswer && !item.id.startsWith('answer-placeholder-') && !item.isOpeningStatement
|
return !!item && item.isAnswer && !item.id.startsWith('answer-placeholder-') && !item.isOpeningStatement
|
||||||
}
|
}
|
||||||
@@ -164,6 +220,7 @@ function getThreadMessages(tree: ChatItemInTree[], targetMessageId?: string): Ch
|
|||||||
export {
|
export {
|
||||||
getProcessedInputsFromUrlParams,
|
getProcessedInputsFromUrlParams,
|
||||||
isValidGeneratedAnswer,
|
isValidGeneratedAnswer,
|
||||||
|
getPrevChatList,
|
||||||
getLastAnswer,
|
getLastAnswer,
|
||||||
buildChatItemTree,
|
buildChatItemTree,
|
||||||
getThreadMessages,
|
getThreadMessages,
|
||||||
|
|||||||
@@ -39,7 +39,11 @@ export default function CheckCode() {
|
|||||||
}
|
}
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const ret = await verifyResetPasswordCode({ email, code, token })
|
const ret = await verifyResetPasswordCode({ email, code, token })
|
||||||
ret.is_valid && router.push(`/reset-password/set-password?${searchParams.toString()}`)
|
if (ret.is_valid) {
|
||||||
|
const params = new URLSearchParams(searchParams)
|
||||||
|
params.set('token', encodeURIComponent(ret.token))
|
||||||
|
router.push(`/reset-password/set-password?${params.toString()}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (error) { console.error(error) }
|
catch (error) { console.error(error) }
|
||||||
finally {
|
finally {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export NEXT_TELEMETRY_DISABLED=${NEXT_TELEMETRY_DISABLED}
|
|||||||
|
|
||||||
export NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS=${TEXT_GENERATION_TIMEOUT_MS}
|
export NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS=${TEXT_GENERATION_TIMEOUT_MS}
|
||||||
export NEXT_PUBLIC_CSP_WHITELIST=${CSP_WHITELIST}
|
export NEXT_PUBLIC_CSP_WHITELIST=${CSP_WHITELIST}
|
||||||
|
export NEXT_PUBLIC_ALLOW_EMBED=${ALLOW_EMBED}
|
||||||
export NEXT_PUBLIC_TOP_K_MAX_VALUE=${TOP_K_MAX_VALUE}
|
export NEXT_PUBLIC_TOP_K_MAX_VALUE=${TOP_K_MAX_VALUE}
|
||||||
export NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH}
|
export NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH}
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,26 @@ import { NextResponse } from 'next/server'
|
|||||||
|
|
||||||
const NECESSARY_DOMAIN = '*.sentry.io http://localhost:* http://127.0.0.1:* https://analytics.google.com googletagmanager.com *.googletagmanager.com https://www.google-analytics.com https://api.github.com'
|
const NECESSARY_DOMAIN = '*.sentry.io http://localhost:* http://127.0.0.1:* https://analytics.google.com googletagmanager.com *.googletagmanager.com https://www.google-analytics.com https://api.github.com'
|
||||||
|
|
||||||
|
const wrapResponseWithXFrameOptions = (response: NextResponse, pathname: string) => {
|
||||||
|
// prevent clickjacking: https://owasp.org/www-community/attacks/Clickjacking
|
||||||
|
// Chatbot page should be allowed to be embedded in iframe. It's a feature
|
||||||
|
if (process.env.NEXT_PUBLIC_ALLOW_EMBED !== 'true' && !pathname.startsWith('/chat'))
|
||||||
|
response.headers.set('X-Frame-Options', 'DENY')
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
export function middleware(request: NextRequest) {
|
export function middleware(request: NextRequest) {
|
||||||
|
const { pathname } = request.nextUrl
|
||||||
|
const requestHeaders = new Headers(request.headers)
|
||||||
|
const response = NextResponse.next({
|
||||||
|
request: {
|
||||||
|
headers: requestHeaders,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const isWhiteListEnabled = !!process.env.NEXT_PUBLIC_CSP_WHITELIST && process.env.NODE_ENV === 'production'
|
const isWhiteListEnabled = !!process.env.NEXT_PUBLIC_CSP_WHITELIST && process.env.NODE_ENV === 'production'
|
||||||
if (!isWhiteListEnabled)
|
if (!isWhiteListEnabled)
|
||||||
return NextResponse.next()
|
return wrapResponseWithXFrameOptions(response, pathname)
|
||||||
|
|
||||||
const whiteList = `${process.env.NEXT_PUBLIC_CSP_WHITELIST} ${NECESSARY_DOMAIN}`
|
const whiteList = `${process.env.NEXT_PUBLIC_CSP_WHITELIST} ${NECESSARY_DOMAIN}`
|
||||||
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
|
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
|
||||||
@@ -33,7 +49,6 @@ export function middleware(request: NextRequest) {
|
|||||||
.replace(/\s{2,}/g, ' ')
|
.replace(/\s{2,}/g, ' ')
|
||||||
.trim()
|
.trim()
|
||||||
|
|
||||||
const requestHeaders = new Headers(request.headers)
|
|
||||||
requestHeaders.set('x-nonce', nonce)
|
requestHeaders.set('x-nonce', nonce)
|
||||||
|
|
||||||
requestHeaders.set(
|
requestHeaders.set(
|
||||||
@@ -41,17 +56,12 @@ export function middleware(request: NextRequest) {
|
|||||||
contentSecurityPolicyHeaderValue,
|
contentSecurityPolicyHeaderValue,
|
||||||
)
|
)
|
||||||
|
|
||||||
const response = NextResponse.next({
|
|
||||||
request: {
|
|
||||||
headers: requestHeaders,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
response.headers.set(
|
response.headers.set(
|
||||||
'Content-Security-Policy',
|
'Content-Security-Policy',
|
||||||
contentSecurityPolicyHeaderValue,
|
contentSecurityPolicyHeaderValue,
|
||||||
)
|
)
|
||||||
|
|
||||||
return response
|
return wrapResponseWithXFrameOptions(response, pathname)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dify-web",
|
"name": "dify-web",
|
||||||
"version": "0.15.5",
|
"version": "0.15.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.17.0"
|
"node": ">=18.17.0"
|
||||||
|
|||||||
@@ -338,7 +338,7 @@ export const sendResetPasswordCode = (email: string, language = 'en-US') =>
|
|||||||
post<CommonResponse & { data: string; message?: string; code?: string }>('/forgot-password', { body: { email, language } })
|
post<CommonResponse & { data: string; message?: string; code?: string }>('/forgot-password', { body: { email, language } })
|
||||||
|
|
||||||
export const verifyResetPasswordCode = (body: { email: string; code: string; token: string }) =>
|
export const verifyResetPasswordCode = (body: { email: string; code: string; token: string }) =>
|
||||||
post<CommonResponse & { is_valid: boolean }>('/forgot-password/validity', { body })
|
post<CommonResponse & { is_valid: boolean; token: string }>('/forgot-password/validity', { body })
|
||||||
|
|
||||||
export const sendDeleteAccountCode = () =>
|
export const sendDeleteAccountCode = () =>
|
||||||
get<CommonResponse & { data: string }>('/account/delete/verify')
|
get<CommonResponse & { data: string }>('/account/delete/verify')
|
||||||
|
|||||||
Reference in New Issue
Block a user