mirror of
https://github.com/langgenius/dify.git
synced 2026-04-26 18:27:15 +08:00
Merge remote-tracking branch 'origin/main' into feat/queue-based-graph-engine
This commit is contained in:
commit
c3f66e2901
@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
npm add -g pnpm@10.15.0
|
corepack enable
|
||||||
cd web && pnpm install
|
cd web && pnpm install
|
||||||
pipx install uv
|
pipx install uv
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import logging
|
|||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_restx import Resource, fields, marshal_with, reqparse
|
from flask_restx import Resource, fields, marshal_with, reqparse
|
||||||
from flask_restx.inputs import int_range
|
from flask_restx.inputs import int_range
|
||||||
|
from sqlalchemy import exists, select
|
||||||
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
|
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
@ -94,21 +95,18 @@ class ChatMessageListApi(Resource):
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
has_more = False
|
|
||||||
if len(history_messages) == args["limit"]:
|
if len(history_messages) == args["limit"]:
|
||||||
current_page_first_message = history_messages[-1]
|
current_page_first_message = history_messages[-1]
|
||||||
rest_count = (
|
|
||||||
db.session.query(Message)
|
has_more = db.session.scalar(
|
||||||
.where(
|
select(
|
||||||
|
exists().where(
|
||||||
Message.conversation_id == conversation.id,
|
Message.conversation_id == conversation.id,
|
||||||
Message.created_at < current_page_first_message.created_at,
|
Message.created_at < current_page_first_message.created_at,
|
||||||
Message.id != current_page_first_message.id,
|
Message.id != current_page_first_message.id,
|
||||||
)
|
)
|
||||||
.count()
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
if rest_count > 0:
|
|
||||||
has_more = True
|
|
||||||
|
|
||||||
history_messages = list(reversed(history_messages))
|
history_messages = list(reversed(history_messages))
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +1,20 @@
|
|||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
|
from flask_restx import Namespace
|
||||||
|
|
||||||
from libs.external_api import ExternalApi
|
from libs.external_api import ExternalApi
|
||||||
|
|
||||||
from .files import FileApi
|
|
||||||
from .remote_files import RemoteFileInfoApi, RemoteFileUploadApi
|
|
||||||
|
|
||||||
bp = Blueprint("web", __name__, url_prefix="/api")
|
bp = Blueprint("web", __name__, url_prefix="/api")
|
||||||
api = ExternalApi(bp)
|
|
||||||
|
|
||||||
# Files
|
api = ExternalApi(
|
||||||
api.add_resource(FileApi, "/files/upload")
|
bp,
|
||||||
|
version="1.0",
|
||||||
|
title="Web API",
|
||||||
|
description="Public APIs for web applications including file uploads, chat interactions, and app management",
|
||||||
|
doc="/docs", # Enable Swagger UI at /api/docs
|
||||||
|
)
|
||||||
|
|
||||||
# Remote files
|
# Create namespace
|
||||||
api.add_resource(RemoteFileInfoApi, "/remote-files/<path:url>")
|
web_ns = Namespace("web", description="Web application API operations", path="/")
|
||||||
api.add_resource(RemoteFileUploadApi, "/remote-files/upload")
|
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
app,
|
app,
|
||||||
@ -21,11 +22,15 @@ from . import (
|
|||||||
completion,
|
completion,
|
||||||
conversation,
|
conversation,
|
||||||
feature,
|
feature,
|
||||||
|
files,
|
||||||
forgot_password,
|
forgot_password,
|
||||||
login,
|
login,
|
||||||
message,
|
message,
|
||||||
passport,
|
passport,
|
||||||
|
remote_files,
|
||||||
saved_message,
|
saved_message,
|
||||||
site,
|
site,
|
||||||
workflow,
|
workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
api.add_namespace(web_ns)
|
||||||
|
|||||||
@ -1,12 +1,21 @@
|
|||||||
from flask_restx import Resource
|
from flask_restx import Resource
|
||||||
|
|
||||||
from controllers.web import api
|
from controllers.web import web_ns
|
||||||
from services.feature_service import FeatureService
|
from services.feature_service import FeatureService
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/system-features")
|
||||||
class SystemFeatureApi(Resource):
|
class SystemFeatureApi(Resource):
|
||||||
|
@web_ns.doc("get_system_features")
|
||||||
|
@web_ns.doc(description="Get system feature flags and configuration")
|
||||||
|
@web_ns.doc(responses={200: "System features retrieved successfully", 500: "Internal server error"})
|
||||||
def get(self):
|
def get(self):
|
||||||
|
"""Get system feature flags and configuration.
|
||||||
|
|
||||||
|
Returns the current system feature flags and configuration
|
||||||
|
that control various functionalities across the platform.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: System feature configuration object
|
||||||
|
"""
|
||||||
return FeatureService.get_system_features().model_dump()
|
return FeatureService.get_system_features().model_dump()
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(SystemFeatureApi, "/system-features")
|
|
||||||
|
|||||||
@ -9,14 +9,50 @@ from controllers.common.errors import (
|
|||||||
TooManyFilesError,
|
TooManyFilesError,
|
||||||
UnsupportedFileTypeError,
|
UnsupportedFileTypeError,
|
||||||
)
|
)
|
||||||
|
from controllers.web import web_ns
|
||||||
from controllers.web.wraps import WebApiResource
|
from controllers.web.wraps import WebApiResource
|
||||||
from fields.file_fields import file_fields
|
from fields.file_fields import build_file_model
|
||||||
from services.file_service import FileService
|
from services.file_service import FileService
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/files/upload")
|
||||||
class FileApi(WebApiResource):
|
class FileApi(WebApiResource):
|
||||||
@marshal_with(file_fields)
|
@web_ns.doc("upload_file")
|
||||||
|
@web_ns.doc(description="Upload a file for use in web applications")
|
||||||
|
@web_ns.doc(
|
||||||
|
responses={
|
||||||
|
201: "File uploaded successfully",
|
||||||
|
400: "Bad request - invalid file or parameters",
|
||||||
|
413: "File too large",
|
||||||
|
415: "Unsupported file type",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@marshal_with(build_file_model(web_ns))
|
||||||
def post(self, app_model, end_user):
|
def post(self, app_model, end_user):
|
||||||
|
"""Upload a file for use in web applications.
|
||||||
|
|
||||||
|
Accepts file uploads for use within web applications, supporting
|
||||||
|
multiple file types with automatic validation and storage.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app_model: The associated application model
|
||||||
|
end_user: The end user uploading the file
|
||||||
|
|
||||||
|
Form Parameters:
|
||||||
|
file: The file to upload (required)
|
||||||
|
source: Optional source type (datasets or None)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: File information including ID, URL, and metadata
|
||||||
|
int: HTTP status code 201 for success
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NoFileUploadedError: No file provided in request
|
||||||
|
TooManyFilesError: Multiple files provided (only one allowed)
|
||||||
|
FilenameNotExistsError: File has no filename
|
||||||
|
FileTooLargeError: File exceeds size limit
|
||||||
|
UnsupportedFileTypeError: File type not supported
|
||||||
|
"""
|
||||||
if "file" not in request.files:
|
if "file" not in request.files:
|
||||||
raise NoFileUploadedError()
|
raise NoFileUploadedError()
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@ from controllers.console.auth.error import (
|
|||||||
)
|
)
|
||||||
from controllers.console.error import EmailSendIpLimitError
|
from controllers.console.error import EmailSendIpLimitError
|
||||||
from controllers.console.wraps import email_password_login_enabled, only_edition_enterprise, setup_required
|
from controllers.console.wraps import email_password_login_enabled, only_edition_enterprise, setup_required
|
||||||
from controllers.web import api
|
from controllers.web import web_ns
|
||||||
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
|
||||||
from libs.password import hash_password, valid_password
|
from libs.password import hash_password, valid_password
|
||||||
@ -24,10 +24,21 @@ from models.account import Account
|
|||||||
from services.account_service import AccountService
|
from services.account_service import AccountService
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/forgot-password")
|
||||||
class ForgotPasswordSendEmailApi(Resource):
|
class ForgotPasswordSendEmailApi(Resource):
|
||||||
@only_edition_enterprise
|
@only_edition_enterprise
|
||||||
@setup_required
|
@setup_required
|
||||||
@email_password_login_enabled
|
@email_password_login_enabled
|
||||||
|
@web_ns.doc("send_forgot_password_email")
|
||||||
|
@web_ns.doc(description="Send password reset email")
|
||||||
|
@web_ns.doc(
|
||||||
|
responses={
|
||||||
|
200: "Password reset email sent successfully",
|
||||||
|
400: "Bad request - invalid email format",
|
||||||
|
404: "Account not found",
|
||||||
|
429: "Too many requests - rate limit exceeded",
|
||||||
|
}
|
||||||
|
)
|
||||||
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")
|
||||||
@ -54,10 +65,16 @@ class ForgotPasswordSendEmailApi(Resource):
|
|||||||
return {"result": "success", "data": token}
|
return {"result": "success", "data": token}
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/forgot-password/validity")
|
||||||
class ForgotPasswordCheckApi(Resource):
|
class ForgotPasswordCheckApi(Resource):
|
||||||
@only_edition_enterprise
|
@only_edition_enterprise
|
||||||
@setup_required
|
@setup_required
|
||||||
@email_password_login_enabled
|
@email_password_login_enabled
|
||||||
|
@web_ns.doc("check_forgot_password_token")
|
||||||
|
@web_ns.doc(description="Verify password reset token validity")
|
||||||
|
@web_ns.doc(
|
||||||
|
responses={200: "Token is valid", 400: "Bad request - invalid token format", 401: "Invalid or expired token"}
|
||||||
|
)
|
||||||
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")
|
||||||
@ -94,10 +111,21 @@ class ForgotPasswordCheckApi(Resource):
|
|||||||
return {"is_valid": True, "email": token_data.get("email"), "token": new_token}
|
return {"is_valid": True, "email": token_data.get("email"), "token": new_token}
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/forgot-password/resets")
|
||||||
class ForgotPasswordResetApi(Resource):
|
class ForgotPasswordResetApi(Resource):
|
||||||
@only_edition_enterprise
|
@only_edition_enterprise
|
||||||
@setup_required
|
@setup_required
|
||||||
@email_password_login_enabled
|
@email_password_login_enabled
|
||||||
|
@web_ns.doc("reset_password")
|
||||||
|
@web_ns.doc(description="Reset user password with verification token")
|
||||||
|
@web_ns.doc(
|
||||||
|
responses={
|
||||||
|
200: "Password reset successfully",
|
||||||
|
400: "Bad request - invalid parameters or password mismatch",
|
||||||
|
401: "Invalid or expired token",
|
||||||
|
404: "Account not found",
|
||||||
|
}
|
||||||
|
)
|
||||||
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")
|
||||||
@ -141,8 +169,3 @@ class ForgotPasswordResetApi(Resource):
|
|||||||
account.password = base64.b64encode(password_hashed).decode()
|
account.password = base64.b64encode(password_hashed).decode()
|
||||||
account.password_salt = base64.b64encode(salt).decode()
|
account.password_salt = base64.b64encode(salt).decode()
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(ForgotPasswordSendEmailApi, "/forgot-password")
|
|
||||||
api.add_resource(ForgotPasswordCheckApi, "/forgot-password/validity")
|
|
||||||
api.add_resource(ForgotPasswordResetApi, "/forgot-password/resets")
|
|
||||||
|
|||||||
@ -9,18 +9,30 @@ from controllers.console.auth.error import (
|
|||||||
)
|
)
|
||||||
from controllers.console.error import AccountBannedError
|
from controllers.console.error import AccountBannedError
|
||||||
from controllers.console.wraps import only_edition_enterprise, setup_required
|
from controllers.console.wraps import only_edition_enterprise, setup_required
|
||||||
from controllers.web import api
|
from controllers.web import web_ns
|
||||||
from libs.helper import email
|
from libs.helper import email
|
||||||
from libs.password import valid_password
|
from libs.password import valid_password
|
||||||
from services.account_service import AccountService
|
from services.account_service import AccountService
|
||||||
from services.webapp_auth_service import WebAppAuthService
|
from services.webapp_auth_service import WebAppAuthService
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/login")
|
||||||
class LoginApi(Resource):
|
class LoginApi(Resource):
|
||||||
"""Resource for web app email/password login."""
|
"""Resource for web app email/password login."""
|
||||||
|
|
||||||
@setup_required
|
@setup_required
|
||||||
@only_edition_enterprise
|
@only_edition_enterprise
|
||||||
|
@web_ns.doc("web_app_login")
|
||||||
|
@web_ns.doc(description="Authenticate user for web application access")
|
||||||
|
@web_ns.doc(
|
||||||
|
responses={
|
||||||
|
200: "Authentication successful",
|
||||||
|
400: "Bad request - invalid email or password format",
|
||||||
|
401: "Authentication failed - email or password mismatch",
|
||||||
|
403: "Account banned or login disabled",
|
||||||
|
404: "Account not found",
|
||||||
|
}
|
||||||
|
)
|
||||||
def post(self):
|
def post(self):
|
||||||
"""Authenticate user and login."""
|
"""Authenticate user and login."""
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
@ -51,9 +63,19 @@ class LoginApi(Resource):
|
|||||||
# return {"result": "success"}
|
# return {"result": "success"}
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/email-code-login")
|
||||||
class EmailCodeLoginSendEmailApi(Resource):
|
class EmailCodeLoginSendEmailApi(Resource):
|
||||||
@setup_required
|
@setup_required
|
||||||
@only_edition_enterprise
|
@only_edition_enterprise
|
||||||
|
@web_ns.doc("send_email_code_login")
|
||||||
|
@web_ns.doc(description="Send email verification code for login")
|
||||||
|
@web_ns.doc(
|
||||||
|
responses={
|
||||||
|
200: "Email code sent successfully",
|
||||||
|
400: "Bad request - invalid email format",
|
||||||
|
404: "Account not found",
|
||||||
|
}
|
||||||
|
)
|
||||||
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")
|
||||||
@ -74,9 +96,20 @@ class EmailCodeLoginSendEmailApi(Resource):
|
|||||||
return {"result": "success", "data": token}
|
return {"result": "success", "data": token}
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/email-code-login/validity")
|
||||||
class EmailCodeLoginApi(Resource):
|
class EmailCodeLoginApi(Resource):
|
||||||
@setup_required
|
@setup_required
|
||||||
@only_edition_enterprise
|
@only_edition_enterprise
|
||||||
|
@web_ns.doc("verify_email_code_login")
|
||||||
|
@web_ns.doc(description="Verify email code and complete login")
|
||||||
|
@web_ns.doc(
|
||||||
|
responses={
|
||||||
|
200: "Email code verified and login successful",
|
||||||
|
400: "Bad request - invalid code or token",
|
||||||
|
401: "Invalid token or expired code",
|
||||||
|
404: "Account not found",
|
||||||
|
}
|
||||||
|
)
|
||||||
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")
|
||||||
@ -104,9 +137,3 @@ class EmailCodeLoginApi(Resource):
|
|||||||
token = WebAppAuthService.login(account=account)
|
token = WebAppAuthService.login(account=account)
|
||||||
AccountService.reset_login_error_rate_limit(args["email"])
|
AccountService.reset_login_error_rate_limit(args["email"])
|
||||||
return {"result": "success", "data": {"access_token": token}}
|
return {"result": "success", "data": {"access_token": token}}
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(LoginApi, "/login")
|
|
||||||
# api.add_resource(LogoutApi, "/logout")
|
|
||||||
api.add_resource(EmailCodeLoginSendEmailApi, "/email-code-login")
|
|
||||||
api.add_resource(EmailCodeLoginApi, "/email-code-login/validity")
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ from sqlalchemy import func, select
|
|||||||
from werkzeug.exceptions import NotFound, Unauthorized
|
from werkzeug.exceptions import NotFound, Unauthorized
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from controllers.web import api
|
from controllers.web import web_ns
|
||||||
from controllers.web.error import WebAppAuthRequiredError
|
from controllers.web.error import WebAppAuthRequiredError
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from libs.passport import PassportService
|
from libs.passport import PassportService
|
||||||
@ -17,9 +17,19 @@ from services.feature_service import FeatureService
|
|||||||
from services.webapp_auth_service import WebAppAuthService, WebAppAuthType
|
from services.webapp_auth_service import WebAppAuthService, WebAppAuthType
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/passport")
|
||||||
class PassportResource(Resource):
|
class PassportResource(Resource):
|
||||||
"""Base resource for passport."""
|
"""Base resource for passport."""
|
||||||
|
|
||||||
|
@web_ns.doc("get_passport")
|
||||||
|
@web_ns.doc(description="Get authentication passport for web application access")
|
||||||
|
@web_ns.doc(
|
||||||
|
responses={
|
||||||
|
200: "Passport retrieved successfully",
|
||||||
|
401: "Unauthorized - missing app code or invalid authentication",
|
||||||
|
404: "Application or user not found",
|
||||||
|
}
|
||||||
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
system_features = FeatureService.get_system_features()
|
system_features = FeatureService.get_system_features()
|
||||||
app_code = request.headers.get("X-App-Code")
|
app_code = request.headers.get("X-App-Code")
|
||||||
@ -94,9 +104,6 @@ class PassportResource(Resource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(PassportResource, "/passport")
|
|
||||||
|
|
||||||
|
|
||||||
def decode_enterprise_webapp_user_id(jwt_token: str | None):
|
def decode_enterprise_webapp_user_id(jwt_token: str | None):
|
||||||
"""
|
"""
|
||||||
Decode the enterprise user session from the Authorization header.
|
Decode the enterprise user session from the Authorization header.
|
||||||
|
|||||||
@ -10,16 +10,44 @@ from controllers.common.errors import (
|
|||||||
RemoteFileUploadError,
|
RemoteFileUploadError,
|
||||||
UnsupportedFileTypeError,
|
UnsupportedFileTypeError,
|
||||||
)
|
)
|
||||||
|
from controllers.web import web_ns
|
||||||
from controllers.web.wraps import WebApiResource
|
from controllers.web.wraps import WebApiResource
|
||||||
from core.file import helpers as file_helpers
|
from core.file import helpers as file_helpers
|
||||||
from core.helper import ssrf_proxy
|
from core.helper import ssrf_proxy
|
||||||
from fields.file_fields import file_fields_with_signed_url, remote_file_info_fields
|
from fields.file_fields import build_file_with_signed_url_model, build_remote_file_info_model
|
||||||
from services.file_service import FileService
|
from services.file_service import FileService
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/remote-files/<path:url>")
|
||||||
class RemoteFileInfoApi(WebApiResource):
|
class RemoteFileInfoApi(WebApiResource):
|
||||||
@marshal_with(remote_file_info_fields)
|
@web_ns.doc("get_remote_file_info")
|
||||||
|
@web_ns.doc(description="Get information about a remote file")
|
||||||
|
@web_ns.doc(
|
||||||
|
responses={
|
||||||
|
200: "Remote file information retrieved successfully",
|
||||||
|
400: "Bad request - invalid URL",
|
||||||
|
404: "Remote file not found",
|
||||||
|
500: "Failed to fetch remote file",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@marshal_with(build_remote_file_info_model(web_ns))
|
||||||
def get(self, app_model, end_user, url):
|
def get(self, app_model, end_user, url):
|
||||||
|
"""Get information about a remote file.
|
||||||
|
|
||||||
|
Retrieves basic information about a file located at a remote URL,
|
||||||
|
including content type and content length.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app_model: The associated application model
|
||||||
|
end_user: The end user making the request
|
||||||
|
url: URL-encoded path to the remote file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Remote file information including type and length
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If the remote file cannot be accessed
|
||||||
|
"""
|
||||||
decoded_url = urllib.parse.unquote(url)
|
decoded_url = urllib.parse.unquote(url)
|
||||||
resp = ssrf_proxy.head(decoded_url)
|
resp = ssrf_proxy.head(decoded_url)
|
||||||
if resp.status_code != httpx.codes.OK:
|
if resp.status_code != httpx.codes.OK:
|
||||||
@ -32,9 +60,42 @@ class RemoteFileInfoApi(WebApiResource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/remote-files/upload")
|
||||||
class RemoteFileUploadApi(WebApiResource):
|
class RemoteFileUploadApi(WebApiResource):
|
||||||
@marshal_with(file_fields_with_signed_url)
|
@web_ns.doc("upload_remote_file")
|
||||||
def post(self, app_model, end_user): # Add app_model and end_user parameters
|
@web_ns.doc(description="Upload a file from a remote URL")
|
||||||
|
@web_ns.doc(
|
||||||
|
responses={
|
||||||
|
201: "Remote file uploaded successfully",
|
||||||
|
400: "Bad request - invalid URL or parameters",
|
||||||
|
413: "File too large",
|
||||||
|
415: "Unsupported file type",
|
||||||
|
500: "Failed to fetch remote file",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@marshal_with(build_file_with_signed_url_model(web_ns))
|
||||||
|
def post(self, app_model, end_user):
|
||||||
|
"""Upload a file from a remote URL.
|
||||||
|
|
||||||
|
Downloads a file from the provided remote URL and uploads it
|
||||||
|
to the platform storage for use in web applications.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app_model: The associated application model
|
||||||
|
end_user: The end user making the request
|
||||||
|
|
||||||
|
JSON Parameters:
|
||||||
|
url: The remote URL to download the file from (required)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: File information including ID, signed URL, and metadata
|
||||||
|
int: HTTP status code 201 for success
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RemoteFileUploadError: Failed to fetch file from remote URL
|
||||||
|
FileTooLargeError: File exceeds size limit
|
||||||
|
UnsupportedFileTypeError: File type not supported
|
||||||
|
"""
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("url", type=str, required=True, help="URL is required")
|
parser.add_argument("url", type=str, required=True, help="URL is required")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|||||||
@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any, Literal, Optional, cast
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_login import UserMixin
|
from flask_login import UserMixin
|
||||||
from sqlalchemy import Float, Index, PrimaryKeyConstraint, String, func, text
|
from sqlalchemy import Float, Index, PrimaryKeyConstraint, String, exists, func, select, text
|
||||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
@ -1554,7 +1554,7 @@ class ApiToken(Base):
|
|||||||
def generate_api_key(prefix, n):
|
def generate_api_key(prefix, n):
|
||||||
while True:
|
while True:
|
||||||
result = prefix + generate_string(n)
|
result = prefix + generate_string(n)
|
||||||
if db.session.query(ApiToken).where(ApiToken.token == result).count() > 0:
|
if db.session.scalar(select(exists().where(ApiToken.token == result))):
|
||||||
continue
|
continue
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Any, Optional, Union
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy import DateTime, orm
|
from sqlalchemy import DateTime, exists, orm, select
|
||||||
|
|
||||||
from core.file.constants import maybe_file_object
|
from core.file.constants import maybe_file_object
|
||||||
from core.file.models import File
|
from core.file.models import File
|
||||||
@ -336,12 +336,13 @@ class Workflow(Base):
|
|||||||
"""
|
"""
|
||||||
from models.tools import WorkflowToolProvider
|
from models.tools import WorkflowToolProvider
|
||||||
|
|
||||||
return (
|
stmt = select(
|
||||||
db.session.query(WorkflowToolProvider)
|
exists().where(
|
||||||
.where(WorkflowToolProvider.tenant_id == self.tenant_id, WorkflowToolProvider.app_id == self.app_id)
|
WorkflowToolProvider.tenant_id == self.tenant_id,
|
||||||
.count()
|
WorkflowToolProvider.app_id == self.app_id,
|
||||||
> 0
|
)
|
||||||
)
|
)
|
||||||
|
return db.session.execute(stmt).scalar_one()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def environment_variables(self) -> Sequence[StringVariable | IntegerVariable | FloatVariable | SecretVariable]:
|
def environment_variables(self) -> Sequence[StringVariable | IntegerVariable | FloatVariable | SecretVariable]:
|
||||||
@ -921,7 +922,7 @@ def _naive_utc_datetime():
|
|||||||
|
|
||||||
class WorkflowDraftVariable(Base):
|
class WorkflowDraftVariable(Base):
|
||||||
"""`WorkflowDraftVariable` record variables and outputs generated during
|
"""`WorkflowDraftVariable` record variables and outputs generated during
|
||||||
debugging worfklow or chatflow.
|
debugging workflow or chatflow.
|
||||||
|
|
||||||
IMPORTANT: This model maintains multiple invariant rules that must be preserved.
|
IMPORTANT: This model maintains multiple invariant rules that must be preserved.
|
||||||
Do not instantiate this class directly with the constructor.
|
Do not instantiate this class directly with the constructor.
|
||||||
|
|||||||
@ -9,7 +9,7 @@ from collections import Counter
|
|||||||
from typing import Any, Literal, Optional
|
from typing import Any, Literal, Optional
|
||||||
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import exists, func, select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from werkzeug.exceptions import NotFound
|
from werkzeug.exceptions import NotFound
|
||||||
|
|
||||||
@ -655,10 +655,8 @@ class DatasetService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def dataset_use_check(dataset_id) -> bool:
|
def dataset_use_check(dataset_id) -> bool:
|
||||||
count = db.session.query(AppDatasetJoin).filter_by(dataset_id=dataset_id).count()
|
stmt = select(exists().where(AppDatasetJoin.dataset_id == dataset_id))
|
||||||
if count > 0:
|
return db.session.execute(stmt).scalar_one()
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_dataset_permission(dataset, user):
|
def check_dataset_permission(dataset, user):
|
||||||
|
|||||||
@ -5,6 +5,7 @@ from collections.abc import Mapping
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from sqlalchemy import exists, select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
@ -190,11 +191,14 @@ class BuiltinToolManageService:
|
|||||||
# update name if provided
|
# update name if provided
|
||||||
if name and name != db_provider.name:
|
if name and name != db_provider.name:
|
||||||
# check if the name is already used
|
# check if the name is already used
|
||||||
if (
|
if session.scalar(
|
||||||
session.query(BuiltinToolProvider)
|
select(
|
||||||
.filter_by(tenant_id=tenant_id, provider=provider, name=name)
|
exists().where(
|
||||||
.count()
|
BuiltinToolProvider.tenant_id == tenant_id,
|
||||||
> 0
|
BuiltinToolProvider.provider == provider,
|
||||||
|
BuiltinToolProvider.name == name,
|
||||||
|
)
|
||||||
|
)
|
||||||
):
|
):
|
||||||
raise ValueError(f"the credential name '{name}' is already used")
|
raise ValueError(f"the credential name '{name}' is already used")
|
||||||
|
|
||||||
@ -246,11 +250,14 @@ class BuiltinToolManageService:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# check if the name is already used
|
# check if the name is already used
|
||||||
if (
|
if session.scalar(
|
||||||
session.query(BuiltinToolProvider)
|
select(
|
||||||
.filter_by(tenant_id=tenant_id, provider=provider, name=name)
|
exists().where(
|
||||||
.count()
|
BuiltinToolProvider.tenant_id == tenant_id,
|
||||||
> 0
|
BuiltinToolProvider.provider == provider,
|
||||||
|
BuiltinToolProvider.name == name,
|
||||||
|
)
|
||||||
|
)
|
||||||
):
|
):
|
||||||
raise ValueError(f"the credential name '{name}' is already used")
|
raise ValueError(f"the credential name '{name}' is already used")
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import uuid
|
|||||||
from collections.abc import Callable, Generator, Mapping, Sequence
|
from collections.abc import Callable, Generator, Mapping, Sequence
|
||||||
from typing import Any, Optional, cast
|
from typing import Any, Optional, cast
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import exists, select
|
||||||
from sqlalchemy.orm import Session, sessionmaker
|
from sqlalchemy.orm import Session, sessionmaker
|
||||||
|
|
||||||
from core.app.app_config.entities import VariableEntityType
|
from core.app.app_config.entities import VariableEntityType
|
||||||
@ -83,15 +83,14 @@ class WorkflowService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def is_workflow_exist(self, app_model: App) -> bool:
|
def is_workflow_exist(self, app_model: App) -> bool:
|
||||||
return (
|
stmt = select(
|
||||||
db.session.query(Workflow)
|
exists().where(
|
||||||
.where(
|
|
||||||
Workflow.tenant_id == app_model.tenant_id,
|
Workflow.tenant_id == app_model.tenant_id,
|
||||||
Workflow.app_id == app_model.id,
|
Workflow.app_id == app_model.id,
|
||||||
Workflow.version == Workflow.VERSION_DRAFT,
|
Workflow.version == Workflow.VERSION_DRAFT,
|
||||||
)
|
)
|
||||||
.count()
|
)
|
||||||
) > 0
|
return db.session.execute(stmt).scalar_one()
|
||||||
|
|
||||||
def get_draft_workflow(self, app_model: App) -> Optional[Workflow]:
|
def get_draft_workflow(self, app_model: App) -> Optional[Workflow]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import time
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
|
from sqlalchemy import exists, select
|
||||||
|
|
||||||
from core.rag.datasource.vdb.vector_factory import Vector
|
from core.rag.datasource.vdb.vector_factory import Vector
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
@ -22,7 +23,7 @@ def disable_annotation_reply_task(job_id: str, app_id: str, tenant_id: str):
|
|||||||
start_at = time.perf_counter()
|
start_at = time.perf_counter()
|
||||||
# get app info
|
# get app info
|
||||||
app = db.session.query(App).where(App.id == app_id, App.tenant_id == tenant_id, App.status == "normal").first()
|
app = db.session.query(App).where(App.id == app_id, App.tenant_id == tenant_id, App.status == "normal").first()
|
||||||
annotations_count = db.session.query(MessageAnnotation).where(MessageAnnotation.app_id == app_id).count()
|
annotations_exists = db.session.scalar(select(exists().where(MessageAnnotation.app_id == app_id)))
|
||||||
if not app:
|
if not app:
|
||||||
logger.info(click.style(f"App not found: {app_id}", fg="red"))
|
logger.info(click.style(f"App not found: {app_id}", fg="red"))
|
||||||
db.session.close()
|
db.session.close()
|
||||||
@ -47,7 +48,7 @@ def disable_annotation_reply_task(job_id: str, app_id: str, tenant_id: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if annotations_count > 0:
|
if annotations_exists:
|
||||||
vector = Vector(dataset, attributes=["doc_id", "annotation_id", "app_id"])
|
vector = Vector(dataset, attributes=["doc_id", "annotation_id", "app_id"])
|
||||||
vector.delete()
|
vector.delete()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@ -27,10 +27,11 @@ const ChunkDetailModal: FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { segment, score, child_chunks } = payload
|
const { segment, score, child_chunks } = payload
|
||||||
const { position, content, sign_content, keywords, document } = segment
|
const { position, content, sign_content, keywords, document, answer } = segment
|
||||||
const isParentChildRetrieval = !!(child_chunks && child_chunks.length > 0)
|
const isParentChildRetrieval = !!(child_chunks && child_chunks.length > 0)
|
||||||
const extension = document.name.split('.').slice(-1)[0] as FileAppearanceTypeEnum
|
const extension = document.name.split('.').slice(-1)[0] as FileAppearanceTypeEnum
|
||||||
const heighClassName = isParentChildRetrieval ? 'h-[min(627px,_80vh)] overflow-y-auto' : 'h-[min(539px,_80vh)] overflow-y-auto'
|
const heighClassName = isParentChildRetrieval ? 'h-[min(627px,_80vh)] overflow-y-auto' : 'h-[min(539px,_80vh)] overflow-y-auto'
|
||||||
|
const labelPrefix = isParentChildRetrieval ? t('datasetDocuments.segment.parentChunk') : t('datasetDocuments.segment.chunk')
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={t(`${i18nPrefix}.chunkDetail`)}
|
title={t(`${i18nPrefix}.chunkDetail`)}
|
||||||
@ -45,7 +46,7 @@ const ChunkDetailModal: FC<Props> = ({
|
|||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between'>
|
||||||
<div className='flex grow items-center space-x-2'>
|
<div className='flex grow items-center space-x-2'>
|
||||||
<SegmentIndexTag
|
<SegmentIndexTag
|
||||||
labelPrefix={`${isParentChildRetrieval ? 'Parent-' : ''}Chunk`}
|
labelPrefix={labelPrefix}
|
||||||
positionId={position}
|
positionId={position}
|
||||||
className={cn('w-fit group-hover:opacity-100')}
|
className={cn('w-fit group-hover:opacity-100')}
|
||||||
/>
|
/>
|
||||||
@ -57,11 +58,29 @@ const ChunkDetailModal: FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
<Score value={score} />
|
<Score value={score} />
|
||||||
</div>
|
</div>
|
||||||
<Markdown
|
{!answer && (
|
||||||
className={cn('!mt-2 !text-text-secondary', heighClassName)}
|
<Markdown
|
||||||
content={sign_content || content}
|
className={cn('!mt-2 !text-text-secondary', heighClassName)}
|
||||||
customDisallowedElements={['input']}
|
content={sign_content || content}
|
||||||
/>
|
customDisallowedElements={['input']}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{answer && (
|
||||||
|
<div>
|
||||||
|
<div className='flex gap-x-1'>
|
||||||
|
<div className='w-4 shrink-0 text-[13px] font-medium leading-[20px] text-text-tertiary'>Q</div>
|
||||||
|
<div className={cn('body-md-regular text-text-secondary line-clamp-20')}>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex gap-x-1'>
|
||||||
|
<div className='w-4 shrink-0 text-[13px] font-medium leading-[20px] text-text-tertiary'>A</div>
|
||||||
|
<div className={cn('body-md-regular text-text-secondary line-clamp-20')}>
|
||||||
|
{answer}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{!isParentChildRetrieval && keywords && keywords.length > 0 && (
|
{!isParentChildRetrieval && keywords && keywords.length > 0 && (
|
||||||
<div className='mt-6'>
|
<div className='mt-6'>
|
||||||
<div className='text-xs font-medium uppercase text-text-tertiary'>{t(`${i18nPrefix}.keyword`)}</div>
|
<div className='text-xs font-medium uppercase text-text-tertiary'>{t(`${i18nPrefix}.keyword`)}</div>
|
||||||
|
|||||||
@ -32,6 +32,7 @@ export const checkOrSetAccessToken = async (appCode?: string | null) => {
|
|||||||
[userId || 'DEFAULT']: res.access_token,
|
[userId || 'DEFAULT']: res.access_token,
|
||||||
}
|
}
|
||||||
localStorage.setItem('token', JSON.stringify(accessTokenJson))
|
localStorage.setItem('token', JSON.stringify(accessTokenJson))
|
||||||
|
localStorage.removeItem(CONVERSATION_ID_INFO)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import type { FC, PropsWithChildren } from 'react'
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
|
import { useGlobalPublicStore } from './global-public-context'
|
||||||
|
|
||||||
type WebAppStore = {
|
type WebAppStore = {
|
||||||
shareCode: string | null
|
shareCode: string | null
|
||||||
@ -56,6 +57,7 @@ const getShareCodeFromPathname = (pathname: string): string | null => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const WebAppStoreProvider: FC<PropsWithChildren> = ({ children }) => {
|
const WebAppStoreProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||||
|
const isGlobalPending = useGlobalPublicStore(s => s.isGlobalPending)
|
||||||
const updateWebAppAccessMode = useWebAppStore(state => state.updateWebAppAccessMode)
|
const updateWebAppAccessMode = useWebAppStore(state => state.updateWebAppAccessMode)
|
||||||
const updateShareCode = useWebAppStore(state => state.updateShareCode)
|
const updateShareCode = useWebAppStore(state => state.updateShareCode)
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
@ -69,7 +71,7 @@ const WebAppStoreProvider: FC<PropsWithChildren> = ({ children }) => {
|
|||||||
}, [shareCode, updateShareCode])
|
}, [shareCode, updateShareCode])
|
||||||
|
|
||||||
const { isFetching, data: accessModeResult } = useGetWebAppAccessModeByCode(shareCode)
|
const { isFetching, data: accessModeResult } = useGetWebAppAccessModeByCode(shareCode)
|
||||||
const [isFetchingAccessToken, setIsFetchingAccessToken] = useState(false)
|
const [isFetchingAccessToken, setIsFetchingAccessToken] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (accessModeResult?.accessMode) {
|
if (accessModeResult?.accessMode) {
|
||||||
@ -86,7 +88,7 @@ const WebAppStoreProvider: FC<PropsWithChildren> = ({ children }) => {
|
|||||||
}
|
}
|
||||||
}, [accessModeResult, updateWebAppAccessMode, shareCode])
|
}, [accessModeResult, updateWebAppAccessMode, shareCode])
|
||||||
|
|
||||||
if (isFetching || isFetchingAccessToken) {
|
if (isGlobalPending || isFetching || isFetchingAccessToken) {
|
||||||
return <div className='flex h-full w-full items-center justify-center'>
|
return <div className='flex h-full w-full items-center justify-center'>
|
||||||
<Loading />
|
<Loading />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import tailwind from 'eslint-plugin-tailwindcss'
|
|||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
import sonar from 'eslint-plugin-sonarjs'
|
import sonar from 'eslint-plugin-sonarjs'
|
||||||
import oxlint from 'eslint-plugin-oxlint'
|
import oxlint from 'eslint-plugin-oxlint'
|
||||||
|
import next from '@next/eslint-plugin-next'
|
||||||
|
|
||||||
// import reactRefresh from 'eslint-plugin-react-refresh'
|
// import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
|
||||||
@ -63,12 +64,14 @@ export default combine(
|
|||||||
}),
|
}),
|
||||||
unicorn(),
|
unicorn(),
|
||||||
node(),
|
node(),
|
||||||
// use nextjs config will break @eslint/config-inspector
|
// Next.js configuration
|
||||||
// use `ESLINT_CONFIG_INSPECTOR=true pnpx @eslint/config-inspector` to check the config
|
|
||||||
// ...process.env.ESLINT_CONFIG_INSPECTOR
|
|
||||||
// ? []
|
|
||||||
{
|
{
|
||||||
|
plugins: {
|
||||||
|
'@next/next': next,
|
||||||
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
...next.configs.recommended.rules,
|
||||||
|
...next.configs['core-web-vitals'].rules,
|
||||||
// performance issue, and not used.
|
// performance issue, and not used.
|
||||||
'@next/next/no-html-link-for-pages': 'off',
|
'@next/next/no-html-link-for-pages': 'off',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -545,6 +545,7 @@ export type Segment = {
|
|||||||
keywords: string[]
|
keywords: string[]
|
||||||
hit_count: number
|
hit_count: number
|
||||||
index_node_hash: string
|
index_node_hash: string
|
||||||
|
answer: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Document = {
|
export type Document = {
|
||||||
|
|||||||
@ -25,7 +25,7 @@
|
|||||||
"start": "cp -r .next/static .next/standalone/.next/static && cp -r public .next/standalone/public && cross-env PORT=$npm_config_port HOSTNAME=$npm_config_host node .next/standalone/server.js",
|
"start": "cp -r .next/static .next/standalone/.next/static && cp -r public .next/standalone/public && cross-env PORT=$npm_config_port HOSTNAME=$npm_config_host node .next/standalone/server.js",
|
||||||
"lint": "pnpx oxlint && pnpm eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache",
|
"lint": "pnpx oxlint && pnpm eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache",
|
||||||
"lint-only-show-error": "pnpx oxlint && pnpm eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --quiet",
|
"lint-only-show-error": "pnpx oxlint && pnpm eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --quiet",
|
||||||
"fix": "next lint --fix",
|
"fix": "eslint --fix .",
|
||||||
"eslint-fix": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --fix",
|
"eslint-fix": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --fix",
|
||||||
"eslint-fix-only-show-error": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --fix --quiet",
|
"eslint-fix-only-show-error": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --fix --quiet",
|
||||||
"eslint-complexity": "eslint --rule 'complexity: [error, {max: 15}]' --quiet",
|
"eslint-complexity": "eslint --rule 'complexity: [error, {max: 15}]' --quiet",
|
||||||
@ -103,14 +103,14 @@
|
|||||||
"mime": "^4.0.4",
|
"mime": "^4.0.4",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"negotiator": "^0.6.3",
|
"negotiator": "^0.6.3",
|
||||||
"next": "~15.3.5",
|
"next": "15.5.0",
|
||||||
"next-themes": "^0.4.3",
|
"next-themes": "^0.4.3",
|
||||||
"pinyin-pro": "^3.25.0",
|
"pinyin-pro": "^3.25.0",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"qs": "^6.13.0",
|
"qs": "^6.13.0",
|
||||||
"react": "~19.1.0",
|
"react": "19.1.1",
|
||||||
"react-18-input-autosize": "^3.0.0",
|
"react-18-input-autosize": "^3.0.0",
|
||||||
"react-dom": "~19.1.0",
|
"react-dom": "19.1.1",
|
||||||
"react-easy-crop": "^5.1.0",
|
"react-easy-crop": "^5.1.0",
|
||||||
"react-error-boundary": "^4.1.2",
|
"react-error-boundary": "^4.1.2",
|
||||||
"react-headless-pagination": "^1.1.6",
|
"react-headless-pagination": "^1.1.6",
|
||||||
@ -161,9 +161,9 @@
|
|||||||
"@happy-dom/jest-environment": "^17.4.4",
|
"@happy-dom/jest-environment": "^17.4.4",
|
||||||
"@mdx-js/loader": "^3.1.0",
|
"@mdx-js/loader": "^3.1.0",
|
||||||
"@mdx-js/react": "^3.1.0",
|
"@mdx-js/react": "^3.1.0",
|
||||||
"@next/bundle-analyzer": "^15.4.1",
|
"@next/bundle-analyzer": "15.5.0",
|
||||||
"@next/eslint-plugin-next": "~15.4.5",
|
"@next/eslint-plugin-next": "15.5.0",
|
||||||
"@next/mdx": "~15.3.5",
|
"@next/mdx": "15.5.0",
|
||||||
"@rgrove/parse-xml": "^4.1.0",
|
"@rgrove/parse-xml": "^4.1.0",
|
||||||
"@storybook/addon-essentials": "8.5.0",
|
"@storybook/addon-essentials": "8.5.0",
|
||||||
"@storybook/addon-interactions": "8.5.0",
|
"@storybook/addon-interactions": "8.5.0",
|
||||||
@ -185,8 +185,8 @@
|
|||||||
"@types/negotiator": "^0.6.3",
|
"@types/negotiator": "^0.6.3",
|
||||||
"@types/node": "18.15.0",
|
"@types/node": "18.15.0",
|
||||||
"@types/qs": "^6.9.16",
|
"@types/qs": "^6.9.16",
|
||||||
"@types/react": "~19.1.8",
|
"@types/react": "19.1.11",
|
||||||
"@types/react-dom": "~19.1.6",
|
"@types/react-dom": "19.1.7",
|
||||||
"@types/react-slider": "^1.3.6",
|
"@types/react-slider": "^1.3.6",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"@types/react-window": "^1.8.8",
|
"@types/react-window": "^1.8.8",
|
||||||
@ -200,7 +200,7 @@
|
|||||||
"code-inspector-plugin": "^0.18.1",
|
"code-inspector-plugin": "^0.18.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.32.0",
|
||||||
"eslint-config-next": "~15.4.5",
|
"eslint-config-next": "15.5.0",
|
||||||
"eslint-plugin-oxlint": "^1.6.0",
|
"eslint-plugin-oxlint": "^1.6.0",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0",
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
@ -223,8 +223,8 @@
|
|||||||
"uglify-js": "^3.19.3"
|
"uglify-js": "^3.19.3"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/react": "~19.1.8",
|
"@types/react": "19.1.11",
|
||||||
"@types/react-dom": "~19.1.6",
|
"@types/react-dom": "19.1.7",
|
||||||
"string-width": "4.2.3"
|
"string-width": "4.2.3"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
|
|||||||
1121
web/pnpm-lock.yaml
generated
1121
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -398,9 +398,7 @@ export const ssePost = async (
|
|||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (!/^[23]\d{2}$/.test(String(res.status))) {
|
if (!/^[23]\d{2}$/.test(String(res.status))) {
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
refreshAccessTokenOrRelogin(TIME_OUT).then(() => {
|
if (isPublicAPI) {
|
||||||
ssePost(url, fetchOptions, otherOptions)
|
|
||||||
}).catch(() => {
|
|
||||||
res.json().then((data: any) => {
|
res.json().then((data: any) => {
|
||||||
if (isPublicAPI) {
|
if (isPublicAPI) {
|
||||||
if (data.code === 'web_app_access_denied')
|
if (data.code === 'web_app_access_denied')
|
||||||
@ -417,7 +415,14 @@ export const ssePost = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
else {
|
||||||
|
refreshAccessTokenOrRelogin(TIME_OUT).then(() => {
|
||||||
|
ssePost(url, fetchOptions, otherOptions)
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
res.json().then((data) => {
|
res.json().then((data) => {
|
||||||
|
|||||||
@ -1,20 +1,12 @@
|
|||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { AccessMode } from '@/models/access-control'
|
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { fetchAppInfo, fetchAppMeta, fetchAppParams, getAppAccessModeByAppCode } from './share'
|
import { fetchAppInfo, fetchAppMeta, fetchAppParams, getAppAccessModeByAppCode } from './share'
|
||||||
|
|
||||||
const NAME_SPACE = 'webapp'
|
const NAME_SPACE = 'webapp'
|
||||||
|
|
||||||
export const useGetWebAppAccessModeByCode = (code: string | null) => {
|
export const useGetWebAppAccessModeByCode = (code: string | null) => {
|
||||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: [NAME_SPACE, 'appAccessMode', code],
|
queryKey: [NAME_SPACE, 'appAccessMode', code],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
if (systemFeatures.webapp_auth.enabled === false) {
|
|
||||||
return {
|
|
||||||
accessMode: AccessMode.PUBLIC,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!code || code.length === 0)
|
if (!code || code.length === 0)
|
||||||
return Promise.reject(new Error('App code is required to get access mode'))
|
return Promise.reject(new Error('App code is required to get access mode'))
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user