Merge remote-tracking branch 'origin/main' into feat/queue-based-graph-engine

This commit is contained in:
-LAN- 2025-08-27 18:05:35 +08:00
commit c3f66e2901
No known key found for this signature in database
GPG Key ID: 6BA0D108DED011FF
24 changed files with 859 additions and 685 deletions

View File

@ -1,6 +1,6 @@
#!/bin/bash
npm add -g pnpm@10.15.0
corepack enable
cd web && pnpm install
pipx install uv

View File

@ -3,6 +3,7 @@ import logging
from flask_login import current_user
from flask_restx import Resource, fields, marshal_with, reqparse
from flask_restx.inputs import int_range
from sqlalchemy import exists, select
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
from controllers.console import api
@ -94,21 +95,18 @@ class ChatMessageListApi(Resource):
.all()
)
has_more = False
if len(history_messages) == args["limit"]:
current_page_first_message = history_messages[-1]
rest_count = (
db.session.query(Message)
.where(
has_more = db.session.scalar(
select(
exists().where(
Message.conversation_id == conversation.id,
Message.created_at < current_page_first_message.created_at,
Message.id != current_page_first_message.id,
)
.count()
)
if rest_count > 0:
has_more = True
)
history_messages = list(reversed(history_messages))

View File

@ -1,19 +1,20 @@
from flask import Blueprint
from flask_restx import Namespace
from libs.external_api import ExternalApi
from .files import FileApi
from .remote_files import RemoteFileInfoApi, RemoteFileUploadApi
bp = Blueprint("web", __name__, url_prefix="/api")
api = ExternalApi(bp)
# Files
api.add_resource(FileApi, "/files/upload")
api = ExternalApi(
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
api.add_resource(RemoteFileInfoApi, "/remote-files/<path:url>")
api.add_resource(RemoteFileUploadApi, "/remote-files/upload")
# Create namespace
web_ns = Namespace("web", description="Web application API operations", path="/")
from . import (
app,
@ -21,11 +22,15 @@ from . import (
completion,
conversation,
feature,
files,
forgot_password,
login,
message,
passport,
remote_files,
saved_message,
site,
workflow,
)
api.add_namespace(web_ns)

View File

@ -1,12 +1,21 @@
from flask_restx import Resource
from controllers.web import api
from controllers.web import web_ns
from services.feature_service import FeatureService
@web_ns.route("/system-features")
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):
"""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()
api.add_resource(SystemFeatureApi, "/system-features")

View File

@ -9,14 +9,50 @@ from controllers.common.errors import (
TooManyFilesError,
UnsupportedFileTypeError,
)
from controllers.web import web_ns
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
@web_ns.route("/files/upload")
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):
"""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:
raise NoFileUploadedError()

View File

@ -16,7 +16,7 @@ from controllers.console.auth.error import (
)
from controllers.console.error import EmailSendIpLimitError
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 libs.helper import email, extract_remote_ip
from libs.password import hash_password, valid_password
@ -24,10 +24,21 @@ from models.account import Account
from services.account_service import AccountService
@web_ns.route("/forgot-password")
class ForgotPasswordSendEmailApi(Resource):
@only_edition_enterprise
@setup_required
@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):
parser = reqparse.RequestParser()
parser.add_argument("email", type=email, required=True, location="json")
@ -54,10 +65,16 @@ class ForgotPasswordSendEmailApi(Resource):
return {"result": "success", "data": token}
@web_ns.route("/forgot-password/validity")
class ForgotPasswordCheckApi(Resource):
@only_edition_enterprise
@setup_required
@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):
parser = reqparse.RequestParser()
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}
@web_ns.route("/forgot-password/resets")
class ForgotPasswordResetApi(Resource):
@only_edition_enterprise
@setup_required
@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):
parser = reqparse.RequestParser()
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_salt = base64.b64encode(salt).decode()
session.commit()
api.add_resource(ForgotPasswordSendEmailApi, "/forgot-password")
api.add_resource(ForgotPasswordCheckApi, "/forgot-password/validity")
api.add_resource(ForgotPasswordResetApi, "/forgot-password/resets")

View File

@ -9,18 +9,30 @@ from controllers.console.auth.error import (
)
from controllers.console.error import AccountBannedError
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.password import valid_password
from services.account_service import AccountService
from services.webapp_auth_service import WebAppAuthService
@web_ns.route("/login")
class LoginApi(Resource):
"""Resource for web app email/password login."""
@setup_required
@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):
"""Authenticate user and login."""
parser = reqparse.RequestParser()
@ -51,9 +63,19 @@ class LoginApi(Resource):
# return {"result": "success"}
@web_ns.route("/email-code-login")
class EmailCodeLoginSendEmailApi(Resource):
@setup_required
@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):
parser = reqparse.RequestParser()
parser.add_argument("email", type=email, required=True, location="json")
@ -74,9 +96,20 @@ class EmailCodeLoginSendEmailApi(Resource):
return {"result": "success", "data": token}
@web_ns.route("/email-code-login/validity")
class EmailCodeLoginApi(Resource):
@setup_required
@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):
parser = reqparse.RequestParser()
parser.add_argument("email", type=str, required=True, location="json")
@ -104,9 +137,3 @@ class EmailCodeLoginApi(Resource):
token = WebAppAuthService.login(account=account)
AccountService.reset_login_error_rate_limit(args["email"])
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")

View File

@ -7,7 +7,7 @@ from sqlalchemy import func, select
from werkzeug.exceptions import NotFound, Unauthorized
from configs import dify_config
from controllers.web import api
from controllers.web import web_ns
from controllers.web.error import WebAppAuthRequiredError
from extensions.ext_database import db
from libs.passport import PassportService
@ -17,9 +17,19 @@ from services.feature_service import FeatureService
from services.webapp_auth_service import WebAppAuthService, WebAppAuthType
@web_ns.route("/passport")
class PassportResource(Resource):
"""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):
system_features = FeatureService.get_system_features()
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):
"""
Decode the enterprise user session from the Authorization header.

View File

@ -10,16 +10,44 @@ from controllers.common.errors import (
RemoteFileUploadError,
UnsupportedFileTypeError,
)
from controllers.web import web_ns
from controllers.web.wraps import WebApiResource
from core.file import helpers as file_helpers
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
@web_ns.route("/remote-files/<path:url>")
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):
"""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)
resp = ssrf_proxy.head(decoded_url)
if resp.status_code != httpx.codes.OK:
@ -32,9 +60,42 @@ class RemoteFileInfoApi(WebApiResource):
}
@web_ns.route("/remote-files/upload")
class RemoteFileUploadApi(WebApiResource):
@marshal_with(file_fields_with_signed_url)
def post(self, app_model, end_user): # Add app_model and end_user parameters
@web_ns.doc("upload_remote_file")
@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.add_argument("url", type=str, required=True, help="URL is required")
args = parser.parse_args()

View File

@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any, Literal, Optional, cast
import sqlalchemy as sa
from flask import request
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 configs import dify_config
@ -1554,7 +1554,7 @@ class ApiToken(Base):
def generate_api_key(prefix, n):
while True:
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
return result

View File

@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Any, Optional, Union
from uuid import uuid4
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.models import File
@ -336,12 +336,13 @@ class Workflow(Base):
"""
from models.tools import WorkflowToolProvider
return (
db.session.query(WorkflowToolProvider)
.where(WorkflowToolProvider.tenant_id == self.tenant_id, WorkflowToolProvider.app_id == self.app_id)
.count()
> 0
stmt = select(
exists().where(
WorkflowToolProvider.tenant_id == self.tenant_id,
WorkflowToolProvider.app_id == self.app_id,
)
)
return db.session.execute(stmt).scalar_one()
@property
def environment_variables(self) -> Sequence[StringVariable | IntegerVariable | FloatVariable | SecretVariable]:
@ -921,7 +922,7 @@ def _naive_utc_datetime():
class WorkflowDraftVariable(Base):
"""`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.
Do not instantiate this class directly with the constructor.

View File

@ -9,7 +9,7 @@ from collections import Counter
from typing import Any, Literal, Optional
from flask_login import current_user
from sqlalchemy import func, select
from sqlalchemy import exists, func, select
from sqlalchemy.orm import Session
from werkzeug.exceptions import NotFound
@ -655,10 +655,8 @@ class DatasetService:
@staticmethod
def dataset_use_check(dataset_id) -> bool:
count = db.session.query(AppDatasetJoin).filter_by(dataset_id=dataset_id).count()
if count > 0:
return True
return False
stmt = select(exists().where(AppDatasetJoin.dataset_id == dataset_id))
return db.session.execute(stmt).scalar_one()
@staticmethod
def check_dataset_permission(dataset, user):

View File

@ -5,6 +5,7 @@ from collections.abc import Mapping
from pathlib import Path
from typing import Any, Optional
from sqlalchemy import exists, select
from sqlalchemy.orm import Session
from configs import dify_config
@ -190,11 +191,14 @@ class BuiltinToolManageService:
# update name if provided
if name and name != db_provider.name:
# check if the name is already used
if (
session.query(BuiltinToolProvider)
.filter_by(tenant_id=tenant_id, provider=provider, name=name)
.count()
> 0
if session.scalar(
select(
exists().where(
BuiltinToolProvider.tenant_id == tenant_id,
BuiltinToolProvider.provider == provider,
BuiltinToolProvider.name == name,
)
)
):
raise ValueError(f"the credential name '{name}' is already used")
@ -246,11 +250,14 @@ class BuiltinToolManageService:
)
else:
# check if the name is already used
if (
session.query(BuiltinToolProvider)
.filter_by(tenant_id=tenant_id, provider=provider, name=name)
.count()
> 0
if session.scalar(
select(
exists().where(
BuiltinToolProvider.tenant_id == tenant_id,
BuiltinToolProvider.provider == provider,
BuiltinToolProvider.name == name,
)
)
):
raise ValueError(f"the credential name '{name}' is already used")

View File

@ -4,7 +4,7 @@ import uuid
from collections.abc import Callable, Generator, Mapping, Sequence
from typing import Any, Optional, cast
from sqlalchemy import select
from sqlalchemy import exists, select
from sqlalchemy.orm import Session, sessionmaker
from core.app.app_config.entities import VariableEntityType
@ -83,15 +83,14 @@ class WorkflowService:
)
def is_workflow_exist(self, app_model: App) -> bool:
return (
db.session.query(Workflow)
.where(
stmt = select(
exists().where(
Workflow.tenant_id == app_model.tenant_id,
Workflow.app_id == app_model.id,
Workflow.version == Workflow.VERSION_DRAFT,
)
.count()
) > 0
)
return db.session.execute(stmt).scalar_one()
def get_draft_workflow(self, app_model: App) -> Optional[Workflow]:
"""

View File

@ -3,6 +3,7 @@ import time
import click
from celery import shared_task
from sqlalchemy import exists, select
from core.rag.datasource.vdb.vector_factory import Vector
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()
# get app info
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:
logger.info(click.style(f"App not found: {app_id}", fg="red"))
db.session.close()
@ -47,7 +48,7 @@ def disable_annotation_reply_task(job_id: str, app_id: str, tenant_id: str):
)
try:
if annotations_count > 0:
if annotations_exists:
vector = Vector(dataset, attributes=["doc_id", "annotation_id", "app_id"])
vector.delete()
except Exception:

View File

@ -27,10 +27,11 @@ const ChunkDetailModal: FC<Props> = ({
}) => {
const { t } = useTranslation()
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 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 labelPrefix = isParentChildRetrieval ? t('datasetDocuments.segment.parentChunk') : t('datasetDocuments.segment.chunk')
return (
<Modal
title={t(`${i18nPrefix}.chunkDetail`)}
@ -45,7 +46,7 @@ const ChunkDetailModal: FC<Props> = ({
<div className='flex items-center justify-between'>
<div className='flex grow items-center space-x-2'>
<SegmentIndexTag
labelPrefix={`${isParentChildRetrieval ? 'Parent-' : ''}Chunk`}
labelPrefix={labelPrefix}
positionId={position}
className={cn('w-fit group-hover:opacity-100')}
/>
@ -57,11 +58,29 @@ const ChunkDetailModal: FC<Props> = ({
</div>
<Score value={score} />
</div>
<Markdown
className={cn('!mt-2 !text-text-secondary', heighClassName)}
content={sign_content || content}
customDisallowedElements={['input']}
/>
{!answer && (
<Markdown
className={cn('!mt-2 !text-text-secondary', heighClassName)}
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 && (
<div className='mt-6'>
<div className='text-xs font-medium uppercase text-text-tertiary'>{t(`${i18nPrefix}.keyword`)}</div>

View File

@ -32,6 +32,7 @@ export const checkOrSetAccessToken = async (appCode?: string | null) => {
[userId || 'DEFAULT']: res.access_token,
}
localStorage.setItem('token', JSON.stringify(accessTokenJson))
localStorage.removeItem(CONVERSATION_ID_INFO)
}
}

View File

@ -11,6 +11,7 @@ import type { FC, PropsWithChildren } from 'react'
import { useEffect } from 'react'
import { useState } from 'react'
import { create } from 'zustand'
import { useGlobalPublicStore } from './global-public-context'
type WebAppStore = {
shareCode: string | null
@ -56,6 +57,7 @@ const getShareCodeFromPathname = (pathname: string): string | null => {
}
const WebAppStoreProvider: FC<PropsWithChildren> = ({ children }) => {
const isGlobalPending = useGlobalPublicStore(s => s.isGlobalPending)
const updateWebAppAccessMode = useWebAppStore(state => state.updateWebAppAccessMode)
const updateShareCode = useWebAppStore(state => state.updateShareCode)
const pathname = usePathname()
@ -69,7 +71,7 @@ const WebAppStoreProvider: FC<PropsWithChildren> = ({ children }) => {
}, [shareCode, updateShareCode])
const { isFetching, data: accessModeResult } = useGetWebAppAccessModeByCode(shareCode)
const [isFetchingAccessToken, setIsFetchingAccessToken] = useState(false)
const [isFetchingAccessToken, setIsFetchingAccessToken] = useState(true)
useEffect(() => {
if (accessModeResult?.accessMode) {
@ -86,7 +88,7 @@ const WebAppStoreProvider: FC<PropsWithChildren> = ({ children }) => {
}
}, [accessModeResult, updateWebAppAccessMode, shareCode])
if (isFetching || isFetchingAccessToken) {
if (isGlobalPending || isFetching || isFetchingAccessToken) {
return <div className='flex h-full w-full items-center justify-center'>
<Loading />
</div>

View File

@ -9,6 +9,7 @@ import tailwind from 'eslint-plugin-tailwindcss'
import reactHooks from 'eslint-plugin-react-hooks'
import sonar from 'eslint-plugin-sonarjs'
import oxlint from 'eslint-plugin-oxlint'
import next from '@next/eslint-plugin-next'
// import reactRefresh from 'eslint-plugin-react-refresh'
@ -63,12 +64,14 @@ export default combine(
}),
unicorn(),
node(),
// use nextjs config will break @eslint/config-inspector
// use `ESLINT_CONFIG_INSPECTOR=true pnpx @eslint/config-inspector` to check the config
// ...process.env.ESLINT_CONFIG_INSPECTOR
// ? []
// Next.js configuration
{
plugins: {
'@next/next': next,
},
rules: {
...next.configs.recommended.rules,
...next.configs['core-web-vitals'].rules,
// performance issue, and not used.
'@next/next/no-html-link-for-pages': 'off',
},

View File

@ -545,6 +545,7 @@ export type Segment = {
keywords: string[]
hit_count: number
index_node_hash: string
answer: string
}
export type Document = {

View File

@ -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",
"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",
"fix": "next lint --fix",
"fix": "eslint --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-complexity": "eslint --rule 'complexity: [error, {max: 15}]' --quiet",
@ -103,14 +103,14 @@
"mime": "^4.0.4",
"mitt": "^3.0.1",
"negotiator": "^0.6.3",
"next": "~15.3.5",
"next": "15.5.0",
"next-themes": "^0.4.3",
"pinyin-pro": "^3.25.0",
"qrcode.react": "^4.2.0",
"qs": "^6.13.0",
"react": "~19.1.0",
"react": "19.1.1",
"react-18-input-autosize": "^3.0.0",
"react-dom": "~19.1.0",
"react-dom": "19.1.1",
"react-easy-crop": "^5.1.0",
"react-error-boundary": "^4.1.2",
"react-headless-pagination": "^1.1.6",
@ -161,9 +161,9 @@
"@happy-dom/jest-environment": "^17.4.4",
"@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0",
"@next/bundle-analyzer": "^15.4.1",
"@next/eslint-plugin-next": "~15.4.5",
"@next/mdx": "~15.3.5",
"@next/bundle-analyzer": "15.5.0",
"@next/eslint-plugin-next": "15.5.0",
"@next/mdx": "15.5.0",
"@rgrove/parse-xml": "^4.1.0",
"@storybook/addon-essentials": "8.5.0",
"@storybook/addon-interactions": "8.5.0",
@ -185,8 +185,8 @@
"@types/negotiator": "^0.6.3",
"@types/node": "18.15.0",
"@types/qs": "^6.9.16",
"@types/react": "~19.1.8",
"@types/react-dom": "~19.1.6",
"@types/react": "19.1.11",
"@types/react-dom": "19.1.7",
"@types/react-slider": "^1.3.6",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/react-window": "^1.8.8",
@ -200,7 +200,7 @@
"code-inspector-plugin": "^0.18.1",
"cross-env": "^7.0.3",
"eslint": "^9.32.0",
"eslint-config-next": "~15.4.5",
"eslint-config-next": "15.5.0",
"eslint-plugin-oxlint": "^1.6.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
@ -223,8 +223,8 @@
"uglify-js": "^3.19.3"
},
"resolutions": {
"@types/react": "~19.1.8",
"@types/react-dom": "~19.1.6",
"@types/react": "19.1.11",
"@types/react-dom": "19.1.7",
"string-width": "4.2.3"
},
"lint-staged": {

File diff suppressed because it is too large Load Diff

View File

@ -398,9 +398,7 @@ export const ssePost = async (
.then((res) => {
if (!/^[23]\d{2}$/.test(String(res.status))) {
if (res.status === 401) {
refreshAccessTokenOrRelogin(TIME_OUT).then(() => {
ssePost(url, fetchOptions, otherOptions)
}).catch(() => {
if (isPublicAPI) {
res.json().then((data: any) => {
if (isPublicAPI) {
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 {
res.json().then((data) => {

View File

@ -1,20 +1,12 @@
import { useGlobalPublicStore } from '@/context/global-public-context'
import { AccessMode } from '@/models/access-control'
import { useQuery } from '@tanstack/react-query'
import { fetchAppInfo, fetchAppMeta, fetchAppParams, getAppAccessModeByAppCode } from './share'
const NAME_SPACE = 'webapp'
export const useGetWebAppAccessModeByCode = (code: string | null) => {
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
return useQuery({
queryKey: [NAME_SPACE, 'appAccessMode', code],
queryFn: () => {
if (systemFeatures.webapp_auth.enabled === false) {
return {
accessMode: AccessMode.PUBLIC,
}
}
if (!code || code.length === 0)
return Promise.reject(new Error('App code is required to get access mode'))