diff --git a/web/app/(shareLayout)/layout.tsx b/web/app/(shareLayout)/layout.tsx
index 83adbd3cae..8db336a17d 100644
--- a/web/app/(shareLayout)/layout.tsx
+++ b/web/app/(shareLayout)/layout.tsx
@@ -1,14 +1,42 @@
-import React from 'react'
+'use client'
+import React, { useEffect, useState } from 'react'
import type { FC } from 'react'
-import type { Metadata } from 'next'
-
-export const metadata: Metadata = {
- icons: 'data:,', // prevent browser from using default favicon
-}
+import { usePathname, useSearchParams } from 'next/navigation'
+import Loading from '../components/base/loading'
+import { useGlobalPublicStore } from '@/context/global-public-context'
+import { AccessMode } from '@/models/access-control'
+import { getAppAccessModeByAppCode } from '@/service/share'
const Layout: FC<{
children: React.ReactNode
}> = ({ children }) => {
+ const isGlobalPending = useGlobalPublicStore(s => s.isGlobalPending)
+ const setWebAppAccessMode = useGlobalPublicStore(s => s.setWebAppAccessMode)
+ const pathname = usePathname()
+ const searchParams = useSearchParams()
+ const redirectUrl = searchParams.get('redirect_url')
+ const [isLoading, setIsLoading] = useState(true)
+ useEffect(() => {
+ (async () => {
+ let appCode: string | null = null
+ if (redirectUrl)
+ appCode = redirectUrl?.split('/').pop() || null
+ else
+ appCode = pathname.split('/').pop() || null
+
+ if (!appCode)
+ return
+ setIsLoading(true)
+ const ret = await getAppAccessModeByAppCode(appCode)
+ setWebAppAccessMode(ret?.accessMode || AccessMode.PUBLIC)
+ setIsLoading(false)
+ })()
+ }, [pathname, redirectUrl, setWebAppAccessMode])
+ if (isLoading || isGlobalPending) {
+ return
+
+
+ }
return (
{children}
diff --git a/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx b/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx
new file mode 100644
index 0000000000..da754794b1
--- /dev/null
+++ b/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx
@@ -0,0 +1,96 @@
+'use client'
+import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
+import { useTranslation } from 'react-i18next'
+import { useState } from 'react'
+import { useRouter, useSearchParams } from 'next/navigation'
+import { useContext } from 'use-context-selector'
+import Countdown from '@/app/components/signin/countdown'
+import Button from '@/app/components/base/button'
+import Input from '@/app/components/base/input'
+import Toast from '@/app/components/base/toast'
+import { sendWebAppResetPasswordCode, verifyWebAppResetPasswordCode } from '@/service/common'
+import I18NContext from '@/context/i18n'
+
+export default function CheckCode() {
+ const { t } = useTranslation()
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const email = decodeURIComponent(searchParams.get('email') as string)
+ const token = decodeURIComponent(searchParams.get('token') as string)
+ const [code, setVerifyCode] = useState('')
+ const [loading, setIsLoading] = useState(false)
+ const { locale } = useContext(I18NContext)
+
+ const verify = async () => {
+ try {
+ if (!code.trim()) {
+ Toast.notify({
+ type: 'error',
+ message: t('login.checkCode.emptyCode'),
+ })
+ return
+ }
+ if (!/\d{6}/.test(code)) {
+ Toast.notify({
+ type: 'error',
+ message: t('login.checkCode.invalidCode'),
+ })
+ return
+ }
+ setIsLoading(true)
+ const ret = await verifyWebAppResetPasswordCode({ email, code, token })
+ if (ret.is_valid) {
+ const params = new URLSearchParams(searchParams)
+ params.set('token', encodeURIComponent(ret.token))
+ router.push(`/webapp-reset-password/set-password?${params.toString()}`)
+ }
+ }
+ catch (error) { console.error(error) }
+ finally {
+ setIsLoading(false)
+ }
+ }
+
+ const resendCode = async () => {
+ try {
+ const res = await sendWebAppResetPasswordCode(email, locale)
+ if (res.result === 'success') {
+ const params = new URLSearchParams(searchParams)
+ params.set('token', encodeURIComponent(res.data))
+ router.replace(`/webapp-reset-password/check-code?${params.toString()}`)
+ }
+ }
+ catch (error) { console.error(error) }
+ }
+
+ return
+
+
+
+
+
{t('login.checkCode.checkYourEmail')}
+
+
+
+ {t('login.checkCode.validTime')}
+
+
+
+
+
+
router.back()} className='flex h-9 cursor-pointer items-center justify-center text-text-tertiary'>
+
+
+
+
{t('login.back')}
+
+
+}
diff --git a/web/app/(shareLayout)/webapp-reset-password/layout.tsx b/web/app/(shareLayout)/webapp-reset-password/layout.tsx
new file mode 100644
index 0000000000..e0ac6b9ad6
--- /dev/null
+++ b/web/app/(shareLayout)/webapp-reset-password/layout.tsx
@@ -0,0 +1,30 @@
+'use client'
+import Header from '@/app/signin/_header'
+
+import cn from '@/utils/classnames'
+import { useGlobalPublicStore } from '@/context/global-public-context'
+
+export default function SignInLayout({ children }: any) {
+ const { systemFeatures } = useGlobalPublicStore()
+ return <>
+
+
+
+
+ {!systemFeatures.branding.enabled &&
+ © {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
+
}
+
+
+ >
+}
diff --git a/web/app/(shareLayout)/webapp-reset-password/page.tsx b/web/app/(shareLayout)/webapp-reset-password/page.tsx
new file mode 100644
index 0000000000..96cd4c5805
--- /dev/null
+++ b/web/app/(shareLayout)/webapp-reset-password/page.tsx
@@ -0,0 +1,104 @@
+'use client'
+import Link from 'next/link'
+import { RiArrowLeftLine, RiLockPasswordLine } from '@remixicon/react'
+import { useTranslation } from 'react-i18next'
+import { useState } from 'react'
+import { useRouter, useSearchParams } from 'next/navigation'
+import { useContext } from 'use-context-selector'
+import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
+import { emailRegex } from '@/config'
+import Button from '@/app/components/base/button'
+import Input from '@/app/components/base/input'
+import Toast from '@/app/components/base/toast'
+import { sendResetPasswordCode } from '@/service/common'
+import I18NContext from '@/context/i18n'
+import { noop } from 'lodash-es'
+import useDocumentTitle from '@/hooks/use-document-title'
+
+export default function CheckCode() {
+ const { t } = useTranslation()
+ useDocumentTitle('')
+ const searchParams = useSearchParams()
+ const router = useRouter()
+ const [email, setEmail] = useState('')
+ const [loading, setIsLoading] = useState(false)
+ const { locale } = useContext(I18NContext)
+
+ const handleGetEMailVerificationCode = async () => {
+ try {
+ if (!email) {
+ Toast.notify({ type: 'error', message: t('login.error.emailEmpty') })
+ return
+ }
+
+ if (!emailRegex.test(email)) {
+ Toast.notify({
+ type: 'error',
+ message: t('login.error.emailInValid'),
+ })
+ return
+ }
+ setIsLoading(true)
+ const res = await sendResetPasswordCode(email, locale)
+ if (res.result === 'success') {
+ localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`)
+ const params = new URLSearchParams(searchParams)
+ params.set('token', encodeURIComponent(res.data))
+ params.set('email', encodeURIComponent(email))
+ router.push(`/webapp-reset-password/check-code?${params.toString()}`)
+ }
+ else if (res.code === 'account_not_found') {
+ Toast.notify({
+ type: 'error',
+ message: t('login.error.registrationNotAllowed'),
+ })
+ }
+ else {
+ Toast.notify({
+ type: 'error',
+ message: res.data,
+ })
+ }
+ }
+ catch (error) {
+ console.error(error)
+ }
+ finally {
+ setIsLoading(false)
+ }
+ }
+
+ return
+
+
+
+
+
{t('login.resetPassword')}
+
+ {t('login.resetPasswordDesc')}
+
+
+
+
+
+
+
+
+
+
{t('login.backToLogin')}
+
+
+}
diff --git a/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx b/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx
new file mode 100644
index 0000000000..9f9a8ad4e3
--- /dev/null
+++ b/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx
@@ -0,0 +1,188 @@
+'use client'
+import { useCallback, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useRouter, useSearchParams } from 'next/navigation'
+import cn from 'classnames'
+import { RiCheckboxCircleFill } from '@remixicon/react'
+import { useCountDown } from 'ahooks'
+import Button from '@/app/components/base/button'
+import { changeWebAppPasswordWithToken } from '@/service/common'
+import Toast from '@/app/components/base/toast'
+import Input from '@/app/components/base/input'
+
+const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
+
+const ChangePasswordForm = () => {
+ const { t } = useTranslation()
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const token = decodeURIComponent(searchParams.get('token') || '')
+
+ const [password, setPassword] = useState('')
+ const [confirmPassword, setConfirmPassword] = useState('')
+ const [showSuccess, setShowSuccess] = useState(false)
+ const [showPassword, setShowPassword] = useState(false)
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false)
+
+ const showErrorMessage = useCallback((message: string) => {
+ Toast.notify({
+ type: 'error',
+ message,
+ })
+ }, [])
+
+ const getSignInUrl = () => {
+ return `/webapp-signin?redirect_url=${searchParams.get('redirect_url') || ''}`
+ }
+
+ const AUTO_REDIRECT_TIME = 5000
+ const [leftTime, setLeftTime] = useState
(undefined)
+ const [countdown] = useCountDown({
+ leftTime,
+ onEnd: () => {
+ router.replace(getSignInUrl())
+ },
+ })
+
+ const valid = useCallback(() => {
+ if (!password.trim()) {
+ showErrorMessage(t('login.error.passwordEmpty'))
+ return false
+ }
+ if (!validPassword.test(password)) {
+ showErrorMessage(t('login.error.passwordInvalid'))
+ return false
+ }
+ if (password !== confirmPassword) {
+ showErrorMessage(t('common.account.notEqual'))
+ return false
+ }
+ return true
+ }, [password, confirmPassword, showErrorMessage, t])
+
+ const handleChangePassword = useCallback(async () => {
+ if (!valid())
+ return
+ try {
+ await changeWebAppPasswordWithToken({
+ url: '/forgot-password/resets',
+ body: {
+ token,
+ new_password: password,
+ password_confirm: confirmPassword,
+ },
+ })
+ setShowSuccess(true)
+ setLeftTime(AUTO_REDIRECT_TIME)
+ }
+ catch (error) {
+ console.error(error)
+ }
+ }, [password, token, valid, confirmPassword])
+
+ return (
+
+ {!showSuccess && (
+
+
+
+ {t('login.changePassword')}
+
+
+ {t('login.changePasswordTip')}
+
+
+
+
+
+ {/* Password */}
+
+
+
+
setPassword(e.target.value)}
+ placeholder={t('login.passwordPlaceholder') || ''}
+ />
+
+
+
+
+
+
{t('login.error.passwordInvalid')}
+
+ {/* Confirm Password */}
+
+
+
+
setConfirmPassword(e.target.value)}
+ placeholder={t('login.confirmPasswordPlaceholder') || ''}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ )}
+ {showSuccess && (
+
+
+
+
+
+
+ {t('login.passwordChangedTip')}
+
+
+
+
+
+
+ )}
+
+ )
+}
+
+export default ChangePasswordForm
diff --git a/web/app/(shareLayout)/webapp-signin/check-code/page.tsx b/web/app/(shareLayout)/webapp-signin/check-code/page.tsx
new file mode 100644
index 0000000000..1b8f18c98f
--- /dev/null
+++ b/web/app/(shareLayout)/webapp-signin/check-code/page.tsx
@@ -0,0 +1,115 @@
+'use client'
+import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
+import { useTranslation } from 'react-i18next'
+import { useCallback, useState } from 'react'
+import { useRouter, useSearchParams } from 'next/navigation'
+import { useContext } from 'use-context-selector'
+import Countdown from '@/app/components/signin/countdown'
+import Button from '@/app/components/base/button'
+import Input from '@/app/components/base/input'
+import Toast from '@/app/components/base/toast'
+import { sendWebAppEMailLoginCode, webAppEmailLoginWithCode } from '@/service/common'
+import I18NContext from '@/context/i18n'
+import { setAccessToken } from '@/app/components/share/utils'
+import { fetchAccessToken } from '@/service/share'
+
+export default function CheckCode() {
+ const { t } = useTranslation()
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const email = decodeURIComponent(searchParams.get('email') as string)
+ const token = decodeURIComponent(searchParams.get('token') as string)
+ const [code, setVerifyCode] = useState('')
+ const [loading, setIsLoading] = useState(false)
+ const { locale } = useContext(I18NContext)
+ const redirectUrl = searchParams.get('redirect_url')
+
+ const getAppCodeFromRedirectUrl = useCallback(() => {
+ const appCode = redirectUrl?.split('/').pop()
+ if (!appCode)
+ return null
+
+ return appCode
+ }, [redirectUrl])
+
+ const verify = async () => {
+ try {
+ const appCode = getAppCodeFromRedirectUrl()
+ if (!code.trim()) {
+ Toast.notify({
+ type: 'error',
+ message: t('login.checkCode.emptyCode'),
+ })
+ return
+ }
+ if (!/\d{6}/.test(code)) {
+ Toast.notify({
+ type: 'error',
+ message: t('login.checkCode.invalidCode'),
+ })
+ return
+ }
+ if (!redirectUrl || !appCode) {
+ Toast.notify({
+ type: 'error',
+ message: t('login.error.redirectUrlMissing'),
+ })
+ return
+ }
+ setIsLoading(true)
+ const ret = await webAppEmailLoginWithCode({ email, code, token })
+ if (ret.result === 'success') {
+ localStorage.setItem('webapp_access_token', ret.data.access_token)
+ const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: ret.data.access_token })
+ await setAccessToken(appCode, tokenResp.access_token)
+ router.replace(redirectUrl)
+ }
+ }
+ catch (error) { console.error(error) }
+ finally {
+ setIsLoading(false)
+ }
+ }
+
+ const resendCode = async () => {
+ try {
+ const ret = await sendWebAppEMailLoginCode(email, locale)
+ if (ret.result === 'success') {
+ const params = new URLSearchParams(searchParams)
+ params.set('token', encodeURIComponent(ret.data))
+ router.replace(`/webapp-signin/check-code?${params.toString()}`)
+ }
+ }
+ catch (error) { console.error(error) }
+ }
+
+ return
+
+
+
+
+
{t('login.checkCode.checkYourEmail')}
+
+
+
+ {t('login.checkCode.validTime')}
+
+
+
+
+
+
router.back()} className='flex h-9 cursor-pointer items-center justify-center text-text-tertiary'>
+
+
+
+
{t('login.back')}
+
+
+}
diff --git a/web/app/(shareLayout)/webapp-signin/components/external-member-sso-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/external-member-sso-auth.tsx
new file mode 100644
index 0000000000..e9b15ae331
--- /dev/null
+++ b/web/app/(shareLayout)/webapp-signin/components/external-member-sso-auth.tsx
@@ -0,0 +1,80 @@
+'use client'
+import { useRouter, useSearchParams } from 'next/navigation'
+import React, { useCallback, useEffect } from 'react'
+import Toast from '@/app/components/base/toast'
+import { fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share'
+import { useGlobalPublicStore } from '@/context/global-public-context'
+import { SSOProtocol } from '@/types/feature'
+import Loading from '@/app/components/base/loading'
+import AppUnavailable from '@/app/components/base/app-unavailable'
+
+const ExternalMemberSSOAuth = () => {
+ const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
+ const searchParams = useSearchParams()
+ const router = useRouter()
+
+ const redirectUrl = searchParams.get('redirect_url')
+
+ const showErrorToast = (message: string) => {
+ Toast.notify({
+ type: 'error',
+ message,
+ })
+ }
+
+ const getAppCodeFromRedirectUrl = useCallback(() => {
+ const appCode = redirectUrl?.split('/').pop()
+ if (!appCode)
+ return null
+
+ return appCode
+ }, [redirectUrl])
+
+ const handleSSOLogin = useCallback(async () => {
+ const appCode = getAppCodeFromRedirectUrl()
+ if (!appCode || !redirectUrl) {
+ showErrorToast('redirect url or app code is invalid.')
+ return
+ }
+
+ switch (systemFeatures.webapp_auth.sso_config.protocol) {
+ case SSOProtocol.SAML: {
+ const samlRes = await fetchWebSAMLSSOUrl(appCode, redirectUrl)
+ router.push(samlRes.url)
+ break
+ }
+ case SSOProtocol.OIDC: {
+ const oidcRes = await fetchWebOIDCSSOUrl(appCode, redirectUrl)
+ router.push(oidcRes.url)
+ break
+ }
+ case SSOProtocol.OAuth2: {
+ const oauth2Res = await fetchWebOAuth2SSOUrl(appCode, redirectUrl)
+ router.push(oauth2Res.url)
+ break
+ }
+ case '':
+ break
+ default:
+ showErrorToast('SSO protocol is not supported.')
+ }
+ }, [getAppCodeFromRedirectUrl, redirectUrl, router, systemFeatures.webapp_auth.sso_config.protocol])
+
+ useEffect(() => {
+ handleSSOLogin()
+ }, [handleSSOLogin])
+
+ if (!systemFeatures.webapp_auth.sso_config.protocol) {
+ return
+ }
+
+ return (
+
+
+
+ )
+}
+
+export default React.memo(ExternalMemberSSOAuth)
diff --git a/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx
new file mode 100644
index 0000000000..29af3e3a57
--- /dev/null
+++ b/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx
@@ -0,0 +1,68 @@
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useRouter, useSearchParams } from 'next/navigation'
+import { useContext } from 'use-context-selector'
+import Input from '@/app/components/base/input'
+import Button from '@/app/components/base/button'
+import { emailRegex } from '@/config'
+import Toast from '@/app/components/base/toast'
+import { sendWebAppEMailLoginCode } from '@/service/common'
+import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
+import I18NContext from '@/context/i18n'
+import { noop } from 'lodash-es'
+
+export default function MailAndCodeAuth() {
+ const { t } = useTranslation()
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const emailFromLink = decodeURIComponent(searchParams.get('email') || '')
+ const [email, setEmail] = useState(emailFromLink)
+ const [loading, setIsLoading] = useState(false)
+ const { locale } = useContext(I18NContext)
+
+ const handleGetEMailVerificationCode = async () => {
+ try {
+ if (!email) {
+ Toast.notify({ type: 'error', message: t('login.error.emailEmpty') })
+ return
+ }
+
+ if (!emailRegex.test(email)) {
+ Toast.notify({
+ type: 'error',
+ message: t('login.error.emailInValid'),
+ })
+ return
+ }
+ setIsLoading(true)
+ const ret = await sendWebAppEMailLoginCode(email, locale)
+ if (ret.result === 'success') {
+ localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`)
+ const params = new URLSearchParams(searchParams)
+ params.set('email', encodeURIComponent(email))
+ params.set('token', encodeURIComponent(ret.data))
+ router.push(`/webapp-signin/check-code?${params.toString()}`)
+ }
+ }
+ catch (error) {
+ console.error(error)
+ }
+ finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+ )
+}
diff --git a/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx
new file mode 100644
index 0000000000..d9e56af1b8
--- /dev/null
+++ b/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx
@@ -0,0 +1,171 @@
+import Link from 'next/link'
+import { useCallback, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useRouter, useSearchParams } from 'next/navigation'
+import { useContext } from 'use-context-selector'
+import Button from '@/app/components/base/button'
+import Toast from '@/app/components/base/toast'
+import { emailRegex } from '@/config'
+import { webAppLogin } from '@/service/common'
+import Input from '@/app/components/base/input'
+import I18NContext from '@/context/i18n'
+import { noop } from 'lodash-es'
+import { setAccessToken } from '@/app/components/share/utils'
+import { fetchAccessToken } from '@/service/share'
+
+type MailAndPasswordAuthProps = {
+ isEmailSetup: boolean
+}
+
+const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
+
+export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAuthProps) {
+ const { t } = useTranslation()
+ const { locale } = useContext(I18NContext)
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const [showPassword, setShowPassword] = useState(false)
+ const emailFromLink = decodeURIComponent(searchParams.get('email') || '')
+ const [email, setEmail] = useState(emailFromLink)
+ const [password, setPassword] = useState('')
+
+ const [isLoading, setIsLoading] = useState(false)
+ const redirectUrl = searchParams.get('redirect_url')
+
+ const getAppCodeFromRedirectUrl = useCallback(() => {
+ const appCode = redirectUrl?.split('/').pop()
+ if (!appCode)
+ return null
+
+ return appCode
+ }, [redirectUrl])
+ const handleEmailPasswordLogin = async () => {
+ const appCode = getAppCodeFromRedirectUrl()
+ if (!email) {
+ Toast.notify({ type: 'error', message: t('login.error.emailEmpty') })
+ return
+ }
+ if (!emailRegex.test(email)) {
+ Toast.notify({
+ type: 'error',
+ message: t('login.error.emailInValid'),
+ })
+ return
+ }
+ if (!password?.trim()) {
+ Toast.notify({ type: 'error', message: t('login.error.passwordEmpty') })
+ return
+ }
+ if (!passwordRegex.test(password)) {
+ Toast.notify({
+ type: 'error',
+ message: t('login.error.passwordInvalid'),
+ })
+ return
+ }
+ if (!redirectUrl || !appCode) {
+ Toast.notify({
+ type: 'error',
+ message: t('login.error.redirectUrlMissing'),
+ })
+ return
+ }
+ try {
+ setIsLoading(true)
+ const loginData: Record = {
+ email,
+ password,
+ language: locale,
+ remember_me: true,
+ }
+
+ const res = await webAppLogin({
+ url: '/login',
+ body: loginData,
+ })
+ if (res.result === 'success') {
+ localStorage.setItem('webapp_access_token', res.data.access_token)
+ const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: res.data.access_token })
+ await setAccessToken(appCode, tokenResp.access_token)
+ router.replace(redirectUrl)
+ }
+ else {
+ Toast.notify({
+ type: 'error',
+ message: res.data,
+ })
+ }
+ }
+
+ finally {
+ setIsLoading(false)
+ }
+ }
+
+ return
+}
diff --git a/web/app/(shareLayout)/webapp-signin/components/sso-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/sso-auth.tsx
new file mode 100644
index 0000000000..5d649322ba
--- /dev/null
+++ b/web/app/(shareLayout)/webapp-signin/components/sso-auth.tsx
@@ -0,0 +1,88 @@
+'use client'
+import { useRouter, useSearchParams } from 'next/navigation'
+import type { FC } from 'react'
+import { useCallback } from 'react'
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
+import Toast from '@/app/components/base/toast'
+import Button from '@/app/components/base/button'
+import { SSOProtocol } from '@/types/feature'
+import { fetchMembersOAuth2SSOUrl, fetchMembersOIDCSSOUrl, fetchMembersSAMLSSOUrl } from '@/service/share'
+
+type SSOAuthProps = {
+ protocol: SSOProtocol | ''
+}
+
+const SSOAuth: FC = ({
+ protocol,
+}) => {
+ const router = useRouter()
+ const { t } = useTranslation()
+ const searchParams = useSearchParams()
+
+ const redirectUrl = searchParams.get('redirect_url')
+ const getAppCodeFromRedirectUrl = useCallback(() => {
+ const appCode = redirectUrl?.split('/').pop()
+ if (!appCode)
+ return null
+
+ return appCode
+ }, [redirectUrl])
+
+ const [isLoading, setIsLoading] = useState(false)
+
+ const handleSSOLogin = () => {
+ const appCode = getAppCodeFromRedirectUrl()
+ if (!redirectUrl || !appCode) {
+ Toast.notify({
+ type: 'error',
+ message: 'invalid redirect URL or app code',
+ })
+ return
+ }
+ setIsLoading(true)
+ if (protocol === SSOProtocol.SAML) {
+ fetchMembersSAMLSSOUrl(appCode, redirectUrl).then((res) => {
+ router.push(res.url)
+ }).finally(() => {
+ setIsLoading(false)
+ })
+ }
+ else if (protocol === SSOProtocol.OIDC) {
+ fetchMembersOIDCSSOUrl(appCode, redirectUrl).then((res) => {
+ router.push(res.url)
+ }).finally(() => {
+ setIsLoading(false)
+ })
+ }
+ else if (protocol === SSOProtocol.OAuth2) {
+ fetchMembersOAuth2SSOUrl(appCode, redirectUrl).then((res) => {
+ router.push(res.url)
+ }).finally(() => {
+ setIsLoading(false)
+ })
+ }
+ else {
+ Toast.notify({
+ type: 'error',
+ message: 'invalid SSO protocol',
+ })
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}
+
+export default SSOAuth
diff --git a/web/app/(shareLayout)/webapp-signin/layout.tsx b/web/app/(shareLayout)/webapp-signin/layout.tsx
new file mode 100644
index 0000000000..a03364d326
--- /dev/null
+++ b/web/app/(shareLayout)/webapp-signin/layout.tsx
@@ -0,0 +1,25 @@
+'use client'
+
+import cn from '@/utils/classnames'
+import { useGlobalPublicStore } from '@/context/global-public-context'
+import useDocumentTitle from '@/hooks/use-document-title'
+
+export default function SignInLayout({ children }: any) {
+ const { systemFeatures } = useGlobalPublicStore()
+ useDocumentTitle('')
+ return <>
+
+
+ {/*
*/}
+
+ {systemFeatures.branding.enabled === false &&
+ © {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
+
}
+
+
+ >
+}
diff --git a/web/app/(shareLayout)/webapp-signin/normalForm.tsx b/web/app/(shareLayout)/webapp-signin/normalForm.tsx
new file mode 100644
index 0000000000..d6bdf607ba
--- /dev/null
+++ b/web/app/(shareLayout)/webapp-signin/normalForm.tsx
@@ -0,0 +1,176 @@
+import React, { useCallback, useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import Link from 'next/link'
+import { RiContractLine, RiDoorLockLine, RiErrorWarningFill } from '@remixicon/react'
+import Loading from '@/app/components/base/loading'
+import MailAndCodeAuth from './components/mail-and-code-auth'
+import MailAndPasswordAuth from './components/mail-and-password-auth'
+import SSOAuth from './components/sso-auth'
+import cn from '@/utils/classnames'
+import { LicenseStatus } from '@/types/feature'
+import { IS_CE_EDITION } from '@/config'
+import { useGlobalPublicStore } from '@/context/global-public-context'
+
+const NormalForm = () => {
+ const { t } = useTranslation()
+
+ const [isLoading, setIsLoading] = useState(true)
+ const { systemFeatures } = useGlobalPublicStore()
+ const [authType, updateAuthType] = useState<'code' | 'password'>('password')
+ const [showORLine, setShowORLine] = useState(false)
+ const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false)
+
+ const init = useCallback(async () => {
+ try {
+ setAllMethodsAreDisabled(!systemFeatures.enable_social_oauth_login && !systemFeatures.enable_email_code_login && !systemFeatures.enable_email_password_login && !systemFeatures.sso_enforced_for_signin)
+ setShowORLine((systemFeatures.enable_social_oauth_login || systemFeatures.sso_enforced_for_signin) && (systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login))
+ updateAuthType(systemFeatures.enable_email_password_login ? 'password' : 'code')
+ }
+ catch (error) {
+ console.error(error)
+ setAllMethodsAreDisabled(true)
+ }
+ finally { setIsLoading(false) }
+ }, [systemFeatures])
+ useEffect(() => {
+ init()
+ }, [init])
+ if (isLoading) {
+ return
+
+
+ }
+ if (systemFeatures.license?.status === LicenseStatus.LOST) {
+ return
+
+
+
+
+
+
+
{t('login.licenseLost')}
+
{t('login.licenseLostTip')}
+
+
+
+ }
+ if (systemFeatures.license?.status === LicenseStatus.EXPIRED) {
+ return
+
+
+
+
+
+
+
{t('login.licenseExpired')}
+
{t('login.licenseExpiredTip')}
+
+
+
+ }
+ if (systemFeatures.license?.status === LicenseStatus.INACTIVE) {
+ return
+
+
+
+
+
+
+
{t('login.licenseInactive')}
+
{t('login.licenseInactiveTip')}
+
+
+
+ }
+
+ return (
+ <>
+
+
+
{t('login.pageTitle')}
+ {!systemFeatures.branding.enabled &&
{t('login.welcome')}
}
+
+
+
+ {systemFeatures.sso_enforced_for_signin &&
+
+
}
+
+
+ {showORLine &&
+
+
+ {t('login.or')}
+
+
}
+ {
+ (systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login) && <>
+ {systemFeatures.enable_email_code_login && authType === 'code' && <>
+
+ {systemFeatures.enable_email_password_login &&
{ updateAuthType('password') }}>
+ {t('login.usePassword')}
+
}
+ >}
+ {systemFeatures.enable_email_password_login && authType === 'password' && <>
+
+ {systemFeatures.enable_email_code_login &&
{ updateAuthType('code') }}>
+ {t('login.useVerificationCode')}
+
}
+ >}
+ >
+ }
+ {allMethodsAreDisabled && <>
+
+
+
+
+
{t('login.noLoginMethod')}
+
{t('login.noLoginMethodTip')}
+
+
+ >}
+ {!systemFeatures.branding.enabled && <>
+
+ {t('login.tosDesc')}
+
+ {t('login.tos')}
+ &
+ {t('login.pp')}
+
+ {IS_CE_EDITION &&
+ {t('login.goToInit')}
+
+ {t('login.setAdminAccount')}
+
}
+ >}
+
+
+
+ >
+ )
+}
+
+export default NormalForm
diff --git a/web/app/(shareLayout)/webapp-signin/page.tsx b/web/app/(shareLayout)/webapp-signin/page.tsx
index 668c3f312c..c12fde38dd 100644
--- a/web/app/(shareLayout)/webapp-signin/page.tsx
+++ b/web/app/(shareLayout)/webapp-signin/page.tsx
@@ -3,19 +3,20 @@ import { useRouter, useSearchParams } from 'next/navigation'
import type { FC } from 'react'
import React, { useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
-import { RiDoorLockLine } from '@remixicon/react'
-import cn from '@/utils/classnames'
import Toast from '@/app/components/base/toast'
-import { fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share'
-import { setAccessToken } from '@/app/components/share/utils'
+import { removeAccessToken, setAccessToken } from '@/app/components/share/utils'
import { useGlobalPublicStore } from '@/context/global-public-context'
-import { SSOProtocol } from '@/types/feature'
import Loading from '@/app/components/base/loading'
import AppUnavailable from '@/app/components/base/app-unavailable'
+import NormalForm from './normalForm'
+import { AccessMode } from '@/models/access-control'
+import ExternalMemberSsoAuth from './components/external-member-sso-auth'
+import { fetchAccessToken } from '@/service/share'
const WebSSOForm: FC = () => {
const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
+ const webAppAccessMode = useGlobalPublicStore(s => s.webAppAccessMode)
const searchParams = useSearchParams()
const router = useRouter()
@@ -23,10 +24,22 @@ const WebSSOForm: FC = () => {
const tokenFromUrl = searchParams.get('web_sso_token')
const message = searchParams.get('message')
- const showErrorToast = (message: string) => {
+ const getSigninUrl = useCallback(() => {
+ const params = new URLSearchParams(searchParams)
+ params.delete('message')
+ return `/webapp-signin?${params.toString()}`
+ }, [searchParams])
+
+ const backToHome = useCallback(() => {
+ removeAccessToken()
+ const url = getSigninUrl()
+ router.replace(url)
+ }, [getSigninUrl, router])
+
+ const showErrorToast = (msg: string) => {
Toast.notify({
type: 'error',
- message,
+ message: msg,
})
}
@@ -38,102 +51,73 @@ const WebSSOForm: FC = () => {
return appCode
}, [redirectUrl])
- const processTokenAndRedirect = useCallback(async () => {
- const appCode = getAppCodeFromRedirectUrl()
- if (!appCode || !tokenFromUrl || !redirectUrl) {
- showErrorToast('redirect url or app code or token is invalid.')
- return
- }
+ useEffect(() => {
+ (async () => {
+ if (message)
+ return
- await setAccessToken(appCode, tokenFromUrl)
- router.push(redirectUrl)
- }, [getAppCodeFromRedirectUrl, redirectUrl, router, tokenFromUrl])
-
- const handleSSOLogin = useCallback(async () => {
- const appCode = getAppCodeFromRedirectUrl()
- if (!appCode || !redirectUrl) {
- showErrorToast('redirect url or app code is invalid.')
- return
- }
-
- switch (systemFeatures.webapp_auth.sso_config.protocol) {
- case SSOProtocol.SAML: {
- const samlRes = await fetchWebSAMLSSOUrl(appCode, redirectUrl)
- router.push(samlRes.url)
- break
+ const appCode = getAppCodeFromRedirectUrl()
+ if (appCode && tokenFromUrl && redirectUrl) {
+ localStorage.setItem('webapp_access_token', tokenFromUrl)
+ const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: tokenFromUrl })
+ await setAccessToken(appCode, tokenResp.access_token)
+ router.replace(redirectUrl)
+ return
}
- case SSOProtocol.OIDC: {
- const oidcRes = await fetchWebOIDCSSOUrl(appCode, redirectUrl)
- router.push(oidcRes.url)
- break
+ if (appCode && redirectUrl && localStorage.getItem('webapp_access_token')) {
+ const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: localStorage.getItem('webapp_access_token') })
+ await setAccessToken(appCode, tokenResp.access_token)
+ router.replace(redirectUrl)
}
- case SSOProtocol.OAuth2: {
- const oauth2Res = await fetchWebOAuth2SSOUrl(appCode, redirectUrl)
- router.push(oauth2Res.url)
- break
- }
- case '':
- break
- default:
- showErrorToast('SSO protocol is not supported.')
- }
- }, [getAppCodeFromRedirectUrl, redirectUrl, router, systemFeatures.webapp_auth.sso_config.protocol])
+ })()
+ }, [getAppCodeFromRedirectUrl, redirectUrl, router, tokenFromUrl, message])
useEffect(() => {
- const init = async () => {
- if (message) {
- showErrorToast(message)
- return
- }
+ if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC && redirectUrl)
+ router.replace(redirectUrl)
+ }, [webAppAccessMode, router, redirectUrl])
- if (!tokenFromUrl) {
- await handleSSOLogin()
- return
- }
-
- await processTokenAndRedirect()
- }
-
- init()
- }, [message, processTokenAndRedirect, tokenFromUrl, handleSSOLogin])
- if (tokenFromUrl)
- return
- if (message) {
+ if (tokenFromUrl) {
return
}
- if (systemFeatures.webapp_auth.enabled) {
- if (systemFeatures.webapp_auth.allow_sso) {
- return (
-
- )
- }
- return
-
-
-
-
-
{t('login.webapp.noLoginMethod')}
-
{t('login.webapp.noLoginMethodTip')}
-
-
+ if (message) {
+ return
+
+
{t('share.login.backToHome')}
}
- else {
+ if (!redirectUrl) {
+ showErrorToast('redirect url is invalid.')
+ return
+ }
+ if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC) {
+ return
+
+
+ }
+ if (!systemFeatures.webapp_auth.enabled) {
return
{t('login.webapp.disabled')}
}
+ if (webAppAccessMode && (webAppAccessMode === AccessMode.ORGANIZATION || webAppAccessMode === AccessMode.SPECIFIC_GROUPS_MEMBERS)) {
+ return
+
+
+ }
+
+ if (webAppAccessMode && webAppAccessMode === AccessMode.EXTERNAL_MEMBERS)
+ return
+
+ return
+
+
{t('share.login.backToHome')}
+
}
export default React.memo(WebSSOForm)
diff --git a/web/app/components/app/app-access-control/index.tsx b/web/app/components/app/app-access-control/index.tsx
index 2f15c8ec48..13faaea957 100644
--- a/web/app/components/app/app-access-control/index.tsx
+++ b/web/app/components/app/app-access-control/index.tsx
@@ -1,6 +1,6 @@
'use client'
-import { Dialog } from '@headlessui/react'
-import { RiBuildingLine, RiGlobalLine } from '@remixicon/react'
+import { Description as DialogDescription, DialogTitle } from '@headlessui/react'
+import { RiBuildingLine, RiGlobalLine, RiVerifiedBadgeLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useCallback, useEffect } from 'react'
import Button from '../../base/button'
@@ -67,8 +67,8 @@ export default function AccessControl(props: AccessControlProps) {
return
- {t('app.accessControlDialog.title')}
- {t('app.accessControlDialog.description')}
+ {t('app.accessControlDialog.title')}
+ {t('app.accessControlDialog.description')}
@@ -80,12 +80,20 @@ export default function AccessControl(props: AccessControlProps) {
{t('app.accessControlDialog.accessItems.organization')}
- {!hideTip &&
}
+
+
+
+
+
{t('app.accessControlDialog.accessItems.external')}
+
+ {!hideTip &&
}
+
+
diff --git a/web/app/components/app/app-access-control/specific-groups-or-members.tsx b/web/app/components/app/app-access-control/specific-groups-or-members.tsx
index f4872f8c99..b30c8f1ba3 100644
--- a/web/app/components/app/app-access-control/specific-groups-or-members.tsx
+++ b/web/app/components/app/app-access-control/specific-groups-or-members.tsx
@@ -3,12 +3,10 @@ import { RiAlertFill, RiCloseCircleFill, RiLockLine, RiOrganizationChart } from
import { useTranslation } from 'react-i18next'
import { useCallback, useEffect } from 'react'
import Avatar from '../../base/avatar'
-import Divider from '../../base/divider'
import Tooltip from '../../base/tooltip'
import Loading from '../../base/loading'
import useAccessControlStore from '../../../../context/access-control-store'
import AddMemberOrGroupDialog from './add-member-or-group-pop'
-import { useGlobalPublicStore } from '@/context/global-public-context'
import type { AccessControlAccount, AccessControlGroup } from '@/models/access-control'
import { AccessMode } from '@/models/access-control'
import { useAppWhiteListSubjects } from '@/service/access-control'
@@ -19,11 +17,6 @@ export default function SpecificGroupsOrMembers() {
const setSpecificGroups = useAccessControlStore(s => s.setSpecificGroups)
const setSpecificMembers = useAccessControlStore(s => s.setSpecificMembers)
const { t } = useTranslation()
- const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
- const hideTip = systemFeatures.webapp_auth.enabled
- && (systemFeatures.webapp_auth.allow_sso
- || systemFeatures.webapp_auth.allow_email_password_login
- || systemFeatures.webapp_auth.allow_email_code_login)
const { isPending, data } = useAppWhiteListSubjects(appId, Boolean(appId) && currentMenu === AccessMode.SPECIFIC_GROUPS_MEMBERS)
useEffect(() => {
@@ -37,7 +30,6 @@ export default function SpecificGroupsOrMembers() {
{t('app.accessControlDialog.accessItems.specific')}
- {!hideTip && }
}
@@ -48,10 +40,6 @@ export default function SpecificGroupsOrMembers() {
{t('app.accessControlDialog.accessItems.specific')}
- {!hideTip && <>
-
-
- >}
diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx
index 8d0028c7d7..5825bb72ee 100644
--- a/web/app/components/app/app-publisher/index.tsx
+++ b/web/app/components/app/app-publisher/index.tsx
@@ -9,11 +9,14 @@ import dayjs from 'dayjs'
import {
RiArrowDownSLine,
RiArrowRightSLine,
+ RiBuildingLine,
+ RiGlobalLine,
RiLockLine,
RiPlanetLine,
RiPlayCircleLine,
RiPlayList2Line,
RiTerminalBoxLine,
+ RiVerifiedBadgeLine,
} from '@remixicon/react'
import { useKeyPress } from 'ahooks'
import { getKeyboardKeyCodeBySystem } from '../../workflow/utils'
@@ -276,10 +279,30 @@ const AppPublisher = ({
setShowAppAccessControl(true)
}}>
-
- {appDetail?.access_mode === AccessMode.ORGANIZATION &&
{t('app.accessControlDialog.accessItems.organization')}
}
- {appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS &&
{t('app.accessControlDialog.accessItems.specific')}
}
- {appDetail?.access_mode === AccessMode.PUBLIC &&
{t('app.accessControlDialog.accessItems.anyone')}
}
+ {appDetail?.access_mode === AccessMode.ORGANIZATION
+ && <>
+
+
{t('app.accessControlDialog.accessItems.organization')}
+ >
+ }
+ {appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS
+ && <>
+
+
{t('app.accessControlDialog.accessItems.specific')}
+ >
+ }
+ {appDetail?.access_mode === AccessMode.PUBLIC
+ && <>
+
+
{t('app.accessControlDialog.accessItems.anyone')}
+ >
+ }
+ {appDetail?.access_mode === AccessMode.EXTERNAL_MEMBERS
+ && <>
+
+
{t('app.accessControlDialog.accessItems.external')}
+ >
+ }
{!isAppAccessSet &&
{t('app.publishApp.notSet')}
}
diff --git a/web/app/components/app/overview/appCard.tsx b/web/app/components/app/overview/appCard.tsx
index 9b283cdf5e..9f3b3ac4a6 100644
--- a/web/app/components/app/overview/appCard.tsx
+++ b/web/app/components/app/overview/appCard.tsx
@@ -5,10 +5,13 @@ import { useTranslation } from 'react-i18next'
import {
RiArrowRightSLine,
RiBookOpenLine,
+ RiBuildingLine,
RiEqualizer2Line,
RiExternalLinkLine,
+ RiGlobalLine,
RiLockLine,
RiPaintBrushLine,
+ RiVerifiedBadgeLine,
RiWindowLine,
} from '@remixicon/react'
import SettingsModal from './settings'
@@ -248,11 +251,30 @@ function AppCard({
-
- {appDetail?.access_mode === AccessMode.ORGANIZATION &&
{t('app.accessControlDialog.accessItems.organization')}
}
- {appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS &&
{t('app.accessControlDialog.accessItems.specific')}
}
- {appDetail?.access_mode === AccessMode.PUBLIC &&
{t('app.accessControlDialog.accessItems.anyone')}
}
-
+ {appDetail?.access_mode === AccessMode.ORGANIZATION
+ && <>
+
+
{t('app.accessControlDialog.accessItems.organization')}
+ >
+ }
+ {appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS
+ && <>
+
+
{t('app.accessControlDialog.accessItems.specific')}
+ >
+ }
+ {appDetail?.access_mode === AccessMode.PUBLIC
+ && <>
+
+
{t('app.accessControlDialog.accessItems.anyone')}
+ >
+ }
+ {appDetail?.access_mode === AccessMode.EXTERNAL_MEMBERS
+ && <>
+
+
{t('app.accessControlDialog.accessItems.external')}
+ >
+ }
{!isAppAccessSet &&
{t('app.publishApp.notSet')}
}
diff --git a/web/app/components/base/app-unavailable.tsx b/web/app/components/base/app-unavailable.tsx
index 4e835cbfcf..928c850262 100644
--- a/web/app/components/base/app-unavailable.tsx
+++ b/web/app/components/base/app-unavailable.tsx
@@ -1,4 +1,5 @@
'use client'
+import classNames from '@/utils/classnames'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
@@ -7,17 +8,19 @@ type IAppUnavailableProps = {
code?: number | string
isUnknownReason?: boolean
unknownReason?: string
+ className?: string
}
const AppUnavailable: FC
= ({
code = 404,
isUnknownReason,
unknownReason,
+ className,
}) => {
const { t } = useTranslation()
return (
-
+
({
- accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS,
userCanAccess: false,
currentConversationId: '',
appPrevChatTree: [],
diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx
index dd7cb14b25..32f74e6457 100644
--- a/web/app/components/base/chat/chat-with-history/hooks.tsx
+++ b/web/app/components/base/chat/chat-with-history/hooks.tsx
@@ -43,9 +43,8 @@ import { useAppFavicon } from '@/hooks/use-app-favicon'
import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
import { noop } from 'lodash-es'
-import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control'
+import { useGetUserCanAccessApp } from '@/service/access-control'
import { useGlobalPublicStore } from '@/context/global-public-context'
-import { AccessMode } from '@/models/access-control'
function getFormattedChatList(messages: any[]) {
const newChatList: ChatItem[] = []
@@ -77,11 +76,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo])
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo)
- const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({
- appId: installedAppInfo?.app.id || appInfo?.app_id,
- isInstalledApp,
- enabled: systemFeatures.webapp_auth.enabled,
- })
const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({
appId: installedAppInfo?.app.id || appInfo?.app_id,
isInstalledApp,
@@ -492,8 +486,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
return {
appInfoError,
- appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission)),
- accessMode: systemFeatures.webapp_auth.enabled ? appAccessMode?.accessMode : AccessMode.PUBLIC,
+ appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && isCheckingPermission),
userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true,
isInstalledApp,
appId,
diff --git a/web/app/components/base/chat/chat-with-history/index.tsx b/web/app/components/base/chat/chat-with-history/index.tsx
index de023e7f58..1fd1383196 100644
--- a/web/app/components/base/chat/chat-with-history/index.tsx
+++ b/web/app/components/base/chat/chat-with-history/index.tsx
@@ -124,7 +124,6 @@ const ChatWithHistoryWrap: FC = ({
const {
appInfoError,
appInfoLoading,
- accessMode,
userCanAccess,
appData,
appParams,
@@ -169,7 +168,6 @@ const ChatWithHistoryWrap: FC = ({
appInfoError,
appInfoLoading,
appData,
- accessMode,
userCanAccess,
appParams,
appMeta,
diff --git a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx
index fd317ccf91..4e50c1cb79 100644
--- a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx
+++ b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx
@@ -19,7 +19,6 @@ import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/re
import DifyLogo from '@/app/components/base/logo/dify-logo'
import type { ConversationItem } from '@/models/share'
import cn from '@/utils/classnames'
-import { AccessMode } from '@/models/access-control'
import { useGlobalPublicStore } from '@/context/global-public-context'
type Props = {
@@ -30,7 +29,6 @@ const Sidebar = ({ isPanel }: Props) => {
const { t } = useTranslation()
const {
isInstalledApp,
- accessMode,
appData,
handleNewConversation,
pinnedConversationList,
@@ -140,7 +138,7 @@ const Sidebar = ({ isPanel }: Props) => {
)}
-
+
{/* powered by */}
{!appData?.custom_config?.remove_webapp_brand && (
diff --git a/web/app/components/base/chat/embedded-chatbot/context.tsx b/web/app/components/base/chat/embedded-chatbot/context.tsx
index 5964efd806..d24265ed9e 100644
--- a/web/app/components/base/chat/embedded-chatbot/context.tsx
+++ b/web/app/components/base/chat/embedded-chatbot/context.tsx
@@ -15,10 +15,8 @@ import type {
ConversationItem,
} from '@/models/share'
import { noop } from 'lodash-es'
-import { AccessMode } from '@/models/access-control'
export type EmbeddedChatbotContextValue = {
- accessMode?: AccessMode
userCanAccess?: boolean
appInfoError?: any
appInfoLoading?: boolean
@@ -58,7 +56,6 @@ export type EmbeddedChatbotContextValue = {
export const EmbeddedChatbotContext = createContext
({
userCanAccess: false,
- accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS,
currentConversationId: '',
appPrevChatList: [],
pinnedConversationList: [],
diff --git a/web/app/components/base/chat/embedded-chatbot/hooks.tsx b/web/app/components/base/chat/embedded-chatbot/hooks.tsx
index 40c56eca7b..0158e8d041 100644
--- a/web/app/components/base/chat/embedded-chatbot/hooks.tsx
+++ b/web/app/components/base/chat/embedded-chatbot/hooks.tsx
@@ -36,9 +36,8 @@ import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
import { noop } from 'lodash-es'
-import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control'
+import { useGetUserCanAccessApp } from '@/service/access-control'
import { useGlobalPublicStore } from '@/context/global-public-context'
-import { AccessMode } from '@/models/access-control'
function getFormattedChatList(messages: any[]) {
const newChatList: ChatItem[] = []
@@ -70,11 +69,6 @@ export const useEmbeddedChatbot = () => {
const isInstalledApp = false
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR('appInfo', fetchAppInfo)
- const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({
- appId: appInfo?.app_id,
- isInstalledApp,
- enabled: systemFeatures.webapp_auth.enabled,
- })
const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({
appId: appInfo?.app_id,
isInstalledApp,
@@ -385,8 +379,7 @@ export const useEmbeddedChatbot = () => {
return {
appInfoError,
- appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission)),
- accessMode: systemFeatures.webapp_auth.enabled ? appAccessMode?.accessMode : AccessMode.PUBLIC,
+ appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && isCheckingPermission),
userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true,
isInstalledApp,
allowResetChat,
diff --git a/web/app/components/share/text-generation/menu-dropdown.tsx b/web/app/components/share/text-generation/menu-dropdown.tsx
index 19b660b083..adb926c7ca 100644
--- a/web/app/components/share/text-generation/menu-dropdown.tsx
+++ b/web/app/components/share/text-generation/menu-dropdown.tsx
@@ -6,9 +6,8 @@ import type { Placement } from '@floating-ui/react'
import {
RiEqualizer2Line,
} from '@remixicon/react'
-import { useRouter } from 'next/navigation'
+import { usePathname, useRouter } from 'next/navigation'
import Divider from '../../base/divider'
-import { removeAccessToken } from '../utils'
import InfoModal from './info-modal'
import ActionButton from '@/app/components/base/action-button'
import {
@@ -19,6 +18,8 @@ import {
import ThemeSwitcher from '@/app/components/base/theme-switcher'
import type { SiteInfo } from '@/models/share'
import cn from '@/utils/classnames'
+import { useGlobalPublicStore } from '@/context/global-public-context'
+import { AccessMode } from '@/models/access-control'
type Props = {
data?: SiteInfo
@@ -31,7 +32,9 @@ const MenuDropdown: FC = ({
placement,
hideLogout,
}) => {
+ const webAppAccessMode = useGlobalPublicStore(s => s.webAppAccessMode)
const router = useRouter()
+ const pathname = usePathname()
const { t } = useTranslation()
const [open, doSetOpen] = useState(false)
const openRef = useRef(open)
@@ -45,9 +48,10 @@ const MenuDropdown: FC = ({
}, [setOpen])
const handleLogout = useCallback(() => {
- removeAccessToken()
- router.replace(`/webapp-signin?redirect_url=${window.location.href}`)
- }, [router])
+ localStorage.removeItem('token')
+ localStorage.removeItem('webapp_access_token')
+ router.replace(`/webapp-signin?redirect_url=${pathname}`)
+ }, [router, pathname])
const [show, setShow] = useState(false)
@@ -92,6 +96,16 @@ const MenuDropdown: FC = ({
className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'
>{t('common.userProfile.about')}
+ {!(hideLogout || webAppAccessMode === AccessMode.EXTERNAL_MEMBERS || webAppAccessMode === AccessMode.PUBLIC) && (
+
+
+ {t('common.userProfile.logout')}
+
+
+ )}
diff --git a/web/app/components/share/utils.ts b/web/app/components/share/utils.ts
index 9ce891a50c..d793d48b48 100644
--- a/web/app/components/share/utils.ts
+++ b/web/app/components/share/utils.ts
@@ -10,8 +10,8 @@ export const getInitialTokenV2 = (): Record => ({
version: 2,
})
-export const checkOrSetAccessToken = async () => {
- const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0]
+export const checkOrSetAccessToken = async (appCode?: string) => {
+ const sharedToken = appCode || globalThis.location.pathname.split('/').slice(-1)[0]
const userId = (await getProcessedSystemVariablesFromUrlParams()).user_id
const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2())
let accessTokenJson = getInitialTokenV2()
@@ -23,8 +23,10 @@ export const checkOrSetAccessToken = async () => {
catch {
}
+
if (!accessTokenJson[sharedToken]?.[userId || 'DEFAULT']) {
- const res = await fetchAccessToken(sharedToken, userId)
+ const webAppAccessToken = localStorage.getItem('webapp_access_token')
+ const res = await fetchAccessToken({ appCode: sharedToken, userId, webAppAccessToken })
accessTokenJson[sharedToken] = {
...accessTokenJson[sharedToken],
[userId || 'DEFAULT']: res.access_token,
@@ -33,7 +35,7 @@ export const checkOrSetAccessToken = async () => {
}
}
-export const setAccessToken = async (sharedToken: string, token: string, user_id?: string) => {
+export const setAccessToken = (sharedToken: string, token: string, user_id?: string) => {
const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2())
let accessTokenJson = getInitialTokenV2()
try {
@@ -69,6 +71,7 @@ export const removeAccessToken = () => {
}
localStorage.removeItem(CONVERSATION_ID_INFO)
+ localStorage.removeItem('webapp_access_token')
delete accessTokenJson[sharedToken]
localStorage.setItem('token', JSON.stringify(accessTokenJson))
diff --git a/web/app/signin/LoginLogo.tsx b/web/app/signin/LoginLogo.tsx
index 0753d1f98a..73dfb88205 100644
--- a/web/app/signin/LoginLogo.tsx
+++ b/web/app/signin/LoginLogo.tsx
@@ -1,8 +1,8 @@
'use client'
import type { FC } from 'react'
import classNames from '@/utils/classnames'
-import { useSelector } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
+import { useTheme } from 'next-themes'
type LoginLogoProps = {
className?: string
@@ -12,11 +12,7 @@ const LoginLogo: FC = ({
className,
}) => {
const { systemFeatures } = useGlobalPublicStore()
- const { theme } = useSelector((s) => {
- return {
- theme: s.theme,
- }
- })
+ const { theme } = useTheme()
let src = theme === 'light' ? '/logo/logo-site.png' : `/logo/logo-site-${theme}.png`
if (systemFeatures.branding.enabled)
diff --git a/web/context/global-public-context.tsx b/web/context/global-public-context.tsx
index 5aa5e7a302..26ad84be65 100644
--- a/web/context/global-public-context.tsx
+++ b/web/context/global-public-context.tsx
@@ -7,19 +7,24 @@ import type { SystemFeatures } from '@/types/feature'
import { defaultSystemFeatures } from '@/types/feature'
import { getSystemFeatures } from '@/service/common'
import Loading from '@/app/components/base/loading'
+import { AccessMode } from '@/models/access-control'
type GlobalPublicStore = {
- isPending: boolean
- setIsPending: (isPending: boolean) => void
+ isGlobalPending: boolean
+ setIsGlobalPending: (isPending: boolean) => void
systemFeatures: SystemFeatures
setSystemFeatures: (systemFeatures: SystemFeatures) => void
+ webAppAccessMode: AccessMode,
+ setWebAppAccessMode: (webAppAccessMode: AccessMode) => void
}
export const useGlobalPublicStore = create(set => ({
- isPending: true,
- setIsPending: (isPending: boolean) => set(() => ({ isPending })),
+ isGlobalPending: true,
+ setIsGlobalPending: (isPending: boolean) => set(() => ({ isGlobalPending: isPending })),
systemFeatures: defaultSystemFeatures,
setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })),
+ webAppAccessMode: AccessMode.PUBLIC,
+ setWebAppAccessMode: (webAppAccessMode: AccessMode) => set(() => ({ webAppAccessMode })),
}))
const GlobalPublicStoreProvider: FC = ({
@@ -29,7 +34,7 @@ const GlobalPublicStoreProvider: FC = ({
queryKey: ['systemFeatures'],
queryFn: getSystemFeatures,
})
- const { setSystemFeatures, setIsPending } = useGlobalPublicStore()
+ const { setSystemFeatures, setIsGlobalPending: setIsPending } = useGlobalPublicStore()
useEffect(() => {
if (data)
setSystemFeatures({ ...defaultSystemFeatures, ...data })
diff --git a/web/hooks/use-document-title.spec.ts b/web/hooks/use-document-title.spec.ts
index 88239ffbdf..a8d3d56cff 100644
--- a/web/hooks/use-document-title.spec.ts
+++ b/web/hooks/use-document-title.spec.ts
@@ -11,7 +11,7 @@ describe('title should be empty if systemFeatures is pending', () => {
act(() => {
useGlobalPublicStore.setState({
systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } },
- isPending: true,
+ isGlobalPending: true,
})
})
it('document title should be empty if set title', () => {
@@ -28,7 +28,7 @@ describe('use default branding', () => {
beforeEach(() => {
act(() => {
useGlobalPublicStore.setState({
- isPending: false,
+ isGlobalPending: false,
systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } },
})
})
@@ -48,7 +48,7 @@ describe('use specific branding', () => {
beforeEach(() => {
act(() => {
useGlobalPublicStore.setState({
- isPending: false,
+ isGlobalPending: false,
systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: true, application_title: 'Test' } },
})
})
diff --git a/web/hooks/use-document-title.ts b/web/hooks/use-document-title.ts
index 10275a196f..2c848a1f56 100644
--- a/web/hooks/use-document-title.ts
+++ b/web/hooks/use-document-title.ts
@@ -3,7 +3,7 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
import { useFavicon, useTitle } from 'ahooks'
export default function useDocumentTitle(title: string) {
- const isPending = useGlobalPublicStore(s => s.isPending)
+ const isPending = useGlobalPublicStore(s => s.isGlobalPending)
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const prefix = title ? `${title} - ` : ''
let titleStr = ''
diff --git a/web/i18n/en-US/app.ts b/web/i18n/en-US/app.ts
index 20a80ba4cd..ccfe23ead6 100644
--- a/web/i18n/en-US/app.ts
+++ b/web/i18n/en-US/app.ts
@@ -197,9 +197,10 @@ const translation = {
},
accessControl: 'Web App Access Control',
accessItemsDescription: {
- anyone: 'Anyone can access the web app',
- specific: 'Only specific groups or members can access the web app',
- organization: 'Anyone in the organization can access the web app',
+ anyone: 'Anyone can access the web app (no login required)',
+ specific: 'Only specific members within the platform can access the Web application',
+ organization: 'All members within the platform can access the Web application',
+ external: 'Only authenticated external users can access the Web application',
},
accessControlDialog: {
title: 'Web App Access Control',
@@ -207,15 +208,16 @@ const translation = {
accessLabel: 'Who has access',
accessItems: {
anyone: 'Anyone with the link',
- specific: 'Specific groups or members',
- organization: 'Only members within the enterprise',
+ specific: 'Specific members within the platform',
+ organization: 'All members within the platform',
+ external: 'Authenticated external users',
},
groups_one: '{{count}} GROUP',
groups_other: '{{count}} GROUPS',
members_one: '{{count}} MEMBER',
members_other: '{{count}} MEMBERS',
noGroupsOrMembers: 'No groups or members selected',
- webAppSSONotEnabledTip: 'Please contact enterprise administrator to configure the web app authentication method.',
+ webAppSSONotEnabledTip: 'Please contact your organization administrator to configure external authentication for the Web application.',
operateGroupAndMember: {
searchPlaceholder: 'Search groups and members',
allMembers: 'All members',
diff --git a/web/i18n/en-US/share-app.ts b/web/i18n/en-US/share-app.ts
index bf99005d71..ab589ffb76 100644
--- a/web/i18n/en-US/share-app.ts
+++ b/web/i18n/en-US/share-app.ts
@@ -77,6 +77,9 @@ const translation = {
atLeastOne: 'Please input at least one row in the uploaded file.',
},
},
+ login: {
+ backToHome: 'Back to Home',
+ },
}
export default translation
diff --git a/web/i18n/ja-JP/app.ts b/web/i18n/ja-JP/app.ts
index b4fc8d4d82..b501bc129e 100644
--- a/web/i18n/ja-JP/app.ts
+++ b/web/i18n/ja-JP/app.ts
@@ -210,30 +210,27 @@ const translation = {
},
accessControl: 'Web アプリアクセス制御',
accessItemsDescription: {
- anyone: '誰でも Web アプリにアクセス可能',
- specific: '特定のグループまたはメンバーのみが Web アプリにアクセス可能',
- organization: '組織内の誰でも Web アプリにアクセス可能',
+ anyone: '誰でもこの web アプリにアクセスできます(ログイン不要)',
+ specific: '特定のプラットフォーム内メンバーのみがこの Web アプリにアクセスできます',
+ organization: 'プラットフォーム内の全メンバーがこの Web アプリにアクセスできます',
+ external: '認証済みの外部ユーザーのみがこの Web アプリにアクセスできます',
},
accessControlDialog: {
title: 'アクセス権限',
description: 'Web アプリのアクセス権限を設定します',
accessLabel: '誰がアクセスできますか',
- accessItemsDescription: {
- anyone: '誰でも Web アプリにアクセス可能です',
- specific: '特定のグループやメンバーが Web アプリにアクセス可能です',
- organization: '組織内の誰でも Web アプリにアクセス可能です',
- },
accessItems: {
- anyone: 'すべてのユーザー',
- specific: '特定のグループメンバー',
- organization: 'グループ内の全員',
+ anyone: 'リンクを知っているすべてのユーザー',
+ specific: '特定のプラットフォーム内メンバー',
+ organization: 'プラットフォーム内の全メンバー',
+ external: '認証済みの外部ユーザー',
},
groups_one: '{{count}} グループ',
groups_other: '{{count}} グループ',
members_one: '{{count}} メンバー',
members_other: '{{count}} メンバー',
noGroupsOrMembers: 'グループまたはメンバーが選択されていません',
- webAppSSONotEnabledTip: 'Web アプリの認証方式設定については、企業管理者へご連絡ください。',
+ webAppSSONotEnabledTip: 'Web アプリの外部認証方式を設定するには、組織の管理者にお問い合わせください。',
operateGroupAndMember: {
searchPlaceholder: 'グループやメンバーを検索',
allMembers: 'すべてのメンバー',
diff --git a/web/i18n/ja-JP/share-app.ts b/web/i18n/ja-JP/share-app.ts
index 9e76f6518a..20dad7faec 100644
--- a/web/i18n/ja-JP/share-app.ts
+++ b/web/i18n/ja-JP/share-app.ts
@@ -73,6 +73,9 @@ const translation = {
atLeastOne: '1 行以上のデータが必要です',
},
},
+ login: {
+ backToHome: 'ホームに戻る',
+ },
}
export default translation
diff --git a/web/i18n/zh-Hans/app.ts b/web/i18n/zh-Hans/app.ts
index bdd7d98d9b..4ec1e65059 100644
--- a/web/i18n/zh-Hans/app.ts
+++ b/web/i18n/zh-Hans/app.ts
@@ -198,30 +198,27 @@ const translation = {
},
accessControl: 'Web 应用访问控制',
accessItemsDescription: {
- anyone: '任何人可以访问 web 应用',
- specific: '特定组或成员可以访问 web 应用',
- organization: '组织内任何人可以访问 web 应用',
+ anyone: '任何人都可以访问该 web 应用(无需登录)',
+ specific: '仅指定的平台内成员可访问该 Web 应用',
+ organization: '平台内所有成员均可访问该 Web 应用',
+ external: '仅经认证的外部用户可访问该 Web 应用',
},
accessControlDialog: {
title: 'Web 应用访问权限',
description: '设置 web 应用访问权限。',
accessLabel: '谁可以访问',
- accessItemsDescription: {
- anyone: '任何人可以访问 web 应用',
- specific: '特定组或成员可以访问 web 应用',
- organization: '组织内任何人可以访问 web 应用',
- },
accessItems: {
anyone: '任何人',
- specific: '特定组或成员',
- organization: '组织内任何人',
+ specific: '平台内指定成员',
+ organization: '平台内所有成员',
+ external: '经认证的外部用户',
},
groups_one: '{{count}} 个组',
groups_other: '{{count}} 个组',
members_one: '{{count}} 个成员',
members_other: '{{count}} 个成员',
noGroupsOrMembers: '未选择分组或成员',
- webAppSSONotEnabledTip: '请联系企业管理员配置 web 应用的身份认证方式。',
+ webAppSSONotEnabledTip: '请联系企业管理员配置 Web 应用外部认证方式。',
operateGroupAndMember: {
searchPlaceholder: '搜索组或成员',
allMembers: '所有成员',
diff --git a/web/i18n/zh-Hans/share-app.ts b/web/i18n/zh-Hans/share-app.ts
index 4ea2ad6f49..ce1270dae8 100644
--- a/web/i18n/zh-Hans/share-app.ts
+++ b/web/i18n/zh-Hans/share-app.ts
@@ -73,6 +73,9 @@ const translation = {
atLeastOne: '上传文件的内容不能少于一条',
},
},
+ login: {
+ backToHome: '返回首页',
+ },
}
export default translation
diff --git a/web/models/access-control.ts b/web/models/access-control.ts
index 8ad9cc6491..911662b5c4 100644
--- a/web/models/access-control.ts
+++ b/web/models/access-control.ts
@@ -7,6 +7,7 @@ export enum AccessMode {
PUBLIC = 'public',
SPECIFIC_GROUPS_MEMBERS = 'private',
ORGANIZATION = 'private_all',
+ EXTERNAL_MEMBERS = 'sso_verified',
}
export type AccessControlGroup = {
diff --git a/web/service/base.ts b/web/service/base.ts
index 4b08736288..c3cafe600b 100644
--- a/web/service/base.ts
+++ b/web/service/base.ts
@@ -109,6 +109,7 @@ function unicodeToChar(text: string) {
}
function requiredWebSSOLogin(message?: string) {
+ removeAccessToken()
const params = new URLSearchParams()
params.append('redirect_url', globalThis.location.pathname)
if (message)
diff --git a/web/service/common.ts b/web/service/common.ts
index e76cfb4196..700cd4bf51 100644
--- a/web/service/common.ts
+++ b/web/service/common.ts
@@ -52,6 +52,9 @@ type LoginResponse = LoginSuccess | LoginFail
export const login: Fetcher }> = ({ url, body }) => {
return post(url, { body }) as Promise
}
+export const webAppLogin: Fetcher }> = ({ url, body }) => {
+ return post(url, { body }, { isPublicAPI: true }) as Promise
+}
export const fetchNewToken: Fetcher }> = ({ body }) => {
return post('/refresh-token', { body }) as Promise
@@ -324,6 +327,16 @@ export const verifyForgotPasswordToken: Fetcher = ({ url, body }) =>
post(url, { body })
+export const sendWebAppForgotPasswordEmail: Fetcher = ({ url, body }) =>
+ post(url, { body }, { isPublicAPI: true })
+
+export const verifyWebAppForgotPasswordToken: Fetcher = ({ url, body }) => {
+ return post(url, { body }, { isPublicAPI: true }) as Promise
+}
+
+export const changeWebAppPasswordWithToken: Fetcher = ({ url, body }) =>
+ post(url, { body }, { isPublicAPI: true })
+
export const uploadRemoteFileInfo = (url: string, isPublic?: boolean) => {
return post<{ id: string; name: string; size: number; mime_type: string; url: string }>('/remote-files/upload', { body: { url } }, { isPublicAPI: isPublic })
}
@@ -340,6 +353,18 @@ export const sendResetPasswordCode = (email: string, language = 'en-US') =>
export const verifyResetPasswordCode = (body: { email: string; code: string; token: string }) =>
post('/forgot-password/validity', { body })
+export const sendWebAppEMailLoginCode = (email: string, language = 'en-US') =>
+ post('/email-code-login', { body: { email, language } }, { isPublicAPI: true })
+
+export const webAppEmailLoginWithCode = (data: { email: string; code: string; token: string }) =>
+ post('/email-code-login/validity', { body: data }, { isPublicAPI: true })
+
+export const sendWebAppResetPasswordCode = (email: string, language = 'en-US') =>
+ post('/forgot-password', { body: { email, language } }, { isPublicAPI: true })
+
+export const verifyWebAppResetPasswordCode = (body: { email: string; code: string; token: string }) =>
+ post('/forgot-password/validity', { body }, { isPublicAPI: true })
+
export const sendDeleteAccountCode = () =>
get('/account/delete/verify')
diff --git a/web/service/share.ts b/web/service/share.ts
index 7fb1562185..6a2a7e5b16 100644
--- a/web/service/share.ts
+++ b/web/service/share.ts
@@ -214,6 +214,34 @@ export const fetchWebOAuth2SSOUrl = async (appCode: string, redirectUrl: string)
}) as Promise<{ url: string }>
}
+export const fetchMembersSAMLSSOUrl = async (appCode: string, redirectUrl: string) => {
+ return (getAction('get', false))(getUrl('/enterprise/sso/members/saml/login', false, ''), {
+ params: {
+ app_code: appCode,
+ redirect_url: redirectUrl,
+ },
+ }) as Promise<{ url: string }>
+}
+
+export const fetchMembersOIDCSSOUrl = async (appCode: string, redirectUrl: string) => {
+ return (getAction('get', false))(getUrl('/enterprise/sso/members/oidc/login', false, ''), {
+ params: {
+ app_code: appCode,
+ redirect_url: redirectUrl,
+ },
+
+ }) as Promise<{ url: string }>
+}
+
+export const fetchMembersOAuth2SSOUrl = async (appCode: string, redirectUrl: string) => {
+ return (getAction('get', false))(getUrl('/enterprise/sso/members/oauth2/login', false, ''), {
+ params: {
+ app_code: appCode,
+ redirect_url: redirectUrl,
+ },
+ }) as Promise<{ url: string }>
+}
+
export const fetchAppMeta = async (isInstalledApp: boolean, installedAppId = '') => {
return (getAction('get', isInstalledApp))(getUrl('meta', isInstalledApp, installedAppId)) as Promise
}
@@ -258,10 +286,13 @@ export const textToAudioStream = (url: string, isPublicAPI: boolean, header: { c
return (getAction('post', !isPublicAPI))(url, { body, header }, { needAllResponseContent: true })
}
-export const fetchAccessToken = async (appCode: string, userId?: string) => {
+export const fetchAccessToken = async ({ appCode, userId, webAppAccessToken }: { appCode: string, userId?: string, webAppAccessToken?: string | null }) => {
const headers = new Headers()
headers.append('X-App-Code', appCode)
- const url = userId ? `/passport?user_id=${encodeURIComponent(userId)}` : '/passport'
+ const params = new URLSearchParams()
+ webAppAccessToken && params.append('web_app_access_token', webAppAccessToken)
+ userId && params.append('user_id', userId)
+ const url = `/passport?${params.toString()}`
return get(url, { headers }) as Promise<{ access_token: string }>
}
@@ -278,3 +309,7 @@ export const getUserCanAccess = (appId: string, isInstalledApp: boolean) => {
return get<{ result: boolean }>(`/webapp/permission?appId=${appId}`)
}
+
+export const getAppAccessModeByAppCode = (appCode: string) => {
+ return get<{ accessMode: AccessMode }>(`/webapp/access-mode?appCode=${appCode}`)
+}
diff --git a/web/service/use-share.ts b/web/service/use-share.ts
new file mode 100644
index 0000000000..b8f96f6cc5
--- /dev/null
+++ b/web/service/use-share.ts
@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query'
+import { getAppAccessModeByAppCode } from './share'
+
+const NAME_SPACE = 'webapp'
+
+export const useAppAccessModeByCode = (code: string | null) => {
+ return useQuery({
+ queryKey: [NAME_SPACE, 'appAccessMode', code],
+ queryFn: () => {
+ if (!code)
+ return null
+
+ return getAppAccessModeByAppCode(code)
+ },
+ enabled: !!code,
+ })
+}
From 1fbbbb735db1237aa36dcc0d976fda617ab5c71b Mon Sep 17 00:00:00 2001
From: XiaoBa <94062266+XiaoBa-Yu@users.noreply.github.com>
Date: Thu, 5 Jun 2025 11:07:54 +0800
Subject: [PATCH 20/56] fix: the locale format(#20662) (#20665)
Co-authored-by: Xiaoba Yu
---
web/app/components/header/maintenance-notice.tsx | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/web/app/components/header/maintenance-notice.tsx b/web/app/components/header/maintenance-notice.tsx
index 78715bb53e..f9c00dd01e 100644
--- a/web/app/components/header/maintenance-notice.tsx
+++ b/web/app/components/header/maintenance-notice.tsx
@@ -1,11 +1,10 @@
import { useState } from 'react'
-import { useContext } from 'use-context-selector'
-import I18n from '@/context/i18n'
import { X } from '@/app/components/base/icons/src/vender/line/general'
import { NOTICE_I18N } from '@/i18n/language'
+import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
const MaintenanceNotice = () => {
- const { locale } = useContext(I18n)
+ const locale = useLanguage()
const [showNotice, setShowNotice] = useState(localStorage.getItem('hide-maintenance-notice') !== '1')
const handleJumpNotice = () => {
From de9c7f2ea44b55191a01e1c4148e201f72392f4d Mon Sep 17 00:00:00 2001
From: geosmart
Date: Thu, 5 Jun 2025 12:11:11 +0800
Subject: [PATCH 21/56] Update template.zh.mdx-fix document update metadata
body param (#20659)
---
web/app/(commonLayout)/datasets/template/template.zh.mdx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/app/(commonLayout)/datasets/template/template.zh.mdx b/web/app/(commonLayout)/datasets/template/template.zh.mdx
index 08ef5d562a..d121a93df2 100644
--- a/web/app/(commonLayout)/datasets/template/template.zh.mdx
+++ b/web/app/(commonLayout)/datasets/template/template.zh.mdx
@@ -2223,7 +2223,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
- document_id (string) 文档 ID
- metadata_list (list) 元数据列表
- id (string) 元数据 ID
- - type (string) 元数据类型
+ - value (string) 元数据值
- name (string) 元数据名称
From d608be6e7f17c60109cf0d7f53ce1892ffac5aff Mon Sep 17 00:00:00 2001
From: GuanMu
Date: Thu, 5 Jun 2025 13:35:32 +0800
Subject: [PATCH 22/56] Add vscode debugger (#20668)
---
.gitignore | 7 ++--
.vscode/README.md | 14 ++++++++
.vscode/launch.json.template | 68 ++++++++++++++++++++++++++++++++++++
3 files changed, 87 insertions(+), 2 deletions(-)
create mode 100644 .vscode/README.md
create mode 100644 .vscode/launch.json.template
diff --git a/.gitignore b/.gitignore
index 8818ab6f65..74a9ef63ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -192,12 +192,12 @@ sdks/python-client/dist
sdks/python-client/dify_client.egg-info
.vscode/*
-!.vscode/launch.json
+!.vscode/launch.json.template
+!.vscode/README.md
pyrightconfig.json
api/.vscode
.idea/
-.vscode
# pnpm
/.pnpm-store
@@ -207,3 +207,6 @@ plugins.jsonl
# mise
mise.toml
+
+# Next.js build output
+.next/
diff --git a/.vscode/README.md b/.vscode/README.md
new file mode 100644
index 0000000000..26516f0540
--- /dev/null
+++ b/.vscode/README.md
@@ -0,0 +1,14 @@
+# Debugging with VS Code
+
+This `launch.json.template` file provides various debug configurations for the Dify project within VS Code / Cursor. To use these configurations, you should copy the contents of this file into a new file named `launch.json` in the same `.vscode` directory.
+
+## How to Use
+
+1. **Create `launch.json`**: If you don't have one, create a file named `launch.json` inside the `.vscode` directory.
+2. **Copy Content**: Copy the entire content from `launch.json.template` into your newly created `launch.json` file.
+3. **Select Debug Configuration**: Go to the Run and Debug view in VS Code / Cursor (Ctrl+Shift+D or Cmd+Shift+D).
+4. **Start Debugging**: Select the desired configuration from the dropdown menu and click the green play button.
+
+## Tips
+
+- If you need to debug with Edge browser instead of Chrome, modify the `serverReadyAction` configuration in the "Next.js: debug full stack" section, change `"debugWithChrome"` to `"debugWithEdge"` to use Microsoft Edge for debugging.
diff --git a/.vscode/launch.json.template b/.vscode/launch.json.template
new file mode 100644
index 0000000000..f5a7f0893b
--- /dev/null
+++ b/.vscode/launch.json.template
@@ -0,0 +1,68 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Python: Flask API",
+ "type": "debugpy",
+ "request": "launch",
+ "module": "flask",
+ "env": {
+ "FLASK_APP": "app.py",
+ "FLASK_ENV": "development",
+ "GEVENT_SUPPORT": "True"
+ },
+ "args": [
+ "run",
+ "--host=0.0.0.0",
+ "--port=5001",
+ "--no-debugger",
+ "--no-reload"
+ ],
+ "jinja": true,
+ "justMyCode": true,
+ "cwd": "${workspaceFolder}/api",
+ "python": "${workspaceFolder}/api/.venv/bin/python"
+ },
+ {
+ "name": "Python: Celery Worker (Solo)",
+ "type": "debugpy",
+ "request": "launch",
+ "module": "celery",
+ "env": {
+ "GEVENT_SUPPORT": "True"
+ },
+ "args": [
+ "-A",
+ "app.celery",
+ "worker",
+ "-P",
+ "solo",
+ "-c",
+ "1",
+ "-Q",
+ "dataset,generation,mail,ops_trace",
+ "--loglevel",
+ "INFO"
+ ],
+ "justMyCode": false,
+ "cwd": "${workspaceFolder}/api",
+ "python": "${workspaceFolder}/api/.venv/bin/python"
+ },
+ {
+ "name": "Next.js: debug full stack",
+ "type": "node",
+ "request": "launch",
+ "program": "${workspaceFolder}/web/node_modules/next/dist/bin/next",
+ "runtimeArgs": ["--inspect"],
+ "skipFiles": ["/**"],
+ "serverReadyAction": {
+ "action": "debugWithChrome",
+ "killOnServerStop": true,
+ "pattern": "- Local:.+(https?://.+)",
+ "uriFormat": "%s",
+ "webRoot": "${workspaceFolder}/web"
+ },
+ "cwd": "${workspaceFolder}/web"
+ }
+ ]
+}
From 3367d4258d75079afc3e505b909184945adfa535 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu, 5 Jun 2025 13:35:40 +0800
Subject: [PATCH 23/56] chore: translate i18n files (#20664)
Co-authored-by: douxc <7553076+douxc@users.noreply.github.com>
---
web/i18n/de-DE/app.ts | 2 ++
web/i18n/de-DE/share-app.ts | 3 +++
web/i18n/es-ES/app.ts | 2 ++
web/i18n/es-ES/common.ts | 1 +
web/i18n/es-ES/share-app.ts | 3 +++
web/i18n/fa-IR/app.ts | 2 ++
web/i18n/fa-IR/share-app.ts | 3 +++
web/i18n/fr-FR/app.ts | 3 +++
web/i18n/fr-FR/share-app.ts | 3 +++
web/i18n/hi-IN/app.ts | 2 ++
web/i18n/hi-IN/share-app.ts | 3 +++
web/i18n/it-IT/app.ts | 2 ++
web/i18n/it-IT/share-app.ts | 3 +++
web/i18n/ko-KR/app.ts | 2 ++
web/i18n/ko-KR/share-app.ts | 3 +++
web/i18n/pl-PL/app.ts | 2 ++
web/i18n/pl-PL/share-app.ts | 3 +++
web/i18n/pt-BR/app.ts | 2 ++
web/i18n/pt-BR/share-app.ts | 3 +++
web/i18n/ro-RO/app.ts | 2 ++
web/i18n/ro-RO/share-app.ts | 3 +++
web/i18n/ru-RU/app.ts | 2 ++
web/i18n/ru-RU/share-app.ts | 3 +++
web/i18n/sl-SI/app.ts | 2 ++
web/i18n/sl-SI/share-app.ts | 3 +++
web/i18n/th-TH/app.ts | 2 ++
web/i18n/th-TH/share-app.ts | 3 +++
web/i18n/tr-TR/app.ts | 2 ++
web/i18n/tr-TR/share-app.ts | 3 +++
web/i18n/uk-UA/app.ts | 2 ++
web/i18n/uk-UA/share-app.ts | 3 +++
web/i18n/vi-VN/app.ts | 2 ++
web/i18n/vi-VN/share-app.ts | 3 +++
web/i18n/zh-Hant/app.ts | 2 ++
web/i18n/zh-Hant/share-app.ts | 3 +++
35 files changed, 87 insertions(+)
diff --git a/web/i18n/de-DE/app.ts b/web/i18n/de-DE/app.ts
index b9fdde58ff..1373dd611b 100644
--- a/web/i18n/de-DE/app.ts
+++ b/web/i18n/de-DE/app.ts
@@ -220,12 +220,14 @@ const translation = {
anyone: 'Jeder kann auf die Webanwendung zugreifen.',
specific: 'Nur bestimmte Gruppen oder Mitglieder können auf die Webanwendung zugreifen.',
organization: 'Jeder in der Organisation kann auf die Webanwendung zugreifen.',
+ external: 'Nur authentifizierte externe Benutzer können auf die Webanwendung zugreifen.',
},
accessControlDialog: {
accessItems: {
anyone: 'Jeder mit dem Link',
specific: 'Spezifische Gruppen oder Mitglieder',
organization: 'Nur Mitglieder innerhalb des Unternehmens',
+ external: 'Authentifizierte externe Benutzer',
},
operateGroupAndMember: {
searchPlaceholder: 'Gruppen und Mitglieder suchen',
diff --git a/web/i18n/de-DE/share-app.ts b/web/i18n/de-DE/share-app.ts
index 462286fa23..33c40917dd 100644
--- a/web/i18n/de-DE/share-app.ts
+++ b/web/i18n/de-DE/share-app.ts
@@ -77,6 +77,9 @@ const translation = {
executions: '{{num}} HINRICHTUNGEN',
execution: 'AUSFÜHRUNG',
},
+ login: {
+ backToHome: 'Zurück zur Startseite',
+ },
}
export default translation
diff --git a/web/i18n/es-ES/app.ts b/web/i18n/es-ES/app.ts
index c183485294..c1147720e7 100644
--- a/web/i18n/es-ES/app.ts
+++ b/web/i18n/es-ES/app.ts
@@ -212,12 +212,14 @@ const translation = {
anyone: 'Cualquiera puede acceder a la aplicación web.',
specific: 'Solo grupos o miembros específicos pueden acceder a la aplicación web',
organization: 'Cualquiera en la organización puede acceder a la aplicación web',
+ external: 'Solo los usuarios externos autenticados pueden acceder a la aplicación web.',
},
accessControlDialog: {
accessItems: {
anyone: 'Cualquiera con el enlace',
specific: 'Grupos o miembros específicos',
organization: 'Solo miembros dentro de la empresa',
+ external: 'Usuarios externos autenticados',
},
operateGroupAndMember: {
searchPlaceholder: 'Buscar grupos y miembros',
diff --git a/web/i18n/es-ES/common.ts b/web/i18n/es-ES/common.ts
index 22c70f6bff..82ed315f1c 100644
--- a/web/i18n/es-ES/common.ts
+++ b/web/i18n/es-ES/common.ts
@@ -654,6 +654,7 @@ const translation = {
auto: 'sistema',
light: 'luz',
theme: 'Tema',
+ dark: 'noche',
},
compliance: {
iso27001: 'Certificación ISO 27001:2022',
diff --git a/web/i18n/es-ES/share-app.ts b/web/i18n/es-ES/share-app.ts
index 41aa35c43e..caeb056d89 100644
--- a/web/i18n/es-ES/share-app.ts
+++ b/web/i18n/es-ES/share-app.ts
@@ -77,6 +77,9 @@ const translation = {
execution: 'EJECUCIÓN',
executions: '{{num}} EJECUCIONES',
},
+ login: {
+ backToHome: 'Volver a Inicio',
+ },
}
export default translation
diff --git a/web/i18n/fa-IR/app.ts b/web/i18n/fa-IR/app.ts
index d37f4e8f90..13473d21f5 100644
--- a/web/i18n/fa-IR/app.ts
+++ b/web/i18n/fa-IR/app.ts
@@ -213,12 +213,14 @@ const translation = {
specific: 'فقط گروهها یا اعضای خاصی میتوانند به اپلیکیشن وب دسترسی پیدا کنند.',
anyone: 'هر کسی میتواند به وباپلیکیشن دسترسی پیدا کند',
organization: 'هر کسی در سازمان میتواند به اپلیکیشن وب دسترسی پیدا کند.',
+ external: 'تنها کاربران خارجی تأیید شده میتوانند به برنامه وب دسترسی پیدا کنند.',
},
accessControlDialog: {
accessItems: {
specific: 'گروهها یا اعضای خاص',
organization: 'فقط اعضای داخل سازمان',
anyone: 'هر کسی که لینک را داشته باشد',
+ external: 'کاربران خارجی تأیید شده',
},
operateGroupAndMember: {
searchPlaceholder: 'گروهها و اعضا را جستجو کنید',
diff --git a/web/i18n/fa-IR/share-app.ts b/web/i18n/fa-IR/share-app.ts
index bf1c0dec50..03ed4e8ea9 100644
--- a/web/i18n/fa-IR/share-app.ts
+++ b/web/i18n/fa-IR/share-app.ts
@@ -73,6 +73,9 @@ const translation = {
executions: '{{num}} اعدام',
execution: 'اجرا',
},
+ login: {
+ backToHome: 'بازگشت به خانه',
+ },
}
export default translation
diff --git a/web/i18n/fr-FR/app.ts b/web/i18n/fr-FR/app.ts
index ffa00c758a..5c0965815e 100644
--- a/web/i18n/fr-FR/app.ts
+++ b/web/i18n/fr-FR/app.ts
@@ -207,17 +207,20 @@ const translation = {
modelNotSupported: 'Modèle non pris en charge',
moreFillTip: 'Affichage d\'un maximum de 10 niveaux d\'imbrication',
configure: 'Configurer',
+ structured: 'systématique',
},
accessItemsDescription: {
anyone: 'Tout le monde peut accéder à l\'application web.',
specific: 'Seules des groupes ou membres spécifiques peuvent accéder à l\'application web.',
organization: 'Toute personne dans l\'organisation peut accéder à l\'application web.',
+ external: 'Seuls les utilisateurs externes authentifiés peuvent accéder à l\'application Web.',
},
accessControlDialog: {
accessItems: {
anyone: 'Quiconque avec le lien',
specific: 'Groupes ou membres spécifiques',
organization: 'Seuls les membres au sein de l\'entreprise',
+ external: 'Utilisateurs externes authentifiés',
},
operateGroupAndMember: {
searchPlaceholder: 'Rechercher des groupes et des membres',
diff --git a/web/i18n/fr-FR/share-app.ts b/web/i18n/fr-FR/share-app.ts
index d0b3a5047e..2374da70e6 100644
--- a/web/i18n/fr-FR/share-app.ts
+++ b/web/i18n/fr-FR/share-app.ts
@@ -77,6 +77,9 @@ const translation = {
executions: '{{num}} EXÉCUTIONS',
execution: 'EXÉCUTION',
},
+ login: {
+ backToHome: 'Retour à l\'accueil',
+ },
}
export default translation
diff --git a/web/i18n/hi-IN/app.ts b/web/i18n/hi-IN/app.ts
index f9486b93ec..3929dfeb6a 100644
--- a/web/i18n/hi-IN/app.ts
+++ b/web/i18n/hi-IN/app.ts
@@ -213,12 +213,14 @@ const translation = {
anyone: 'कोई भी वेब ऐप तक पहुँच सकता है',
organization: 'संस्थान के किसी भी व्यक्ति को वेब ऐप तक पहुंच प्राप्त है',
specific: 'केवल विशेष समूह या सदस्य ही वेब ऐप तक पहुंच सकते हैं',
+ external: 'केवल प्रमाणित बाहरी उपयोगकर्ता वेब अनुप्रयोग तक पहुँच सकते हैं',
},
accessControlDialog: {
accessItems: {
anyone: 'लिंक के साथ कोई भी',
specific: 'विशिष्ट समूह या सदस्य',
organization: 'केवल उद्यम के भीतर के सदस्य',
+ external: 'प्रमाणित बाहरी उपयोगकर्ता',
},
operateGroupAndMember: {
searchPlaceholder: 'समूहों और सदस्यों की खोज करें',
diff --git a/web/i18n/hi-IN/share-app.ts b/web/i18n/hi-IN/share-app.ts
index e0296fda83..a1e716b5bc 100644
--- a/web/i18n/hi-IN/share-app.ts
+++ b/web/i18n/hi-IN/share-app.ts
@@ -80,6 +80,9 @@ const translation = {
execution: 'अनु执行',
executions: '{{num}} फाँसी',
},
+ login: {
+ backToHome: 'होम पर वापस',
+ },
}
export default translation
diff --git a/web/i18n/it-IT/app.ts b/web/i18n/it-IT/app.ts
index f6855873db..2bd5069b6c 100644
--- a/web/i18n/it-IT/app.ts
+++ b/web/i18n/it-IT/app.ts
@@ -224,12 +224,14 @@ const translation = {
anyone: 'Chiunque può accedere all\'app web',
specific: 'Solo gruppi o membri specifici possono accedere all\'app web.',
organization: 'Qualsiasi persona nell\'organizzazione può accedere all\'app web',
+ external: 'Solo gli utenti esterni autenticati possono accedere all\'applicazione Web',
},
accessControlDialog: {
accessItems: {
anyone: 'Chiunque con il link',
specific: 'Gruppi o membri specifici',
organization: 'Solo i membri all\'interno dell\'impresa',
+ external: 'Utenti esterni autenticati',
},
operateGroupAndMember: {
searchPlaceholder: 'Cerca gruppi e membri',
diff --git a/web/i18n/it-IT/share-app.ts b/web/i18n/it-IT/share-app.ts
index 2e1c96a396..4c6c18ff33 100644
--- a/web/i18n/it-IT/share-app.ts
+++ b/web/i18n/it-IT/share-app.ts
@@ -79,6 +79,9 @@ const translation = {
execution: 'ESECUZIONE',
executions: '{{num}} ESECUZIONI',
},
+ login: {
+ backToHome: 'Torna alla home',
+ },
}
export default translation
diff --git a/web/i18n/ko-KR/app.ts b/web/i18n/ko-KR/app.ts
index 0ab08251fb..7227fd3171 100644
--- a/web/i18n/ko-KR/app.ts
+++ b/web/i18n/ko-KR/app.ts
@@ -209,12 +209,14 @@ const translation = {
anyone: '누구나 웹 앱에 접근할 수 있습니다.',
specific: '특정 그룹이나 회원만 웹 앱에 접근할 수 있습니다.',
organization: '조직 내 모든 사람이 웹 애플리케이션에 접근할 수 있습니다.',
+ external: '인증된 외부 사용자만 웹 애플리케이션에 접근할 수 있습니다.',
},
accessControlDialog: {
accessItems: {
anyone: '링크가 있는 누구나',
specific: '특정 그룹 또는 구성원',
organization: '기업 내의 회원만',
+ external: '인증된 외부 사용자',
},
operateGroupAndMember: {
searchPlaceholder: '그룹 및 구성원 검색',
diff --git a/web/i18n/ko-KR/share-app.ts b/web/i18n/ko-KR/share-app.ts
index 1ee44f2816..3958b4f93e 100644
--- a/web/i18n/ko-KR/share-app.ts
+++ b/web/i18n/ko-KR/share-app.ts
@@ -73,6 +73,9 @@ const translation = {
execution: '실행',
executions: '{{num}} 처형',
},
+ login: {
+ backToHome: '홈으로 돌아가기',
+ },
}
export default translation
diff --git a/web/i18n/pl-PL/app.ts b/web/i18n/pl-PL/app.ts
index 54759154ca..856a64c868 100644
--- a/web/i18n/pl-PL/app.ts
+++ b/web/i18n/pl-PL/app.ts
@@ -220,12 +220,14 @@ const translation = {
anyone: 'Każdy może uzyskać dostęp do aplikacji webowej',
specific: 'Tylko określone grupy lub członkowie mogą uzyskać dostęp do aplikacji internetowej',
organization: 'Każdy w organizacji ma dostęp do aplikacji internetowej.',
+ external: 'Tylko uwierzytelnieni zewnętrzni użytkownicy mogą uzyskać dostęp do aplikacji internetowej.',
},
accessControlDialog: {
accessItems: {
anyone: 'Każdy z linkiem',
specific: 'Specyficzne grupy lub członkowie',
organization: 'Tylko członkowie w obrębie przedsiębiorstwa',
+ external: 'Uwierzytelnieni użytkownicy zewnętrzni',
},
operateGroupAndMember: {
searchPlaceholder: 'Szukaj grup i członków',
diff --git a/web/i18n/pl-PL/share-app.ts b/web/i18n/pl-PL/share-app.ts
index 80619cf4fc..617f66d994 100644
--- a/web/i18n/pl-PL/share-app.ts
+++ b/web/i18n/pl-PL/share-app.ts
@@ -78,6 +78,9 @@ const translation = {
executions: '{{num}} EGZEKUCJI',
execution: 'WYKONANIE',
},
+ login: {
+ backToHome: 'Powrót do strony głównej',
+ },
}
export default translation
diff --git a/web/i18n/pt-BR/app.ts b/web/i18n/pt-BR/app.ts
index 5dd1753cac..766053456a 100644
--- a/web/i18n/pt-BR/app.ts
+++ b/web/i18n/pt-BR/app.ts
@@ -213,12 +213,14 @@ const translation = {
anyone: 'Qualquer pessoa pode acessar o aplicativo web',
specific: 'Apenas grupos ou membros específicos podem acessar o aplicativo web',
organization: 'Qualquer pessoa na organização pode acessar o aplicativo web',
+ external: 'Apenas usuários externos autenticados podem acessar o aplicativo Web.',
},
accessControlDialog: {
accessItems: {
anyone: 'Qualquer pessoa com o link',
specific: 'Grupos específicos ou membros',
organization: 'Apenas membros dentro da empresa',
+ external: 'Usuários externos autenticados',
},
operateGroupAndMember: {
searchPlaceholder: 'Pesquisar grupos e membros',
diff --git a/web/i18n/pt-BR/share-app.ts b/web/i18n/pt-BR/share-app.ts
index d8bca03089..9a9d7db632 100644
--- a/web/i18n/pt-BR/share-app.ts
+++ b/web/i18n/pt-BR/share-app.ts
@@ -77,6 +77,9 @@ const translation = {
executions: '{{num}} EXECUÇÕES',
execution: 'EXECUÇÃO',
},
+ login: {
+ backToHome: 'Voltar para a página inicial',
+ },
}
export default translation
diff --git a/web/i18n/ro-RO/app.ts b/web/i18n/ro-RO/app.ts
index adf82aa38e..cd267e7b66 100644
--- a/web/i18n/ro-RO/app.ts
+++ b/web/i18n/ro-RO/app.ts
@@ -213,12 +213,14 @@ const translation = {
specific: 'Numai grupuri sau membri specifici pot accesa aplicația web.',
organization: 'Oricine din organizație poate accesa aplicația web',
anyone: 'Oricine poate accesa aplicația web',
+ external: 'Numai utilizatorii externi autentificați pot accesa aplicația web',
},
accessControlDialog: {
accessItems: {
anyone: 'Oricine are linkul',
specific: 'Grupuri sau membri specifici',
organization: 'Numai membrii din cadrul întreprinderii',
+ external: 'Utilizatori extern autentificați',
},
operateGroupAndMember: {
searchPlaceholder: 'Caută grupuri și membri',
diff --git a/web/i18n/ro-RO/share-app.ts b/web/i18n/ro-RO/share-app.ts
index 2cb39a0485..41e38812c5 100644
--- a/web/i18n/ro-RO/share-app.ts
+++ b/web/i18n/ro-RO/share-app.ts
@@ -77,6 +77,9 @@ const translation = {
execution: 'EXECUȚIE',
executions: '{{num}} EXECUȚII',
},
+ login: {
+ backToHome: 'Înapoi la Acasă',
+ },
}
export default translation
diff --git a/web/i18n/ru-RU/app.ts b/web/i18n/ru-RU/app.ts
index fa73e33197..428d4c4e57 100644
--- a/web/i18n/ru-RU/app.ts
+++ b/web/i18n/ru-RU/app.ts
@@ -213,12 +213,14 @@ const translation = {
anyone: 'Любой может получить доступ к веб-приложению',
specific: 'Только определенные группы или участники могут получить доступ к веб-приложению.',
organization: 'Любой в организации может получить доступ к веб-приложению',
+ external: 'Только аутентифицированные внешние пользователи могут получить доступ к веб-приложению.',
},
accessControlDialog: {
accessItems: {
anyone: 'Кто угодно с ссылкой',
specific: 'Конкретные группы или члены',
organization: 'Только члены внутри предприятия',
+ external: 'Аутентифицированные внешние пользователи',
},
operateGroupAndMember: {
searchPlaceholder: 'Искать группы и участников',
diff --git a/web/i18n/ru-RU/share-app.ts b/web/i18n/ru-RU/share-app.ts
index b2850fa276..dafbe9d6b1 100644
--- a/web/i18n/ru-RU/share-app.ts
+++ b/web/i18n/ru-RU/share-app.ts
@@ -77,6 +77,9 @@ const translation = {
execution: 'ИСПОЛНЕНИЕ',
executions: '{{num}} ВЫПОЛНЕНИЯ',
},
+ login: {
+ backToHome: 'Назад на главную',
+ },
}
export default translation
diff --git a/web/i18n/sl-SI/app.ts b/web/i18n/sl-SI/app.ts
index 6241d40f30..4ac445872d 100644
--- a/web/i18n/sl-SI/app.ts
+++ b/web/i18n/sl-SI/app.ts
@@ -213,12 +213,14 @@ const translation = {
anyone: 'Vsakdo lahko dostopa do spletne aplikacije',
specific: 'Samo določenim skupinam ali članom je omogočen dostop do spletne aplikacije',
organization: 'Vsakdo v organizaciji lahko dostopa do spletne aplikacije',
+ external: 'Samo avtentificirani zunanji uporabniki lahko dostopajo do spletne aplikacije.',
},
accessControlDialog: {
accessItems: {
anyone: 'Kdorkoli s povezavo',
specific: 'Specifične skupine ali člani',
organization: 'Samo člani znotraj podjetja',
+ external: 'Avtorizirani zunanji uporabniki',
},
operateGroupAndMember: {
searchPlaceholder: 'Išči skupine in člane',
diff --git a/web/i18n/sl-SI/share-app.ts b/web/i18n/sl-SI/share-app.ts
index 28d62b2336..8b7fe87cbd 100644
--- a/web/i18n/sl-SI/share-app.ts
+++ b/web/i18n/sl-SI/share-app.ts
@@ -74,6 +74,9 @@ const translation = {
execution: 'IZVEDBA',
executions: '{{num}} IZVRŠITEV',
},
+ login: {
+ backToHome: 'Nazaj na začetno stran',
+ },
}
export default translation
diff --git a/web/i18n/th-TH/app.ts b/web/i18n/th-TH/app.ts
index 9204c71d32..0979d07f51 100644
--- a/web/i18n/th-TH/app.ts
+++ b/web/i18n/th-TH/app.ts
@@ -209,12 +209,14 @@ const translation = {
anyone: 'ใครก็สามารถเข้าถึงเว็บแอปได้',
specific: 'สมาชิกหรือกลุ่มเฉพาะเท่านั้นที่สามารถเข้าถึงแอปเว็บได้',
organization: 'ใครก็ได้ในองค์กรสามารถเข้าถึงแอปเว็บได้',
+ external: 'ผู้ใช้งานภายนอกที่ได้รับการยืนยันตัวตนเท่านั้นที่สามารถเข้าถึงแอปพลิเคชันเว็บได้',
},
accessControlDialog: {
accessItems: {
specific: 'กลุ่มหรือสมาชิกเฉพาะ',
organization: 'เฉพาะสมาชิกภายในองค์กร',
anyone: 'ใครก็ตามที่มีลิงก์',
+ external: 'ผู้ใช้ภายนอกที่ได้รับการตรวจสอบแล้ว',
},
operateGroupAndMember: {
searchPlaceholder: 'ค้นหากลุ่มและสมาชิก',
diff --git a/web/i18n/th-TH/share-app.ts b/web/i18n/th-TH/share-app.ts
index fd4a8f386c..eca049b9a2 100644
--- a/web/i18n/th-TH/share-app.ts
+++ b/web/i18n/th-TH/share-app.ts
@@ -73,6 +73,9 @@ const translation = {
execution: 'การดำเนินการ',
executions: '{{num}} การประหารชีวิต',
},
+ login: {
+ backToHome: 'กลับไปที่หน้าแรก',
+ },
}
export default translation
diff --git a/web/i18n/tr-TR/app.ts b/web/i18n/tr-TR/app.ts
index 995cc9c795..5e55ffa349 100644
--- a/web/i18n/tr-TR/app.ts
+++ b/web/i18n/tr-TR/app.ts
@@ -209,12 +209,14 @@ const translation = {
anyone: 'Herkes web uygulamasına erişebilir',
organization: 'Kuruluşta herkes web uygulamasına erişebilir.',
specific: 'Sadece belirli gruplar veya üyeler web uygulamasına erişebilir.',
+ external: 'Sadece kimliği doğrulanmış dış kullanıcılar Web uygulamasına erişebilir',
},
accessControlDialog: {
accessItems: {
anyone: 'Bağlantıya sahip olan herkes',
organization: 'Sadece işletme içindeki üyeler',
specific: 'Belirli gruplar veya üyeler',
+ external: 'Kimliği onaylanmış harici kullanıcılar',
},
operateGroupAndMember: {
searchPlaceholder: 'Grupları ve üyeleri ara',
diff --git a/web/i18n/tr-TR/share-app.ts b/web/i18n/tr-TR/share-app.ts
index 184f44e147..e7ad4fcd68 100644
--- a/web/i18n/tr-TR/share-app.ts
+++ b/web/i18n/tr-TR/share-app.ts
@@ -73,6 +73,9 @@ const translation = {
execution: 'İFRAZAT',
executions: '{{num}} İDAM',
},
+ login: {
+ backToHome: 'Ana Sayfaya Dön',
+ },
}
export default translation
diff --git a/web/i18n/uk-UA/app.ts b/web/i18n/uk-UA/app.ts
index 4bbb0dcbf1..6e5ff5dc74 100644
--- a/web/i18n/uk-UA/app.ts
+++ b/web/i18n/uk-UA/app.ts
@@ -213,12 +213,14 @@ const translation = {
anyone: 'Будь-хто може отримати доступ до веб-додатку',
specific: 'Тільки окремі групи або члени можуть отримати доступ до веб-додатку.',
organization: 'Будь-хто в організації може отримати доступ до веб-додатку.',
+ external: 'Тільки перевірені зовнішні користувачі можуть отримати доступ до веб-застосунку.',
},
accessControlDialog: {
accessItems: {
anyone: 'Кожен, у кого є посилання',
specific: 'Конкретні групи або члени',
organization: 'Тільки члени підприємства',
+ external: 'Аутентифіковані зовнішні користувачі',
},
operateGroupAndMember: {
searchPlaceholder: 'Шукати групи та учасників',
diff --git a/web/i18n/uk-UA/share-app.ts b/web/i18n/uk-UA/share-app.ts
index 058925ff15..92f25545d9 100644
--- a/web/i18n/uk-UA/share-app.ts
+++ b/web/i18n/uk-UA/share-app.ts
@@ -73,6 +73,9 @@ const translation = {
execution: 'ВИКОНАННЯ',
executions: '{{num}} ВИКОНАНЬ',
},
+ login: {
+ backToHome: 'Повернутися на головну',
+ },
}
export default translation
diff --git a/web/i18n/vi-VN/app.ts b/web/i18n/vi-VN/app.ts
index 243454d011..c5f1a7496d 100644
--- a/web/i18n/vi-VN/app.ts
+++ b/web/i18n/vi-VN/app.ts
@@ -213,12 +213,14 @@ const translation = {
anyone: 'Mọi người đều có thể truy cập ứng dụng web.',
specific: 'Chỉ những nhóm hoặc thành viên cụ thể mới có thể truy cập ứng dụng web.',
organization: 'Bất kỳ ai trong tổ chức đều có thể truy cập ứng dụng web.',
+ external: 'Chỉ những người dùng bên ngoài đã xác thực mới có thể truy cập vào ứng dụng Web.',
},
accessControlDialog: {
accessItems: {
anyone: 'Ai có liên kết',
specific: 'Các nhóm hoặc thành viên cụ thể',
organization: 'Chỉ các thành viên trong doanh nghiệp',
+ external: 'Người dùng bên ngoài được xác thực',
},
operateGroupAndMember: {
searchPlaceholder: 'Tìm kiếm nhóm và thành viên',
diff --git a/web/i18n/vi-VN/share-app.ts b/web/i18n/vi-VN/share-app.ts
index a55f9b8476..12a31bd40b 100644
--- a/web/i18n/vi-VN/share-app.ts
+++ b/web/i18n/vi-VN/share-app.ts
@@ -73,6 +73,9 @@ const translation = {
executions: '{{num}} ÁN TỬ HÌNH',
execution: 'THI HÀNH',
},
+ login: {
+ backToHome: 'Trở về Trang Chủ',
+ },
}
export default translation
diff --git a/web/i18n/zh-Hant/app.ts b/web/i18n/zh-Hant/app.ts
index c393fc949e..c43b2ee308 100644
--- a/web/i18n/zh-Hant/app.ts
+++ b/web/i18n/zh-Hant/app.ts
@@ -212,12 +212,14 @@ const translation = {
anyone: '任何人都可以訪問這個網絡應用程式',
specific: '只有特定的群體或成員可以訪問這個網絡應用程序',
organization: '組織中的任何人都可以訪問該網絡應用程序',
+ external: '只有經過身份驗證的外部用戶才能訪問該網絡應用程序',
},
accessControlDialog: {
accessItems: {
anyone: '擁有鏈接的人',
specific: '特定群體或成員',
organization: '只有企業內部成員',
+ external: '經過驗證的外部用戶',
},
operateGroupAndMember: {
searchPlaceholder: '搜尋群組和成員',
diff --git a/web/i18n/zh-Hant/share-app.ts b/web/i18n/zh-Hant/share-app.ts
index 54d2ff98b6..e25aa0c0de 100644
--- a/web/i18n/zh-Hant/share-app.ts
+++ b/web/i18n/zh-Hant/share-app.ts
@@ -73,6 +73,9 @@ const translation = {
execution: '執行',
executions: '{{num}} 執行',
},
+ login: {
+ backToHome: '返回首頁',
+ },
}
export default translation
From 837f769960932b67bf1157bdbe1e0c682ea5e177 Mon Sep 17 00:00:00 2001
From: minglu7 <1347866672@qq.com>
Date: Thu, 5 Jun 2025 14:33:24 +0800
Subject: [PATCH 24/56] fix: update text_to_audio method to send data as JSON
(#20663)
---
sdks/python-client/dify_client/client.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/sdks/python-client/dify_client/client.py b/sdks/python-client/dify_client/client.py
index ee1b5c57e1..d885dc6fb7 100644
--- a/sdks/python-client/dify_client/client.py
+++ b/sdks/python-client/dify_client/client.py
@@ -47,7 +47,7 @@ class DifyClient:
def text_to_audio(self, text: str, user: str, streaming: bool = False):
data = {"text": text, "user": user, "streaming": streaming}
- return self._send_request("POST", "/text-to-audio", data=data)
+ return self._send_request("POST", "/text-to-audio", json=data)
def get_meta(self, user):
params = {"user": user}
From 0ccf8cb23ebcffd097b6fbee86200239f771473e Mon Sep 17 00:00:00 2001
From: Novice <857526207@qq.com>
Date: Thu, 5 Jun 2025 14:56:41 +0800
Subject: [PATCH 25/56] fix: agent moderation not working (#20673)
---
web/app/components/base/chat/chat/hooks.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts
index 28f297b90e..10fb455d33 100644
--- a/web/app/components/base/chat/chat/hooks.ts
+++ b/web/app/components/base/chat/chat/hooks.ts
@@ -366,7 +366,7 @@ export const useChat = (
if (!newResponseItem)
return
- const isUseAgentThought = newResponseItem.agent_thoughts?.length > 0
+ const isUseAgentThought = newResponseItem.agent_thoughts?.length > 0 && newResponseItem.agent_thoughts[newResponseItem.agent_thoughts?.length - 1].thought === newResponseItem.answer
updateChatTreeNode(responseItem.id, {
content: isUseAgentThought ? '' : newResponseItem.answer,
log: [
From 3fb9b41fe52472812cb64fd04b6c85b02b8efb90 Mon Sep 17 00:00:00 2001
From: HaiyangP <46739135+HaiyangPeng@users.noreply.github.com>
Date: Thu, 5 Jun 2025 14:59:55 +0800
Subject: [PATCH 26/56] A more concise and effective extractor for excel and
csv files (#20625)
Co-authored-by: haiyangpengai
---
.../workflow/nodes/document_extractor/node.py | 48 +++++-
.../nodes/test_document_extractor_node.py | 140 +++++++-----------
2 files changed, 94 insertions(+), 94 deletions(-)
diff --git a/api/core/workflow/nodes/document_extractor/node.py b/api/core/workflow/nodes/document_extractor/node.py
index d39eb9c932..429fed2d04 100644
--- a/api/core/workflow/nodes/document_extractor/node.py
+++ b/api/core/workflow/nodes/document_extractor/node.py
@@ -397,19 +397,44 @@ def _extract_text_from_csv(file_content: bytes) -> str:
if not rows:
return ""
- # Create Markdown table
- markdown_table = "| " + " | ".join(rows[0]) + " |\n"
- markdown_table += "| " + " | ".join(["---"] * len(rows[0])) + " |\n"
- for row in rows[1:]:
- markdown_table += "| " + " | ".join(row) + " |\n"
+ # Combine multi-line text in the header row
+ header_row = [cell.replace("\n", " ").replace("\r", "") for cell in rows[0]]
- return markdown_table.strip()
+ # Create Markdown table
+ markdown_table = "| " + " | ".join(header_row) + " |\n"
+ markdown_table += "| " + " | ".join(["-" * len(col) for col in rows[0]]) + " |\n"
+
+ # Process each data row and combine multi-line text in each cell
+ for row in rows[1:]:
+ processed_row = [cell.replace("\n", " ").replace("\r", "") for cell in row]
+ markdown_table += "| " + " | ".join(processed_row) + " |\n"
+
+ return markdown_table
except Exception as e:
raise TextExtractionError(f"Failed to extract text from CSV: {str(e)}") from e
def _extract_text_from_excel(file_content: bytes) -> str:
"""Extract text from an Excel file using pandas."""
+
+ def _construct_markdown_table(df: pd.DataFrame) -> str:
+ """Manually construct a Markdown table from a DataFrame."""
+ # Construct the header row
+ header_row = "| " + " | ".join(df.columns) + " |"
+
+ # Construct the separator row
+ separator_row = "| " + " | ".join(["-" * len(col) for col in df.columns]) + " |"
+
+ # Construct the data rows
+ data_rows = []
+ for _, row in df.iterrows():
+ data_row = "| " + " | ".join(map(str, row)) + " |"
+ data_rows.append(data_row)
+
+ # Combine all rows into a single string
+ markdown_table = "\n".join([header_row, separator_row] + data_rows)
+ return markdown_table
+
try:
excel_file = pd.ExcelFile(io.BytesIO(file_content))
markdown_table = ""
@@ -417,8 +442,15 @@ def _extract_text_from_excel(file_content: bytes) -> str:
try:
df = excel_file.parse(sheet_name=sheet_name)
df.dropna(how="all", inplace=True)
- # Create Markdown table two times to separate tables with a newline
- markdown_table += df.to_markdown(index=False, floatfmt="") + "\n\n"
+
+ # Combine multi-line text in each cell into a single line
+ df = df.applymap(lambda x: " ".join(str(x).splitlines()) if isinstance(x, str) else x) # type: ignore
+
+ # Combine multi-line text in column names into a single line
+ df.columns = pd.Index([" ".join(col.splitlines()) for col in df.columns])
+
+ # Manually construct the Markdown table
+ markdown_table += _construct_markdown_table(df) + "\n\n"
except Exception as e:
continue
return markdown_table
diff --git a/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py b/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py
index 35d83449c3..4cb1aa93f9 100644
--- a/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py
+++ b/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py
@@ -1,5 +1,7 @@
+import io
from unittest.mock import Mock, patch
+import pandas as pd
import pytest
from docx.oxml.text.paragraph import CT_P
@@ -187,145 +189,134 @@ def test_node_type(document_extractor_node):
@patch("pandas.ExcelFile")
def test_extract_text_from_excel_single_sheet(mock_excel_file):
- """Test extracting text from Excel file with single sheet."""
- # Mock DataFrame
- mock_df = Mock()
- mock_df.dropna = Mock()
- mock_df.to_markdown.return_value = "| Name | Age |\n|------|-----|\n| John | 25 |"
+ """Test extracting text from Excel file with single sheet and multiline content."""
+
+ # Test multi-line cell
+ data = {"Name\nwith\nnewline": ["John\nDoe", "Jane\nSmith"], "Age": [25, 30]}
+
+ df = pd.DataFrame(data)
# Mock ExcelFile
mock_excel_instance = Mock()
mock_excel_instance.sheet_names = ["Sheet1"]
- mock_excel_instance.parse.return_value = mock_df
+ mock_excel_instance.parse.return_value = df
mock_excel_file.return_value = mock_excel_instance
file_content = b"fake_excel_content"
result = _extract_text_from_excel(file_content)
+ expected_manual = "| Name with newline | Age |\n| ----------------- | --- |\n\
+| John Doe | 25 |\n| Jane Smith | 30 |\n\n"
- expected = "| Name | Age |\n|------|-----|\n| John | 25 |\n\n"
- assert result == expected
- mock_excel_file.assert_called_once()
- mock_df.dropna.assert_called_once_with(how="all", inplace=True)
- mock_df.to_markdown.assert_called_once_with(index=False, floatfmt="")
+ assert expected_manual == result
+ mock_excel_instance.parse.assert_called_once_with(sheet_name="Sheet1")
@patch("pandas.ExcelFile")
def test_extract_text_from_excel_multiple_sheets(mock_excel_file):
- """Test extracting text from Excel file with multiple sheets."""
- # Mock DataFrames for different sheets
- mock_df1 = Mock()
- mock_df1.dropna = Mock()
- mock_df1.to_markdown.return_value = "| Product | Price |\n|---------|-------|\n| Apple | 1.50 |"
+ """Test extracting text from Excel file with multiple sheets and multiline content."""
- mock_df2 = Mock()
- mock_df2.dropna = Mock()
- mock_df2.to_markdown.return_value = "| City | Population |\n|------|------------|\n| NYC | 8000000 |"
+ # Test multi-line cell
+ data1 = {"Product\nName": ["Apple\nRed", "Banana\nYellow"], "Price": [1.50, 0.99]}
+ df1 = pd.DataFrame(data1)
+
+ data2 = {"City\nName": ["New\nYork", "Los\nAngeles"], "Population": [8000000, 3900000]}
+ df2 = pd.DataFrame(data2)
# Mock ExcelFile
mock_excel_instance = Mock()
mock_excel_instance.sheet_names = ["Products", "Cities"]
- mock_excel_instance.parse.side_effect = [mock_df1, mock_df2]
+ mock_excel_instance.parse.side_effect = [df1, df2]
mock_excel_file.return_value = mock_excel_instance
file_content = b"fake_excel_content_multiple_sheets"
result = _extract_text_from_excel(file_content)
- expected = (
- "| Product | Price |\n|---------|-------|\n| Apple | 1.50 |\n\n"
- "| City | Population |\n|------|------------|\n| NYC | 8000000 |\n\n"
- )
- assert result == expected
+ expected_manual1 = "| Product Name | Price |\n| ------------ | ----- |\n\
+| Apple Red | 1.5 |\n| Banana Yellow | 0.99 |\n\n"
+ expected_manual2 = "| City Name | Population |\n| --------- | ---------- |\n\
+| New York | 8000000 |\n| Los Angeles | 3900000 |\n\n"
+
+ assert expected_manual1 in result
+ assert expected_manual2 in result
+
assert mock_excel_instance.parse.call_count == 2
@patch("pandas.ExcelFile")
def test_extract_text_from_excel_empty_sheets(mock_excel_file):
"""Test extracting text from Excel file with empty sheets."""
- # Mock empty DataFrame
- mock_df = Mock()
- mock_df.dropna = Mock()
- mock_df.to_markdown.return_value = ""
+
+ # Empty excel
+ df = pd.DataFrame()
# Mock ExcelFile
mock_excel_instance = Mock()
mock_excel_instance.sheet_names = ["EmptySheet"]
- mock_excel_instance.parse.return_value = mock_df
+ mock_excel_instance.parse.return_value = df
mock_excel_file.return_value = mock_excel_instance
file_content = b"fake_excel_empty_content"
result = _extract_text_from_excel(file_content)
- expected = "\n\n"
+ expected = "| |\n| |\n\n"
assert result == expected
+ mock_excel_instance.parse.assert_called_once_with(sheet_name="EmptySheet")
+
@patch("pandas.ExcelFile")
def test_extract_text_from_excel_sheet_parse_error(mock_excel_file):
"""Test handling of sheet parsing errors - should continue with other sheets."""
- # Mock DataFrames - one successful, one that raises exception
- mock_df_success = Mock()
- mock_df_success.dropna = Mock()
- mock_df_success.to_markdown.return_value = "| Data | Value |\n|------|-------|\n| Test | 123 |"
+
+ # Test error
+ data = {"Data": ["Test"], "Value": [123]}
+ df = pd.DataFrame(data)
# Mock ExcelFile
mock_excel_instance = Mock()
mock_excel_instance.sheet_names = ["GoodSheet", "BadSheet"]
- mock_excel_instance.parse.side_effect = [mock_df_success, Exception("Parse error")]
+ mock_excel_instance.parse.side_effect = [df, Exception("Parse error")]
mock_excel_file.return_value = mock_excel_instance
file_content = b"fake_excel_mixed_content"
result = _extract_text_from_excel(file_content)
- expected = "| Data | Value |\n|------|-------|\n| Test | 123 |\n\n"
- assert result == expected
+ expected_manual = "| Data | Value |\n| ---- | ----- |\n| Test | 123 |\n\n"
+ assert expected_manual == result
-@patch("pandas.ExcelFile")
-def test_extract_text_from_excel_file_error(mock_excel_file):
- """Test handling of Excel file reading errors."""
- mock_excel_file.side_effect = Exception("Invalid Excel file")
-
- file_content = b"invalid_excel_content"
-
- with pytest.raises(Exception) as exc_info:
- _extract_text_from_excel(file_content)
-
- # Note: The function should raise TextExtractionError, but since it's not imported in the test,
- # we check for the general Exception pattern
- assert "Failed to extract text from Excel file" in str(exc_info.value)
+ assert mock_excel_instance.parse.call_count == 2
@patch("pandas.ExcelFile")
def test_extract_text_from_excel_io_bytesio_usage(mock_excel_file):
"""Test that BytesIO is properly used with the file content."""
- import io
- # Mock DataFrame
- mock_df = Mock()
- mock_df.dropna = Mock()
- mock_df.to_markdown.return_value = "| Test | Data |\n|------|------|\n| 1 | A |"
+ # Test bytesio
+ data = {"Test": [1], "Data": ["A"]}
+ df = pd.DataFrame(data)
# Mock ExcelFile
mock_excel_instance = Mock()
mock_excel_instance.sheet_names = ["TestSheet"]
- mock_excel_instance.parse.return_value = mock_df
+ mock_excel_instance.parse.return_value = df
mock_excel_file.return_value = mock_excel_instance
file_content = b"test_excel_bytes"
result = _extract_text_from_excel(file_content)
- # Verify that ExcelFile was called with a BytesIO object
mock_excel_file.assert_called_once()
- call_args = mock_excel_file.call_args[0][0]
- assert isinstance(call_args, io.BytesIO)
+ call_arg = mock_excel_file.call_args[0][0]
+ assert isinstance(call_arg, io.BytesIO)
- expected = "| Test | Data |\n|------|------|\n| 1 | A |\n\n"
- assert result == expected
+ expected_manual = "| Test | Data |\n| ---- | ---- |\n| 1 | A |\n\n"
+ assert expected_manual == result
@patch("pandas.ExcelFile")
def test_extract_text_from_excel_all_sheets_fail(mock_excel_file):
"""Test when all sheets fail to parse - should return empty string."""
+
# Mock ExcelFile
mock_excel_instance = Mock()
mock_excel_instance.sheet_names = ["BadSheet1", "BadSheet2"]
@@ -335,29 +326,6 @@ def test_extract_text_from_excel_all_sheets_fail(mock_excel_file):
file_content = b"fake_excel_all_bad_sheets"
result = _extract_text_from_excel(file_content)
- # Should return empty string when all sheets fail
assert result == ""
-
-@patch("pandas.ExcelFile")
-def test_extract_text_from_excel_markdown_formatting(mock_excel_file):
- """Test that markdown formatting parameters are correctly applied."""
- # Mock DataFrame
- mock_df = Mock()
- mock_df.dropna = Mock()
- mock_df.to_markdown.return_value = "| Float | Int |\n|-------|-----|\n| 123456.78 | 42 |"
-
- # Mock ExcelFile
- mock_excel_instance = Mock()
- mock_excel_instance.sheet_names = ["NumberSheet"]
- mock_excel_instance.parse.return_value = mock_df
- mock_excel_file.return_value = mock_excel_instance
-
- file_content = b"fake_excel_numbers"
- result = _extract_text_from_excel(file_content)
-
- # Verify to_markdown was called with correct parameters
- mock_df.to_markdown.assert_called_once_with(index=False, floatfmt="")
-
- expected = "| Float | Int |\n|-------|-----|\n| 123456.78 | 42 |\n\n"
- assert result == expected
+ assert mock_excel_instance.parse.call_count == 2
From 59dc7c880e6deca3c1f77f726dec435a1544dd01 Mon Sep 17 00:00:00 2001
From: KVOJJJin
Date: Thu, 5 Jun 2025 15:47:42 +0800
Subject: [PATCH 27/56] Fix: style of radio checked (#20681)
---
web/app/components/base/radio/component/radio/index.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/app/components/base/radio/component/radio/index.tsx b/web/app/components/base/radio/component/radio/index.tsx
index 7788226484..aa4e6d0c7f 100644
--- a/web/app/components/base/radio/component/radio/index.tsx
+++ b/web/app/components/base/radio/component/radio/index.tsx
@@ -38,7 +38,7 @@ export default function Radio({
const divClassName = `
flex items-center py-1 relative
px-7 cursor-pointer text-text-secondary rounded
- bg-components-option-card-option-bg hover:bg-components-option-card-option-bg-hover hover:shadow-xs
+ hover:bg-components-option-card-option-bg-hover hover:shadow-xs
`
return (
From 7094680e2392159b3bebd6fa6fa782147f295af2 Mon Sep 17 00:00:00 2001
From: Nite Knite
Date: Thu, 5 Jun 2025 17:02:26 +0800
Subject: [PATCH 28/56] feat: reorder app types (#20685)
---
web/app/(commonLayout)/apps/Apps.tsx | 4 ++--
web/app/components/app/create-app-modal/index.tsx | 15 +++------------
web/app/components/app/type-selector/index.tsx | 2 +-
3 files changed, 6 insertions(+), 15 deletions(-)
diff --git a/web/app/(commonLayout)/apps/Apps.tsx b/web/app/(commonLayout)/apps/Apps.tsx
index 1b7ff39383..d0cc7ff91f 100644
--- a/web/app/(commonLayout)/apps/Apps.tsx
+++ b/web/app/(commonLayout)/apps/Apps.tsx
@@ -88,11 +88,11 @@ const Apps = () => {
const anchorRef = useRef(null)
const options = [
{ value: 'all', text: t('app.types.all'), icon: },
+ { value: 'workflow', text: t('app.types.workflow'), icon: },
+ { value: 'advanced-chat', text: t('app.types.advanced'), icon: },
{ value: 'chat', text: t('app.types.chatbot'), icon: },
{ value: 'agent-chat', text: t('app.types.agent'), icon: },
{ value: 'completion', text: t('app.types.completion'), icon: },
- { value: 'advanced-chat', text: t('app.types.advanced'), icon: },
- { value: 'workflow', text: t('app.types.workflow'), icon: },
]
useEffect(() => {
diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx
index 46cb495801..6e5547d08a 100644
--- a/web/app/components/app/create-app-modal/index.tsx
+++ b/web/app/components/app/create-app-modal/index.tsx
@@ -1,9 +1,9 @@
'use client'
-import { useCallback, useEffect, useRef, useState } from 'react'
+import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import { useRouter, useSearchParams } from 'next/navigation'
+import { useRouter } from 'next/navigation'
import { useContext, useContextSelector } from 'use-context-selector'
import { RiArrowRightLine, RiArrowRightSLine, RiCommandLine, RiCornerDownLeftLine, RiExchange2Fill } from '@remixicon/react'
import Link from 'next/link'
@@ -19,7 +19,6 @@ import AppsContext, { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { ToastContext } from '@/app/components/base/toast'
import type { AppMode } from '@/types/app'
-import { AppModes } from '@/types/app'
import { createApp } from '@/service/apps'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
@@ -56,14 +55,6 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate }: CreateAppProps)
const isCreatingRef = useRef(false)
- const searchParams = useSearchParams()
-
- useEffect(() => {
- const category = searchParams.get('category')
- if (category && AppModes.includes(category as AppMode))
- setAppMode(category as AppMode)
- }, [searchParams])
-
const onCreate = useCallback(async () => {
if (!appMode) {
notify({ type: 'error', message: t('app.newApp.appTypeRequired') })
@@ -128,7 +119,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate }: CreateAppProps)
onClick={() => {
setAppMode('workflow')
}} />
- void
}
-const allTypes: AppMode[] = ['chat', 'agent-chat', 'completion', 'advanced-chat', 'workflow']
+const allTypes: AppMode[] = ['workflow', 'advanced-chat', 'chat', 'agent-chat', 'completion']
const AppTypeSelector = ({ value, onChange }: AppSelectorProps) => {
const [open, setOpen] = useState(false)
From f76f70f0b6119a448f6d7ddf0c27d00009ab060f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=B9=9B=E9=9C=B2=E5=85=88=E7=94=9F?=
Date: Thu, 5 Jun 2025 23:05:50 +0800
Subject: [PATCH 29/56] Fix builtin_providers for tools. (#20697)
Signed-off-by: zhanluxianshen
---
api/core/tools/builtin_tool/_position.yaml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/api/core/tools/builtin_tool/_position.yaml b/api/core/tools/builtin_tool/_position.yaml
index b5875e2075..0e811de311 100644
--- a/api/core/tools/builtin_tool/_position.yaml
+++ b/api/core/tools/builtin_tool/_position.yaml
@@ -1,3 +1,4 @@
+- audio
- code
- time
-- qrcode
+- webscraper
From 138ad6e8b33368a66a1956ca5ddc087d5928317e Mon Sep 17 00:00:00 2001
From: jefferyvvv <33647240+jefferyvvv@users.noreply.github.com>
Date: Thu, 5 Jun 2025 23:09:00 +0800
Subject: [PATCH 30/56] fix: opensearch fulltext search with metadata filtering
dsl error (#20702)
Co-authored-by: wenjun.gu
---
api/core/rag/datasource/vdb/opensearch/opensearch_vector.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py b/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py
index 6991598ce6..b156be1a22 100644
--- a/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py
+++ b/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py
@@ -209,10 +209,10 @@ class OpenSearchVector(BaseVector):
return docs
def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]:
- full_text_query = {"query": {"match": {Field.CONTENT_KEY.value: query}}}
+ full_text_query = {"query": {"bool": {"must": [{"match": {Field.CONTENT_KEY.value: query}}]}}}
document_ids_filter = kwargs.get("document_ids_filter")
if document_ids_filter:
- full_text_query["query"]["terms"] = {"metadata.document_id": document_ids_filter}
+ full_text_query["query"]["bool"]["filter"] = [{"terms": {"metadata.document_id": document_ids_filter}}]
response = self._client.search(index=self._collection_name.lower(), body=full_text_query)
From 38554c5f3e50eb9a5f28c06d1ecdca669ce8e3c8 Mon Sep 17 00:00:00 2001
From: Yeuoly <45712896+Yeuoly@users.noreply.github.com>
Date: Thu, 5 Jun 2025 23:36:33 +0800
Subject: [PATCH 31/56] fix(inner_api/plugin/wraps): refresh user model after
creation in get user function (#20704)
---
api/controllers/inner_api/plugin/wraps.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/api/controllers/inner_api/plugin/wraps.py b/api/controllers/inner_api/plugin/wraps.py
index b2849a7962..50408e0929 100644
--- a/api/controllers/inner_api/plugin/wraps.py
+++ b/api/controllers/inner_api/plugin/wraps.py
@@ -32,6 +32,7 @@ def get_user(tenant_id: str, user_id: str | None) -> Account | EndUser:
)
session.add(user_model)
session.commit()
+ session.refresh(user_model)
else:
user_model = AccountService.load_user(user_id)
if not user_model:
From 4f14d7c0ca49e44e7b4d0fc1623b2beff9a440ec Mon Sep 17 00:00:00 2001
From: Bowen Liang
Date: Fri, 6 Jun 2025 09:09:31 +0800
Subject: [PATCH 32/56] chore: bump uv to 0.7.x (#20692)
---
.github/actions/setup-uv/action.yml | 2 +-
api/Dockerfile | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/actions/setup-uv/action.yml b/.github/actions/setup-uv/action.yml
index a596be63f7..0499b44dba 100644
--- a/.github/actions/setup-uv/action.yml
+++ b/.github/actions/setup-uv/action.yml
@@ -8,7 +8,7 @@ inputs:
uv-version:
description: UV version to set up
required: true
- default: '0.6.14'
+ default: '~=0.7.11'
uv-lockfile:
description: Path to the UV lockfile to restore cache from
required: true
diff --git a/api/Dockerfile b/api/Dockerfile
index cff696ff56..7e4997507f 100644
--- a/api/Dockerfile
+++ b/api/Dockerfile
@@ -4,7 +4,7 @@ FROM python:3.12-slim-bookworm AS base
WORKDIR /app/api
# Install uv
-ENV UV_VERSION=0.6.14
+ENV UV_VERSION=0.7.11
RUN pip install --no-cache-dir uv==${UV_VERSION}
From 4271602cfca984aa999f505cf2e30d43c61ad280 Mon Sep 17 00:00:00 2001
From: jefferyvvv <33647240+jefferyvvv@users.noreply.github.com>
Date: Fri, 6 Jun 2025 09:10:01 +0800
Subject: [PATCH 33/56] fix: opensearch metadata filtering returns empty
(#20701)
Co-authored-by: wenjun.gu
Co-authored-by: crazywoola <427733928@qq.com>
---
api/core/rag/datasource/vdb/opensearch/opensearch_vector.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py b/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py
index b156be1a22..6a6c2b73ef 100644
--- a/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py
+++ b/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py
@@ -255,7 +255,8 @@ class OpenSearchVector(BaseVector):
Field.METADATA_KEY.value: {
"type": "object",
"properties": {
- "doc_id": {"type": "keyword"} # Map doc_id to keyword type
+ "doc_id": {"type": "keyword"}, # Map doc_id to keyword type
+ "document_id": {"type": "keyword"},
},
},
}
From 4f0c9fdf2b02ac4ad0b3fa4a85abf92f3531025e Mon Sep 17 00:00:00 2001
From: Joel
Date: Fri, 6 Jun 2025 10:44:21 +0800
Subject: [PATCH 34/56] chore: remove repeat public api and service api panel
(#20715)
---
.../[appId]/overview/chartView.tsx | 32 +++++++++++--------
.../[appId]/overview/page.tsx | 8 ++---
.../[appId]/overview/tracing/panel.tsx | 4 +--
3 files changed, 23 insertions(+), 21 deletions(-)
diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx
index 4afba06eae..32822e3315 100644
--- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx
+++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx
@@ -18,9 +18,10 @@ const queryDateFormat = 'YYYY-MM-DD HH:mm'
export type IChartViewProps = {
appId: string
+ headerRight: React.ReactNode
}
-export default function ChartView({ appId }: IChartViewProps) {
+export default function ChartView({ appId, headerRight }: IChartViewProps) {
const { t } = useTranslation()
const appDetail = useAppStore(state => state.appDetail)
const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow'
@@ -46,19 +47,22 @@ export default function ChartView({ appId }: IChartViewProps) {
return (
-
-
{t('appOverview.analysis.title')}
-
({ value: k, name: t(`appLog.filter.period.${v.name}`) }))}
- className='mt-0 !w-40'
- onSelect={(item) => {
- const id = item.value
- const value = TIME_PERIOD_MAPPING[id]?.value ?? '-1'
- const name = item.name || t('appLog.filter.period.allTime')
- onSelect({ value, name })
- }}
- defaultValue={'2'}
- />
+
+
+ {t('appOverview.analysis.title')}
+ ({ value: k, name: t(`appLog.filter.period.${v.name}`) }))}
+ className='mt-0 !w-40'
+ onSelect={(item) => {
+ const id = item.value
+ const value = TIME_PERIOD_MAPPING[id]?.value ?? '-1'
+ const name = item.name || t('appLog.filter.period.allTime')
+ onSelect({ value, name })
+ }}
+ defaultValue={'2'}
+ />
+
+ {headerRight}
{!isWorkflow && (
diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx
index 0f1bb7e18d..fc97f5e669 100644
--- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx
+++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx
@@ -1,6 +1,5 @@
import React from 'react'
import ChartView from './chartView'
-import CardView from './cardView'
import TracingPanel from './tracing/panel'
import ApikeyInfoPanel from '@/app/components/app/overview/apikey-info-panel'
@@ -18,9 +17,10 @@ const Overview = async (props: IDevelopProps) => {
return (
)
}
diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx
index 8575117c41..bc85f3a734 100644
--- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx
+++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx
@@ -154,7 +154,6 @@ const Panel: FC = () => {
if (!isLoaded) {
return (
-
@@ -163,8 +162,7 @@ const Panel: FC = () => {
}
return (
-
-
+
Date: Fri, 6 Jun 2025 10:48:28 +0800
Subject: [PATCH 35/56] chore: replace pseudo-random generators with secrets
module (#20616)
---
api/.ruff.toml | 1 +
api/core/helper/moderation.py | 4 ++--
api/core/workflow/nodes/http_request/executor.py | 5 +++--
api/libs/helper.py | 4 ++--
api/services/account_service.py | 7 +++----
api/services/dataset_service.py | 4 ++--
api/services/webapp_auth_service.py | 4 ++--
api/tests/unit_tests/core/helper/test_ssrf_proxy.py | 4 ++--
8 files changed, 17 insertions(+), 16 deletions(-)
diff --git a/api/.ruff.toml b/api/.ruff.toml
index 41a24abad9..facb0d5419 100644
--- a/api/.ruff.toml
+++ b/api/.ruff.toml
@@ -43,6 +43,7 @@ select = [
"S307", # suspicious-eval-usage, disallow use of `eval` and `ast.literal_eval`
"S301", # suspicious-pickle-usage, disallow use of `pickle` and its wrappers.
"S302", # suspicious-marshal-usage, disallow use of `marshal` module
+ "S311", # suspicious-non-cryptographic-random-usage
]
ignore = [
diff --git a/api/core/helper/moderation.py b/api/core/helper/moderation.py
index 6a5982eca4..a324ac2767 100644
--- a/api/core/helper/moderation.py
+++ b/api/core/helper/moderation.py
@@ -1,5 +1,5 @@
import logging
-import random
+import secrets
from typing import cast
from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity
@@ -38,7 +38,7 @@ def check_moderation(tenant_id: str, model_config: ModelConfigWithCredentialsEnt
if len(text_chunks) == 0:
return True
- text_chunk = random.choice(text_chunks)
+ text_chunk = secrets.choice(text_chunks)
try:
model_provider_factory = ModelProviderFactory(tenant_id)
diff --git a/api/core/workflow/nodes/http_request/executor.py b/api/core/workflow/nodes/http_request/executor.py
index e28ac6343b..2c83b00d4a 100644
--- a/api/core/workflow/nodes/http_request/executor.py
+++ b/api/core/workflow/nodes/http_request/executor.py
@@ -1,8 +1,9 @@
import base64
import json
+import secrets
+import string
from collections.abc import Mapping
from copy import deepcopy
-from random import randint
from typing import Any, Literal
from urllib.parse import urlencode, urlparse
@@ -434,4 +435,4 @@ def _generate_random_string(n: int) -> str:
>>> _generate_random_string(5)
'abcde'
"""
- return "".join([chr(randint(97, 122)) for _ in range(n)])
+ return "".join(secrets.choice(string.ascii_lowercase) for _ in range(n))
diff --git a/api/libs/helper.py b/api/libs/helper.py
index 463ba3308b..e78a782fbe 100644
--- a/api/libs/helper.py
+++ b/api/libs/helper.py
@@ -1,7 +1,7 @@
import json
import logging
-import random
import re
+import secrets
import string
import subprocess
import time
@@ -176,7 +176,7 @@ def generate_string(n):
letters_digits = string.ascii_letters + string.digits
result = ""
for i in range(n):
- result += random.choice(letters_digits)
+ result += secrets.choice(letters_digits)
return result
diff --git a/api/services/account_service.py b/api/services/account_service.py
index ac84a46299..14d238467d 100644
--- a/api/services/account_service.py
+++ b/api/services/account_service.py
@@ -1,7 +1,6 @@
import base64
import json
import logging
-import random
import secrets
import uuid
from datetime import UTC, datetime, timedelta
@@ -261,7 +260,7 @@ class AccountService:
@staticmethod
def generate_account_deletion_verification_code(account: Account) -> tuple[str, str]:
- code = "".join([str(random.randint(0, 9)) for _ in range(6)])
+ code = "".join([str(secrets.randbelow(exclusive_upper_bound=10)) for _ in range(6)])
token = TokenManager.generate_token(
account=account, token_type="account_deletion", additional_data={"code": code}
)
@@ -429,7 +428,7 @@ class AccountService:
additional_data: dict[str, Any] = {},
):
if not code:
- code = "".join([str(random.randint(0, 9)) for _ in range(6)])
+ code = "".join([str(secrets.randbelow(exclusive_upper_bound=10)) for _ in range(6)])
additional_data["code"] = code
token = TokenManager.generate_token(
account=account, email=email, token_type="reset_password", additional_data=additional_data
@@ -456,7 +455,7 @@ class AccountService:
raise EmailCodeLoginRateLimitExceededError()
- code = "".join([str(random.randint(0, 9)) for _ in range(6)])
+ code = "".join([str(secrets.randbelow(exclusive_upper_bound=10)) for _ in range(6)])
token = TokenManager.generate_token(
account=account, email=email, token_type="email_code_login", additional_data={"code": code}
)
diff --git a/api/services/dataset_service.py b/api/services/dataset_service.py
index 4a5e9b3520..e98b47921f 100644
--- a/api/services/dataset_service.py
+++ b/api/services/dataset_service.py
@@ -2,7 +2,7 @@ import copy
import datetime
import json
import logging
-import random
+import secrets
import time
import uuid
from collections import Counter
@@ -970,7 +970,7 @@ class DocumentService:
documents.append(document)
batch = document.batch
else:
- batch = time.strftime("%Y%m%d%H%M%S") + str(random.randint(100000, 999999))
+ batch = time.strftime("%Y%m%d%H%M%S") + str(100000 + secrets.randbelow(exclusive_upper_bound=900000))
# save process rule
if not dataset_process_rule:
process_rule = knowledge_config.process_rule
diff --git a/api/services/webapp_auth_service.py b/api/services/webapp_auth_service.py
index 79d5217de7..d83303056b 100644
--- a/api/services/webapp_auth_service.py
+++ b/api/services/webapp_auth_service.py
@@ -1,4 +1,4 @@
-import random
+import secrets
from datetime import UTC, datetime, timedelta
from typing import Any, Optional, cast
@@ -66,7 +66,7 @@ class WebAppAuthService:
if email is None:
raise ValueError("Email must be provided.")
- code = "".join([str(random.randint(0, 9)) for _ in range(6)])
+ code = "".join([str(secrets.randbelow(exclusive_upper_bound=10)) for _ in range(6)])
token = TokenManager.generate_token(
account=account, email=email, token_type="webapp_email_code_login", additional_data={"code": code}
)
diff --git a/api/tests/unit_tests/core/helper/test_ssrf_proxy.py b/api/tests/unit_tests/core/helper/test_ssrf_proxy.py
index c688d3952b..37749f0c66 100644
--- a/api/tests/unit_tests/core/helper/test_ssrf_proxy.py
+++ b/api/tests/unit_tests/core/helper/test_ssrf_proxy.py
@@ -1,4 +1,4 @@
-import random
+import secrets
from unittest.mock import MagicMock, patch
import pytest
@@ -34,7 +34,7 @@ def test_retry_logic_success(mock_request):
side_effects = []
for _ in range(SSRF_DEFAULT_MAX_RETRIES):
- status_code = random.choice(STATUS_FORCELIST)
+ status_code = secrets.choice(STATUS_FORCELIST)
mock_response = MagicMock()
mock_response.status_code = status_code
side_effects.append(mock_response)
From 85859b67237c28dbec67f73b7c8e7e8484c62800 Mon Sep 17 00:00:00 2001
From: Joel
Date: Fri, 6 Jun 2025 10:53:57 +0800
Subject: [PATCH 36/56] feat: add browser list (#20717)
---
web/package.json | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/web/package.json b/web/package.json
index affbef9382..ff4214f966 100644
--- a/web/package.json
+++ b/web/package.json
@@ -5,6 +5,18 @@
"engines": {
"node": ">=v22.11.0"
},
+ "browserslist": [
+ "last 1 Chrome version",
+ "last 1 Firefox version",
+ "last 1 Edge version",
+ "last 1 Safari version",
+ "iOS >=15",
+ "Android >= 10",
+ "and_chr >= 126",
+ "and_ff >= 137",
+ "and_uc >= 15.5",
+ "and_qq >= 14.9"
+ ],
"scripts": {
"dev": "cross-env NODE_OPTIONS='--inspect' next dev",
"build": "next build",
From 723b69cf8d64bd69045934d74163b0b68f2f3859 Mon Sep 17 00:00:00 2001
From: Joel
Date: Fri, 6 Jun 2025 16:15:37 +0800
Subject: [PATCH 37/56] chore: chart panel ui enhance (#20743)
---
.../[appId]/overview/chartView.tsx | 32 ++++++++++---------
.../[appId]/overview/tracing/panel.tsx | 13 --------
2 files changed, 17 insertions(+), 28 deletions(-)
diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx
index 32822e3315..646c8bd93d 100644
--- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx
+++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx
@@ -47,22 +47,24 @@ export default function ChartView({ appId, headerRight }: IChartViewProps) {
return (
-
-
-
{t('appOverview.analysis.title')}
-
({ value: k, name: t(`appLog.filter.period.${v.name}`) }))}
- className='mt-0 !w-40'
- onSelect={(item) => {
- const id = item.value
- const value = TIME_PERIOD_MAPPING[id]?.value ?? '-1'
- const name = item.name || t('appLog.filter.period.allTime')
- onSelect({ value, name })
- }}
- defaultValue={'2'}
- />
+
+
{t('common.appMenus.overview')}
+
+
+ ({ value: k, name: t(`appLog.filter.period.${v.name}`) }))}
+ className='mt-0 !w-40'
+ onSelect={(item) => {
+ const id = item.value
+ const value = TIME_PERIOD_MAPPING[id]?.value ?? '-1'
+ const name = item.name || t('appLog.filter.period.allTime')
+ onSelect({ value, name })
+ }}
+ defaultValue={'2'}
+ />
+
+ {headerRight}
- {headerRight}
{!isWorkflow && (
diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx
index bc85f3a734..76e90ecf19 100644
--- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx
+++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx
@@ -23,19 +23,6 @@ import Divider from '@/app/components/base/divider'
const I18N_PREFIX = 'app.tracing'
-const Title = ({
- className,
-}: {
- className?: string
-}) => {
- const { t } = useTranslation()
-
- return (
-
- {t('common.appMenus.overview')}
-
- )
-}
const Panel: FC = () => {
const { t } = useTranslation()
const pathname = usePathname()
From 37c3283450c9bec0d8b44d2335be85603d0f294c Mon Sep 17 00:00:00 2001
From: jefferyvvv <33647240+jefferyvvv@users.noreply.github.com>
Date: Fri, 6 Jun 2025 16:29:15 +0800
Subject: [PATCH 38/56] fix: opensearch vector search falls back to keyword
search (#20723)
Co-authored-by: wenjun.gu
---
.../datasource/vdb/opensearch/opensearch_vector.py | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py b/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py
index 6a6c2b73ef..0abb3c0077 100644
--- a/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py
+++ b/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py
@@ -184,7 +184,16 @@ class OpenSearchVector(BaseVector):
}
document_ids_filter = kwargs.get("document_ids_filter")
if document_ids_filter:
- query["query"] = {"terms": {"metadata.document_id": document_ids_filter}}
+ query["query"] = {
+ "script_score": {
+ "query": {"bool": {"filter": [{"terms": {Field.DOCUMENT_ID.value: document_ids_filter}}]}},
+ "script": {
+ "source": "knn_score",
+ "lang": "knn",
+ "params": {"field": Field.VECTOR.value, "query_value": query_vector, "space_type": "l2"},
+ },
+ }
+ }
try:
response = self._client.search(index=self._collection_name.lower(), body=query)
From 0c8447fd0e48851f862f050e951296ad39844071 Mon Sep 17 00:00:00 2001
From: Joel
Date: Fri, 6 Jun 2025 16:44:36 +0800
Subject: [PATCH 39/56] fix: missing bot name in orchestrate (#20747)
---
web/app/components/base/chat/chat/index.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/app/components/base/chat/chat/index.tsx b/web/app/components/base/chat/chat/index.tsx
index c0842af0c4..801daa6589 100644
--- a/web/app/components/base/chat/chat/index.tsx
+++ b/web/app/components/base/chat/chat/index.tsx
@@ -303,7 +303,7 @@ const Chat: FC = ({
{
!noChatInput && (
Date: Fri, 6 Jun 2025 21:03:59 +0800
Subject: [PATCH 40/56] feat(api): Adjust `WorkflowDraftVariable` and
`WorkflowNodeExecutionModel` (#20746)
- Add `node_execution_id` column to `WorkflowDraftVariable`, allowing efficient implementation of
the "Reset to last run value" feature.
- Add additional index for `WorkflowNodeExecutionModel` to improve the performance of last run lookup.
Closes #20745.
---
...w_draft_varaibles_add_node_execution_id.py | 60 +++++++++++++
api/models/workflow.py | 87 +++++++++++++------
2 files changed, 121 insertions(+), 26 deletions(-)
create mode 100644 api/migrations/versions/2025_06_06_1424-4474872b0ee6_workflow_draft_varaibles_add_node_execution_id.py
diff --git a/api/migrations/versions/2025_06_06_1424-4474872b0ee6_workflow_draft_varaibles_add_node_execution_id.py b/api/migrations/versions/2025_06_06_1424-4474872b0ee6_workflow_draft_varaibles_add_node_execution_id.py
new file mode 100644
index 0000000000..d7a5d116c9
--- /dev/null
+++ b/api/migrations/versions/2025_06_06_1424-4474872b0ee6_workflow_draft_varaibles_add_node_execution_id.py
@@ -0,0 +1,60 @@
+"""`workflow_draft_varaibles` add `node_execution_id` column, add an index for `workflow_node_executions`.
+
+Revision ID: 4474872b0ee6
+Revises: 2adcbe1f5dfb
+Create Date: 2025-06-06 14:24:44.213018
+
+"""
+from alembic import op
+import models as models
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '4474872b0ee6'
+down_revision = '2adcbe1f5dfb'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # `CREATE INDEX CONCURRENTLY` cannot run within a transaction, so use the `autocommit_block`
+ # context manager to wrap the index creation statement.
+ # Reference:
+ #
+ # - https://www.postgresql.org/docs/current/sql-createindex.html#:~:text=Another%20difference%20is,CREATE%20INDEX%20CONCURRENTLY%20cannot.
+ # - https://alembic.sqlalchemy.org/en/latest/api/runtime.html#alembic.runtime.migration.MigrationContext.autocommit_block
+ with op.get_context().autocommit_block():
+ op.create_index(
+ op.f('workflow_node_executions_tenant_id_idx'),
+ "workflow_node_executions",
+ ['tenant_id', 'workflow_id', 'node_id', sa.literal_column('created_at DESC')],
+ unique=False,
+ postgresql_concurrently=True,
+ )
+
+ with op.batch_alter_table('workflow_draft_variables', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('node_execution_id', models.types.StringUUID(), nullable=True))
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+
+ # `DROP INDEX CONCURRENTLY` cannot run within a transaction, so use the `autocommit_block`
+ # context manager to wrap the index creation statement.
+ # Reference:
+ #
+ # - https://www.postgresql.org/docs/current/sql-createindex.html#:~:text=Another%20difference%20is,CREATE%20INDEX%20CONCURRENTLY%20cannot.
+ # - https://alembic.sqlalchemy.org/en/latest/api/runtime.html#alembic.runtime.migration.MigrationContext.autocommit_block
+ # `DROP INDEX CONCURRENTLY` cannot run within a transaction, so commit existing transactions first.
+ # Reference:
+ #
+ # https://www.postgresql.org/docs/current/sql-createindex.html#:~:text=Another%20difference%20is,CREATE%20INDEX%20CONCURRENTLY%20cannot.
+ with op.get_context().autocommit_block():
+ op.drop_index(op.f('workflow_node_executions_tenant_id_idx'), postgresql_concurrently=True)
+
+ with op.batch_alter_table('workflow_draft_variables', schema=None) as batch_op:
+ batch_op.drop_column('node_execution_id')
+
+ # ### end Alembic commands ###
diff --git a/api/models/workflow.py b/api/models/workflow.py
index e868fb77a7..2fff045543 100644
--- a/api/models/workflow.py
+++ b/api/models/workflow.py
@@ -16,8 +16,8 @@ if TYPE_CHECKING:
from models.model import AppMode
import sqlalchemy as sa
-from sqlalchemy import UniqueConstraint, func
-from sqlalchemy.orm import Mapped, mapped_column
+from sqlalchemy import Index, PrimaryKeyConstraint, UniqueConstraint, func
+from sqlalchemy.orm import Mapped, declared_attr, mapped_column
from constants import DEFAULT_FILE_NUMBER_LIMITS, HIDDEN_VALUE
from core.helper import encrypter
@@ -590,28 +590,48 @@ class WorkflowNodeExecutionModel(Base):
"""
__tablename__ = "workflow_node_executions"
- __table_args__ = (
- db.PrimaryKeyConstraint("id", name="workflow_node_execution_pkey"),
- db.Index(
- "workflow_node_execution_workflow_run_idx",
- "tenant_id",
- "app_id",
- "workflow_id",
- "triggered_from",
- "workflow_run_id",
- ),
- db.Index(
- "workflow_node_execution_node_run_idx", "tenant_id", "app_id", "workflow_id", "triggered_from", "node_id"
- ),
- db.Index(
- "workflow_node_execution_id_idx",
- "tenant_id",
- "app_id",
- "workflow_id",
- "triggered_from",
- "node_execution_id",
- ),
- )
+
+ @declared_attr
+ def __table_args__(cls): # noqa
+ return (
+ PrimaryKeyConstraint("id", name="workflow_node_execution_pkey"),
+ Index(
+ "workflow_node_execution_workflow_run_idx",
+ "tenant_id",
+ "app_id",
+ "workflow_id",
+ "triggered_from",
+ "workflow_run_id",
+ ),
+ Index(
+ "workflow_node_execution_node_run_idx",
+ "tenant_id",
+ "app_id",
+ "workflow_id",
+ "triggered_from",
+ "node_id",
+ ),
+ Index(
+ "workflow_node_execution_id_idx",
+ "tenant_id",
+ "app_id",
+ "workflow_id",
+ "triggered_from",
+ "node_execution_id",
+ ),
+ Index(
+ # The first argument is the index name,
+ # which we leave as `None`` to allow auto-generation by the ORM.
+ None,
+ cls.tenant_id,
+ cls.workflow_id,
+ cls.node_id,
+ # MyPy may flag the following line because it doesn't recognize that
+ # the `declared_attr` decorator passes the receiving class as the first
+ # argument to this method, allowing us to reference class attributes.
+ cls.created_at.desc(), # type: ignore
+ ),
+ )
id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
tenant_id: Mapped[str] = mapped_column(StringUUID)
@@ -885,14 +905,29 @@ class WorkflowDraftVariable(Base):
selector: Mapped[str] = mapped_column(sa.String(255), nullable=False, name="selector")
+ # The data type of this variable's value
value_type: Mapped[SegmentType] = mapped_column(EnumText(SegmentType, length=20))
- # JSON string
+
+ # The variable's value serialized as a JSON string
value: Mapped[str] = mapped_column(sa.Text, nullable=False, name="value")
- # visible
+ # Controls whether the variable should be displayed in the variable inspection panel
visible: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=True)
+
+ # Determines whether this variable can be modified by users
editable: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=False)
+ # The `node_execution_id` field identifies the workflow node execution that created this variable.
+ # It corresponds to the `id` field in the `WorkflowNodeExecutionModel` model.
+ #
+ # This field is not `None` for system variables and node variables, and is `None`
+ # for conversation variables.
+ node_execution_id: Mapped[str | None] = mapped_column(
+ StringUUID,
+ nullable=True,
+ default=None,
+ )
+
def get_selector(self) -> list[str]:
selector = json.loads(self.selector)
if not isinstance(selector, list):
From e6e76852d5fd630890a28c7e11a76e35bf32ce27 Mon Sep 17 00:00:00 2001
From: Bharat Ramanathan
Date: Sat, 7 Jun 2025 20:36:23 +0530
Subject: [PATCH 41/56] Add support for W&B dedicated cloud instances in Weave
tracing integration (#20765)
Co-authored-by: crazywoola <427733928@qq.com>
---
api/core/ops/entities/config_entity.py | 9 +++++++++
api/core/ops/ops_trace_manager.py | 2 +-
api/core/ops/weave_trace/weave_trace.py | 15 ++++++++++++---
.../overview/tracing/provider-config-modal.tsx | 8 ++++++++
.../[appId]/overview/tracing/type.ts | 1 +
5 files changed, 31 insertions(+), 4 deletions(-)
diff --git a/api/core/ops/entities/config_entity.py b/api/core/ops/entities/config_entity.py
index f2d1bd305a..c988bf48d1 100644
--- a/api/core/ops/entities/config_entity.py
+++ b/api/core/ops/entities/config_entity.py
@@ -98,6 +98,7 @@ class WeaveConfig(BaseTracingConfig):
entity: str | None = None
project: str
endpoint: str = "https://trace.wandb.ai"
+ host: str | None = None
@field_validator("endpoint")
@classmethod
@@ -109,6 +110,14 @@ class WeaveConfig(BaseTracingConfig):
return v
+ @field_validator("host")
+ @classmethod
+ def validate_host(cls, v, info: ValidationInfo):
+ if v is not None and v != "":
+ if not v.startswith(("https://", "http://")):
+ raise ValueError("host must start with https:// or http://")
+ return v
+
OPS_FILE_PATH = "ops_trace/"
OPS_TRACE_FAILED_KEY = "FAILED_OPS_TRACE"
diff --git a/api/core/ops/ops_trace_manager.py b/api/core/ops/ops_trace_manager.py
index dc4cfc48db..e0dfe0c312 100644
--- a/api/core/ops/ops_trace_manager.py
+++ b/api/core/ops/ops_trace_manager.py
@@ -81,7 +81,7 @@ class OpsTraceProviderConfigMap(dict[str, dict[str, Any]]):
return {
"config_class": WeaveConfig,
"secret_keys": ["api_key"],
- "other_keys": ["project", "entity", "endpoint"],
+ "other_keys": ["project", "entity", "endpoint", "host"],
"trace_instance": WeaveDataTrace,
}
diff --git a/api/core/ops/weave_trace/weave_trace.py b/api/core/ops/weave_trace/weave_trace.py
index cfc8a505bb..3917348a91 100644
--- a/api/core/ops/weave_trace/weave_trace.py
+++ b/api/core/ops/weave_trace/weave_trace.py
@@ -40,9 +40,14 @@ class WeaveDataTrace(BaseTraceInstance):
self.weave_api_key = weave_config.api_key
self.project_name = weave_config.project
self.entity = weave_config.entity
+ self.host = weave_config.host
+
+ # Login with API key first, including host if provided
+ if self.host:
+ login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True, host=self.host)
+ else:
+ login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True)
- # Login with API key first
- login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True)
if not login_status:
logger.error("Failed to login to Weights & Biases with the provided API key")
raise ValueError("Weave login failed")
@@ -386,7 +391,11 @@ class WeaveDataTrace(BaseTraceInstance):
def api_check(self):
try:
- login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True)
+ if self.host:
+ login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True, host=self.host)
+ else:
+ login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True)
+
if not login_status:
raise ValueError("Weave login failed")
else:
diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx
index c0b52a9b10..b6c97add48 100644
--- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx
+++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx
@@ -55,6 +55,7 @@ const weaveConfigTemplate = {
entity: '',
project: '',
endpoint: '',
+ host: '',
}
const ProviderConfigModal: FC = ({
@@ -226,6 +227,13 @@ const ProviderConfigModal: FC = ({
onChange={handleConfigChange('endpoint')}
placeholder={'https://trace.wandb.ai/'}
/>
+
>
)}
{type === TracingProvider.langSmith && (
diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts
index 386c58974e..ed468caf65 100644
--- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts
+++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts
@@ -29,4 +29,5 @@ export type WeaveConfig = {
entity: string
project: string
endpoint: string
+ host: string
}
From 65c7c01d90e5af44d4c9ff41689a855a7a82c772 Mon Sep 17 00:00:00 2001
From: yihong
Date: Sat, 7 Jun 2025 23:06:46 +0800
Subject: [PATCH 42/56] fix: clean up two unreachable code (#20773)
Signed-off-by: yihong0618
---
api/core/rag/datasource/vdb/oracle/oraclevector.py | 1 -
.../utils/dataset_retriever/dataset_multi_retriever_tool.py | 2 --
2 files changed, 3 deletions(-)
diff --git a/api/core/rag/datasource/vdb/oracle/oraclevector.py b/api/core/rag/datasource/vdb/oracle/oraclevector.py
index 6b9dd9c561..d1c8142b3d 100644
--- a/api/core/rag/datasource/vdb/oracle/oraclevector.py
+++ b/api/core/rag/datasource/vdb/oracle/oraclevector.py
@@ -303,7 +303,6 @@ class OracleVector(BaseVector):
return docs
else:
return [Document(page_content="", metadata={})]
- return []
def delete(self) -> None:
with self._get_connection() as conn:
diff --git a/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py b/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py
index 93d3fcc49d..2cbc4b9821 100644
--- a/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py
+++ b/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py
@@ -153,8 +153,6 @@ class DatasetMultiRetrieverTool(DatasetRetrieverBaseTool):
return str("\n".join(document_context_list))
return ""
- raise RuntimeError("not segments found")
-
def _retriever(
self,
flask_app: Flask,
From d6a8af03b4d2a56a30c6e42a2967347a6435c7ae Mon Sep 17 00:00:00 2001
From: NFish
Date: Mon, 9 Jun 2025 15:44:49 +0800
Subject: [PATCH 43/56] Fix/add webapp no permission page (#20819)
---
web/app/(shareLayout)/webapp-signin/page.tsx | 6 ++--
.../components/app/app-publisher/index.tsx | 6 ++--
web/app/components/base/app-unavailable.tsx | 2 +-
.../base/chat/chat-with-history/index.tsx | 32 +++++++++++++++++--
.../base/chat/embedded-chatbot/index.tsx | 32 ++++++++++++++++---
.../share/text-generation/index.tsx | 23 +++++++++++--
web/app/components/share/utils.ts | 18 +----------
web/service/base.ts | 14 +++++---
8 files changed, 95 insertions(+), 38 deletions(-)
diff --git a/web/app/(shareLayout)/webapp-signin/page.tsx b/web/app/(shareLayout)/webapp-signin/page.tsx
index c12fde38dd..07b7c88430 100644
--- a/web/app/(shareLayout)/webapp-signin/page.tsx
+++ b/web/app/(shareLayout)/webapp-signin/page.tsx
@@ -23,10 +23,12 @@ const WebSSOForm: FC = () => {
const redirectUrl = searchParams.get('redirect_url')
const tokenFromUrl = searchParams.get('web_sso_token')
const message = searchParams.get('message')
+ const code = searchParams.get('code')
const getSigninUrl = useCallback(() => {
const params = new URLSearchParams(searchParams)
params.delete('message')
+ params.delete('code')
return `/webapp-signin?${params.toString()}`
}, [searchParams])
@@ -85,8 +87,8 @@ const WebSSOForm: FC = () => {
if (message) {
return
-
-
{t('share.login.backToHome')}
+
+
{code === '403' ? t('common.userProfile.logout') : t('share.login.backToHome')}
}
if (!redirectUrl) {
diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx
index 5825bb72ee..1485964198 100644
--- a/web/app/components/app/app-publisher/index.tsx
+++ b/web/app/components/app/app-publisher/index.tsx
@@ -278,7 +278,7 @@ const AppPublisher = ({
onClick={() => {
setShowAppAccessControl(true)
}}>
-
+
{appDetail?.access_mode === AccessMode.ORGANIZATION
&& <>
@@ -288,7 +288,9 @@ const AppPublisher = ({
{appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS
&& <>
-
{t('app.accessControlDialog.accessItems.specific')}
+
+ {t('app.accessControlDialog.accessItems.specific')}
+
>
}
{appDetail?.access_mode === AccessMode.PUBLIC
diff --git a/web/app/components/base/app-unavailable.tsx b/web/app/components/base/app-unavailable.tsx
index 928c850262..c501d36118 100644
--- a/web/app/components/base/app-unavailable.tsx
+++ b/web/app/components/base/app-unavailable.tsx
@@ -21,7 +21,7 @@ const AppUnavailable: FC
= ({
return (
-
{code}
diff --git a/web/app/components/base/chat/chat-with-history/index.tsx b/web/app/components/base/chat/chat-with-history/index.tsx
index 1fd1383196..fe8e7b430d 100644
--- a/web/app/components/base/chat/chat-with-history/index.tsx
+++ b/web/app/components/base/chat/chat-with-history/index.tsx
@@ -1,5 +1,7 @@
+'use client'
import type { FC } from 'react'
import {
+ useCallback,
useEffect,
useState,
} from 'react'
@@ -17,10 +19,12 @@ import ChatWrapper from './chat-wrapper'
import type { InstalledApp } from '@/models/explore'
import Loading from '@/app/components/base/loading'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
-import { checkOrSetAccessToken } from '@/app/components/share/utils'
+import { checkOrSetAccessToken, removeAccessToken } from '@/app/components/share/utils'
import AppUnavailable from '@/app/components/base/app-unavailable'
import cn from '@/utils/classnames'
import useDocumentTitle from '@/hooks/use-document-title'
+import { useTranslation } from 'react-i18next'
+import { usePathname, useRouter, useSearchParams } from 'next/navigation'
type ChatWithHistoryProps = {
className?: string
@@ -38,6 +42,7 @@ const ChatWithHistory: FC
= ({
isMobile,
themeBuilder,
sidebarCollapseState,
+ isInstalledApp,
} = useChatWithHistoryContext()
const isSidebarCollapsed = sidebarCollapseState
const customConfig = appData?.custom_config
@@ -51,13 +56,34 @@ const ChatWithHistory: FC = ({
useDocumentTitle(site?.title || 'Chat')
+ const { t } = useTranslation()
+ const searchParams = useSearchParams()
+ const router = useRouter()
+ const pathname = usePathname()
+ const getSigninUrl = useCallback(() => {
+ const params = new URLSearchParams(searchParams)
+ params.delete('message')
+ params.set('redirect_url', pathname)
+ return `/webapp-signin?${params.toString()}`
+ }, [searchParams, pathname])
+
+ const backToHome = useCallback(() => {
+ removeAccessToken()
+ const url = getSigninUrl()
+ router.replace(url)
+ }, [getSigninUrl, router])
+
if (appInfoLoading) {
return (
)
}
- if (!userCanAccess)
- return
+ if (!userCanAccess) {
+ return
+
+ {!isInstalledApp &&
{t('common.userProfile.logout')}}
+
+ }
if (appInfoError) {
return (
diff --git a/web/app/components/base/chat/embedded-chatbot/index.tsx b/web/app/components/base/chat/embedded-chatbot/index.tsx
index 002d142542..c54afd78ea 100644
--- a/web/app/components/base/chat/embedded-chatbot/index.tsx
+++ b/web/app/components/base/chat/embedded-chatbot/index.tsx
@@ -1,4 +1,6 @@
+'use client'
import {
+ useCallback,
useEffect,
useState,
} from 'react'
@@ -12,7 +14,7 @@ import { useEmbeddedChatbot } from './hooks'
import { isDify } from './utils'
import { useThemeContext } from './theme/theme-context'
import { CssTransform } from './theme/utils'
-import { checkOrSetAccessToken } from '@/app/components/share/utils'
+import { checkOrSetAccessToken, removeAccessToken } from '@/app/components/share/utils'
import AppUnavailable from '@/app/components/base/app-unavailable'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import Loading from '@/app/components/base/loading'
@@ -23,6 +25,7 @@ import DifyLogo from '@/app/components/base/logo/dify-logo'
import cn from '@/utils/classnames'
import useDocumentTitle from '@/hooks/use-document-title'
import { useGlobalPublicStore } from '@/context/global-public-context'
+import { usePathname, useRouter, useSearchParams } from 'next/navigation'
const Chatbot = () => {
const {
@@ -36,6 +39,7 @@ const Chatbot = () => {
chatShouldReloadKey,
handleNewConversation,
themeBuilder,
+ isInstalledApp,
} = useEmbeddedChatbotContext()
const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
@@ -51,6 +55,22 @@ const Chatbot = () => {
useDocumentTitle(site?.title || 'Chat')
+ const searchParams = useSearchParams()
+ const router = useRouter()
+ const pathname = usePathname()
+ const getSigninUrl = useCallback(() => {
+ const params = new URLSearchParams(searchParams)
+ params.delete('message')
+ params.set('redirect_url', pathname)
+ return `/webapp-signin?${params.toString()}`
+ }, [searchParams, pathname])
+
+ const backToHome = useCallback(() => {
+ removeAccessToken()
+ const url = getSigninUrl()
+ router.replace(url)
+ }, [getSigninUrl, router])
+
if (appInfoLoading) {
return (
<>
@@ -66,8 +86,12 @@ const Chatbot = () => {
)
}
- if (!userCanAccess)
- return
+ if (!userCanAccess) {
+ return
+
+ {!isInstalledApp &&
{t('common.userProfile.logout')}}
+
+ }
if (appInfoError) {
return (
@@ -141,7 +165,6 @@ const EmbeddedChatbotWrapper = () => {
appInfoError,
appInfoLoading,
appData,
- accessMode,
userCanAccess,
appParams,
appMeta,
@@ -176,7 +199,6 @@ const EmbeddedChatbotWrapper = () => {
return = ({
)
+ const getSigninUrl = useCallback(() => {
+ const params = new URLSearchParams(searchParams)
+ params.delete('message')
+ params.set('redirect_url', pathname)
+ return `/webapp-signin?${params.toString()}`
+ }, [searchParams, pathname])
+
+ const backToHome = useCallback(() => {
+ removeAccessToken()
+ const url = getSigninUrl()
+ router.replace(url)
+ }, [getSigninUrl, router])
+
if (!appId || !siteInfo || !promptConfig || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission))) {
return (
)
}
- if (systemFeatures.webapp_auth.enabled && !userCanAccessResult?.result)
- return
+ if (systemFeatures.webapp_auth.enabled && !userCanAccessResult?.result) {
+ return
+
+ {!isInstalledApp &&
{t('common.userProfile.logout')}}
+
+ }
return (
{
- const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0]
-
- const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2())
- let accessTokenJson = getInitialTokenV2()
- try {
- accessTokenJson = JSON.parse(accessToken)
- if (isTokenV1(accessTokenJson))
- accessTokenJson = getInitialTokenV2()
- }
- catch {
-
- }
-
- localStorage.removeItem(CONVERSATION_ID_INFO)
+ localStorage.removeItem('token')
localStorage.removeItem('webapp_access_token')
-
- delete accessTokenJson[sharedToken]
- localStorage.setItem('token', JSON.stringify(accessTokenJson))
}
diff --git a/web/service/base.ts b/web/service/base.ts
index c3cafe600b..ba398c07a6 100644
--- a/web/service/base.ts
+++ b/web/service/base.ts
@@ -108,12 +108,13 @@ function unicodeToChar(text: string) {
})
}
-function requiredWebSSOLogin(message?: string) {
- removeAccessToken()
+function requiredWebSSOLogin(message?: string, code?: number) {
const params = new URLSearchParams()
params.append('redirect_url', globalThis.location.pathname)
if (message)
params.append('message', message)
+ if (code)
+ params.append('code', String(code))
globalThis.location.href = `/webapp-signin?${params.toString()}`
}
@@ -403,10 +404,12 @@ export const ssePost = async (
res.json().then((data: any) => {
if (isPublicAPI) {
if (data.code === 'web_app_access_denied')
- requiredWebSSOLogin(data.message)
+ requiredWebSSOLogin(data.message, 403)
- if (data.code === 'web_sso_auth_required')
+ if (data.code === 'web_sso_auth_required') {
+ removeAccessToken()
requiredWebSSOLogin()
+ }
if (data.code === 'unauthorized') {
removeAccessToken()
@@ -484,10 +487,11 @@ export const request = async
(url: string, options = {}, otherOptions?: IOther
const { code, message } = errRespData
// webapp sso
if (code === 'web_app_access_denied') {
- requiredWebSSOLogin(message)
+ requiredWebSSOLogin(message, 403)
return Promise.reject(err)
}
if (code === 'web_sso_auth_required') {
+ removeAccessToken()
requiredWebSSOLogin()
return Promise.reject(err)
}
From ab62a9662c9d56048d6414339c951fd9182bce9f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?=
Date: Mon, 9 Jun 2025 16:09:27 +0800
Subject: [PATCH 44/56] fix: some dark mode display incorrect (#20788)
---
web/app/components/base/chat/chat/chat-input-area/index.tsx | 2 +-
web/app/components/base/markdown-blocks/think-block.tsx | 4 ++--
.../nodes/_base/components/variable/var-reference-vars.tsx | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/web/app/components/base/chat/chat/chat-input-area/index.tsx b/web/app/components/base/chat/chat/chat-input-area/index.tsx
index 52490e4024..cbfa3168e9 100644
--- a/web/app/components/base/chat/chat/chat-input-area/index.tsx
+++ b/web/app/components/base/chat/chat/chat-input-area/index.tsx
@@ -192,7 +192,7 @@ const ChatInputArea = ({