From 3bead19f19ad85ef5fec91d7e15cb0ad95f89ce0 Mon Sep 17 00:00:00 2001 From: NFish Date: Wed, 2 Jul 2025 14:10:19 +0800 Subject: [PATCH 01/48] wip: enhance user experience by refining authorization verification process --- .../components/authenticated-layout.tsx | 30 +++++++ web/app/(shareLayout)/components/splash.tsx | 80 ++++++++++++++++++ web/app/(shareLayout)/layout.tsx | 57 ++----------- .../(shareLayout)/webapp-signin/layout.tsx | 9 +- .../webapp-signin/normalForm.tsx | 1 + web/app/(shareLayout)/webapp-signin/page.tsx | 83 ++----------------- .../(shareLayout)/workflow/[token]/page.tsx | 5 +- .../share/text-generation/index.tsx | 6 +- .../share/text-generation/menu-dropdown.tsx | 4 +- web/app/components/share/utils.ts | 2 +- web/context/global-public-context.tsx | 5 -- web/context/web-app-context.tsx | 74 +++++++++++++++++ web/i18n/en-US/login.ts | 1 + web/i18n/ja-JP/login.ts | 1 + web/i18n/zh-Hans/login.ts | 1 + web/service/base.ts | 4 +- web/service/use-share.ts | 12 ++- 17 files changed, 233 insertions(+), 142 deletions(-) create mode 100644 web/app/(shareLayout)/components/authenticated-layout.tsx create mode 100644 web/app/(shareLayout)/components/splash.tsx create mode 100644 web/context/web-app-context.tsx diff --git a/web/app/(shareLayout)/components/authenticated-layout.tsx b/web/app/(shareLayout)/components/authenticated-layout.tsx new file mode 100644 index 0000000000..7fe55c3060 --- /dev/null +++ b/web/app/(shareLayout)/components/authenticated-layout.tsx @@ -0,0 +1,30 @@ +'use client' + +import Loading from '@/app/components/base/loading' +import { useWebAppStore } from '@/context/web-app-context' +import React, { useEffect, useState } from 'react' + +const AuthenticatedLayout = ({ children }: { children: React.ReactNode }) => { + const shareCode = useWebAppStore(s => s.shareCode) + const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode) + const [isLoading, setIsLoading] = useState(true) + useEffect(() => { + (async () => { + try { + setIsLoading(true) + } + catch (error) { console.error(error) } + finally { + setIsLoading(false) + } + })() + }, [webAppAccessMode, shareCode]) + if (isLoading) { + return
+ +
+ } + return <>{children} +} + +export default React.memo(AuthenticatedLayout) diff --git a/web/app/(shareLayout)/components/splash.tsx b/web/app/(shareLayout)/components/splash.tsx new file mode 100644 index 0000000000..4fe9efe4dd --- /dev/null +++ b/web/app/(shareLayout)/components/splash.tsx @@ -0,0 +1,80 @@ +'use client' +import type { FC, PropsWithChildren } from 'react' +import { useEffect } from 'react' +import { useCallback } from 'react' +import { useWebAppStore } from '@/context/web-app-context' +import { useRouter, useSearchParams } from 'next/navigation' +import AppUnavailable from '@/app/components/base/app-unavailable' +import { checkOrSetAccessToken, removeAccessToken, setAccessToken } from '@/app/components/share/utils' +import { useTranslation } from 'react-i18next' +import { fetchAccessToken } from '@/service/share' +import Loading from '@/app/components/base/loading' +import { AccessMode } from '@/models/access-control' + +const Splash: FC = ({ children }) => { + const { t } = useTranslation() + const shareCode = useWebAppStore(s => s.shareCode) + const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode) + const searchParams = useSearchParams() + const router = useRouter() + 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]) + + const backToHome = useCallback(() => { + removeAccessToken() + const url = getSigninUrl() + router.replace(url) + }, [getSigninUrl, router]) + + useEffect(() => { + (async () => { + if (message) + return + if (shareCode && tokenFromUrl && redirectUrl) { + localStorage.setItem('webapp_access_token', tokenFromUrl) + const tokenResp = await fetchAccessToken({ appCode: shareCode, webAppAccessToken: tokenFromUrl }) + await setAccessToken(shareCode, tokenResp.access_token) + router.replace(decodeURIComponent(redirectUrl)) + return + } + if (shareCode && redirectUrl && localStorage.getItem('webapp_access_token')) { + const tokenResp = await fetchAccessToken({ appCode: shareCode, webAppAccessToken: localStorage.getItem('webapp_access_token') }) + await setAccessToken(shareCode, tokenResp.access_token) + router.replace(decodeURIComponent(redirectUrl)) + return + } + if (webAppAccessMode === AccessMode.PUBLIC && redirectUrl) { + await checkOrSetAccessToken(shareCode) + router.replace(decodeURIComponent(redirectUrl)) + } + })() + }, [shareCode, redirectUrl, router, tokenFromUrl, message, webAppAccessMode]) + + if (message) { + return
+ + {code === '403' ? t('common.userProfile.logout') : t('share.login.backToHome')} +
+ } + if (tokenFromUrl) { + return
+ +
+ } + if (webAppAccessMode === AccessMode.PUBLIC && redirectUrl) { + return
+ +
+ } + return <>{children} +} + +export default Splash diff --git a/web/app/(shareLayout)/layout.tsx b/web/app/(shareLayout)/layout.tsx index d057ba7599..5af913cac9 100644 --- a/web/app/(shareLayout)/layout.tsx +++ b/web/app/(shareLayout)/layout.tsx @@ -1,54 +1,15 @@ -'use client' -import React, { useEffect, useState } from 'react' -import type { FC } from 'react' -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' +import type { FC, PropsWithChildren } from 'react' +import WebAppStoreProvider from '@/context/web-app-context' +import Splash from './components/splash' -const Layout: FC<{ - children: React.ReactNode -}> = ({ children }) => { - const isGlobalPending = useGlobalPublicStore(s => s.isGlobalPending) - const setWebAppAccessMode = useGlobalPublicStore(s => s.setWebAppAccessMode) - const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) - const pathname = usePathname() - const searchParams = useSearchParams() - const redirectUrl = searchParams.get('redirect_url') - const [isLoading, setIsLoading] = useState(true) - useEffect(() => { - (async () => { - if (!isGlobalPending && !systemFeatures.webapp_auth.enabled) { - setIsLoading(false) - return - } - - let appCode: string | null = null - if (redirectUrl) { - const url = new URL(`${window.location.origin}${decodeURIComponent(redirectUrl)}`) - appCode = url.pathname.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, isGlobalPending, systemFeatures.webapp_auth.enabled]) - if (isLoading || isGlobalPending) { - return
- -
- } +const Layout: FC = ({ children }) => { return (
- {children} + + + {children} + +
) } diff --git a/web/app/(shareLayout)/webapp-signin/layout.tsx b/web/app/(shareLayout)/webapp-signin/layout.tsx index a03364d326..7649982072 100644 --- a/web/app/(shareLayout)/webapp-signin/layout.tsx +++ b/web/app/(shareLayout)/webapp-signin/layout.tsx @@ -3,10 +3,13 @@ import cn from '@/utils/classnames' import { useGlobalPublicStore } from '@/context/global-public-context' import useDocumentTitle from '@/hooks/use-document-title' +import type { PropsWithChildren } from 'react' +import { useTranslation } from 'react-i18next' -export default function SignInLayout({ children }: any) { - const { systemFeatures } = useGlobalPublicStore() - useDocumentTitle('') +export default function SignInLayout({ children }: PropsWithChildren) { + const { t } = useTranslation() + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) + useDocumentTitle(t('login.webapp.login')) return <>
diff --git a/web/app/(shareLayout)/webapp-signin/normalForm.tsx b/web/app/(shareLayout)/webapp-signin/normalForm.tsx index d6bdf607ba..44006a9f1e 100644 --- a/web/app/(shareLayout)/webapp-signin/normalForm.tsx +++ b/web/app/(shareLayout)/webapp-signin/normalForm.tsx @@ -1,3 +1,4 @@ +'use client' import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import Link from 'next/link' diff --git a/web/app/(shareLayout)/webapp-signin/page.tsx b/web/app/(shareLayout)/webapp-signin/page.tsx index 967516c416..1c6209b902 100644 --- a/web/app/(shareLayout)/webapp-signin/page.tsx +++ b/web/app/(shareLayout)/webapp-signin/page.tsx @@ -1,36 +1,30 @@ 'use client' import { useRouter, useSearchParams } from 'next/navigation' import type { FC } from 'react' -import React, { useCallback, useEffect } from 'react' +import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' -import Toast from '@/app/components/base/toast' -import { removeAccessToken, setAccessToken } from '@/app/components/share/utils' +import { removeAccessToken } from '@/app/components/share/utils' import { useGlobalPublicStore } from '@/context/global-public-context' -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' +import { useWebAppStore } from '@/context/web-app-context' const WebSSOForm: FC = () => { const { t } = useTranslation() const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) - const webAppAccessMode = useGlobalPublicStore(s => s.webAppAccessMode) + const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode) const searchParams = useSearchParams() const router = useRouter() 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') + const params = new URLSearchParams() + params.append('redirect_url', redirectUrl || '') return `/webapp-signin?${params.toString()}` - }, [searchParams]) + }, [redirectUrl]) const backToHome = useCallback(() => { removeAccessToken() @@ -38,73 +32,12 @@ const WebSSOForm: FC = () => { router.replace(url) }, [getSigninUrl, router]) - const showErrorToast = (msg: string) => { - Toast.notify({ - type: 'error', - message: msg, - }) - } - - const getAppCodeFromRedirectUrl = useCallback(() => { - if (!redirectUrl) - return null - const url = new URL(`${window.location.origin}${decodeURIComponent(redirectUrl)}`) - const appCode = url.pathname.split('/').pop() - if (!appCode) - return null - - return appCode - }, [redirectUrl]) - - useEffect(() => { - (async () => { - if (message) - return - - 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(decodeURIComponent(redirectUrl)) - return - } - 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(decodeURIComponent(redirectUrl)) - } - })() - }, [getAppCodeFromRedirectUrl, redirectUrl, router, tokenFromUrl, message]) - - useEffect(() => { - if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC && redirectUrl) - router.replace(decodeURIComponent(redirectUrl)) - }, [webAppAccessMode, router, redirectUrl]) - - if (tokenFromUrl) { - return
- -
- } - - if (message) { - return
- - {code === '403' ? t('common.userProfile.logout') : t('share.login.backToHome')} -
- } if (!redirectUrl) { - showErrorToast('redirect url is invalid.') return
} - if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC) { - return
- -
- } + if (!systemFeatures.webapp_auth.enabled) { return

{t('login.webapp.disabled')}

diff --git a/web/app/(shareLayout)/workflow/[token]/page.tsx b/web/app/(shareLayout)/workflow/[token]/page.tsx index e93bc8c1af..4f5923e91f 100644 --- a/web/app/(shareLayout)/workflow/[token]/page.tsx +++ b/web/app/(shareLayout)/workflow/[token]/page.tsx @@ -1,10 +1,13 @@ import React from 'react' import Main from '@/app/components/share/text-generation' +import AuthenticatedLayout from '../../components/authenticated-layout' const Workflow = () => { return ( -
+ +
+ ) } diff --git a/web/app/components/share/text-generation/index.tsx b/web/app/components/share/text-generation/index.tsx index 4be6b18958..6397b57785 100644 --- a/web/app/components/share/text-generation/index.tsx +++ b/web/app/components/share/text-generation/index.tsx @@ -9,7 +9,7 @@ import { import { useBoolean } from 'ahooks' import { usePathname, useRouter, useSearchParams } from 'next/navigation' import TabHeader from '../../base/tab-header' -import { checkOrSetAccessToken, removeAccessToken } from '../utils' +import { removeAccessToken } from '../utils' import MenuDropdown from './menu-dropdown' import RunBatch from './run-batch' import ResDownload from './run-batch/res-download' @@ -376,8 +376,8 @@ const TextGeneration: FC = ({ } const fetchInitData = async () => { - if (!isInstalledApp) - await checkOrSetAccessToken() + // if (!isInstalledApp) + // await checkOrSetAccessToken() return Promise.all([ isInstalledApp diff --git a/web/app/components/share/text-generation/menu-dropdown.tsx b/web/app/components/share/text-generation/menu-dropdown.tsx index adb926c7ca..1c1b6adfe8 100644 --- a/web/app/components/share/text-generation/menu-dropdown.tsx +++ b/web/app/components/share/text-generation/menu-dropdown.tsx @@ -18,8 +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' +import { useWebAppStore } from '@/context/web-app-context' type Props = { data?: SiteInfo @@ -32,7 +32,7 @@ const MenuDropdown: FC = ({ placement, hideLogout, }) => { - const webAppAccessMode = useGlobalPublicStore(s => s.webAppAccessMode) + const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode) const router = useRouter() const pathname = usePathname() const { t } = useTranslation() diff --git a/web/app/components/share/utils.ts b/web/app/components/share/utils.ts index 8a897ab59a..0c6457fb0c 100644 --- a/web/app/components/share/utils.ts +++ b/web/app/components/share/utils.ts @@ -10,7 +10,7 @@ export const getInitialTokenV2 = (): Record => ({ version: 2, }) -export const checkOrSetAccessToken = async (appCode?: string) => { +export const checkOrSetAccessToken = async (appCode?: string | null) => { const sharedToken = appCode || globalThis.location.pathname.split('/').slice(-1)[0] const userId = (await getProcessedSystemVariablesFromUrlParams()).user_id const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2()) diff --git a/web/context/global-public-context.tsx b/web/context/global-public-context.tsx index 26ad84be65..324ac019c8 100644 --- a/web/context/global-public-context.tsx +++ b/web/context/global-public-context.tsx @@ -7,15 +7,12 @@ 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 = { isGlobalPending: boolean setIsGlobalPending: (isPending: boolean) => void systemFeatures: SystemFeatures setSystemFeatures: (systemFeatures: SystemFeatures) => void - webAppAccessMode: AccessMode, - setWebAppAccessMode: (webAppAccessMode: AccessMode) => void } export const useGlobalPublicStore = create(set => ({ @@ -23,8 +20,6 @@ export const useGlobalPublicStore = create(set => ({ setIsGlobalPending: (isPending: boolean) => set(() => ({ isGlobalPending: isPending })), systemFeatures: defaultSystemFeatures, setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })), - webAppAccessMode: AccessMode.PUBLIC, - setWebAppAccessMode: (webAppAccessMode: AccessMode) => set(() => ({ webAppAccessMode })), })) const GlobalPublicStoreProvider: FC = ({ diff --git a/web/context/web-app-context.tsx b/web/context/web-app-context.tsx new file mode 100644 index 0000000000..28a3312b0d --- /dev/null +++ b/web/context/web-app-context.tsx @@ -0,0 +1,74 @@ +'use client' + +import Loading from '@/app/components/base/loading' +import { AccessMode } from '@/models/access-control' +import { useAppAccessModeByCode } from '@/service/use-share' +import type { App } from '@/types/app' +import { usePathname, useSearchParams } from 'next/navigation' +import type { FC, PropsWithChildren } from 'react' +import { useEffect } from 'react' +import { useState } from 'react' +import { create } from 'zustand' + +type WebAppStore = { + shareCode: string | null + updateShareCode: (shareCode: string | null) => void + appInfo: App | null + updateAppInfo: (appInfo: App | null) => void + webAppAccessMode: AccessMode + updateWebAppAccessMode: (accessMode: AccessMode) => void +} + +export const useWebAppStore = create(set => ({ + shareCode: null, + updateShareCode: (shareCode: string | null) => set(() => ({ shareCode })), + appInfo: null, + updateAppInfo: (appInfo: App | null) => set(() => ({ appInfo })), + webAppAccessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS, + updateWebAppAccessMode: (accessMode: AccessMode) => set(() => ({ webAppAccessMode: accessMode })), +})) + +const getShareCodeFromRedirectUrl = (redirectUrl: string | null): string | null => { + if (!redirectUrl || redirectUrl.length === 0) + return null + const url = new URL(`${window.location.origin}${decodeURIComponent(redirectUrl)}`) + return url.pathname.split('/').pop() || null +} +const getShareCodeFromPathname = (pathname: string): string | null => { + const code = pathname.split('/').pop() || null + if (code === 'webapp-signin') + return null + return code +} + +const WebAppStoreProvider: FC = ({ children }) => { + const updateWebAppAccessMode = useWebAppStore(state => state.updateWebAppAccessMode) + const updateShareCode = useWebAppStore(state => state.updateShareCode) + const pathname = usePathname() + const searchParams = useSearchParams() + const redirectUrlParam = searchParams.get('redirect_url') + const [shareCode, setShareCode] = useState(null) + useEffect(() => { + const shareCodeFromRedirect = getShareCodeFromRedirectUrl(redirectUrlParam) + const shareCodeFromPathname = getShareCodeFromPathname(pathname) + const newShareCode = shareCodeFromRedirect || shareCodeFromPathname + setShareCode(newShareCode) + updateShareCode(newShareCode) + }, [pathname, redirectUrlParam, updateShareCode]) + const { isFetching, data: accessModeResult } = useAppAccessModeByCode(shareCode) + useEffect(() => { + if (accessModeResult?.accessMode) + updateWebAppAccessMode(accessModeResult.accessMode) + }, [accessModeResult, updateWebAppAccessMode]) + if (isFetching) { + return
+ +
+ } + return ( + <> + {children} + + ) +} +export default WebAppStoreProvider diff --git a/web/i18n/en-US/login.ts b/web/i18n/en-US/login.ts index 0beb631d24..d47eb7c079 100644 --- a/web/i18n/en-US/login.ts +++ b/web/i18n/en-US/login.ts @@ -105,6 +105,7 @@ const translation = { licenseInactive: 'License Inactive', licenseInactiveTip: 'The Dify Enterprise license for your workspace is inactive. Please contact your administrator to continue using Dify.', webapp: { + login: 'Login', noLoginMethod: 'Authentication method not configured for web app', noLoginMethodTip: 'Please contact the system admin to add an authentication method.', disabled: 'Webapp authentication is disabled. Please contact the system admin to enable it. You can try to use the app directly.', diff --git a/web/i18n/ja-JP/login.ts b/web/i18n/ja-JP/login.ts index 84ab9eecd0..b37700eba2 100644 --- a/web/i18n/ja-JP/login.ts +++ b/web/i18n/ja-JP/login.ts @@ -106,6 +106,7 @@ const translation = { licenseExpired: 'ライセンスの有効期限が切れています', licenseLostTip: 'Dify ライセンスサーバーへの接続に失敗しました。続けて Dify を使用するために管理者に連絡してください。', webapp: { + login: 'ログイン', noLoginMethod: 'Web アプリに対して認証方法が構成されていません', noLoginMethodTip: 'システム管理者に連絡して、認証方法を追加してください。', disabled: 'Web アプリの認証が無効になっています。システム管理者に連絡して有効にしてください。直接アプリを使用してみてください。', diff --git a/web/i18n/zh-Hans/login.ts b/web/i18n/zh-Hans/login.ts index a37fc104eb..b63630e288 100644 --- a/web/i18n/zh-Hans/login.ts +++ b/web/i18n/zh-Hans/login.ts @@ -106,6 +106,7 @@ const translation = { licenseInactive: '许可证未激活', licenseInactiveTip: '您所在空间的 Dify Enterprise 许可证尚未激活,请联系管理员以继续使用 Dify。', webapp: { + login: '登录', noLoginMethod: 'Web 应用未配置身份认证方式', noLoginMethodTip: '请联系系统管理员添加身份认证方式', disabled: 'Web 应用身份认证已禁用,请联系系统管理员启用。您也可以尝试直接使用应用。', diff --git a/web/service/base.ts b/web/service/base.ts index 49377be912..8ffacaa0f1 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -413,7 +413,7 @@ export const ssePost = async ( if (data.code === 'unauthorized') { removeAccessToken() - globalThis.location.reload() + requiredWebSSOLogin() } } }) @@ -507,7 +507,7 @@ export const request = async(url: string, options = {}, otherOptions?: IOther } = otherOptionsForBaseFetch if (isPublicAPI && code === 'unauthorized') { removeAccessToken() - globalThis.location.reload() + requiredWebSSOLogin() return Promise.reject(err) } if (code === 'init_validate_failed' && IS_CE_EDITION && !silent) { diff --git a/web/service/use-share.ts b/web/service/use-share.ts index b8f96f6cc5..46181c4878 100644 --- a/web/service/use-share.ts +++ b/web/service/use-share.ts @@ -1,14 +1,22 @@ +import { useGlobalPublicStore } from '@/context/global-public-context' +import { AccessMode } from '@/models/access-control' import { useQuery } from '@tanstack/react-query' import { getAppAccessModeByAppCode } from './share' const NAME_SPACE = 'webapp' export const useAppAccessModeByCode = (code: string | null) => { + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) return useQuery({ queryKey: [NAME_SPACE, 'appAccessMode', code], queryFn: () => { - if (!code) - return null + if (systemFeatures.webapp_auth.enabled === false) { + return { + accessMode: AccessMode.PUBLIC, + } + } + if (!code || code.length === 0) + return Promise.reject(new Error('App code is required to get access mode')) return getAppAccessModeByAppCode(code) }, From 434c80986690cf8118a2d1e3ed231d9a60e42d36 Mon Sep 17 00:00:00 2001 From: NFish Date: Tue, 8 Jul 2025 14:24:54 +0800 Subject: [PATCH 02/48] don't add search params when opening detail links from marketplace. --- .../components/plugins/marketplace/list/card-wrapper.tsx | 4 ++-- web/app/components/plugins/marketplace/utils.ts | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/web/app/components/plugins/marketplace/list/card-wrapper.tsx b/web/app/components/plugins/marketplace/list/card-wrapper.tsx index 5e4b621796..8d9f3a5b10 100644 --- a/web/app/components/plugins/marketplace/list/card-wrapper.tsx +++ b/web/app/components/plugins/marketplace/list/card-wrapper.tsx @@ -1,7 +1,7 @@ 'use client' import { useTheme } from 'next-themes' import { RiArrowRightUpLine } from '@remixicon/react' -import { getPluginLinkInMarketplace } from '../utils' +import { getPluginDetailLinkInMarketplace, getPluginLinkInMarketplace } from '../utils' import Card from '@/app/components/plugins/card' import CardMoreInfo from '@/app/components/plugins/card/card-more-info' import type { Plugin } from '@/app/components/plugins/types' @@ -83,7 +83,7 @@ const CardWrapper = ({ return ( { + if (plugin.type === 'bundle') + return `/bundles/${plugin.org}/${plugin.name}` + return `/plugins/${plugin.org}/${plugin.name}` +} + export const getMarketplacePluginsByCollectionId = async (collectionId: string, query?: CollectionsAndPluginsSearchParams) => { let plugins = [] as Plugin[] From e4b6c33bddaac83fbb9ec207d7353fa3d26f09d5 Mon Sep 17 00:00:00 2001 From: NFish Date: Tue, 8 Jul 2025 19:55:46 +0800 Subject: [PATCH 03/48] wip: Adjust the authentication process to improve user experience --- .../components/authenticated-layout.tsx | 82 ++++++++++++++--- web/app/components/base/chat/types.ts | 10 +++ .../share/text-generation/index.tsx | 90 ++++--------------- web/context/web-app-context.tsx | 25 ++++-- web/models/share.ts | 2 +- web/service/access-control.ts | 16 ++-- web/service/use-share.ts | 31 ++++++- 7 files changed, 155 insertions(+), 101 deletions(-) diff --git a/web/app/(shareLayout)/components/authenticated-layout.tsx b/web/app/(shareLayout)/components/authenticated-layout.tsx index 7fe55c3060..9fbda545ff 100644 --- a/web/app/(shareLayout)/components/authenticated-layout.tsx +++ b/web/app/(shareLayout)/components/authenticated-layout.tsx @@ -1,25 +1,81 @@ 'use client' +import AppUnavailable from '@/app/components/base/app-unavailable' import Loading from '@/app/components/base/loading' +import { removeAccessToken } from '@/app/components/share/utils' import { useWebAppStore } from '@/context/web-app-context' -import React, { useEffect, useState } from 'react' +import { useGetUserCanAccessApp } from '@/service/access-control' +import { useGetWebAppInfo, useGetWebAppMeta, useGetWebAppParams } from '@/service/use-share' +import { usePathname, useRouter, useSearchParams } from 'next/navigation' +import React, { useCallback, useEffect } from 'react' +import { useTranslation } from 'react-i18next' const AuthenticatedLayout = ({ children }: { children: React.ReactNode }) => { + const { t } = useTranslation() const shareCode = useWebAppStore(s => s.shareCode) const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode) - const [isLoading, setIsLoading] = useState(true) + const updateAppInfo = useWebAppStore(s => s.updateAppInfo) + const updateAppParams = useWebAppStore(s => s.updateAppParams) + const updateWebAppMeta = useWebAppStore(s => s.updateWebAppMeta) + const updateUserCanAccessApp = useWebAppStore(s => s.updateUserCanAccessApp) + const { isFetching: isFetchingAppParams, data: appParams, error: appParamsError } = useGetWebAppParams() + const { isFetching: isFetchingAppInfo, data: appInfo, error: appInfoError } = useGetWebAppInfo() + const { isFetching: isFetchingAppMeta, data: appMeta, error: appMetaError } = useGetWebAppMeta() + const { isFetching: isFetchingUserCanAccessApp, data: userCanAccessApp, error: useCanAccessAppError } = useGetUserCanAccessApp({ appId: appInfo?.app_id, isInstalledApp: false }) + useEffect(() => { - (async () => { - try { - setIsLoading(true) - } - catch (error) { console.error(error) } - finally { - setIsLoading(false) - } - })() - }, [webAppAccessMode, shareCode]) - if (isLoading) { + if (appInfo) + updateAppInfo(appInfo) + if (appParams) + updateAppParams(appParams) + if (appMeta) + updateWebAppMeta(appMeta) + updateUserCanAccessApp(Boolean(userCanAccessApp && userCanAccessApp?.result)) + }, [appInfo, appMeta, appParams, updateAppInfo, updateAppParams, updateUserCanAccessApp, updateWebAppMeta, userCanAccessApp]) + + const router = useRouter() + const pathname = usePathname() + const searchParams = useSearchParams() + 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 (appInfoError) { + return
+ +
+ } + if (appParamsError) { + return
+ +
+ } + if (appMetaError) { + return
+ +
+ } + if (useCanAccessAppError) { + return
+ +
+ } + if (userCanAccessApp && !userCanAccessApp.result) { + return
+ + {t('common.userProfile.logout')} +
+ } + if (isFetchingAppInfo || isFetchingAppParams || isFetchingAppMeta || isFetchingUserCanAccessApp) { return
diff --git a/web/app/components/base/chat/types.ts b/web/app/components/base/chat/types.ts index 91f9bc976b..c463879a53 100644 --- a/web/app/components/base/chat/types.ts +++ b/web/app/components/base/chat/types.ts @@ -49,6 +49,16 @@ export type ChatConfig = Omit & { questionEditEnable?: boolean supportFeedback?: boolean supportCitationHitInfo?: boolean + system_parameters: { + audio_file_size_limit: number + file_size_limit: number + image_file_size_limit: number + video_file_size_limit: number + workflow_file_upload_limit: number + } + more_like_this: { + enabled: boolean + } } export type WorkflowProcess = { diff --git a/web/app/components/share/text-generation/index.tsx b/web/app/components/share/text-generation/index.tsx index 6397b57785..9b2d2d38a6 100644 --- a/web/app/components/share/text-generation/index.tsx +++ b/web/app/components/share/text-generation/index.tsx @@ -7,16 +7,14 @@ import { RiErrorWarningFill, } from '@remixicon/react' import { useBoolean } from 'ahooks' -import { usePathname, useRouter, useSearchParams } from 'next/navigation' +import { useSearchParams } from 'next/navigation' import TabHeader from '../../base/tab-header' -import { removeAccessToken } from '../utils' import MenuDropdown from './menu-dropdown' import RunBatch from './run-batch' import ResDownload from './run-batch/res-download' -import AppUnavailable from '../../base/app-unavailable' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import RunOnce from '@/app/components/share/text-generation/run-once' -import { fetchSavedMessage as doFetchSavedMessage, fetchAppInfo, fetchAppParams, removeMessage, saveMessage } from '@/service/share' +import { fetchSavedMessage as doFetchSavedMessage, removeMessage, saveMessage } from '@/service/share' import type { SiteInfo } from '@/models/share' import type { MoreLikeThisConfig, @@ -39,10 +37,10 @@ import { Resolution, TransferMethod } from '@/types/app' import { useAppFavicon } from '@/hooks/use-app-favicon' import DifyLogo from '@/app/components/base/logo/dify-logo' import cn from '@/utils/classnames' -import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control' import { AccessMode } from '@/models/access-control' import { useGlobalPublicStore } from '@/context/global-public-context' import useDocumentTitle from '@/hooks/use-document-title' +import { useWebAppStore } from '@/context/web-app-context' const GROUP_SIZE = 5 // to avoid RPM(Request per minute) limit. The group task finished then the next group. enum TaskStatus { @@ -83,9 +81,6 @@ const TextGeneration: FC = ({ const mode = searchParams.get('mode') || 'create' const [currentTab, setCurrentTab] = useState(['create', 'batch'].includes(mode) ? mode : 'create') - const router = useRouter() - const pathname = usePathname() - // Notice this situation isCallBatchAPI but not in batch tab const [isCallBatchAPI, setIsCallBatchAPI] = useState(false) const isInBatchTab = currentTab === 'batch' @@ -103,23 +98,12 @@ const TextGeneration: FC = ({ const [moreLikeThisConfig, setMoreLikeThisConfig] = useState(null) const [textToSpeechConfig, setTextToSpeechConfig] = useState(null) - const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({ - appId, - isInstalledApp, - enabled: systemFeatures.webapp_auth.enabled, - }) - const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ - appId, - isInstalledApp, - enabled: systemFeatures.webapp_auth.enabled, - }) - // save message const [savedMessages, setSavedMessages] = useState([]) - const fetchSavedMessage = async () => { + const fetchSavedMessage = useCallback(async () => { const res: any = await doFetchSavedMessage(isInstalledApp, installedAppInfo?.id) setSavedMessages(res.data) - } + }, [isInstalledApp, installedAppInfo?.id]) const handleSaveMessage = async (messageId: string) => { await saveMessage(messageId, isInstalledApp, installedAppInfo?.id) notify({ type: 'success', message: t('common.api.saved') }) @@ -375,34 +359,14 @@ const TextGeneration: FC = ({ } } - const fetchInitData = async () => { - // if (!isInstalledApp) - // await checkOrSetAccessToken() - - return Promise.all([ - isInstalledApp - ? { - app_id: installedAppInfo?.id, - site: { - title: installedAppInfo?.app.name, - prompt_public: false, - copyright: '', - icon: installedAppInfo?.app.icon, - icon_background: installedAppInfo?.app.icon_background, - }, - plan: 'basic', - } - : fetchAppInfo(), - fetchAppParams(isInstalledApp, installedAppInfo?.id), - !isWorkflow - ? fetchSavedMessage() - : {}, - ]) - } - + const appData = useWebAppStore(s => s.appInfo) + const appParams = useWebAppStore(s => s.appParams) + const accessMode = useWebAppStore(s => s.webAppAccessMode) useEffect(() => { (async () => { - const [appData, appParams]: any = await fetchInitData() + if (!appData || !appParams) + return + !isWorkflow && fetchSavedMessage() const { app_id: appId, site: siteInfo, custom_config } = appData setAppId(appId) setSiteInfo(siteInfo as SiteInfo) @@ -413,11 +377,11 @@ const TextGeneration: FC = ({ setVisionConfig({ // legacy of image upload compatible ...file_upload, - transfer_methods: file_upload.allowed_file_upload_methods || file_upload.allowed_upload_methods, + transfer_methods: file_upload?.allowed_file_upload_methods || file_upload?.allowed_upload_methods, // legacy of image upload compatible - image_file_size_limit: appParams?.system_parameters?.image_file_size_limit, + image_file_size_limit: appParams?.system_parameters.image_file_size_limit, fileUploadConfig: appParams?.system_parameters, - }) + } as any) const prompt_variables = userInputsFormToPromptVariables(user_input_form) setPromptConfig({ prompt_template: '', // placeholder for future @@ -426,7 +390,7 @@ const TextGeneration: FC = ({ setMoreLikeThisConfig(more_like_this) setTextToSpeechConfig(text_to_speech) })() - }, []) + }, [appData, appParams, fetchSavedMessage, isWorkflow]) // Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client. useDocumentTitle(siteInfo?.title || t('share.generation.title')) @@ -528,32 +492,12 @@ const TextGeneration: FC = ({
) - 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))) { + if (!appId || !siteInfo || !promptConfig) { return (
) } - if (systemFeatures.webapp_auth.enabled && !userCanAccessResult?.result) { - return
- - {!isInstalledApp && {t('common.userProfile.logout')}} -
- } - return (
= ({ imageUrl={siteInfo.icon_url} />
{siteInfo.title}
- +
{siteInfo.description && (
{siteInfo.description}
diff --git a/web/context/web-app-context.tsx b/web/context/web-app-context.tsx index 28a3312b0d..55f95e4811 100644 --- a/web/context/web-app-context.tsx +++ b/web/context/web-app-context.tsx @@ -1,9 +1,10 @@ 'use client' +import type { ChatConfig } from '@/app/components/base/chat/types' import Loading from '@/app/components/base/loading' import { AccessMode } from '@/models/access-control' -import { useAppAccessModeByCode } from '@/service/use-share' -import type { App } from '@/types/app' +import type { AppData, AppMeta } from '@/models/share' +import { useGetWebAppAccessModeByCode } from '@/service/use-share' import { usePathname, useSearchParams } from 'next/navigation' import type { FC, PropsWithChildren } from 'react' import { useEffect } from 'react' @@ -13,19 +14,31 @@ import { create } from 'zustand' type WebAppStore = { shareCode: string | null updateShareCode: (shareCode: string | null) => void - appInfo: App | null - updateAppInfo: (appInfo: App | null) => void + appInfo: AppData | null + updateAppInfo: (appInfo: AppData | null) => void + appParams: ChatConfig | null + updateAppParams: (appParams: ChatConfig | null) => void webAppAccessMode: AccessMode updateWebAppAccessMode: (accessMode: AccessMode) => void + appMeta: AppMeta | null + updateWebAppMeta: (appMeta: AppMeta | null) => void + userCanAccessApp: boolean + updateUserCanAccessApp: (canAccess: boolean) => void } export const useWebAppStore = create(set => ({ shareCode: null, updateShareCode: (shareCode: string | null) => set(() => ({ shareCode })), appInfo: null, - updateAppInfo: (appInfo: App | null) => set(() => ({ appInfo })), + updateAppInfo: (appInfo: AppData | null) => set(() => ({ appInfo })), + appParams: null, + updateAppParams: (appParams: ChatConfig | null) => set(() => ({ appParams })), webAppAccessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS, updateWebAppAccessMode: (accessMode: AccessMode) => set(() => ({ webAppAccessMode: accessMode })), + appMeta: null, + updateWebAppMeta: (appMeta: AppMeta | null) => set(() => ({ appMeta })), + userCanAccessApp: false, + updateUserCanAccessApp: (canAccess: boolean) => set(() => ({ userCanAccessApp: canAccess })), })) const getShareCodeFromRedirectUrl = (redirectUrl: string | null): string | null => { @@ -55,7 +68,7 @@ const WebAppStoreProvider: FC = ({ children }) => { setShareCode(newShareCode) updateShareCode(newShareCode) }, [pathname, redirectUrlParam, updateShareCode]) - const { isFetching, data: accessModeResult } = useAppAccessModeByCode(shareCode) + const { isFetching, data: accessModeResult } = useGetWebAppAccessModeByCode(shareCode) useEffect(() => { if (accessModeResult?.accessMode) updateWebAppAccessMode(accessModeResult.accessMode) diff --git a/web/models/share.ts b/web/models/share.ts index 3521365e82..b4fde6d89d 100644 --- a/web/models/share.ts +++ b/web/models/share.ts @@ -35,7 +35,7 @@ export type AppMeta = { export type AppData = { app_id: string can_replace_logo?: boolean - custom_config?: Record + custom_config: Record enable_site?: boolean end_user_id?: string site: SiteInfo diff --git a/web/service/access-control.ts b/web/service/access-control.ts index 36999bf8f3..07984bd57c 100644 --- a/web/service/access-control.ts +++ b/web/service/access-control.ts @@ -3,6 +3,7 @@ import { get, post } from './base' import { getAppAccessMode, getUserCanAccess } from './share' import type { AccessControlAccount, AccessControlGroup, AccessMode, Subject } from '@/models/access-control' import type { App } from '@/types/app' +import { useGlobalPublicStore } from '@/context/global-public-context' const NAME_SPACE = 'access-control' @@ -79,15 +80,18 @@ export const useGetAppAccessMode = ({ appId, isInstalledApp = true, enabled }: { }) } -export const useGetUserCanAccessApp = ({ appId, isInstalledApp = true, enabled }: { appId?: string; isInstalledApp?: boolean; enabled: boolean }) => { +export const useGetUserCanAccessApp = ({ appId, isInstalledApp = true }: { appId?: string; isInstalledApp?: boolean; }) => { + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) return useQuery({ queryKey: [NAME_SPACE, 'user-can-access-app', appId], - queryFn: () => getUserCanAccess(appId!, isInstalledApp), - enabled: !!appId && enabled, + queryFn: () => { + if (systemFeatures.webapp_auth.enabled) + return getUserCanAccess(appId!, isInstalledApp) + else + return { result: true } + }, + enabled: !!appId, staleTime: 0, gcTime: 0, - initialData: { - result: !enabled, - }, }) } diff --git a/web/service/use-share.ts b/web/service/use-share.ts index 46181c4878..63f18bf0e0 100644 --- a/web/service/use-share.ts +++ b/web/service/use-share.ts @@ -1,11 +1,11 @@ import { useGlobalPublicStore } from '@/context/global-public-context' import { AccessMode } from '@/models/access-control' import { useQuery } from '@tanstack/react-query' -import { getAppAccessModeByAppCode } from './share' +import { fetchAppInfo, fetchAppMeta, fetchAppParams, getAppAccessModeByAppCode } from './share' const NAME_SPACE = 'webapp' -export const useAppAccessModeByCode = (code: string | null) => { +export const useGetWebAppAccessModeByCode = (code: string | null) => { const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) return useQuery({ queryKey: [NAME_SPACE, 'appAccessMode', code], @@ -23,3 +23,30 @@ export const useAppAccessModeByCode = (code: string | null) => { enabled: !!code, }) } + +export const useGetWebAppInfo = () => { + return useQuery({ + queryKey: [NAME_SPACE, 'appInfo'], + queryFn: () => { + return fetchAppInfo() + }, + }) +} + +export const useGetWebAppParams = () => { + return useQuery({ + queryKey: [NAME_SPACE, 'appParams'], + queryFn: () => { + return fetchAppParams(false) + }, + }) +} + +export const useGetWebAppMeta = () => { + return useQuery({ + queryKey: [NAME_SPACE, 'appMeta'], + queryFn: () => { + return fetchAppMeta(false) + }, + }) +} From 207d0f78f5fd18d309321dbe52c4ee55faf6f234 Mon Sep 17 00:00:00 2001 From: NFish Date: Wed, 9 Jul 2025 14:48:32 +0800 Subject: [PATCH 04/48] fix: allow update plugin install settings --- web/app/components/plugins/plugin-page/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/app/components/plugins/plugin-page/index.tsx b/web/app/components/plugins/plugin-page/index.tsx index bf2d327d31..94fd3fee9b 100644 --- a/web/app/components/plugins/plugin-page/index.tsx +++ b/web/app/components/plugins/plugin-page/index.tsx @@ -136,7 +136,7 @@ const PluginPage = ({ const options = usePluginPageContext(v => v.options) const activeTab = usePluginPageContext(v => v.activeTab) const setActiveTab = usePluginPageContext(v => v.setActiveTab) - const { enable_marketplace, branding } = useGlobalPublicStore(s => s.systemFeatures) + const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) const isPluginsTab = useMemo(() => activeTab === PLUGIN_PAGE_TABS_MAP.plugins, [activeTab]) const isExploringMarketplace = useMemo(() => { @@ -225,7 +225,7 @@ const PluginPage = ({ ) } { - canSetPermissions && !branding.enabled && ( + canSetPermissions && ( From 513af987290c16da1e1e15f59a310413dba69d83 Mon Sep 17 00:00:00 2001 From: NFish Date: Thu, 10 Jul 2025 17:27:12 +0800 Subject: [PATCH 05/48] perf: use component to handle webapp info fetch, authorization --- .../explore/installed/[appId]/page.tsx | 14 +-- web/app/(shareLayout)/chat/[token]/page.tsx | 5 +- .../(shareLayout)/chatbot/[token]/page.tsx | 5 +- .../(shareLayout)/completion/[token]/page.tsx | 5 +- .../base/chat/chat-with-history/context.tsx | 8 +- .../base/chat/chat-with-history/hooks.tsx | 22 +---- .../base/chat/chat-with-history/index.tsx | 50 +--------- .../base/chat/embedded-chatbot/index.tsx | 94 ------------------- .../explore/installed-app/index.tsx | 83 ++++++++++++++-- .../share/text-generation/index.tsx | 8 +- web/models/share.ts | 2 +- web/service/access-control.ts | 12 +-- web/service/explore.ts | 5 + web/service/share.ts | 7 -- web/service/use-explore.ts | 50 ++++++++++ 15 files changed, 166 insertions(+), 204 deletions(-) create mode 100644 web/service/use-explore.ts diff --git a/web/app/(commonLayout)/explore/installed/[appId]/page.tsx b/web/app/(commonLayout)/explore/installed/[appId]/page.tsx index 938a03992b..e288c62b5d 100644 --- a/web/app/(commonLayout)/explore/installed/[appId]/page.tsx +++ b/web/app/(commonLayout)/explore/installed/[appId]/page.tsx @@ -1,16 +1,18 @@ -import type { FC } from 'react' import React from 'react' import Main from '@/app/components/explore/installed-app' export type IInstalledAppProps = { - params: Promise<{ + params: { appId: string - }> + } } -const InstalledApp: FC = async ({ params }) => { +// Using Next.js page convention for async server components +async function InstalledApp({ params }: IInstalledAppProps) { + const appId = (await params).appId return ( -
+
) } -export default React.memo(InstalledApp) + +export default InstalledApp diff --git a/web/app/(shareLayout)/chat/[token]/page.tsx b/web/app/(shareLayout)/chat/[token]/page.tsx index 640c40378f..8ce67585f0 100644 --- a/web/app/(shareLayout)/chat/[token]/page.tsx +++ b/web/app/(shareLayout)/chat/[token]/page.tsx @@ -1,10 +1,13 @@ 'use client' import React from 'react' import ChatWithHistoryWrap from '@/app/components/base/chat/chat-with-history' +import AuthenticatedLayout from '../../components/authenticated-layout' const Chat = () => { return ( - + + + ) } diff --git a/web/app/(shareLayout)/chatbot/[token]/page.tsx b/web/app/(shareLayout)/chatbot/[token]/page.tsx index 6196afecc4..5323d0dacc 100644 --- a/web/app/(shareLayout)/chatbot/[token]/page.tsx +++ b/web/app/(shareLayout)/chatbot/[token]/page.tsx @@ -1,10 +1,13 @@ 'use client' import React from 'react' import EmbeddedChatbot from '@/app/components/base/chat/embedded-chatbot' +import AuthenticatedLayout from '../../components/authenticated-layout' const Chatbot = () => { return ( - + + + ) } diff --git a/web/app/(shareLayout)/completion/[token]/page.tsx b/web/app/(shareLayout)/completion/[token]/page.tsx index e8bc9d79f5..ae91338b9a 100644 --- a/web/app/(shareLayout)/completion/[token]/page.tsx +++ b/web/app/(shareLayout)/completion/[token]/page.tsx @@ -1,9 +1,12 @@ import React from 'react' import Main from '@/app/components/share/text-generation' +import AuthenticatedLayout from '../../components/authenticated-layout' const Completion = () => { return ( -
+ +
+ ) } diff --git a/web/app/components/base/chat/chat-with-history/context.tsx b/web/app/components/base/chat/chat-with-history/context.tsx index 5bf1514774..3a5dc793d6 100644 --- a/web/app/components/base/chat/chat-with-history/context.tsx +++ b/web/app/components/base/chat/chat-with-history/context.tsx @@ -18,11 +18,8 @@ import type { import { noop } from 'lodash-es' export type ChatWithHistoryContextValue = { - appInfoError?: any - appInfoLoading?: boolean - appMeta?: AppMeta - appData?: AppData - userCanAccess?: boolean + appMeta?: AppMeta | null + appData?: AppData | null appParams?: ChatConfig appChatListDataLoading?: boolean currentConversationId: string @@ -62,7 +59,6 @@ export type ChatWithHistoryContextValue = { } export const ChatWithHistoryContext = createContext({ - userCanAccess: false, currentConversationId: '', appPrevChatTree: [], pinnedConversationList: [], 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 32f74e6457..be935a70ba 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -21,9 +21,6 @@ import { addFileInfos, sortAgentSorts } from '../../../tools/utils' import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' import { delConversation, - fetchAppInfo, - fetchAppMeta, - fetchAppParams, fetchChatList, fetchConversations, generationConversationName, @@ -43,8 +40,7 @@ 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 { useGetUserCanAccessApp } from '@/service/access-control' -import { useGlobalPublicStore } from '@/context/global-public-context' +import { useWebAppStore } from '@/context/web-app-context' function getFormattedChatList(messages: any[]) { const newChatList: ChatItem[] = [] @@ -74,13 +70,9 @@ function getFormattedChatList(messages: any[]) { 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: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ - appId: installedAppInfo?.app.id || appInfo?.app_id, - isInstalledApp, - enabled: systemFeatures.webapp_auth.enabled, - }) + const appInfo = useWebAppStore(s => s.appInfo) + const appParams = useWebAppStore(s => s.appParams) + const appMeta = useWebAppStore(s => s.appMeta) useAppFavicon({ enable: !installedAppInfo, @@ -107,6 +99,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { use_icon_as_answer_icon: app.use_icon_as_answer_icon, }, plan: 'basic', + custom_config: null, } as AppData } @@ -166,8 +159,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { return currentConversationId }, [currentConversationId, newConversationId]) - const { data: appParams } = useSWR(['appParams', isInstalledApp, appId], () => fetchAppParams(isInstalledApp, appId)) - const { data: appMeta } = useSWR(['appMeta', isInstalledApp, appId], () => fetchAppMeta(isInstalledApp, appId)) const { data: appPinnedConversationData, mutate: mutateAppPinnedConversationData } = useSWR(['appConversationData', isInstalledApp, appId, true], () => fetchConversations(isInstalledApp, appId, undefined, true, 100)) const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100)) const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId)) @@ -485,9 +476,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { }, [isInstalledApp, appId, t, notify]) return { - appInfoError, - appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && isCheckingPermission), - userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true, isInstalledApp, appId, currentConversationId, 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 fe8e7b430d..cceb21b295 100644 --- a/web/app/components/base/chat/chat-with-history/index.tsx +++ b/web/app/components/base/chat/chat-with-history/index.tsx @@ -1,7 +1,6 @@ 'use client' import type { FC } from 'react' import { - useCallback, useEffect, useState, } from 'react' @@ -19,12 +18,10 @@ 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, removeAccessToken } from '@/app/components/share/utils' +import { checkOrSetAccessToken } 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 @@ -33,16 +30,12 @@ const ChatWithHistory: FC = ({ className, }) => { const { - userCanAccess, - appInfoError, appData, - appInfoLoading, appChatListDataLoading, chatShouldReloadKey, isMobile, themeBuilder, sidebarCollapseState, - isInstalledApp, } = useChatWithHistoryContext() const isSidebarCollapsed = sidebarCollapseState const customConfig = appData?.custom_config @@ -56,41 +49,6 @@ 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
- - {!isInstalledApp && {t('common.userProfile.logout')}} -
- } - - if (appInfoError) { - return ( - - ) - } - return (
= ({ const themeBuilder = useThemeContext() const { - appInfoError, - appInfoLoading, - userCanAccess, appData, appParams, appMeta, @@ -191,10 +146,7 @@ const ChatWithHistoryWrap: FC = ({ return ( { const { - userCanAccess, isMobile, allowResetChat, - appInfoError, - appInfoLoading, appData, appChatListDataLoading, chatShouldReloadKey, handleNewConversation, themeBuilder, - isInstalledApp, } = useEmbeddedChatbotContext() const { t } = useTranslation() const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) @@ -55,58 +45,6 @@ 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 ( - <> - {!isMobile && } - {isMobile && ( -
-
- -
-
- )} - - ) - } - - if (!userCanAccess) { - return
- - {!isInstalledApp && {t('common.userProfile.logout')}} -
- } - - if (appInfoError) { - return ( - <> - {!isMobile && } - {isMobile && ( -
-
- -
-
- )} - - ) - } return (
{ const themeBuilder = useThemeContext() const { - appInfoError, - appInfoLoading, appData, userCanAccess, appParams, @@ -200,8 +136,6 @@ const EmbeddedChatbotWrapper = () => { return { } const EmbeddedChatbot = () => { - const [initialized, setInitialized] = useState(false) - const [appUnavailable, setAppUnavailable] = useState(false) - const [isUnknownReason, setIsUnknownReason] = useState(false) - - useAsyncEffect(async () => { - if (!initialized) { - try { - await checkOrSetAccessToken() - } - catch (e: any) { - if (e.status === 404) { - setAppUnavailable(true) - } - else { - setIsUnknownReason(true) - setAppUnavailable(true) - } - } - setInitialized(true) - } - }, []) - - if (!initialized) - return null - - if (appUnavailable) - return - return } diff --git a/web/app/components/explore/installed-app/index.tsx b/web/app/components/explore/installed-app/index.tsx index 71013fc2e1..212b44934a 100644 --- a/web/app/components/explore/installed-app/index.tsx +++ b/web/app/components/explore/installed-app/index.tsx @@ -1,11 +1,17 @@ 'use client' import type { FC } from 'react' +import { useEffect } from 'react' import React from 'react' import { useContext } from 'use-context-selector' import ExploreContext from '@/context/explore-context' import TextGenerationApp from '@/app/components/share/text-generation' import Loading from '@/app/components/base/loading' import ChatWithHistory from '@/app/components/base/chat/chat-with-history' +import { useWebAppStore } from '@/context/web-app-context' +import AppUnavailable from '../../base/app-unavailable' +import { useGetUserCanAccessApp } from '@/service/access-control' +import { useGetInstalledAppAccessModeByAppId, useGetInstalledAppMeta, useGetInstalledAppParams } from '@/service/use-explore' +import type { AppData } from '@/models/share' export type IInstalledAppProps = { id: string @@ -15,14 +21,79 @@ const InstalledApp: FC = ({ id, }) => { const { installedApps } = useContext(ExploreContext) + const updateAppInfo = useWebAppStore(s => s.updateAppInfo) const installedApp = installedApps.find(item => item.id === id) + const updateWebAppAccessMode = useWebAppStore(s => s.updateWebAppAccessMode) + const updateAppParams = useWebAppStore(s => s.updateAppParams) + const updateWebAppMeta = useWebAppStore(s => s.updateWebAppMeta) + const updateUserCanAccessApp = useWebAppStore(s => s.updateUserCanAccessApp) + const { isFetching: isFetchingWebAppAccessMode, data: webAppAccessMode, error: webAppAccessModeError } = useGetInstalledAppAccessModeByAppId(installedApp?.id ?? null) + const { isFetching: isFetchingAppParams, data: appParams, error: appParamsError } = useGetInstalledAppParams(installedApp?.id ?? null) + const { isFetching: isFetchingAppMeta, data: appMeta, error: appMetaError } = useGetInstalledAppMeta(installedApp?.id ?? null) + const { data: userCanAccessApp, error: useCanAccessAppError } = useGetUserCanAccessApp({ appId: installedApp?.app.id, isInstalledApp: true }) - if (!installedApp) { - return ( -
- -
- ) + useEffect(() => { + if (!installedApp) { + updateAppInfo(null) + } + else { + const { id, app } = installedApp + updateAppInfo({ + app_id: id, + site: { + title: app.name, + icon_type: app.icon_type, + icon: app.icon, + icon_background: app.icon_background, + icon_url: app.icon_url, + prompt_public: false, + copyright: '', + show_workflow_steps: true, + use_icon_as_answer_icon: app.use_icon_as_answer_icon, + }, + plan: 'basic', + custom_config: null, + } as AppData) + } + + if (appParams) + updateAppParams(appParams) + if (appMeta) + updateWebAppMeta(appMeta) + if (webAppAccessMode) + updateWebAppAccessMode(webAppAccessMode.accessMode) + updateUserCanAccessApp(Boolean(userCanAccessApp && userCanAccessApp?.result)) + }, [installedApp, appMeta, appParams, updateAppInfo, updateAppParams, updateUserCanAccessApp, updateWebAppMeta, userCanAccessApp, webAppAccessMode, updateWebAppAccessMode]) + + if (appParamsError) { + return
+ +
+ } + if (appMetaError) { + return
+ +
+ } + if (useCanAccessAppError) { + return
+ +
+ } + if (webAppAccessModeError) { + return
+ +
+ } + if (userCanAccessApp && !userCanAccessApp.result) { + return
+ +
+ } + if (isFetchingAppParams || isFetchingAppMeta || isFetchingWebAppAccessMode || !installedApp) { + return
+ +
} return ( diff --git a/web/app/components/share/text-generation/index.tsx b/web/app/components/share/text-generation/index.tsx index 9b2d2d38a6..ae6e733e49 100644 --- a/web/app/components/share/text-generation/index.tsx +++ b/web/app/components/share/text-generation/index.tsx @@ -101,16 +101,16 @@ const TextGeneration: FC = ({ // save message const [savedMessages, setSavedMessages] = useState([]) const fetchSavedMessage = useCallback(async () => { - const res: any = await doFetchSavedMessage(isInstalledApp, installedAppInfo?.id) + const res: any = await doFetchSavedMessage(isInstalledApp, appId) setSavedMessages(res.data) - }, [isInstalledApp, installedAppInfo?.id]) + }, [isInstalledApp, appId]) const handleSaveMessage = async (messageId: string) => { - await saveMessage(messageId, isInstalledApp, installedAppInfo?.id) + await saveMessage(messageId, isInstalledApp, appId) notify({ type: 'success', message: t('common.api.saved') }) fetchSavedMessage() } const handleRemoveSavedMessage = async (messageId: string) => { - await removeMessage(messageId, isInstalledApp, installedAppInfo?.id) + await removeMessage(messageId, isInstalledApp, appId) notify({ type: 'success', message: t('common.api.remove') }) fetchSavedMessage() } diff --git a/web/models/share.ts b/web/models/share.ts index b4fde6d89d..1e3b6d6bb7 100644 --- a/web/models/share.ts +++ b/web/models/share.ts @@ -35,7 +35,7 @@ export type AppMeta = { export type AppData = { app_id: string can_replace_logo?: boolean - custom_config: Record + custom_config: Record | null enable_site?: boolean end_user_id?: string site: SiteInfo diff --git a/web/service/access-control.ts b/web/service/access-control.ts index 07984bd57c..d4cc9eb792 100644 --- a/web/service/access-control.ts +++ b/web/service/access-control.ts @@ -1,6 +1,6 @@ import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { get, post } from './base' -import { getAppAccessMode, getUserCanAccess } from './share' +import { getUserCanAccess } from './share' import type { AccessControlAccount, AccessControlGroup, AccessMode, Subject } from '@/models/access-control' import type { App } from '@/types/app' import { useGlobalPublicStore } from '@/context/global-public-context' @@ -70,16 +70,6 @@ export const useUpdateAccessMode = () => { }) } -export const useGetAppAccessMode = ({ appId, isInstalledApp = true, enabled }: { appId?: string; isInstalledApp?: boolean; enabled: boolean }) => { - return useQuery({ - queryKey: [NAME_SPACE, 'app-access-mode', appId], - queryFn: () => getAppAccessMode(appId!, isInstalledApp), - enabled: !!appId && enabled, - staleTime: 0, - gcTime: 0, - }) -} - export const useGetUserCanAccessApp = ({ appId, isInstalledApp = true }: { appId?: string; isInstalledApp?: boolean; }) => { const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) return useQuery({ diff --git a/web/service/explore.ts b/web/service/explore.ts index e9e17416d1..6a440d7f5d 100644 --- a/web/service/explore.ts +++ b/web/service/explore.ts @@ -1,5 +1,6 @@ import { del, get, patch, post } from './base' import type { App, AppCategory } from '@/models/explore' +import type { AccessMode } from '@/models/access-control' export const fetchAppList = () => { return get<{ @@ -39,3 +40,7 @@ export const updatePinStatus = (id: string, isPinned: boolean) => { export const getToolProviders = () => { return get('/workspaces/current/tool-providers') } + +export const getAppAccessModeByAppId = (appId: string) => { + return get<{ accessMode: AccessMode }>(`/enterprise/webapp/app/access-mode?appId=${appId}`) +} diff --git a/web/service/share.ts b/web/service/share.ts index 6a2a7e5b16..8c33b85522 100644 --- a/web/service/share.ts +++ b/web/service/share.ts @@ -296,13 +296,6 @@ export const fetchAccessToken = async ({ appCode, userId, webAppAccessToken }: { return get(url, { headers }) as Promise<{ access_token: string }> } -export const getAppAccessMode = (appId: string, isInstalledApp: boolean) => { - if (isInstalledApp) - return consoleGet<{ accessMode: AccessMode }>(`/enterprise/webapp/app/access-mode?appId=${appId}`) - - return get<{ accessMode: AccessMode }>(`/webapp/access-mode?appId=${appId}`) -} - export const getUserCanAccess = (appId: string, isInstalledApp: boolean) => { if (isInstalledApp) return consoleGet<{ result: boolean }>(`/enterprise/webapp/permission?appId=${appId}`) diff --git a/web/service/use-explore.ts b/web/service/use-explore.ts new file mode 100644 index 0000000000..1c892a041c --- /dev/null +++ b/web/service/use-explore.ts @@ -0,0 +1,50 @@ +import { useGlobalPublicStore } from '@/context/global-public-context' +import { AccessMode } from '@/models/access-control' +import { useQuery } from '@tanstack/react-query' +import { getAppAccessModeByAppId } from './explore' +import { fetchAppMeta, fetchAppParams } from './share' + +const NAME_SPACE = 'explore' + +export const useGetInstalledAppAccessModeByAppId = (appId: string | null) => { + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) + return useQuery({ + queryKey: [NAME_SPACE, 'appAccessMode', appId], + queryFn: () => { + if (systemFeatures.webapp_auth.enabled === false) { + return { + accessMode: AccessMode.PUBLIC, + } + } + if (!appId || appId.length === 0) + return Promise.reject(new Error('App code is required to get access mode')) + + return getAppAccessModeByAppId(appId) + }, + enabled: !!appId, + }) +} + +export const useGetInstalledAppParams = (appId: string | null) => { + return useQuery({ + queryKey: [NAME_SPACE, 'appParams', appId], + queryFn: () => { + if (!appId || appId.length === 0) + return Promise.reject(new Error('App ID is required to get app params')) + return fetchAppParams(true, appId) + }, + enabled: !!appId, + }) +} + +export const useGetInstalledAppMeta = (appId: string | null) => { + return useQuery({ + queryKey: [NAME_SPACE, 'appMeta', appId], + queryFn: () => { + if (!appId || appId.length === 0) + return Promise.reject(new Error('App ID is required to get app meta')) + return fetchAppMeta(true, appId) + }, + enabled: !!appId, + }) +} From 8ead8961cea3e2952ba305d034bfda8288192974 Mon Sep 17 00:00:00 2001 From: NFish Date: Thu, 10 Jul 2025 17:43:14 +0800 Subject: [PATCH 06/48] fix: check user can access web app in background task --- web/app/(shareLayout)/components/authenticated-layout.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/web/app/(shareLayout)/components/authenticated-layout.tsx b/web/app/(shareLayout)/components/authenticated-layout.tsx index 9fbda545ff..e3cfc8e6a8 100644 --- a/web/app/(shareLayout)/components/authenticated-layout.tsx +++ b/web/app/(shareLayout)/components/authenticated-layout.tsx @@ -12,8 +12,6 @@ import { useTranslation } from 'react-i18next' const AuthenticatedLayout = ({ children }: { children: React.ReactNode }) => { const { t } = useTranslation() - const shareCode = useWebAppStore(s => s.shareCode) - const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode) const updateAppInfo = useWebAppStore(s => s.updateAppInfo) const updateAppParams = useWebAppStore(s => s.updateAppParams) const updateWebAppMeta = useWebAppStore(s => s.updateWebAppMeta) @@ -21,7 +19,7 @@ const AuthenticatedLayout = ({ children }: { children: React.ReactNode }) => { const { isFetching: isFetchingAppParams, data: appParams, error: appParamsError } = useGetWebAppParams() const { isFetching: isFetchingAppInfo, data: appInfo, error: appInfoError } = useGetWebAppInfo() const { isFetching: isFetchingAppMeta, data: appMeta, error: appMetaError } = useGetWebAppMeta() - const { isFetching: isFetchingUserCanAccessApp, data: userCanAccessApp, error: useCanAccessAppError } = useGetUserCanAccessApp({ appId: appInfo?.app_id, isInstalledApp: false }) + const { data: userCanAccessApp, error: useCanAccessAppError } = useGetUserCanAccessApp({ appId: appInfo?.app_id, isInstalledApp: false }) useEffect(() => { if (appInfo) @@ -75,7 +73,7 @@ const AuthenticatedLayout = ({ children }: { children: React.ReactNode }) => { {t('common.userProfile.logout')}
} - if (isFetchingAppInfo || isFetchingAppParams || isFetchingAppMeta || isFetchingUserCanAccessApp) { + if (isFetchingAppInfo || isFetchingAppParams || isFetchingAppMeta) { return
From fe4e2f7921e4362f699cb07f2bcd75f5774632ba Mon Sep 17 00:00:00 2001 From: le0zh Date: Fri, 11 Jul 2025 15:07:32 +0800 Subject: [PATCH 07/48] feat: support var in suggested questions (#17340) Co-authored-by: crazywoola <427733928@qq.com> --- web/app/components/base/chat/chat/hooks.ts | 5 ++--- .../features/new-feature-panel/conversation-opener/modal.tsx | 1 + web/app/components/workflow/panel/debug-and-preview/hooks.ts | 4 ++-- web/i18n/de-DE/app-debug.ts | 1 + web/i18n/en-US/app-debug.ts | 1 + web/i18n/es-ES/app-debug.ts | 1 + web/i18n/fa-IR/app-debug.ts | 1 + web/i18n/fr-FR/app-debug.ts | 1 + web/i18n/hi-IN/app-debug.ts | 1 + web/i18n/it-IT/app-debug.ts | 1 + web/i18n/ja-JP/app-debug.ts | 1 + web/i18n/ko-KR/app-debug.ts | 1 + web/i18n/pl-PL/app-debug.ts | 1 + web/i18n/pt-BR/app-debug.ts | 1 + web/i18n/ru-RU/app-debug.ts | 1 + web/i18n/tr-TR/app-debug.ts | 1 + web/i18n/uk-UA/app-debug.ts | 1 + web/i18n/vi-VN/app-debug.ts | 1 + web/i18n/zh-Hans/app-debug.ts | 1 + web/i18n/zh-Hant/app-debug.ts | 1 + 20 files changed, 22 insertions(+), 5 deletions(-) diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts index 10fb455d33..17373cec9d 100644 --- a/web/app/components/base/chat/chat/hooks.ts +++ b/web/app/components/base/chat/chat/hooks.ts @@ -83,12 +83,11 @@ export const useChat = ( const ret = [...threadMessages] if (config?.opening_statement) { const index = threadMessages.findIndex(item => item.isOpeningStatement) - if (index > -1) { ret[index] = { ...ret[index], content: getIntroduction(config.opening_statement), - suggestedQuestions: config.suggested_questions, + suggestedQuestions: config.suggested_questions?.map(item => getIntroduction(item)), } } else { @@ -97,7 +96,7 @@ export const useChat = ( content: getIntroduction(config.opening_statement), isAnswer: true, isOpeningStatement: true, - suggestedQuestions: config.suggested_questions, + suggestedQuestions: config.suggested_questions?.map(item => getIntroduction(item)), }) } } diff --git a/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx b/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx index 117c8a5558..51e33c43d2 100644 --- a/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx +++ b/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx @@ -130,6 +130,7 @@ const OpeningSettingModal = ({ { const value = e.target.value setTempSuggestedQuestions(tempSuggestedQuestions.map((item, i) => { diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks.ts b/web/app/components/workflow/panel/debug-and-preview/hooks.ts index 26d604ef11..d98cf49812 100644 --- a/web/app/components/workflow/panel/debug-and-preview/hooks.ts +++ b/web/app/components/workflow/panel/debug-and-preview/hooks.ts @@ -92,7 +92,7 @@ export const useChat = ( ret[index] = { ...ret[index], content: getIntroduction(config.opening_statement), - suggestedQuestions: config.suggested_questions, + suggestedQuestions: config.suggested_questions?.map((item: string) => getIntroduction(item)), } } else { @@ -101,7 +101,7 @@ export const useChat = ( content: getIntroduction(config.opening_statement), isAnswer: true, isOpeningStatement: true, - suggestedQuestions: config.suggested_questions, + suggestedQuestions: config.suggested_questions?.map((item: string) => getIntroduction(item)), }) } } diff --git a/web/i18n/de-DE/app-debug.ts b/web/i18n/de-DE/app-debug.ts index 4022e755e9..1adaf6f05d 100644 --- a/web/i18n/de-DE/app-debug.ts +++ b/web/i18n/de-DE/app-debug.ts @@ -298,6 +298,7 @@ const translation = { add: 'Hinzufügen', writeOpener: 'Eröffnung schreiben', placeholder: 'Schreiben Sie hier Ihre Eröffnungsnachricht, Sie können Variablen verwenden, versuchen Sie {{Variable}} zu tippen.', + openingQuestionPlaceholder: 'Sie können Variablen verwenden, versuchen Sie {{variable}} einzugeben.', openingQuestion: 'Eröffnungsfragen', noDataPlaceHolder: 'Den Dialog mit dem Benutzer zu beginnen, kann helfen, in konversationellen Anwendungen eine engere Verbindung mit ihnen herzustellen.', diff --git a/web/i18n/en-US/app-debug.ts b/web/i18n/en-US/app-debug.ts index 5282dab360..938cb27c12 100644 --- a/web/i18n/en-US/app-debug.ts +++ b/web/i18n/en-US/app-debug.ts @@ -446,6 +446,7 @@ const translation = { writeOpener: 'Edit opener', placeholder: 'Write your opener message here, you can use variables, try type {{variable}}.', openingQuestion: 'Opening Questions', + openingQuestionPlaceholder: 'You can use variables, try typing {{variable}}.', noDataPlaceHolder: 'Starting the conversation with the user can help AI establish a closer connection with them in conversational applications.', varTip: 'You can use variables, try type {{variable}}', diff --git a/web/i18n/es-ES/app-debug.ts b/web/i18n/es-ES/app-debug.ts index 8c986bf669..afdea66338 100644 --- a/web/i18n/es-ES/app-debug.ts +++ b/web/i18n/es-ES/app-debug.ts @@ -329,6 +329,7 @@ const translation = { writeOpener: 'Escribir apertura', placeholder: 'Escribe tu mensaje de apertura aquí, puedes usar variables, intenta escribir {{variable}}.', openingQuestion: 'Preguntas de Apertura', + openingQuestionPlaceholder: 'Puede usar variables, intente escribir {{variable}}.', noDataPlaceHolder: 'Iniciar la conversación con el usuario puede ayudar a la IA a establecer una conexión más cercana con ellos en aplicaciones de conversación.', varTip: 'Puedes usar variables, intenta escribir {{variable}}', tooShort: 'Se requieren al menos 20 palabras en la indicación inicial para generar una apertura de conversación.', diff --git a/web/i18n/fa-IR/app-debug.ts b/web/i18n/fa-IR/app-debug.ts index 5cf9c15efe..75085ef30e 100644 --- a/web/i18n/fa-IR/app-debug.ts +++ b/web/i18n/fa-IR/app-debug.ts @@ -364,6 +364,7 @@ const translation = { writeOpener: 'نوشتن آغازگر', placeholder: 'پیام آغازگر خود را اینجا بنویسید، می‌توانید از متغیرها استفاده کنید، سعی کنید {{variable}} را تایپ کنید.', openingQuestion: 'سوالات آغازین', + openingQuestionPlaceholder: 'می‌توانید از متغیرها استفاده کنید، سعی کنید {{variable}} را تایپ کنید.', noDataPlaceHolder: 'شروع مکالمه با کاربر می‌تواند به AI کمک کند تا ارتباط نزدیک‌تری با آنها برقرار کند.', varTip: 'می‌توانید از متغیرها استفاده کنید، سعی کنید {{variable}} را تایپ کنید', tooShort: 'حداقل 20 کلمه از پرسش اولیه برای تولید نظرات آغازین مکالمه مورد نیاز است.', diff --git a/web/i18n/fr-FR/app-debug.ts b/web/i18n/fr-FR/app-debug.ts index 6671092930..f3984c0435 100644 --- a/web/i18n/fr-FR/app-debug.ts +++ b/web/i18n/fr-FR/app-debug.ts @@ -317,6 +317,7 @@ const translation = { writeOpener: 'Écrire l\'introduction', placeholder: 'Rédigez votre message d\'ouverture ici, vous pouvez utiliser des variables, essayez de taper {{variable}}.', openingQuestion: 'Questions d\'ouverture', + openingQuestionPlaceholder: 'Vous pouvez utiliser des variables, essayez de taper {{variable}}.', noDataPlaceHolder: 'Commencer la conversation avec l\'utilisateur peut aider l\'IA à établir une connexion plus proche avec eux dans les applications conversationnelles.', varTip: 'Vous pouvez utiliser des variables, essayez de taper {{variable}}', diff --git a/web/i18n/hi-IN/app-debug.ts b/web/i18n/hi-IN/app-debug.ts index 3f4b06c08b..ded2af4132 100644 --- a/web/i18n/hi-IN/app-debug.ts +++ b/web/i18n/hi-IN/app-debug.ts @@ -362,6 +362,7 @@ const translation = { placeholder: 'यहां अपना प्रारंभक संदेश लिखें, आप वेरिएबल्स का उपयोग कर सकते हैं, {{variable}} टाइप करने का प्रयास करें।', openingQuestion: 'प्रारंभिक प्रश्न', + openingQuestionPlaceholder: 'आप वेरिएबल्स का उपयोग कर सकते हैं, {{variable}} टाइप करके देखें।', noDataPlaceHolder: 'उपयोगकर्ता के साथ संवाद प्रारंभ करने से एआई को संवादात्मक अनुप्रयोगों में उनके साथ निकट संबंध स्थापित करने में मदद मिल सकती है।', varTip: diff --git a/web/i18n/it-IT/app-debug.ts b/web/i18n/it-IT/app-debug.ts index c8b3c08302..bfa75b282b 100644 --- a/web/i18n/it-IT/app-debug.ts +++ b/web/i18n/it-IT/app-debug.ts @@ -365,6 +365,7 @@ const translation = { placeholder: 'Scrivi qui il tuo messaggio introduttivo, puoi usare variabili, prova a scrivere {{variable}}.', openingQuestion: 'Domande iniziali', + openingQuestionPlaceholder: 'Puoi usare variabili, prova a digitare {{variable}}.', noDataPlaceHolder: 'Iniziare la conversazione con l\'utente può aiutare l\'IA a stabilire un legame più stretto con loro nelle applicazioni conversazionali.', varTip: 'Puoi usare variabili, prova a scrivere {{variable}}', diff --git a/web/i18n/ja-JP/app-debug.ts b/web/i18n/ja-JP/app-debug.ts index f862f3f2f7..decbe4863e 100644 --- a/web/i18n/ja-JP/app-debug.ts +++ b/web/i18n/ja-JP/app-debug.ts @@ -434,6 +434,7 @@ const translation = { writeOpener: 'オープナーを書く', placeholder: 'ここにオープナーメッセージを書いてください。変数を使用できます。{{variable}} を入力してみてください。', openingQuestion: '開始質問', + openingQuestionPlaceholder: '変数を使用できます。{{variable}} と入力してみてください。', noDataPlaceHolder: 'ユーザーとの会話を開始すると、会話アプリケーションで彼らとのより密接な関係を築くのに役立ちます。', varTip: '変数を使用できます。{{variable}} を入力してみてください', diff --git a/web/i18n/ko-KR/app-debug.ts b/web/i18n/ko-KR/app-debug.ts index 3c5a3f4b1f..b84946841f 100644 --- a/web/i18n/ko-KR/app-debug.ts +++ b/web/i18n/ko-KR/app-debug.ts @@ -328,6 +328,7 @@ const translation = { writeOpener: '오프너 작성', placeholder: '여기에 오프너 메시지를 작성하세요. 변수를 사용할 수 있습니다. {{variable}}를 입력해보세요.', openingQuestion: '시작 질문', + openingQuestionPlaceholder: '변수를 사용할 수 있습니다. {{variable}}을(를) 입력해 보세요.', noDataPlaceHolder: '사용자와의 대화를 시작하면 대화 애플리케이션에서 그들과 더 밀접한 관계를 구축하는 데 도움이 됩니다.', varTip: '변수를 사용할 수 있습니다. {{variable}}를 입력해보세요.', tooShort: '대화 시작에는 최소 20 단어의 초기 프롬프트가 필요합니다.', diff --git a/web/i18n/pl-PL/app-debug.ts b/web/i18n/pl-PL/app-debug.ts index 48b44c0cbb..dcd286d351 100644 --- a/web/i18n/pl-PL/app-debug.ts +++ b/web/i18n/pl-PL/app-debug.ts @@ -360,6 +360,7 @@ const translation = { placeholder: 'Tutaj napisz swoją wiadomość wprowadzającą, możesz użyć zmiennych, spróbuj wpisać {{variable}}.', openingQuestion: 'Pytania otwierające', + openingQuestionPlaceholder: 'Możesz używać zmiennych, spróbuj wpisać {{variable}}.', noDataPlaceHolder: 'Rozpoczynanie rozmowy z użytkownikiem może pomóc AI nawiązać bliższe połączenie z nim w aplikacjach konwersacyjnych.', varTip: 'Możesz używać zmiennych, spróbuj wpisać {{variable}}', diff --git a/web/i18n/pt-BR/app-debug.ts b/web/i18n/pt-BR/app-debug.ts index 64f7a85fe7..96d78dc9a3 100644 --- a/web/i18n/pt-BR/app-debug.ts +++ b/web/i18n/pt-BR/app-debug.ts @@ -334,6 +334,7 @@ const translation = { writeOpener: 'Escrever abertura', placeholder: 'Escreva sua mensagem de abertura aqui, você pode usar variáveis, tente digitar {{variável}}.', openingQuestion: 'Perguntas de Abertura', + openingQuestionPlaceholder: 'Você pode usar variáveis, tente digitar {{variable}}.', noDataPlaceHolder: 'Iniciar a conversa com o usuário pode ajudar a IA a estabelecer uma conexão mais próxima com eles em aplicativos de conversação.', varTip: 'Você pode usar variáveis, tente digitar {{variável}}', diff --git a/web/i18n/ru-RU/app-debug.ts b/web/i18n/ru-RU/app-debug.ts index 00cd6e8a75..5d4dbb53d3 100644 --- a/web/i18n/ru-RU/app-debug.ts +++ b/web/i18n/ru-RU/app-debug.ts @@ -370,6 +370,7 @@ const translation = { writeOpener: 'Написать начальное сообщение', placeholder: 'Напишите здесь свое начальное сообщение, вы можете использовать переменные, попробуйте ввести {{variable}}.', openingQuestion: 'Начальные вопросы', + openingQuestionPlaceholder: 'Вы можете использовать переменные, попробуйте ввести {{variable}}.', noDataPlaceHolder: 'Начало разговора с пользователем может помочь ИИ установить более тесную связь с ним в диалоговых приложениях.', varTip: 'Вы можете использовать переменные, попробуйте ввести {{variable}}', diff --git a/web/i18n/tr-TR/app-debug.ts b/web/i18n/tr-TR/app-debug.ts index f08d221d45..6ed0e0a5eb 100644 --- a/web/i18n/tr-TR/app-debug.ts +++ b/web/i18n/tr-TR/app-debug.ts @@ -368,6 +368,7 @@ const translation = { writeOpener: 'Başlangıç mesajı yaz', placeholder: 'Başlangıç mesajınızı buraya yazın, değişkenler kullanabilirsiniz, örneğin {{variable}} yazmayı deneyin.', openingQuestion: 'Açılış Soruları', + openingQuestionPlaceholder: 'Değişkenler kullanabilirsiniz, {{variable}} yazmayı deneyin.', noDataPlaceHolder: 'Kullanıcı ile konuşmayı başlatmak, AI\'ın konuşma uygulamalarında onlarla daha yakın bir bağlantı kurmasına yardımcı olabilir.', varTip: 'Değişkenler kullanabilirsiniz, örneğin {{variable}} yazmayı deneyin', diff --git a/web/i18n/uk-UA/app-debug.ts b/web/i18n/uk-UA/app-debug.ts index 7e410ffef9..70bbebe37e 100644 --- a/web/i18n/uk-UA/app-debug.ts +++ b/web/i18n/uk-UA/app-debug.ts @@ -328,6 +328,7 @@ const translation = { writeOpener: 'Напишіть вступне повідомлення', // Write opener placeholder: 'Напишіть тут своє вступне повідомлення, ви можете використовувати змінні, спробуйте ввести {{variable}}.', // Write your opener message here... openingQuestion: 'Відкриваючі питання', // Opening Questions + openingQuestionPlaceholder: 'Ви можете використовувати змінні, спробуйте ввести {{variable}}.', noDataPlaceHolder: 'Початок розмови з користувачем може допомогти ШІ встановити більш тісний зв’язок з ним у розмовних застосунках.', // ... conversational applications. varTip: 'Ви можете використовувати змінні, спробуйте ввести {{variable}}', // You can use variables, try type {{variable}} tooShort: 'Для створення вступних зауважень для розмови потрібно принаймні 20 слів вступного запиту.', // ... are required to generate an opening remarks for the conversation. diff --git a/web/i18n/vi-VN/app-debug.ts b/web/i18n/vi-VN/app-debug.ts index c091cb5abb..cd57f78e79 100644 --- a/web/i18n/vi-VN/app-debug.ts +++ b/web/i18n/vi-VN/app-debug.ts @@ -328,6 +328,7 @@ const translation = { writeOpener: 'Viết câu mở đầu', placeholder: 'Viết thông điệp mở đầu của bạn ở đây, bạn có thể sử dụng biến, hãy thử nhập {{biến}}.', openingQuestion: 'Câu hỏi mở đầu', + openingQuestionPlaceholder: 'Bạn có thể sử dụng biến, hãy thử nhập {{variable}}.', noDataPlaceHolder: 'Bắt đầu cuộc trò chuyện với người dùng có thể giúp AI thiết lập mối quan hệ gần gũi hơn với họ trong các ứng dụng trò chuyện.', varTip: 'Bạn có thể sử dụng biến, hãy thử nhập {{biến}}', tooShort: 'Cần ít nhất 20 từ trong lời nhắc ban đầu để tạo ra các câu mở đầu cho cuộc trò chuyện.', diff --git a/web/i18n/zh-Hans/app-debug.ts b/web/i18n/zh-Hans/app-debug.ts index 4f84b396d0..3728d86e58 100644 --- a/web/i18n/zh-Hans/app-debug.ts +++ b/web/i18n/zh-Hans/app-debug.ts @@ -436,6 +436,7 @@ const translation = { writeOpener: '编写开场白', placeholder: '在这里写下你的开场白,你可以使用变量,尝试输入 {{variable}}。', openingQuestion: '开场问题', + openingQuestionPlaceholder: '可以使用变量,尝试输入 {{variable}}。', noDataPlaceHolder: '在对话型应用中,让 AI 主动说第一段话可以拉近与用户间的距离。', varTip: '你可以使用变量,试试输入 {{variable}}', diff --git a/web/i18n/zh-Hant/app-debug.ts b/web/i18n/zh-Hant/app-debug.ts index 16374992b5..b31b9a9d66 100644 --- a/web/i18n/zh-Hant/app-debug.ts +++ b/web/i18n/zh-Hant/app-debug.ts @@ -313,6 +313,7 @@ const translation = { writeOpener: '編寫開場白', placeholder: '在這裡寫下你的開場白,你可以使用變數,嘗試輸入 {{variable}}。', openingQuestion: '開場問題', + openingQuestionPlaceholder: '可以使用變量,嘗試輸入 {{variable}}。', noDataPlaceHolder: '在對話型應用中,讓 AI 主動說第一段話可以拉近與使用者間的距離。', varTip: '你可以使用變數,試試輸入 {{variable}}', From 2a85f28963b6e055ec0b439704d10d66475c4784 Mon Sep 17 00:00:00 2001 From: Garden12138 <847686279@qq.com> Date: Fri, 11 Jul 2025 15:18:42 +0800 Subject: [PATCH 08/48] =?UTF-8?q?fix=EF=BC=9AFixed=20the=20problem=20of=20?= =?UTF-8?q?plugin=20installation=20failure=20caused=20by=20incons=E2=80=A6?= =?UTF-8?q?=20(#22156)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/services/plugin/plugin_service.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/api/services/plugin/plugin_service.py b/api/services/plugin/plugin_service.py index d7fb4a7c1b..0f22afd8dd 100644 --- a/api/services/plugin/plugin_service.py +++ b/api/services/plugin/plugin_service.py @@ -427,6 +427,9 @@ class PluginService: manager = PluginInstaller() + # collect actual plugin_unique_identifiers + actual_plugin_unique_identifiers = [] + metas = [] features = FeatureService.get_system_features() # check if already downloaded @@ -437,6 +440,8 @@ class PluginService: # check if the plugin is available to install PluginService._check_plugin_installation_scope(plugin_decode_response.verification) # already downloaded, skip + actual_plugin_unique_identifiers.append(plugin_unique_identifier) + metas.append({"plugin_unique_identifier": plugin_unique_identifier}) except Exception: # plugin not installed, download and upload pkg pkg = download_plugin_pkg(plugin_unique_identifier) @@ -447,17 +452,15 @@ class PluginService: ) # check if the plugin is available to install PluginService._check_plugin_installation_scope(response.verification) + # use response plugin_unique_identifier + actual_plugin_unique_identifiers.append(response.unique_identifier) + metas.append({"plugin_unique_identifier": response.unique_identifier}) return manager.install_from_identifiers( tenant_id, - plugin_unique_identifiers, + actual_plugin_unique_identifiers, PluginInstallationSource.Marketplace, - [ - { - "plugin_unique_identifier": plugin_unique_identifier, - } - for plugin_unique_identifier in plugin_unique_identifiers - ], + metas, ) @staticmethod From 1d85979a7464d8d4d01e582eb6083d4206ea46ba Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 11 Jul 2025 16:41:25 +0800 Subject: [PATCH 09/48] chore:extract last run common logic (#22214) --- .../workflow-app/components/workflow-main.tsx | 8 +- .../components/workflow-app/hooks/index.ts | 2 +- .../hooks/use-inspect-vars-crud.ts | 234 +---------------- .../workflow-app/hooks/use-workflow-run.ts | 9 +- .../hooks/use-fetch-workflow-inspect-vars.ts | 19 +- .../hooks/use-inspect-vars-crud-common.ts | 240 ++++++++++++++++++ 6 files changed, 275 insertions(+), 237 deletions(-) rename web/app/components/{workflow-app => workflow}/hooks/use-fetch-workflow-inspect-vars.ts (85%) create mode 100644 web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts diff --git a/web/app/components/workflow-app/components/workflow-main.tsx b/web/app/components/workflow-app/components/workflow-main.tsx index f0b9bab2cf..d425e6f595 100644 --- a/web/app/components/workflow-app/components/workflow-main.tsx +++ b/web/app/components/workflow-app/components/workflow-main.tsx @@ -15,7 +15,7 @@ import { useWorkflowRun, useWorkflowStartRun, } from '../hooks' -import { useWorkflowStore } from '@/app/components/workflow/store' +import { useStore, useWorkflowStore } from '@/app/components/workflow/store' type WorkflowMainProps = Pick const WorkflowMain = ({ @@ -64,7 +64,11 @@ const WorkflowMain = ({ handleWorkflowStartRunInChatflow, handleWorkflowStartRunInWorkflow, } = useWorkflowStartRun() - const { fetchInspectVars } = useSetWorkflowVarsWithValue() + const appId = useStore(s => s.appId) + const { fetchInspectVars } = useSetWorkflowVarsWithValue({ + flowId: appId, + ...useConfigsMap(), + }) const { hasNodeInspectVars, hasSetInspectVar, diff --git a/web/app/components/workflow-app/hooks/index.ts b/web/app/components/workflow-app/hooks/index.ts index 1ee7c030b9..9e4f94965b 100644 --- a/web/app/components/workflow-app/hooks/index.ts +++ b/web/app/components/workflow-app/hooks/index.ts @@ -5,6 +5,6 @@ export * from './use-workflow-run' export * from './use-workflow-start-run' export * from './use-is-chat-mode' export * from './use-workflow-refresh-draft' -export * from './use-fetch-workflow-inspect-vars' +export * from '../../workflow/hooks/use-fetch-workflow-inspect-vars' export * from './use-inspect-vars-crud' export * from './use-configs-map' diff --git a/web/app/components/workflow-app/hooks/use-inspect-vars-crud.ts b/web/app/components/workflow-app/hooks/use-inspect-vars-crud.ts index ce052b7ed4..109ee85f96 100644 --- a/web/app/components/workflow-app/hooks/use-inspect-vars-crud.ts +++ b/web/app/components/workflow-app/hooks/use-inspect-vars-crud.ts @@ -1,234 +1,16 @@ -import { fetchNodeInspectVars } from '@/service/workflow' -import { useStore, useWorkflowStore } from '@/app/components/workflow/store' -import type { ValueSelector } from '@/app/components/workflow/types' -import type { VarInInspect } from '@/types/workflow' -import { VarInInspectType } from '@/types/workflow' -import { - useDeleteAllInspectorVars, - useDeleteInspectVar, - useDeleteNodeInspectorVars, - useEditInspectorVar, - useInvalidateConversationVarValues, - useInvalidateSysVarValues, - useResetConversationVar, - useResetToLastRunValue, -} from '@/service/use-workflow' -import { useCallback } from 'react' -import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' -import produce from 'immer' -import type { Node } from '@/app/components/workflow/types' -import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync' -import { useEdgesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-edges-interactions-without-sync' +import { useStore } from '@/app/components/workflow/store' +import { useInspectVarsCrudCommon } from '../../workflow/hooks/use-inspect-vars-crud-common' import { useConfigsMap } from './use-configs-map' export const useInspectVarsCrud = () => { - const workflowStore = useWorkflowStore() const appId = useStore(s => s.appId) - const { conversationVarsUrl, systemVarsUrl } = useConfigsMap() - const invalidateConversationVarValues = useInvalidateConversationVarValues(conversationVarsUrl) - const { mutateAsync: doResetConversationVar } = useResetConversationVar(appId) - const { mutateAsync: doResetToLastRunValue } = useResetToLastRunValue(appId) - const invalidateSysVarValues = useInvalidateSysVarValues(systemVarsUrl) - - const { mutateAsync: doDeleteAllInspectorVars } = useDeleteAllInspectorVars(appId) - const { mutate: doDeleteNodeInspectorVars } = useDeleteNodeInspectorVars(appId) - const { mutate: doDeleteInspectVar } = useDeleteInspectVar(appId) - - const { mutateAsync: doEditInspectorVar } = useEditInspectorVar(appId) - const { handleCancelNodeSuccessStatus } = useNodesInteractionsWithoutSync() - const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync() - const getNodeInspectVars = useCallback((nodeId: string) => { - const { nodesWithInspectVars } = workflowStore.getState() - const node = nodesWithInspectVars.find(node => node.nodeId === nodeId) - return node - }, [workflowStore]) - - const getVarId = useCallback((nodeId: string, varName: string) => { - const node = getNodeInspectVars(nodeId) - if (!node) - return undefined - const varId = node.vars.find((varItem) => { - return varItem.selector[1] === varName - })?.id - return varId - }, [getNodeInspectVars]) - - const getInspectVar = useCallback((nodeId: string, name: string): VarInInspect | undefined => { - const node = getNodeInspectVars(nodeId) - if (!node) - return undefined - - const variable = node.vars.find((varItem) => { - return varItem.name === name - }) - return variable - }, [getNodeInspectVars]) - - const hasSetInspectVar = useCallback((nodeId: string, name: string, sysVars: VarInInspect[], conversationVars: VarInInspect[]) => { - const isEnv = isENV([nodeId]) - if (isEnv) // always have value - return true - const isSys = isSystemVar([nodeId]) - if (isSys) - return sysVars.some(varItem => varItem.selector?.[1]?.replace('sys.', '') === name) - const isChatVar = isConversationVar([nodeId]) - if (isChatVar) - return conversationVars.some(varItem => varItem.selector?.[1] === name) - return getInspectVar(nodeId, name) !== undefined - }, [getInspectVar]) - - const hasNodeInspectVars = useCallback((nodeId: string) => { - return !!getNodeInspectVars(nodeId) - }, [getNodeInspectVars]) - - const fetchInspectVarValue = useCallback(async (selector: ValueSelector) => { - const { - appId, - setNodeInspectVars, - } = workflowStore.getState() - const nodeId = selector[0] - const isSystemVar = nodeId === 'sys' - const isConversationVar = nodeId === 'conversation' - if (isSystemVar) { - invalidateSysVarValues() - return - } - if (isConversationVar) { - invalidateConversationVarValues() - return - } - const vars = await fetchNodeInspectVars(appId, nodeId) - setNodeInspectVars(nodeId, vars) - }, [workflowStore, invalidateSysVarValues, invalidateConversationVarValues]) - - // after last run would call this - const appendNodeInspectVars = useCallback((nodeId: string, payload: VarInInspect[], allNodes: Node[]) => { - const { - nodesWithInspectVars, - setNodesWithInspectVars, - } = workflowStore.getState() - const nodes = produce(nodesWithInspectVars, (draft) => { - const nodeInfo = allNodes.find(node => node.id === nodeId) - if (nodeInfo) { - const index = draft.findIndex(node => node.nodeId === nodeId) - if (index === -1) { - draft.unshift({ - nodeId, - nodeType: nodeInfo.data.type, - title: nodeInfo.data.title, - vars: payload, - nodePayload: nodeInfo.data, - }) - } - else { - draft[index].vars = payload - // put the node to the topAdd commentMore actions - draft.unshift(draft.splice(index, 1)[0]) - } - } - }) - setNodesWithInspectVars(nodes) - handleCancelNodeSuccessStatus(nodeId) - }, [workflowStore, handleCancelNodeSuccessStatus]) - - const hasNodeInspectVar = useCallback((nodeId: string, varId: string) => { - const { nodesWithInspectVars } = workflowStore.getState() - const targetNode = nodesWithInspectVars.find(item => item.nodeId === nodeId) - if(!targetNode || !targetNode.vars) - return false - return targetNode.vars.some(item => item.id === varId) - }, [workflowStore]) - - const deleteInspectVar = useCallback(async (nodeId: string, varId: string) => { - const { deleteInspectVar } = workflowStore.getState() - if(hasNodeInspectVar(nodeId, varId)) { - await doDeleteInspectVar(varId) - deleteInspectVar(nodeId, varId) - } - }, [doDeleteInspectVar, workflowStore, hasNodeInspectVar]) - - const resetConversationVar = useCallback(async (varId: string) => { - await doResetConversationVar(varId) - invalidateConversationVarValues() - }, [doResetConversationVar, invalidateConversationVarValues]) - - const deleteNodeInspectorVars = useCallback(async (nodeId: string) => { - const { deleteNodeInspectVars } = workflowStore.getState() - if (hasNodeInspectVars(nodeId)) { - await doDeleteNodeInspectorVars(nodeId) - deleteNodeInspectVars(nodeId) - } - }, [doDeleteNodeInspectorVars, workflowStore, hasNodeInspectVars]) - - const deleteAllInspectorVars = useCallback(async () => { - const { deleteAllInspectVars } = workflowStore.getState() - await doDeleteAllInspectorVars() - await invalidateConversationVarValues() - await invalidateSysVarValues() - deleteAllInspectVars() - handleEdgeCancelRunningStatus() - }, [doDeleteAllInspectorVars, invalidateConversationVarValues, invalidateSysVarValues, workflowStore, handleEdgeCancelRunningStatus]) - - const editInspectVarValue = useCallback(async (nodeId: string, varId: string, value: any) => { - const { setInspectVarValue } = workflowStore.getState() - await doEditInspectorVar({ - varId, - value, - }) - setInspectVarValue(nodeId, varId, value) - if (nodeId === VarInInspectType.conversation) - invalidateConversationVarValues() - if (nodeId === VarInInspectType.system) - invalidateSysVarValues() - }, [doEditInspectorVar, invalidateConversationVarValues, invalidateSysVarValues, workflowStore]) - - const renameInspectVarName = useCallback(async (nodeId: string, oldName: string, newName: string) => { - const { renameInspectVarName } = workflowStore.getState() - const varId = getVarId(nodeId, oldName) - if (!varId) - return - - const newSelector = [nodeId, newName] - await doEditInspectorVar({ - varId, - name: newName, - }) - renameInspectVarName(nodeId, varId, newSelector) - }, [doEditInspectorVar, getVarId, workflowStore]) - - const isInspectVarEdited = useCallback((nodeId: string, name: string) => { - const inspectVar = getInspectVar(nodeId, name) - if (!inspectVar) - return false - - return inspectVar.edited - }, [getInspectVar]) - - const resetToLastRunVar = useCallback(async (nodeId: string, varId: string) => { - const { resetToLastRunVar } = workflowStore.getState() - const isSysVar = nodeId === 'sys' - const data = await doResetToLastRunValue(varId) - - if(isSysVar) - invalidateSysVarValues() - else - resetToLastRunVar(nodeId, varId, data.value) - }, [doResetToLastRunValue, invalidateSysVarValues, workflowStore]) + const configsMap = useConfigsMap() + const apis = useInspectVarsCrudCommon({ + flowId: appId, + ...configsMap, + }) return { - hasNodeInspectVars, - hasSetInspectVar, - fetchInspectVarValue, - editInspectVarValue, - renameInspectVarName, - appendNodeInspectVars, - deleteInspectVar, - deleteNodeInspectorVars, - deleteAllInspectorVars, - isInspectVarEdited, - resetToLastRunVar, - invalidateSysVarValues, - resetConversationVar, - invalidateConversationVarValues, + ...apis, } } diff --git a/web/app/components/workflow-app/hooks/use-workflow-run.ts b/web/app/components/workflow-app/hooks/use-workflow-run.ts index 4c34d2ffb1..c303211715 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-run.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-run.ts @@ -20,7 +20,8 @@ import type { VersionHistory } from '@/types/workflow' import { noop } from 'lodash-es' import { useNodesSyncDraft } from './use-nodes-sync-draft' import { useInvalidAllLastRun } from '@/service/use-workflow' -import { useSetWorkflowVarsWithValue } from './use-fetch-workflow-inspect-vars' +import { useSetWorkflowVarsWithValue } from '../../workflow/hooks/use-fetch-workflow-inspect-vars' +import { useConfigsMap } from './use-configs-map' export const useWorkflowRun = () => { const store = useStoreApi() @@ -32,7 +33,11 @@ export const useWorkflowRun = () => { const pathname = usePathname() const appId = useAppStore.getState().appDetail?.id const invalidAllLastRun = useInvalidAllLastRun(appId as string) - const { fetchInspectVars } = useSetWorkflowVarsWithValue() + const configsMap = useConfigsMap() + const { fetchInspectVars } = useSetWorkflowVarsWithValue({ + flowId: appId as string, + ...configsMap, + }) const { handleWorkflowStarted, diff --git a/web/app/components/workflow-app/hooks/use-fetch-workflow-inspect-vars.ts b/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts similarity index 85% rename from web/app/components/workflow-app/hooks/use-fetch-workflow-inspect-vars.ts rename to web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts index 07580c097e..27a9ea9d2d 100644 --- a/web/app/components/workflow-app/hooks/use-fetch-workflow-inspect-vars.ts +++ b/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts @@ -6,12 +6,20 @@ import type { Node } from '@/app/components/workflow/types' import { fetchAllInspectVars } from '@/service/workflow' import { useInvalidateConversationVarValues, useInvalidateSysVarValues } from '@/service/use-workflow' import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync' -import { useConfigsMap } from './use-configs-map' -export const useSetWorkflowVarsWithValue = () => { +type Params = { + flowId: string + conversationVarsUrl: string + systemVarsUrl: string +} + +export const useSetWorkflowVarsWithValue = ({ + flowId, + conversationVarsUrl, + systemVarsUrl, +}: Params) => { const workflowStore = useWorkflowStore() const store = useStoreApi() - const { conversationVarsUrl, systemVarsUrl } = useConfigsMap() const invalidateConversationVarValues = useInvalidateConversationVarValues(conversationVarsUrl) const invalidateSysVarValues = useInvalidateSysVarValues(systemVarsUrl) const { handleCancelAllNodeSuccessStatus } = useNodesInteractionsWithoutSync() @@ -58,13 +66,12 @@ export const useSetWorkflowVarsWithValue = () => { }, [workflowStore, store]) const fetchInspectVars = useCallback(async () => { - const { appId } = workflowStore.getState() invalidateConversationVarValues() invalidateSysVarValues() - const data = await fetchAllInspectVars(appId) + const data = await fetchAllInspectVars(flowId) setInspectVarsToStore(data) handleCancelAllNodeSuccessStatus() // to make sure clear node output show the unset status - }, [workflowStore, invalidateConversationVarValues, invalidateSysVarValues, setInspectVarsToStore, handleCancelAllNodeSuccessStatus]) + }, [invalidateConversationVarValues, invalidateSysVarValues, flowId, setInspectVarsToStore, handleCancelAllNodeSuccessStatus]) return { fetchInspectVars, } diff --git a/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts b/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts new file mode 100644 index 0000000000..ffcfd81666 --- /dev/null +++ b/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts @@ -0,0 +1,240 @@ +import { fetchNodeInspectVars } from '@/service/workflow' +import { useWorkflowStore } from '@/app/components/workflow/store' +import type { ValueSelector } from '@/app/components/workflow/types' +import type { VarInInspect } from '@/types/workflow' +import { VarInInspectType } from '@/types/workflow' +import { + useDeleteAllInspectorVars, + useDeleteInspectVar, + useDeleteNodeInspectorVars, + useEditInspectorVar, + useInvalidateConversationVarValues, + useInvalidateSysVarValues, + useResetConversationVar, + useResetToLastRunValue, +} from '@/service/use-workflow' +import { useCallback } from 'react' +import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' +import produce from 'immer' +import type { Node } from '@/app/components/workflow/types' +import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync' +import { useEdgesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-edges-interactions-without-sync' + +type Params = { + flowId: string + conversationVarsUrl: string + systemVarsUrl: string +} +export const useInspectVarsCrudCommon = ({ + flowId, + conversationVarsUrl, + systemVarsUrl, +}: Params) => { + const workflowStore = useWorkflowStore() + const invalidateConversationVarValues = useInvalidateConversationVarValues(conversationVarsUrl!) + const { mutateAsync: doResetConversationVar } = useResetConversationVar(flowId) + const { mutateAsync: doResetToLastRunValue } = useResetToLastRunValue(flowId) + const invalidateSysVarValues = useInvalidateSysVarValues(systemVarsUrl!) + + const { mutateAsync: doDeleteAllInspectorVars } = useDeleteAllInspectorVars(flowId) + const { mutate: doDeleteNodeInspectorVars } = useDeleteNodeInspectorVars(flowId) + const { mutate: doDeleteInspectVar } = useDeleteInspectVar(flowId) + + const { mutateAsync: doEditInspectorVar } = useEditInspectorVar(flowId) + const { handleCancelNodeSuccessStatus } = useNodesInteractionsWithoutSync() + const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync() + const getNodeInspectVars = useCallback((nodeId: string) => { + const { nodesWithInspectVars } = workflowStore.getState() + const node = nodesWithInspectVars.find(node => node.nodeId === nodeId) + return node + }, [workflowStore]) + + const getVarId = useCallback((nodeId: string, varName: string) => { + const node = getNodeInspectVars(nodeId) + if (!node) + return undefined + const varId = node.vars.find((varItem) => { + return varItem.selector[1] === varName + })?.id + return varId + }, [getNodeInspectVars]) + + const getInspectVar = useCallback((nodeId: string, name: string): VarInInspect | undefined => { + const node = getNodeInspectVars(nodeId) + if (!node) + return undefined + + const variable = node.vars.find((varItem) => { + return varItem.name === name + }) + return variable + }, [getNodeInspectVars]) + + const hasSetInspectVar = useCallback((nodeId: string, name: string, sysVars: VarInInspect[], conversationVars: VarInInspect[]) => { + const isEnv = isENV([nodeId]) + if (isEnv) // always have value + return true + const isSys = isSystemVar([nodeId]) + if (isSys) + return sysVars.some(varItem => varItem.selector?.[1]?.replace('sys.', '') === name) + const isChatVar = isConversationVar([nodeId]) + if (isChatVar) + return conversationVars.some(varItem => varItem.selector?.[1] === name) + return getInspectVar(nodeId, name) !== undefined + }, [getInspectVar]) + + const hasNodeInspectVars = useCallback((nodeId: string) => { + return !!getNodeInspectVars(nodeId) + }, [getNodeInspectVars]) + + const fetchInspectVarValue = useCallback(async (selector: ValueSelector) => { + const { + appId, + setNodeInspectVars, + } = workflowStore.getState() + const nodeId = selector[0] + const isSystemVar = nodeId === 'sys' + const isConversationVar = nodeId === 'conversation' + if (isSystemVar) { + invalidateSysVarValues() + return + } + if (isConversationVar) { + invalidateConversationVarValues() + return + } + const vars = await fetchNodeInspectVars(appId, nodeId) + setNodeInspectVars(nodeId, vars) + }, [workflowStore, invalidateSysVarValues, invalidateConversationVarValues]) + + // after last run would call this + const appendNodeInspectVars = useCallback((nodeId: string, payload: VarInInspect[], allNodes: Node[]) => { + const { + nodesWithInspectVars, + setNodesWithInspectVars, + } = workflowStore.getState() + const nodes = produce(nodesWithInspectVars, (draft) => { + const nodeInfo = allNodes.find(node => node.id === nodeId) + if (nodeInfo) { + const index = draft.findIndex(node => node.nodeId === nodeId) + if (index === -1) { + draft.unshift({ + nodeId, + nodeType: nodeInfo.data.type, + title: nodeInfo.data.title, + vars: payload, + nodePayload: nodeInfo.data, + }) + } + else { + draft[index].vars = payload + // put the node to the topAdd commentMore actions + draft.unshift(draft.splice(index, 1)[0]) + } + } + }) + setNodesWithInspectVars(nodes) + handleCancelNodeSuccessStatus(nodeId) + }, [workflowStore, handleCancelNodeSuccessStatus]) + + const hasNodeInspectVar = useCallback((nodeId: string, varId: string) => { + const { nodesWithInspectVars } = workflowStore.getState() + const targetNode = nodesWithInspectVars.find(item => item.nodeId === nodeId) + if(!targetNode || !targetNode.vars) + return false + return targetNode.vars.some(item => item.id === varId) + }, [workflowStore]) + + const deleteInspectVar = useCallback(async (nodeId: string, varId: string) => { + const { deleteInspectVar } = workflowStore.getState() + if(hasNodeInspectVar(nodeId, varId)) { + await doDeleteInspectVar(varId) + deleteInspectVar(nodeId, varId) + } + }, [doDeleteInspectVar, workflowStore, hasNodeInspectVar]) + + const resetConversationVar = useCallback(async (varId: string) => { + await doResetConversationVar(varId) + invalidateConversationVarValues() + }, [doResetConversationVar, invalidateConversationVarValues]) + + const deleteNodeInspectorVars = useCallback(async (nodeId: string) => { + const { deleteNodeInspectVars } = workflowStore.getState() + if (hasNodeInspectVars(nodeId)) { + await doDeleteNodeInspectorVars(nodeId) + deleteNodeInspectVars(nodeId) + } + }, [doDeleteNodeInspectorVars, workflowStore, hasNodeInspectVars]) + + const deleteAllInspectorVars = useCallback(async () => { + const { deleteAllInspectVars } = workflowStore.getState() + await doDeleteAllInspectorVars() + await invalidateConversationVarValues() + await invalidateSysVarValues() + deleteAllInspectVars() + handleEdgeCancelRunningStatus() + }, [doDeleteAllInspectorVars, invalidateConversationVarValues, invalidateSysVarValues, workflowStore, handleEdgeCancelRunningStatus]) + + const editInspectVarValue = useCallback(async (nodeId: string, varId: string, value: any) => { + const { setInspectVarValue } = workflowStore.getState() + await doEditInspectorVar({ + varId, + value, + }) + setInspectVarValue(nodeId, varId, value) + if (nodeId === VarInInspectType.conversation) + invalidateConversationVarValues() + if (nodeId === VarInInspectType.system) + invalidateSysVarValues() + }, [doEditInspectorVar, invalidateConversationVarValues, invalidateSysVarValues, workflowStore]) + + const renameInspectVarName = useCallback(async (nodeId: string, oldName: string, newName: string) => { + const { renameInspectVarName } = workflowStore.getState() + const varId = getVarId(nodeId, oldName) + if (!varId) + return + + const newSelector = [nodeId, newName] + await doEditInspectorVar({ + varId, + name: newName, + }) + renameInspectVarName(nodeId, varId, newSelector) + }, [doEditInspectorVar, getVarId, workflowStore]) + + const isInspectVarEdited = useCallback((nodeId: string, name: string) => { + const inspectVar = getInspectVar(nodeId, name) + if (!inspectVar) + return false + + return inspectVar.edited + }, [getInspectVar]) + + const resetToLastRunVar = useCallback(async (nodeId: string, varId: string) => { + const { resetToLastRunVar } = workflowStore.getState() + const isSysVar = nodeId === 'sys' + const data = await doResetToLastRunValue(varId) + + if(isSysVar) + invalidateSysVarValues() + else + resetToLastRunVar(nodeId, varId, data.value) + }, [doResetToLastRunValue, invalidateSysVarValues, workflowStore]) + + return { + hasNodeInspectVars, + hasSetInspectVar, + fetchInspectVarValue, + editInspectVarValue, + renameInspectVarName, + appendNodeInspectVars, + deleteInspectVar, + deleteNodeInspectorVars, + deleteAllInspectorVars, + isInspectVarEdited, + resetToLastRunVar, + invalidateSysVarValues, + resetConversationVar, + invalidateConversationVarValues, + } +} From 2d3c5b3b7c3365a5e037e85dbbb9ced174a51862 Mon Sep 17 00:00:00 2001 From: HyaCinth <88471803+HyaCiovo@users.noreply.github.com> Date: Fri, 11 Jul 2025 17:52:16 +0800 Subject: [PATCH 10/48] fix(emoji-picker): Adjust the style of the emoji picker (#22161) (#22231) --- web/app/components/base/emoji-picker/Inner.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/app/components/base/emoji-picker/Inner.tsx b/web/app/components/base/emoji-picker/Inner.tsx index 8d05967f33..6fc4b67181 100644 --- a/web/app/components/base/emoji-picker/Inner.tsx +++ b/web/app/components/base/emoji-picker/Inner.tsx @@ -101,7 +101,7 @@ const EmojiPickerInner: FC = ({
-
+
{isSearching && <>

Search

@@ -170,7 +170,7 @@ const EmojiPickerInner: FC = ({ 'flex h-8 w-8 items-center justify-center rounded-lg p-1', ) } style={{ background: color }}> - {selectedEmoji !== '' && } + {selectedEmoji !== '' && }
})} From 76d21743fd9f164bbca1e97f224246750b5e83d3 Mon Sep 17 00:00:00 2001 From: HyaCinth <88471803+HyaCiovo@users.noreply.github.com> Date: Fri, 11 Jul 2025 17:54:09 +0800 Subject: [PATCH 11/48] fix(web): Optimize AppInfo Component Layout (#22212) (#22218) --- web/app/components/app-sidebar/app-info.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/web/app/components/app-sidebar/app-info.tsx b/web/app/components/app-sidebar/app-info.tsx index c28cc20df5..3817ebf5a4 100644 --- a/web/app/components/app-sidebar/app-info.tsx +++ b/web/app/components/app-sidebar/app-info.tsx @@ -308,13 +308,11 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx operations={operations} />
-
- -
+