From 6ca44eea28fe3410097ba3dc599b09650b416392 Mon Sep 17 00:00:00 2001 From: Coding On Star <447357187@qq.com> Date: Tue, 30 Dec 2025 18:06:47 +0800 Subject: [PATCH 01/37] feat: integrate Google Analytics event tracking and update CSP for script sources (#30365) Co-authored-by: CodingOnStar --- web/app/components/app-initializer.tsx | 38 +++++++++++++- .../base/amplitude/AmplitudeProvider.tsx | 1 + web/app/components/base/ga/index.tsx | 50 +++++++++++-------- web/app/signup/set-password/page.tsx | 26 +++++++++- web/global.d.ts | 20 +++++++- web/utils/gtag.ts | 14 ++++++ 6 files changed, 125 insertions(+), 24 deletions(-) create mode 100644 web/utils/gtag.ts diff --git a/web/app/components/app-initializer.tsx b/web/app/components/app-initializer.tsx index 0f710abf39..e30646eb3f 100644 --- a/web/app/components/app-initializer.tsx +++ b/web/app/components/app-initializer.tsx @@ -1,14 +1,18 @@ 'use client' import type { ReactNode } from 'react' +import Cookies from 'js-cookie' import { usePathname, useRouter, useSearchParams } from 'next/navigation' +import { parseAsString, useQueryState } from 'nuqs' import { useCallback, useEffect, useState } from 'react' import { EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION, EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, } from '@/app/education-apply/constants' import { fetchSetupStatus } from '@/service/common' +import { sendGAEvent } from '@/utils/gtag' import { resolvePostLoginRedirect } from '../signin/utils/post-login-redirect' +import { trackEvent } from './base/amplitude' type AppInitializerProps = { children: ReactNode @@ -22,6 +26,10 @@ export const AppInitializer = ({ // Tokens are now stored in cookies, no need to check localStorage const pathname = usePathname() const [init, setInit] = useState(false) + const [oauthNewUser, setOauthNewUser] = useQueryState( + 'oauth_new_user', + parseAsString.withOptions({ history: 'replace' }), + ) const isSetupFinished = useCallback(async () => { try { @@ -45,6 +53,34 @@ export const AppInitializer = ({ (async () => { const action = searchParams.get('action') + if (oauthNewUser === 'true') { + let utmInfo = null + const utmInfoStr = Cookies.get('utm_info') + if (utmInfoStr) { + try { + utmInfo = JSON.parse(utmInfoStr) + } + catch (e) { + console.error('Failed to parse utm_info cookie:', e) + } + } + + // Track registration event with UTM params + trackEvent(utmInfo ? 'user_registration_success_with_utm' : 'user_registration_success', { + method: 'oauth', + ...utmInfo, + }) + + sendGAEvent(utmInfo ? 'user_registration_success_with_utm' : 'user_registration_success', { + method: 'oauth', + ...utmInfo, + }) + + // Clean up: remove utm_info cookie and URL params + Cookies.remove('utm_info') + setOauthNewUser(null) + } + if (action === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION) localStorage.setItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'yes') @@ -67,7 +103,7 @@ export const AppInitializer = ({ router.replace('/signin') } })() - }, [isSetupFinished, router, pathname, searchParams]) + }, [isSetupFinished, router, pathname, searchParams, oauthNewUser, setOauthNewUser]) return init ? children : null } diff --git a/web/app/components/base/amplitude/AmplitudeProvider.tsx b/web/app/components/base/amplitude/AmplitudeProvider.tsx index 91c3713a07..87ef516835 100644 --- a/web/app/components/base/amplitude/AmplitudeProvider.tsx +++ b/web/app/components/base/amplitude/AmplitudeProvider.tsx @@ -68,6 +68,7 @@ const AmplitudeProvider: FC = ({ pageViews: true, formInteractions: true, fileDownloads: true, + attribution: true, }, }) diff --git a/web/app/components/base/ga/index.tsx b/web/app/components/base/ga/index.tsx index 2d5fe101d0..eb991092e0 100644 --- a/web/app/components/base/ga/index.tsx +++ b/web/app/components/base/ga/index.tsx @@ -1,3 +1,4 @@ +import type { UnsafeUnwrappedHeaders } from 'next/headers' import type { FC } from 'react' import { headers } from 'next/headers' import Script from 'next/script' @@ -18,45 +19,54 @@ export type IGAProps = { gaType: GaType } -const GA: FC = async ({ +const extractNonceFromCSP = (cspHeader: string | null): string | undefined => { + if (!cspHeader) + return undefined + const nonceMatch = cspHeader.match(/'nonce-([^']+)'/) + return nonceMatch ? nonceMatch[1] : undefined +} + +const GA: FC = ({ gaType, }) => { if (IS_CE_EDITION) return null - const nonce = process.env.NODE_ENV === 'production' ? (await headers()).get('x-nonce') ?? '' : '' + const cspHeader = process.env.NODE_ENV === 'production' + ? (headers() as unknown as UnsafeUnwrappedHeaders).get('content-security-policy') + : null + const nonce = extractNonceFromCSP(cspHeader) return ( <> - + {/* Initialize dataLayer first */} + nonce={nonce} + /> + {/* Load GA script */} + + nonce={nonce} + /> - ) } export default React.memo(GA) diff --git a/web/app/signup/set-password/page.tsx b/web/app/signup/set-password/page.tsx index 4ce69192d5..69af045f1a 100644 --- a/web/app/signup/set-password/page.tsx +++ b/web/app/signup/set-password/page.tsx @@ -1,5 +1,6 @@ 'use client' import type { MailRegisterResponse } from '@/service/use-common' +import Cookies from 'js-cookie' import { useRouter, useSearchParams } from 'next/navigation' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -10,6 +11,20 @@ import Toast from '@/app/components/base/toast' import { validPassword } from '@/config' import { useMailRegister } from '@/service/use-common' import { cn } from '@/utils/classnames' +import { sendGAEvent } from '@/utils/gtag' + +const parseUtmInfo = () => { + const utmInfoStr = Cookies.get('utm_info') + if (!utmInfoStr) + return null + try { + return JSON.parse(utmInfoStr) + } + catch (e) { + console.error('Failed to parse utm_info cookie:', e) + return null + } +} const ChangePasswordForm = () => { const { t } = useTranslation() @@ -55,11 +70,18 @@ const ChangePasswordForm = () => { }) const { result } = res as MailRegisterResponse if (result === 'success') { - // Track registration success event - trackEvent('user_registration_success', { + const utmInfo = parseUtmInfo() + trackEvent(utmInfo ? 'user_registration_success_with_utm' : 'user_registration_success', { method: 'email', + ...utmInfo, }) + sendGAEvent(utmInfo ? 'user_registration_success_with_utm' : 'user_registration_success', { + method: 'email', + ...utmInfo, + }) + Cookies.remove('utm_info') // Clean up: remove utm_info cookie + Toast.notify({ type: 'success', message: t('api.actionSuccess', { ns: 'common' }), diff --git a/web/global.d.ts b/web/global.d.ts index 0ccadf7887..5d0adcfa09 100644 --- a/web/global.d.ts +++ b/web/global.d.ts @@ -9,4 +9,22 @@ declare module 'lamejs/src/js/Lame'; declare module 'lamejs/src/js/BitStream'; declare module 'react-18-input-autosize'; -export { } +declare global { + // Google Analytics gtag types + type GtagEventParams = { + [key: string]: unknown + } + + type Gtag = { + (command: 'config', targetId: string, config?: GtagEventParams): void + (command: 'event', eventName: string, eventParams?: GtagEventParams): void + (command: 'js', date: Date): void + (command: 'set', config: GtagEventParams): void + } + + // eslint-disable-next-line ts/consistent-type-definitions -- interface required for declaration merging + interface Window { + gtag?: Gtag + dataLayer?: unknown[] + } +} diff --git a/web/utils/gtag.ts b/web/utils/gtag.ts new file mode 100644 index 0000000000..5af51a6564 --- /dev/null +++ b/web/utils/gtag.ts @@ -0,0 +1,14 @@ +/** + * Send Google Analytics event + * @param eventName - event name + * @param eventParams - event params + */ +export const sendGAEvent = ( + eventName: string, + eventParams?: GtagEventParams, +): void => { + if (typeof window === 'undefined' || typeof (window as any).gtag !== 'function') { + return + } + (window as any).gtag('event', eventName, eventParams) +} From 69589807fdab2ba71fd506a3e73c42442e75c037 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Wed, 31 Dec 2025 08:32:55 +0800 Subject: [PATCH 02/37] refactor: Replace direct `process.env.NODE_ENV` checks with `IS_PROD` and `IS_DEV` constants. (#30383) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Asuka Minato --- .../components/base/date-and-time-picker/utils/dayjs.ts | 3 ++- web/app/components/base/error-boundary/index.tsx | 9 +++++---- web/app/components/base/ga/index.tsx | 4 ++-- web/app/components/base/zendesk/index.tsx | 4 ++-- web/app/components/header/github-star/index.tsx | 3 ++- web/service/use-common.ts | 3 ++- 6 files changed, 15 insertions(+), 11 deletions(-) diff --git a/web/app/components/base/date-and-time-picker/utils/dayjs.ts b/web/app/components/base/date-and-time-picker/utils/dayjs.ts index b37808209c..23307895a7 100644 --- a/web/app/components/base/date-and-time-picker/utils/dayjs.ts +++ b/web/app/components/base/date-and-time-picker/utils/dayjs.ts @@ -3,6 +3,7 @@ import type { Day } from '../types' import dayjs from 'dayjs' import timezone from 'dayjs/plugin/timezone' import utc from 'dayjs/plugin/utc' +import { IS_PROD } from '@/config' import tz from '@/utils/timezone.json' dayjs.extend(utc) @@ -131,7 +132,7 @@ export type ToDayjsOptions = { } const warnParseFailure = (value: string) => { - if (process.env.NODE_ENV !== 'production') + if (!IS_PROD) console.warn('[TimePicker] Failed to parse time value', value) } diff --git a/web/app/components/base/error-boundary/index.tsx b/web/app/components/base/error-boundary/index.tsx index b041bba4d6..9cb4b70cf5 100644 --- a/web/app/components/base/error-boundary/index.tsx +++ b/web/app/components/base/error-boundary/index.tsx @@ -4,6 +4,7 @@ import { RiAlertLine, RiBugLine } from '@remixicon/react' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' import Button from '@/app/components/base/button' +import { IS_DEV } from '@/config' import { cn } from '@/utils/classnames' type ErrorBoundaryState = { @@ -54,7 +55,7 @@ class ErrorBoundaryInner extends React.Component< } componentDidCatch(error: Error, errorInfo: ErrorInfo) { - if (process.env.NODE_ENV === 'development') { + if (IS_DEV) { console.error('ErrorBoundary caught an error:', error) console.error('Error Info:', errorInfo) } @@ -262,13 +263,13 @@ export function withErrorBoundary

( // Simple error fallback component export const ErrorFallback: React.FC<{ error: Error - resetErrorBoundary: () => void -}> = ({ error, resetErrorBoundary }) => { + resetErrorBoundaryAction: () => void +}> = ({ error, resetErrorBoundaryAction }) => { return (

Oops! Something went wrong

{error.message}

-
diff --git a/web/app/components/base/ga/index.tsx b/web/app/components/base/ga/index.tsx index eb991092e0..03475b3bb4 100644 --- a/web/app/components/base/ga/index.tsx +++ b/web/app/components/base/ga/index.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react' import { headers } from 'next/headers' import Script from 'next/script' import * as React from 'react' -import { IS_CE_EDITION } from '@/config' +import { IS_CE_EDITION, IS_PROD } from '@/config' export enum GaType { admin = 'admin', @@ -32,7 +32,7 @@ const GA: FC = ({ if (IS_CE_EDITION) return null - const cspHeader = process.env.NODE_ENV === 'production' + const cspHeader = IS_PROD ? (headers() as unknown as UnsafeUnwrappedHeaders).get('content-security-policy') : null const nonce = extractNonceFromCSP(cspHeader) diff --git a/web/app/components/base/zendesk/index.tsx b/web/app/components/base/zendesk/index.tsx index dc6ce1e655..e12a128a02 100644 --- a/web/app/components/base/zendesk/index.tsx +++ b/web/app/components/base/zendesk/index.tsx @@ -1,13 +1,13 @@ import { headers } from 'next/headers' import Script from 'next/script' import { memo } from 'react' -import { IS_CE_EDITION, ZENDESK_WIDGET_KEY } from '@/config' +import { IS_CE_EDITION, IS_PROD, ZENDESK_WIDGET_KEY } from '@/config' const Zendesk = async () => { if (IS_CE_EDITION || !ZENDESK_WIDGET_KEY) return null - const nonce = process.env.NODE_ENV === 'production' ? (await headers()).get('x-nonce') ?? '' : '' + const nonce = IS_PROD ? (await headers()).get('x-nonce') ?? '' : '' return ( <> diff --git a/web/app/components/header/github-star/index.tsx b/web/app/components/header/github-star/index.tsx index 01f30fba31..e91bdcca2c 100644 --- a/web/app/components/header/github-star/index.tsx +++ b/web/app/components/header/github-star/index.tsx @@ -3,6 +3,7 @@ import type { FC } from 'react' import type { GithubRepo } from '@/models/common' import { RiLoader2Line } from '@remixicon/react' import { useQuery } from '@tanstack/react-query' +import { IS_DEV } from '@/config' const defaultData = { stargazers_count: 110918, @@ -21,7 +22,7 @@ const GithubStar: FC<{ className: string }> = (props) => { const { isFetching, isError, data } = useQuery({ queryKey: ['github-star'], queryFn: getStar, - enabled: process.env.NODE_ENV !== 'development', + enabled: !IS_DEV, retry: false, placeholderData: defaultData, }) diff --git a/web/service/use-common.ts b/web/service/use-common.ts index 77bb2a11fa..a1edb041c0 100644 --- a/web/service/use-common.ts +++ b/web/service/use-common.ts @@ -23,6 +23,7 @@ import type { } from '@/models/common' import type { RETRIEVE_METHOD } from '@/types/app' import { useMutation, useQuery } from '@tanstack/react-query' +import { IS_DEV } from '@/config' import { get, post } from './base' import { useInvalid } from './use-base' @@ -85,7 +86,7 @@ export const useUserProfile = () => { profile, meta: { currentVersion: response.headers.get('x-version'), - currentEnv: process.env.NODE_ENV === 'development' + currentEnv: IS_DEV ? 'DEVELOPMENT' : response.headers.get('x-env'), }, From 3a59ae96177139507786f8885624b44ba360c22b Mon Sep 17 00:00:00 2001 From: zyssyz123 <916125788@qq.com> Date: Wed, 31 Dec 2025 10:10:58 +0800 Subject: [PATCH 03/37] feat: add oauth_new_user flag for frontend when user oauth login (#30370) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: hj24 --- api/controllers/console/auth/oauth.py | 13 ++++++++---- .../controllers/console/auth/test_oauth.py | 20 ++++++++++--------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index 7ad1e56373..c20e83d36f 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -124,7 +124,7 @@ class OAuthCallback(Resource): return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin/invite-settings?invite_token={invite_token}") try: - account = _generate_account(provider, user_info) + account, oauth_new_user = _generate_account(provider, user_info) except AccountNotFoundError: return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=Account not found.") except (WorkSpaceNotFoundError, WorkSpaceNotAllowedCreateError): @@ -159,7 +159,10 @@ class OAuthCallback(Resource): ip_address=extract_remote_ip(request), ) - response = redirect(f"{dify_config.CONSOLE_WEB_URL}") + base_url = dify_config.CONSOLE_WEB_URL + query_char = "&" if "?" in base_url else "?" + target_url = f"{base_url}{query_char}oauth_new_user={str(oauth_new_user).lower()}" + response = redirect(target_url) set_access_token_to_cookie(request, response, token_pair.access_token) set_refresh_token_to_cookie(request, response, token_pair.refresh_token) @@ -177,9 +180,10 @@ def _get_account_by_openid_or_email(provider: str, user_info: OAuthUserInfo) -> return account -def _generate_account(provider: str, user_info: OAuthUserInfo): +def _generate_account(provider: str, user_info: OAuthUserInfo) -> tuple[Account, bool]: # Get account by openid or email. account = _get_account_by_openid_or_email(provider, user_info) + oauth_new_user = False if account: tenants = TenantService.get_join_tenants(account) @@ -193,6 +197,7 @@ def _generate_account(provider: str, user_info: OAuthUserInfo): tenant_was_created.send(new_tenant) if not account: + oauth_new_user = True if not FeatureService.get_system_features().is_allow_register: if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(user_info.email): raise AccountRegisterError( @@ -220,4 +225,4 @@ def _generate_account(provider: str, user_info: OAuthUserInfo): # Link account AccountService.link_account_integrate(provider, user_info.id, account) - return account + return account, oauth_new_user diff --git a/api/tests/unit_tests/controllers/console/auth/test_oauth.py b/api/tests/unit_tests/controllers/console/auth/test_oauth.py index 399caf8c4d..3ddfcdb832 100644 --- a/api/tests/unit_tests/controllers/console/auth/test_oauth.py +++ b/api/tests/unit_tests/controllers/console/auth/test_oauth.py @@ -171,7 +171,7 @@ class TestOAuthCallback: ): mock_config.CONSOLE_WEB_URL = "http://localhost:3000" mock_get_providers.return_value = {"github": oauth_setup["provider"]} - mock_generate_account.return_value = oauth_setup["account"] + mock_generate_account.return_value = (oauth_setup["account"], True) mock_account_service.login.return_value = oauth_setup["token_pair"] with app.test_request_context("/auth/oauth/github/callback?code=test_code"): @@ -179,7 +179,7 @@ class TestOAuthCallback: oauth_setup["provider"].get_access_token.assert_called_once_with("test_code") oauth_setup["provider"].get_user_info.assert_called_once_with("access_token") - mock_redirect.assert_called_once_with("http://localhost:3000") + mock_redirect.assert_called_once_with("http://localhost:3000?oauth_new_user=true") @pytest.mark.parametrize( ("exception", "expected_error"), @@ -223,7 +223,7 @@ class TestOAuthCallback: # This documents actual behavior. See test_defensive_check_for_closed_account_status for details ( AccountStatus.CLOSED.value, - "http://localhost:3000", + "http://localhost:3000?oauth_new_user=false", ), ], ) @@ -260,7 +260,7 @@ class TestOAuthCallback: account = MagicMock() account.status = account_status account.id = "123" - mock_generate_account.return_value = account + mock_generate_account.return_value = (account, False) # Mock login for CLOSED status mock_token_pair = MagicMock() @@ -296,7 +296,7 @@ class TestOAuthCallback: mock_account = MagicMock() mock_account.status = AccountStatus.PENDING - mock_generate_account.return_value = mock_account + mock_generate_account.return_value = (mock_account, False) mock_token_pair = MagicMock() mock_token_pair.access_token = "jwt_access_token" @@ -360,7 +360,7 @@ class TestOAuthCallback: closed_account.status = AccountStatus.CLOSED closed_account.id = "123" closed_account.name = "Closed Account" - mock_generate_account.return_value = closed_account + mock_generate_account.return_value = (closed_account, False) # Mock successful login (current behavior) mock_token_pair = MagicMock() @@ -374,7 +374,7 @@ class TestOAuthCallback: resource.get("github") # Verify current behavior: login succeeds (this is NOT ideal) - mock_redirect.assert_called_once_with("http://localhost:3000") + mock_redirect.assert_called_once_with("http://localhost:3000?oauth_new_user=false") mock_account_service.login.assert_called_once() # Document expected behavior in comments: @@ -458,8 +458,9 @@ class TestAccountGeneration: with pytest.raises(AccountRegisterError): _generate_account("github", user_info) else: - result = _generate_account("github", user_info) + result, oauth_new_user = _generate_account("github", user_info) assert result == mock_account + assert oauth_new_user == should_create if should_create: mock_register_service.register.assert_called_once_with( @@ -490,9 +491,10 @@ class TestAccountGeneration: mock_tenant_service.create_tenant.return_value = mock_new_tenant with app.test_request_context(headers={"Accept-Language": "en-US,en;q=0.9"}): - result = _generate_account("github", user_info) + result, oauth_new_user = _generate_account("github", user_info) assert result == mock_account + assert oauth_new_user is False mock_tenant_service.create_tenant.assert_called_once_with("Test User's Workspace") mock_tenant_service.create_tenant_member.assert_called_once_with( mock_new_tenant, mock_account, role="owner" From de53c78125a76abb97deb2c17eba40d95d796ebb Mon Sep 17 00:00:00 2001 From: quicksand Date: Wed, 31 Dec 2025 10:11:25 +0800 Subject: [PATCH 04/37] fix(web): template creation permission for app templates (#30367) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 非法操作 --- .../app/create-app-dialog/app-list/index.spec.tsx | 8 +------- .../components/app/create-app-dialog/app-list/index.tsx | 5 +---- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/web/app/components/app/create-app-dialog/app-list/index.spec.tsx b/web/app/components/app/create-app-dialog/app-list/index.spec.tsx index d873b4243e..e0f459ee75 100644 --- a/web/app/components/app/create-app-dialog/app-list/index.spec.tsx +++ b/web/app/components/app/create-app-dialog/app-list/index.spec.tsx @@ -14,13 +14,6 @@ vi.mock('ahooks', () => ({ vi.mock('@/context/app-context', () => ({ useAppContext: () => ({ isCurrentWorkspaceEditor: true }), })) -vi.mock('use-context-selector', async () => { - const actual = await vi.importActual('use-context-selector') - return { - ...actual, - useContext: () => ({ hasEditPermission: true }), - } -}) vi.mock('nuqs', () => ({ useQueryState: () => ['Recommended', vi.fn()], })) @@ -119,6 +112,7 @@ describe('Apps', () => { fireEvent.click(screen.getAllByTestId('app-card')[0]) expect(screen.getByTestId('create-from-template-modal')).toBeInTheDocument() }) + it('shows no template message when list is empty', () => { mockUseExploreAppList.mockReturnValueOnce({ data: { allList: [], categories: [] }, diff --git a/web/app/components/app/create-app-dialog/app-list/index.tsx b/web/app/components/app/create-app-dialog/app-list/index.tsx index 6a30df0dd9..b967ba7d55 100644 --- a/web/app/components/app/create-app-dialog/app-list/index.tsx +++ b/web/app/components/app/create-app-dialog/app-list/index.tsx @@ -8,7 +8,6 @@ import { useRouter } from 'next/navigation' import * as React from 'react' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import AppTypeSelector from '@/app/components/app/type-selector' import { trackEvent } from '@/app/components/base/amplitude' import Divider from '@/app/components/base/divider' @@ -19,7 +18,6 @@ import CreateAppModal from '@/app/components/explore/create-app-modal' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useAppContext } from '@/context/app-context' -import ExploreContext from '@/context/explore-context' import { DSLImportMode } from '@/models/app' import { importDSL } from '@/service/apps' import { fetchAppDetail } from '@/service/explore' @@ -47,7 +45,6 @@ const Apps = ({ const { t } = useTranslation() const { isCurrentWorkspaceEditor } = useAppContext() const { push } = useRouter() - const { hasEditPermission } = useContext(ExploreContext) const allCategoriesEn = AppCategories.RECOMMENDED const [keywords, setKeywords] = useState('') @@ -214,7 +211,7 @@ const Apps = ({ { setCurrApp(app) setIsShowCreateModal(true) From fb5edd0bf65191fa0c6841795213c8c5d9c9b089 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Wed, 31 Dec 2025 11:24:35 +0900 Subject: [PATCH 05/37] =?UTF-8?q?refactor:=20split=20changes=20for=20api/s?= =?UTF-8?q?ervices/tools/api=5Ftools=5Fmanage=5Fservi=E2=80=A6=20(#29899)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/.ruff.toml | 6 +++++- api/core/tools/utils/parser.py | 2 +- .../tools/api_tools_manage_service.py | 20 ++++++++----------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/api/.ruff.toml b/api/.ruff.toml index 7206f7fa0f..8db0cbcb21 100644 --- a/api/.ruff.toml +++ b/api/.ruff.toml @@ -1,4 +1,8 @@ -exclude = ["migrations/*"] +exclude = [ + "migrations/*", + ".git", + ".git/**", +] line-length = 120 [format] diff --git a/api/core/tools/utils/parser.py b/api/core/tools/utils/parser.py index 3486182192..584975de05 100644 --- a/api/core/tools/utils/parser.py +++ b/api/core/tools/utils/parser.py @@ -378,7 +378,7 @@ class ApiBasedToolSchemaParser: @staticmethod def auto_parse_to_tool_bundle( content: str, extra_info: dict | None = None, warning: dict | None = None - ) -> tuple[list[ApiToolBundle], str]: + ) -> tuple[list[ApiToolBundle], ApiProviderSchemaType]: """ auto parse to tool bundle diff --git a/api/services/tools/api_tools_manage_service.py b/api/services/tools/api_tools_manage_service.py index 250d29f335..c32157919b 100644 --- a/api/services/tools/api_tools_manage_service.py +++ b/api/services/tools/api_tools_manage_service.py @@ -85,7 +85,9 @@ class ApiToolManageService: raise ValueError(f"invalid schema: {str(e)}") @staticmethod - def convert_schema_to_tool_bundles(schema: str, extra_info: dict | None = None) -> tuple[list[ApiToolBundle], str]: + def convert_schema_to_tool_bundles( + schema: str, extra_info: dict | None = None + ) -> tuple[list[ApiToolBundle], ApiProviderSchemaType]: """ convert schema to tool bundles @@ -103,7 +105,7 @@ class ApiToolManageService: provider_name: str, icon: dict, credentials: dict, - schema_type: str, + schema_type: ApiProviderSchemaType, schema: str, privacy_policy: str, custom_disclaimer: str, @@ -112,9 +114,6 @@ class ApiToolManageService: """ create api tool provider """ - if schema_type not in [member.value for member in ApiProviderSchemaType]: - raise ValueError(f"invalid schema type {schema}") - provider_name = provider_name.strip() # check if the provider exists @@ -241,18 +240,15 @@ class ApiToolManageService: original_provider: str, icon: dict, credentials: dict, - schema_type: str, + _schema_type: ApiProviderSchemaType, schema: str, - privacy_policy: str, + privacy_policy: str | None, custom_disclaimer: str, labels: list[str], ): """ update api tool provider """ - if schema_type not in [member.value for member in ApiProviderSchemaType]: - raise ValueError(f"invalid schema type {schema}") - provider_name = provider_name.strip() # check if the provider exists @@ -277,7 +273,7 @@ class ApiToolManageService: provider.icon = json.dumps(icon) provider.schema = schema provider.description = extra_info.get("description", "") - provider.schema_type_str = ApiProviderSchemaType.OPENAPI + provider.schema_type_str = schema_type provider.tools_str = json.dumps(jsonable_encoder(tool_bundles)) provider.privacy_policy = privacy_policy provider.custom_disclaimer = custom_disclaimer @@ -356,7 +352,7 @@ class ApiToolManageService: tool_name: str, credentials: dict, parameters: dict, - schema_type: str, + schema_type: ApiProviderSchemaType, schema: str, ): """ From e6f3528bb05c18598457fa1fe504dc77d9b48726 Mon Sep 17 00:00:00 2001 From: Jasonfish Date: Wed, 31 Dec 2025 10:26:28 +0800 Subject: [PATCH 06/37] fix: Incorrect REDIS ssl variable used for Celery causing Celery unable to start (#29605) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> --- api/extensions/ext_celery.py | 3 +-- api/schedule/queue_monitor_task.py | 5 +++++ .../unit_tests/extensions/test_celery_ssl.py | 15 +++++---------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/api/extensions/ext_celery.py b/api/extensions/ext_celery.py index 5cf4984709..764df5d178 100644 --- a/api/extensions/ext_celery.py +++ b/api/extensions/ext_celery.py @@ -12,9 +12,8 @@ from dify_app import DifyApp def _get_celery_ssl_options() -> dict[str, Any] | None: """Get SSL configuration for Celery broker/backend connections.""" - # Use REDIS_USE_SSL for consistency with the main Redis client # Only apply SSL if we're using Redis as broker/backend - if not dify_config.REDIS_USE_SSL: + if not dify_config.BROKER_USE_SSL: return None # Check if Celery is actually using Redis diff --git a/api/schedule/queue_monitor_task.py b/api/schedule/queue_monitor_task.py index db610df290..77d6b5a138 100644 --- a/api/schedule/queue_monitor_task.py +++ b/api/schedule/queue_monitor_task.py @@ -16,6 +16,11 @@ celery_redis = Redis( port=redis_config.get("port") or 6379, password=redis_config.get("password") or None, db=int(redis_config.get("virtual_host")) if redis_config.get("virtual_host") else 1, + ssl=bool(dify_config.BROKER_USE_SSL), + ssl_ca_certs=dify_config.REDIS_SSL_CA_CERTS if dify_config.BROKER_USE_SSL else None, + ssl_cert_reqs=getattr(dify_config, "REDIS_SSL_CERT_REQS", None) if dify_config.BROKER_USE_SSL else None, + ssl_certfile=getattr(dify_config, "REDIS_SSL_CERTFILE", None) if dify_config.BROKER_USE_SSL else None, + ssl_keyfile=getattr(dify_config, "REDIS_SSL_KEYFILE", None) if dify_config.BROKER_USE_SSL else None, ) logger = logging.getLogger(__name__) diff --git a/api/tests/unit_tests/extensions/test_celery_ssl.py b/api/tests/unit_tests/extensions/test_celery_ssl.py index fc7a090ef9..d3a4d69f07 100644 --- a/api/tests/unit_tests/extensions/test_celery_ssl.py +++ b/api/tests/unit_tests/extensions/test_celery_ssl.py @@ -8,11 +8,12 @@ class TestCelerySSLConfiguration: """Test suite for Celery SSL configuration.""" def test_get_celery_ssl_options_when_ssl_disabled(self): - """Test SSL options when REDIS_USE_SSL is False.""" - mock_config = MagicMock() - mock_config.REDIS_USE_SSL = False + """Test SSL options when BROKER_USE_SSL is False.""" + from configs import DifyConfig - with patch("extensions.ext_celery.dify_config", mock_config): + dify_config = DifyConfig(CELERY_BROKER_URL="redis://localhost:6379/0") + + with patch("extensions.ext_celery.dify_config", dify_config): from extensions.ext_celery import _get_celery_ssl_options result = _get_celery_ssl_options() @@ -21,7 +22,6 @@ class TestCelerySSLConfiguration: def test_get_celery_ssl_options_when_broker_not_redis(self): """Test SSL options when broker is not Redis.""" mock_config = MagicMock() - mock_config.REDIS_USE_SSL = True mock_config.CELERY_BROKER_URL = "amqp://localhost:5672" with patch("extensions.ext_celery.dify_config", mock_config): @@ -33,7 +33,6 @@ class TestCelerySSLConfiguration: def test_get_celery_ssl_options_with_cert_none(self): """Test SSL options with CERT_NONE requirement.""" mock_config = MagicMock() - mock_config.REDIS_USE_SSL = True mock_config.CELERY_BROKER_URL = "redis://localhost:6379/0" mock_config.REDIS_SSL_CERT_REQS = "CERT_NONE" mock_config.REDIS_SSL_CA_CERTS = None @@ -53,7 +52,6 @@ class TestCelerySSLConfiguration: def test_get_celery_ssl_options_with_cert_required(self): """Test SSL options with CERT_REQUIRED and certificates.""" mock_config = MagicMock() - mock_config.REDIS_USE_SSL = True mock_config.CELERY_BROKER_URL = "rediss://localhost:6380/0" mock_config.REDIS_SSL_CERT_REQS = "CERT_REQUIRED" mock_config.REDIS_SSL_CA_CERTS = "/path/to/ca.crt" @@ -73,7 +71,6 @@ class TestCelerySSLConfiguration: def test_get_celery_ssl_options_with_cert_optional(self): """Test SSL options with CERT_OPTIONAL requirement.""" mock_config = MagicMock() - mock_config.REDIS_USE_SSL = True mock_config.CELERY_BROKER_URL = "redis://localhost:6379/0" mock_config.REDIS_SSL_CERT_REQS = "CERT_OPTIONAL" mock_config.REDIS_SSL_CA_CERTS = "/path/to/ca.crt" @@ -91,7 +88,6 @@ class TestCelerySSLConfiguration: def test_get_celery_ssl_options_with_invalid_cert_reqs(self): """Test SSL options with invalid cert requirement defaults to CERT_NONE.""" mock_config = MagicMock() - mock_config.REDIS_USE_SSL = True mock_config.CELERY_BROKER_URL = "redis://localhost:6379/0" mock_config.REDIS_SSL_CERT_REQS = "INVALID_VALUE" mock_config.REDIS_SSL_CA_CERTS = None @@ -108,7 +104,6 @@ class TestCelerySSLConfiguration: def test_celery_init_applies_ssl_to_broker_and_backend(self): """Test that SSL options are applied to both broker and backend when using Redis.""" mock_config = MagicMock() - mock_config.REDIS_USE_SSL = True mock_config.CELERY_BROKER_URL = "redis://localhost:6379/0" mock_config.CELERY_BACKEND = "redis" mock_config.CELERY_RESULT_BACKEND = "redis://localhost:6379/0" From 925168383b931086d74184894813c602d67dae22 Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Wed, 31 Dec 2025 10:28:14 +0800 Subject: [PATCH 07/37] fix: keyword search now matches both content and keywords fields (#29619) Co-authored-by: Claude Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../console/datasets/datasets_segments.py | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/api/controllers/console/datasets/datasets_segments.py b/api/controllers/console/datasets/datasets_segments.py index e73abc2555..5a536af6d2 100644 --- a/api/controllers/console/datasets/datasets_segments.py +++ b/api/controllers/console/datasets/datasets_segments.py @@ -3,10 +3,12 @@ import uuid from flask import request from flask_restx import Resource, marshal from pydantic import BaseModel, Field -from sqlalchemy import select +from sqlalchemy import String, cast, func, or_, select +from sqlalchemy.dialects.postgresql import JSONB from werkzeug.exceptions import Forbidden, NotFound import services +from configs import dify_config from controllers.common.schema import register_schema_models from controllers.console import console_ns from controllers.console.app.error import ProviderNotInitializeError @@ -143,7 +145,29 @@ class DatasetDocumentSegmentListApi(Resource): query = query.where(DocumentSegment.hit_count >= hit_count_gte) if keyword: - query = query.where(DocumentSegment.content.ilike(f"%{keyword}%")) + # Search in both content and keywords fields + # Use database-specific methods for JSON array search + if dify_config.SQLALCHEMY_DATABASE_URI_SCHEME == "postgresql": + # PostgreSQL: Use jsonb_array_elements_text to properly handle Unicode/Chinese text + keywords_condition = func.array_to_string( + func.array( + select(func.jsonb_array_elements_text(cast(DocumentSegment.keywords, JSONB))) + .correlate(DocumentSegment) + .scalar_subquery() + ), + ",", + ).ilike(f"%{keyword}%") + else: + # MySQL: Cast JSON to string for pattern matching + # MySQL stores Chinese text directly in JSON without Unicode escaping + keywords_condition = cast(DocumentSegment.keywords, String).ilike(f"%{keyword}%") + + query = query.where( + or_( + DocumentSegment.content.ilike(f"%{keyword}%"), + keywords_condition, + ) + ) if args.enabled.lower() != "all": if args.enabled.lower() == "true": From 9007109a6bf97ddd1766e13313a36d16b6d11182 Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Wed, 31 Dec 2025 10:30:15 +0800 Subject: [PATCH 08/37] fix: [xxx](xxx) render as xxx](xxx) (#30392) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/core/rag/cleaner/clean_processor.py | 44 ++-- api/core/tools/utils/text_processing_utils.py | 6 + .../unit_tests/core/rag/cleaner/__init__.py | 0 .../core/rag/cleaner/test_clean_processor.py | 213 ++++++++++++++++++ .../unit_tests/utils/test_text_processing.py | 5 + 5 files changed, 255 insertions(+), 13 deletions(-) create mode 100644 api/tests/unit_tests/core/rag/cleaner/__init__.py create mode 100644 api/tests/unit_tests/core/rag/cleaner/test_clean_processor.py diff --git a/api/core/rag/cleaner/clean_processor.py b/api/core/rag/cleaner/clean_processor.py index 9cb009035b..e182c35b99 100644 --- a/api/core/rag/cleaner/clean_processor.py +++ b/api/core/rag/cleaner/clean_processor.py @@ -27,26 +27,44 @@ class CleanProcessor: pattern = r"([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)" text = re.sub(pattern, "", text) - # Remove URL but keep Markdown image URLs - # First, temporarily replace Markdown image URLs with a placeholder - markdown_image_pattern = r"!\[.*?\]\((https?://[^\s)]+)\)" - placeholders: list[str] = [] + # Remove URL but keep Markdown image URLs and link URLs + # Replace the ENTIRE markdown link/image with a single placeholder to protect + # the link text (which might also be a URL) from being removed + markdown_link_pattern = r"\[([^\]]*)\]\((https?://[^)]+)\)" + markdown_image_pattern = r"!\[.*?\]\((https?://[^)]+)\)" + placeholders: list[tuple[str, str, str]] = [] # (type, text, url) - def replace_with_placeholder(match, placeholders=placeholders): + def replace_markdown_with_placeholder(match, placeholders=placeholders): + link_type = "link" + link_text = match.group(1) + url = match.group(2) + placeholder = f"__MARKDOWN_PLACEHOLDER_{len(placeholders)}__" + placeholders.append((link_type, link_text, url)) + return placeholder + + def replace_image_with_placeholder(match, placeholders=placeholders): + link_type = "image" url = match.group(1) - placeholder = f"__MARKDOWN_IMAGE_URL_{len(placeholders)}__" - placeholders.append(url) - return f"![image]({placeholder})" + placeholder = f"__MARKDOWN_PLACEHOLDER_{len(placeholders)}__" + placeholders.append((link_type, "image", url)) + return placeholder - text = re.sub(markdown_image_pattern, replace_with_placeholder, text) + # Protect markdown links first + text = re.sub(markdown_link_pattern, replace_markdown_with_placeholder, text) + # Then protect markdown images + text = re.sub(markdown_image_pattern, replace_image_with_placeholder, text) # Now remove all remaining URLs - url_pattern = r"https?://[^\s)]+" + url_pattern = r"https?://\S+" text = re.sub(url_pattern, "", text) - # Finally, restore the Markdown image URLs - for i, url in enumerate(placeholders): - text = text.replace(f"__MARKDOWN_IMAGE_URL_{i}__", url) + # Restore the Markdown links and images + for i, (link_type, text_or_alt, url) in enumerate(placeholders): + placeholder = f"__MARKDOWN_PLACEHOLDER_{i}__" + if link_type == "link": + text = text.replace(placeholder, f"[{text_or_alt}]({url})") + else: # image + text = text.replace(placeholder, f"![{text_or_alt}]({url})") return text def filter_string(self, text): diff --git a/api/core/tools/utils/text_processing_utils.py b/api/core/tools/utils/text_processing_utils.py index 0f9a91a111..4bfaa5e49b 100644 --- a/api/core/tools/utils/text_processing_utils.py +++ b/api/core/tools/utils/text_processing_utils.py @@ -4,6 +4,7 @@ import re def remove_leading_symbols(text: str) -> str: """ Remove leading punctuation or symbols from the given text. + Preserves markdown links like [text](url) at the start. Args: text (str): The input text to process. @@ -11,6 +12,11 @@ def remove_leading_symbols(text: str) -> str: Returns: str: The text with leading punctuation or symbols removed. """ + # Check if text starts with a markdown link - preserve it + markdown_link_pattern = r"^\[([^\]]+)\]\((https?://[^)]+)\)" + if re.match(markdown_link_pattern, text): + return text + # Match Unicode ranges for punctuation and symbols # FIXME this pattern is confused quick fix for #11868 maybe refactor it later pattern = r'^[\[\]\u2000-\u2025\u2027-\u206F\u2E00-\u2E7F\u3000-\u300F\u3011-\u303F"#$%&\'()*+,./:;<=>?@^_`~]+' diff --git a/api/tests/unit_tests/core/rag/cleaner/__init__.py b/api/tests/unit_tests/core/rag/cleaner/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/unit_tests/core/rag/cleaner/test_clean_processor.py b/api/tests/unit_tests/core/rag/cleaner/test_clean_processor.py new file mode 100644 index 0000000000..65ee62b8dd --- /dev/null +++ b/api/tests/unit_tests/core/rag/cleaner/test_clean_processor.py @@ -0,0 +1,213 @@ +from core.rag.cleaner.clean_processor import CleanProcessor + + +class TestCleanProcessor: + """Test cases for CleanProcessor.clean method.""" + + def test_clean_default_removal_of_invalid_symbols(self): + """Test default cleaning removes invalid symbols.""" + # Test <| replacement + assert CleanProcessor.clean("text<|with<|invalid", None) == "text replacement + assert CleanProcessor.clean("text|>with|>invalid", None) == "text>with>invalid" + + # Test removal of control characters + text_with_control = "normal\x00text\x1fwith\x07control\x7fchars" + expected = "normaltextwithcontrolchars" + assert CleanProcessor.clean(text_with_control, None) == expected + + # Test U+FFFE removal + text_with_ufffe = "normal\ufffepadding" + expected = "normalpadding" + assert CleanProcessor.clean(text_with_ufffe, None) == expected + + def test_clean_with_none_process_rule(self): + """Test cleaning with None process_rule - only default cleaning applied.""" + text = "Hello<|World\x00" + expected = "Hello becomes >, control chars and U+FFFE are removed + assert CleanProcessor.clean(text, None) == "<<>>" + + def test_clean_multiple_markdown_links_preserved(self): + """Test multiple markdown links are all preserved.""" + process_rule = {"rules": {"pre_processing_rules": [{"id": "remove_urls_emails", "enabled": True}]}} + + text = "[One](https://one.com) [Two](http://two.org) [Three](https://three.net)" + expected = "[One](https://one.com) [Two](http://two.org) [Three](https://three.net)" + assert CleanProcessor.clean(text, process_rule) == expected + + def test_clean_markdown_link_text_as_url(self): + """Test markdown link where the link text itself is a URL.""" + process_rule = {"rules": {"pre_processing_rules": [{"id": "remove_urls_emails", "enabled": True}]}} + + # Link text that looks like URL should be preserved + text = "[https://text-url.com](https://actual-url.com)" + expected = "[https://text-url.com](https://actual-url.com)" + assert CleanProcessor.clean(text, process_rule) == expected + + # Text URL without markdown should be removed + text = "https://text-url.com https://actual-url.com" + expected = " " + assert CleanProcessor.clean(text, process_rule) == expected + + def test_clean_complex_markdown_link_content(self): + """Test markdown links with complex content - known limitation with brackets in link text.""" + process_rule = {"rules": {"pre_processing_rules": [{"id": "remove_urls_emails", "enabled": True}]}} + + # Note: The regex pattern [^\]]* cannot handle ] within link text + # This is a known limitation - the pattern stops at the first ] + text = "[Text with [brackets] and (parens)](https://example.com)" + # Actual behavior: only matches up to first ], URL gets removed + expected = "[Text with [brackets] and (parens)](" + assert CleanProcessor.clean(text, process_rule) == expected + + # Test that properly formatted markdown links work + text = "[Text with (parens) and symbols](https://example.com)" + expected = "[Text with (parens) and symbols](https://example.com)" + assert CleanProcessor.clean(text, process_rule) == expected diff --git a/api/tests/unit_tests/utils/test_text_processing.py b/api/tests/unit_tests/utils/test_text_processing.py index 11e017464a..bf61162a66 100644 --- a/api/tests/unit_tests/utils/test_text_processing.py +++ b/api/tests/unit_tests/utils/test_text_processing.py @@ -15,6 +15,11 @@ from core.tools.utils.text_processing_utils import remove_leading_symbols ("", ""), (" ", " "), ("【测试】", "【测试】"), + # Markdown link preservation - should be preserved if text starts with a markdown link + ("[Google](https://google.com) is a search engine", "[Google](https://google.com) is a search engine"), + ("[Example](http://example.com) some text", "[Example](http://example.com) some text"), + # Leading symbols before markdown link are removed, including the opening bracket [ + ("@[Test](https://example.com)", "Test](https://example.com)"), ], ) def test_remove_leading_symbols(input_text, expected_output): From 64dc98e60703ec4f52cdc5685d97f796363153d5 Mon Sep 17 00:00:00 2001 From: Sai Date: Wed, 31 Dec 2025 10:45:43 +0800 Subject: [PATCH 09/37] fix: workflow incorrectly marked as completed while nodes are still executing (#30251) Co-authored-by: sai <> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../graph_traversal/skip_propagator.py | 1 + .../graph_engine/graph_traversal/__init__.py | 1 + .../graph_traversal/test_skip_propagator.py | 307 ++++++++++++++++++ 3 files changed, 309 insertions(+) create mode 100644 api/tests/unit_tests/core/workflow/graph_engine/graph_traversal/__init__.py create mode 100644 api/tests/unit_tests/core/workflow/graph_engine/graph_traversal/test_skip_propagator.py diff --git a/api/core/workflow/graph_engine/graph_traversal/skip_propagator.py b/api/core/workflow/graph_engine/graph_traversal/skip_propagator.py index 78f8ecdcdf..b9c9243963 100644 --- a/api/core/workflow/graph_engine/graph_traversal/skip_propagator.py +++ b/api/core/workflow/graph_engine/graph_traversal/skip_propagator.py @@ -60,6 +60,7 @@ class SkipPropagator: if edge_states["has_taken"]: # Enqueue node self._state_manager.enqueue_node(downstream_node_id) + self._state_manager.start_execution(downstream_node_id) return # All edges are skipped, propagate skip to this node diff --git a/api/tests/unit_tests/core/workflow/graph_engine/graph_traversal/__init__.py b/api/tests/unit_tests/core/workflow/graph_engine/graph_traversal/__init__.py new file mode 100644 index 0000000000..cf8811dc2b --- /dev/null +++ b/api/tests/unit_tests/core/workflow/graph_engine/graph_traversal/__init__.py @@ -0,0 +1 @@ +"""Tests for graph traversal components.""" diff --git a/api/tests/unit_tests/core/workflow/graph_engine/graph_traversal/test_skip_propagator.py b/api/tests/unit_tests/core/workflow/graph_engine/graph_traversal/test_skip_propagator.py new file mode 100644 index 0000000000..0019020ede --- /dev/null +++ b/api/tests/unit_tests/core/workflow/graph_engine/graph_traversal/test_skip_propagator.py @@ -0,0 +1,307 @@ +"""Unit tests for skip propagator.""" + +from unittest.mock import MagicMock, create_autospec + +from core.workflow.graph import Edge, Graph +from core.workflow.graph_engine.graph_state_manager import GraphStateManager +from core.workflow.graph_engine.graph_traversal.skip_propagator import SkipPropagator + + +class TestSkipPropagator: + """Test suite for SkipPropagator.""" + + def test_propagate_skip_from_edge_with_unknown_edges_stops_processing(self) -> None: + """When there are unknown incoming edges, propagation should stop.""" + # Arrange + mock_graph = create_autospec(Graph) + mock_state_manager = create_autospec(GraphStateManager) + + # Create a mock edge + mock_edge = MagicMock(spec=Edge) + mock_edge.id = "edge_1" + mock_edge.head = "node_2" + + # Setup graph edges dict + mock_graph.edges = {"edge_1": mock_edge} + + # Setup incoming edges + incoming_edges = [MagicMock(spec=Edge), MagicMock(spec=Edge)] + mock_graph.get_incoming_edges.return_value = incoming_edges + + # Setup state manager to return has_unknown=True + mock_state_manager.analyze_edge_states.return_value = { + "has_unknown": True, + "has_taken": False, + "all_skipped": False, + } + + propagator = SkipPropagator(mock_graph, mock_state_manager) + + # Act + propagator.propagate_skip_from_edge("edge_1") + + # Assert + mock_graph.get_incoming_edges.assert_called_once_with("node_2") + mock_state_manager.analyze_edge_states.assert_called_once_with(incoming_edges) + # Should not call any other state manager methods + mock_state_manager.enqueue_node.assert_not_called() + mock_state_manager.start_execution.assert_not_called() + mock_state_manager.mark_node_skipped.assert_not_called() + + def test_propagate_skip_from_edge_with_taken_edge_enqueues_node(self) -> None: + """When there is at least one taken edge, node should be enqueued.""" + # Arrange + mock_graph = create_autospec(Graph) + mock_state_manager = create_autospec(GraphStateManager) + + # Create a mock edge + mock_edge = MagicMock(spec=Edge) + mock_edge.id = "edge_1" + mock_edge.head = "node_2" + + mock_graph.edges = {"edge_1": mock_edge} + incoming_edges = [MagicMock(spec=Edge)] + mock_graph.get_incoming_edges.return_value = incoming_edges + + # Setup state manager to return has_taken=True + mock_state_manager.analyze_edge_states.return_value = { + "has_unknown": False, + "has_taken": True, + "all_skipped": False, + } + + propagator = SkipPropagator(mock_graph, mock_state_manager) + + # Act + propagator.propagate_skip_from_edge("edge_1") + + # Assert + mock_state_manager.enqueue_node.assert_called_once_with("node_2") + mock_state_manager.start_execution.assert_called_once_with("node_2") + mock_state_manager.mark_node_skipped.assert_not_called() + + def test_propagate_skip_from_edge_with_all_skipped_propagates_to_node(self) -> None: + """When all incoming edges are skipped, should propagate skip to node.""" + # Arrange + mock_graph = create_autospec(Graph) + mock_state_manager = create_autospec(GraphStateManager) + + # Create a mock edge + mock_edge = MagicMock(spec=Edge) + mock_edge.id = "edge_1" + mock_edge.head = "node_2" + + mock_graph.edges = {"edge_1": mock_edge} + incoming_edges = [MagicMock(spec=Edge)] + mock_graph.get_incoming_edges.return_value = incoming_edges + + # Setup state manager to return all_skipped=True + mock_state_manager.analyze_edge_states.return_value = { + "has_unknown": False, + "has_taken": False, + "all_skipped": True, + } + + propagator = SkipPropagator(mock_graph, mock_state_manager) + + # Act + propagator.propagate_skip_from_edge("edge_1") + + # Assert + mock_state_manager.mark_node_skipped.assert_called_once_with("node_2") + mock_state_manager.enqueue_node.assert_not_called() + mock_state_manager.start_execution.assert_not_called() + + def test_propagate_skip_to_node_marks_node_and_outgoing_edges_skipped(self) -> None: + """_propagate_skip_to_node should mark node and all outgoing edges as skipped.""" + # Arrange + mock_graph = create_autospec(Graph) + mock_state_manager = create_autospec(GraphStateManager) + + # Create outgoing edges + edge1 = MagicMock(spec=Edge) + edge1.id = "edge_2" + edge1.head = "node_downstream_1" # Set head for propagate_skip_from_edge + + edge2 = MagicMock(spec=Edge) + edge2.id = "edge_3" + edge2.head = "node_downstream_2" + + # Setup graph edges dict for propagate_skip_from_edge + mock_graph.edges = {"edge_2": edge1, "edge_3": edge2} + mock_graph.get_outgoing_edges.return_value = [edge1, edge2] + + # Setup get_incoming_edges to return empty list to stop recursion + mock_graph.get_incoming_edges.return_value = [] + + propagator = SkipPropagator(mock_graph, mock_state_manager) + + # Use mock to call private method + # Act + propagator._propagate_skip_to_node("node_1") + + # Assert + mock_state_manager.mark_node_skipped.assert_called_once_with("node_1") + mock_state_manager.mark_edge_skipped.assert_any_call("edge_2") + mock_state_manager.mark_edge_skipped.assert_any_call("edge_3") + assert mock_state_manager.mark_edge_skipped.call_count == 2 + # Should recursively propagate from each edge + # Since propagate_skip_from_edge is called, we need to verify it was called + # But we can't directly verify due to recursion. We'll trust the logic. + + def test_skip_branch_paths_marks_unselected_edges_and_propagates(self) -> None: + """skip_branch_paths should mark all unselected edges as skipped and propagate.""" + # Arrange + mock_graph = create_autospec(Graph) + mock_state_manager = create_autospec(GraphStateManager) + + # Create unselected edges + edge1 = MagicMock(spec=Edge) + edge1.id = "edge_1" + edge1.head = "node_downstream_1" + + edge2 = MagicMock(spec=Edge) + edge2.id = "edge_2" + edge2.head = "node_downstream_2" + + unselected_edges = [edge1, edge2] + + # Setup graph edges dict + mock_graph.edges = {"edge_1": edge1, "edge_2": edge2} + # Setup get_incoming_edges to return empty list to stop recursion + mock_graph.get_incoming_edges.return_value = [] + + propagator = SkipPropagator(mock_graph, mock_state_manager) + + # Act + propagator.skip_branch_paths(unselected_edges) + + # Assert + mock_state_manager.mark_edge_skipped.assert_any_call("edge_1") + mock_state_manager.mark_edge_skipped.assert_any_call("edge_2") + assert mock_state_manager.mark_edge_skipped.call_count == 2 + # propagate_skip_from_edge should be called for each edge + # We can't directly verify due to the mock, but the logic is covered + + def test_propagate_skip_from_edge_recursively_propagates_through_graph(self) -> None: + """Skip propagation should recursively propagate through the graph.""" + # Arrange + mock_graph = create_autospec(Graph) + mock_state_manager = create_autospec(GraphStateManager) + + # Create edge chain: edge_1 -> node_2 -> edge_3 -> node_4 + edge1 = MagicMock(spec=Edge) + edge1.id = "edge_1" + edge1.head = "node_2" + + edge3 = MagicMock(spec=Edge) + edge3.id = "edge_3" + edge3.head = "node_4" + + mock_graph.edges = {"edge_1": edge1, "edge_3": edge3} + + # Setup get_incoming_edges to return different values based on node + def get_incoming_edges_side_effect(node_id): + if node_id == "node_2": + return [edge1] + elif node_id == "node_4": + return [edge3] + return [] + + mock_graph.get_incoming_edges.side_effect = get_incoming_edges_side_effect + + # Setup get_outgoing_edges to return different values based on node + def get_outgoing_edges_side_effect(node_id): + if node_id == "node_2": + return [edge3] + elif node_id == "node_4": + return [] # No outgoing edges, stops recursion + return [] + + mock_graph.get_outgoing_edges.side_effect = get_outgoing_edges_side_effect + + # Setup state manager to return all_skipped for both nodes + mock_state_manager.analyze_edge_states.return_value = { + "has_unknown": False, + "has_taken": False, + "all_skipped": True, + } + + propagator = SkipPropagator(mock_graph, mock_state_manager) + + # Act + propagator.propagate_skip_from_edge("edge_1") + + # Assert + # Should mark node_2 as skipped + mock_state_manager.mark_node_skipped.assert_any_call("node_2") + # Should mark edge_3 as skipped + mock_state_manager.mark_edge_skipped.assert_any_call("edge_3") + # Should propagate to node_4 + mock_state_manager.mark_node_skipped.assert_any_call("node_4") + assert mock_state_manager.mark_node_skipped.call_count == 2 + + def test_propagate_skip_from_edge_with_mixed_edge_states_handles_correctly(self) -> None: + """Test with mixed edge states (some unknown, some taken, some skipped).""" + # Arrange + mock_graph = create_autospec(Graph) + mock_state_manager = create_autospec(GraphStateManager) + + mock_edge = MagicMock(spec=Edge) + mock_edge.id = "edge_1" + mock_edge.head = "node_2" + + mock_graph.edges = {"edge_1": mock_edge} + incoming_edges = [MagicMock(spec=Edge), MagicMock(spec=Edge), MagicMock(spec=Edge)] + mock_graph.get_incoming_edges.return_value = incoming_edges + + # Test 1: has_unknown=True, has_taken=False, all_skipped=False + mock_state_manager.analyze_edge_states.return_value = { + "has_unknown": True, + "has_taken": False, + "all_skipped": False, + } + + propagator = SkipPropagator(mock_graph, mock_state_manager) + + # Act + propagator.propagate_skip_from_edge("edge_1") + + # Assert - should stop processing + mock_state_manager.enqueue_node.assert_not_called() + mock_state_manager.mark_node_skipped.assert_not_called() + + # Reset mocks for next test + mock_state_manager.reset_mock() + mock_graph.reset_mock() + + # Test 2: has_unknown=False, has_taken=True, all_skipped=False + mock_state_manager.analyze_edge_states.return_value = { + "has_unknown": False, + "has_taken": True, + "all_skipped": False, + } + + # Act + propagator.propagate_skip_from_edge("edge_1") + + # Assert - should enqueue node + mock_state_manager.enqueue_node.assert_called_once_with("node_2") + mock_state_manager.start_execution.assert_called_once_with("node_2") + + # Reset mocks for next test + mock_state_manager.reset_mock() + mock_graph.reset_mock() + + # Test 3: has_unknown=False, has_taken=False, all_skipped=True + mock_state_manager.analyze_edge_states.return_value = { + "has_unknown": False, + "has_taken": False, + "all_skipped": True, + } + + # Act + propagator.propagate_skip_from_edge("edge_1") + + # Assert - should propagate skip + mock_state_manager.mark_node_skipped.assert_called_once_with("node_2") From 2aaaa4bd34689e8b62b893f50d254db363dd8f0d Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Wed, 31 Dec 2025 11:13:22 +0800 Subject: [PATCH 10/37] feat(web): migrate from es-toolkit/compat to native es-toolkit (#30244) (#30246) --- web/__mocks__/provider-context.ts | 3 ++- .../[appId]/overview/time-range-picker/date-picker.tsx | 2 +- web/app/(shareLayout)/webapp-reset-password/page.tsx | 2 +- .../webapp-signin/components/mail-and-code-auth.tsx | 2 +- .../webapp-signin/components/mail-and-password-auth.tsx | 2 +- .../(commonLayout)/account-page/email-change-modal.tsx | 2 +- .../app/annotation/batch-add-annotation-modal/index.tsx | 2 +- .../app/configuration/base/operation-btn/index.tsx | 2 +- .../app/configuration/config-prompt/simple-prompt-input.tsx | 2 +- .../app/configuration/config/agent/prompt-editor.tsx | 2 +- .../dataset-config/params-config/weighted-score.tsx | 2 +- .../configuration/dataset-config/settings-modal/index.tsx | 2 +- .../debug/debug-with-multiple-model/context.tsx | 2 +- .../debug-with-multiple-model/text-generation-item.tsx | 3 ++- web/app/components/app/configuration/debug/hooks.tsx | 2 +- web/app/components/app/configuration/debug/index.tsx | 3 ++- .../app/configuration/hooks/use-advanced-prompt-config.ts | 2 +- web/app/components/app/configuration/index.tsx | 3 ++- .../app/configuration/tools/external-data-tool-modal.tsx | 2 +- web/app/components/app/create-from-dsl-modal/index.tsx | 2 +- web/app/components/app/duplicate-modal/index.tsx | 2 +- web/app/components/app/log/index.tsx | 2 +- web/app/components/app/log/list.tsx | 3 ++- .../apikey-info-panel/apikey-info-panel.test-utils.tsx | 2 +- web/app/components/app/switch-app-modal/index.tsx | 2 +- web/app/components/app/workflow-log/index.tsx | 2 +- web/app/components/base/agent-log-modal/detail.tsx | 3 ++- web/app/components/base/app-icon-picker/index.tsx | 2 +- web/app/components/base/chat/chat-with-history/context.tsx | 2 +- web/app/components/base/chat/chat-with-history/hooks.tsx | 2 +- web/app/components/base/chat/chat/hooks.ts | 3 ++- web/app/components/base/chat/embedded-chatbot/context.tsx | 2 +- web/app/components/base/chat/embedded-chatbot/hooks.tsx | 2 +- web/app/components/base/emoji-picker/index.tsx | 2 +- .../new-feature-panel/conversation-opener/modal.tsx | 2 +- .../moderation/moderation-setting-modal.tsx | 2 +- web/app/components/base/file-uploader/hooks.ts | 2 +- web/app/components/base/file-uploader/pdf-preview.tsx | 2 +- web/app/components/base/file-uploader/store.tsx | 2 +- web/app/components/base/fullscreen-modal/index.tsx | 2 +- web/app/components/base/image-uploader/image-preview.tsx | 2 +- web/app/components/base/input/index.tsx | 2 +- web/app/components/base/modal/index.tsx | 2 +- web/app/components/base/modal/modal.tsx | 2 +- web/app/components/base/pagination/pagination.tsx | 2 +- .../context-block/context-block-replacement-block.tsx | 2 +- .../base/prompt-editor/plugins/context-block/index.tsx | 2 +- .../history-block/history-block-replacement-block.tsx | 2 +- .../base/prompt-editor/plugins/history-block/index.tsx | 2 +- web/app/components/base/radio-card/index.tsx | 2 +- web/app/components/base/tag-management/panel.tsx | 2 +- web/app/components/base/tag-management/tag-remove-modal.tsx | 2 +- web/app/components/base/toast/index.spec.tsx | 2 +- web/app/components/base/toast/index.tsx | 2 +- .../components/base/with-input-validation/index.spec.tsx | 2 +- .../common/document-status-with-action/index-failed.tsx | 2 +- .../create-options/create-from-dsl-modal/index.tsx | 2 +- web/app/components/datasets/create/step-two/index.tsx | 2 +- .../datasets/documents/detail/batch-modal/index.tsx | 2 +- .../detail/completed/common/full-screen-drawer.tsx | 2 +- .../detail/completed/common/regeneration-modal.tsx | 2 +- .../datasets/documents/detail/completed/index.tsx | 2 +- .../documents/detail/settings/pipeline-settings/index.tsx | 2 +- web/app/components/datasets/documents/list.tsx | 3 ++- web/app/components/datasets/documents/operations.tsx | 2 +- .../datasets/metadata/metadata-dataset/create-content.tsx | 2 +- web/app/components/datasets/rename-modal/index.tsx | 2 +- web/app/components/explore/create-app-modal/index.tsx | 2 +- .../account-setting/api-based-extension-page/modal.tsx | 2 +- .../data-source-page/data-source-notion/index.tsx | 2 +- .../account-setting/data-source-page/panel/config-item.tsx | 2 +- .../members-page/edit-workspace-modal/index.tsx | 2 +- .../account-setting/members-page/invite-modal/index.tsx | 2 +- .../account-setting/members-page/invited-modal/index.tsx | 2 +- .../members-page/transfer-ownership-modal/index.tsx | 2 +- web/app/components/header/account-setting/menu-dialog.tsx | 2 +- web/app/components/header/app-selector/index.tsx | 2 +- web/app/components/plugins/base/deprecation-notice.tsx | 2 +- web/app/components/plugins/marketplace/context.tsx | 3 ++- .../subscription-list/edit/apikey-edit-modal.tsx | 2 +- .../subscription-list/edit/manual-edit-modal.tsx | 2 +- .../subscription-list/edit/oauth-edit-modal.tsx | 2 +- web/app/components/plugins/plugin-page/context.tsx | 2 +- web/app/components/plugins/plugin-page/empty/index.tsx | 2 +- web/app/components/plugins/plugin-page/index.tsx | 2 +- .../plugins/plugin-page/install-plugin-dropdown.tsx | 2 +- .../panel/input-field/field-list/field-list-container.tsx | 2 +- .../components/publish-as-knowledge-pipeline-modal.tsx | 2 +- web/app/components/tools/labels/selector.tsx | 2 +- web/app/components/tools/mcp/modal.tsx | 2 +- .../tools/setting/build-in/config-credentials.tsx | 2 +- .../components/tools/workflow-tool/confirm-modal/index.tsx | 2 +- web/app/components/workflow-app/hooks/use-workflow-run.ts | 2 +- .../workflow/block-selector/market-place-plugin/list.tsx | 2 +- web/app/components/workflow/custom-edge.tsx | 2 +- web/app/components/workflow/dsl-export-confirm-modal.tsx | 2 +- web/app/components/workflow/hooks-store/store.ts | 4 +--- web/app/components/workflow/hooks/use-nodes-layout.ts | 2 +- web/app/components/workflow/index.tsx | 2 +- .../workflow/nodes/_base/components/agent-strategy.tsx | 2 +- .../nodes/_base/components/editor/code-editor/index.tsx | 2 +- .../workflow/nodes/_base/components/file-type-item.tsx | 2 +- .../nodes/_base/components/input-support-select-var.tsx | 2 +- .../workflow/nodes/_base/components/next-step/index.tsx | 2 +- .../workflow/nodes/_base/components/next-step/operator.tsx | 2 +- .../nodes/_base/components/panel-operator/change-block.tsx | 2 +- .../workflow/nodes/_base/components/variable/utils.ts | 3 ++- .../_base/components/variable/var-reference-picker.tsx | 2 +- .../nodes/_base/components/variable/var-reference-vars.tsx | 2 +- .../variable/variable-label/base/variable-label.tsx | 2 +- .../workflow/nodes/_base/hooks/use-one-step-run.ts | 3 ++- .../workflow/nodes/assigner/components/var-list/index.tsx | 2 +- .../nodes/if-else/components/condition-number-input.tsx | 2 +- .../workflow/nodes/if-else/components/condition-wrap.tsx | 2 +- web/app/components/workflow/nodes/iteration/use-config.ts | 2 +- .../metadata/condition-list/condition-value-method.tsx | 2 +- .../components/metadata/metadata-filter/index.tsx | 2 +- .../workflow/nodes/knowledge-retrieval/use-config.ts | 2 +- .../components/workflow/nodes/knowledge-retrieval/utils.ts | 6 ++---- .../json-schema-config-modal/visual-editor/context.tsx | 2 +- .../json-schema-config-modal/visual-editor/hooks.ts | 2 +- .../workflow/nodes/llm/use-single-run-form-params.ts | 2 +- .../nodes/loop/components/condition-number-input.tsx | 2 +- .../nodes/parameter-extractor/use-single-run-form-params.ts | 2 +- .../nodes/question-classifier/components/class-list.tsx | 2 +- .../nodes/question-classifier/use-single-run-form-params.ts | 2 +- .../components/workflow/nodes/start/components/var-item.tsx | 2 +- .../workflow/nodes/tool/components/input-var-list.tsx | 2 +- .../nodes/variable-assigner/components/var-list/index.tsx | 2 +- .../note-editor/plugins/link-editor-plugin/component.tsx | 2 +- .../note-editor/plugins/link-editor-plugin/hooks.ts | 2 +- .../panel/chat-variable-panel/components/variable-item.tsx | 2 +- .../panel/debug-and-preview/conversation-variable-modal.tsx | 3 ++- .../components/workflow/panel/debug-and-preview/index.tsx | 3 ++- web/app/components/workflow/panel/env-panel/env-item.tsx | 2 +- .../workflow/panel/global-variable-panel/item.tsx | 2 +- .../components/workflow/run/utils/format-log/agent/index.ts | 2 +- web/app/components/workflow/run/utils/format-log/index.ts | 2 +- .../workflow/run/utils/format-log/iteration/index.spec.ts | 2 +- .../workflow/run/utils/format-log/loop/index.spec.ts | 2 +- web/app/components/workflow/utils/elk-layout.ts | 2 +- web/app/components/workflow/utils/workflow-init.ts | 4 +--- web/app/components/workflow/workflow-history-store.tsx | 2 +- web/app/education-apply/education-apply-page.tsx | 2 +- web/app/reset-password/page.tsx | 2 +- web/app/signin/components/mail-and-password-auth.tsx | 2 +- web/app/signin/invite-settings/page.tsx | 2 +- web/app/signup/components/input-mail.tsx | 2 +- web/context/app-context.tsx | 2 +- web/context/datasets-context.tsx | 2 +- web/context/debug-configuration.ts | 2 +- web/context/explore-context.ts | 2 +- web/context/mitt-context.tsx | 2 +- web/context/modal-context.tsx | 2 +- web/context/provider-context.tsx | 2 +- web/i18n-config/i18next-config.ts | 2 +- web/package.json | 1 - web/pnpm-lock.yaml | 3 --- web/service/use-plugins.ts | 2 +- web/utils/index.ts | 2 +- 160 files changed, 172 insertions(+), 169 deletions(-) diff --git a/web/__mocks__/provider-context.ts b/web/__mocks__/provider-context.ts index c69a2ad1d2..373c2f86d3 100644 --- a/web/__mocks__/provider-context.ts +++ b/web/__mocks__/provider-context.ts @@ -1,6 +1,7 @@ import type { Plan, UsagePlanInfo } from '@/app/components/billing/type' import type { ProviderContextState } from '@/context/provider-context' -import { merge, noop } from 'es-toolkit/compat' +import { merge } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { defaultPlan } from '@/app/components/billing/config' // Avoid being mocked in tests diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx index 5f72e7df63..368c3dcfc3 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx @@ -4,7 +4,7 @@ import type { FC } from 'react' import type { TriggerProps } from '@/app/components/base/date-and-time-picker/types' import { RiCalendarLine } from '@remixicon/react' import dayjs from 'dayjs' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useCallback } from 'react' import Picker from '@/app/components/base/date-and-time-picker/date-picker' diff --git a/web/app/(shareLayout)/webapp-reset-password/page.tsx b/web/app/(shareLayout)/webapp-reset-password/page.tsx index ec75e15a00..9b9a853cdd 100644 --- a/web/app/(shareLayout)/webapp-reset-password/page.tsx +++ b/web/app/(shareLayout)/webapp-reset-password/page.tsx @@ -1,6 +1,6 @@ 'use client' import { RiArrowLeftLine, RiLockPasswordLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' import { useState } from 'react' diff --git a/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx index f79911099f..5aa9d9f141 100644 --- a/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx +++ b/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx @@ -1,4 +1,4 @@ -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useRouter, useSearchParams } from 'next/navigation' import { useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx index ae70675e7a..23ac83e76c 100644 --- a/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx +++ b/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx @@ -1,5 +1,5 @@ 'use client' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' import { useCallback, useState } from 'react' diff --git a/web/app/account/(commonLayout)/account-page/email-change-modal.tsx b/web/app/account/(commonLayout)/account-page/email-change-modal.tsx index e74ca9ed41..87ca6a689c 100644 --- a/web/app/account/(commonLayout)/account-page/email-change-modal.tsx +++ b/web/app/account/(commonLayout)/account-page/email-change-modal.tsx @@ -1,6 +1,6 @@ import type { ResponseError } from '@/service/fetch' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useRouter } from 'next/navigation' import * as React from 'react' import { useState } from 'react' diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx index ab487d9b91..be1518b708 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/app/configuration/base/operation-btn/index.tsx b/web/app/components/app/configuration/base/operation-btn/index.tsx index d78d9e81cc..d33b632071 100644 --- a/web/app/components/app/configuration/base/operation-btn/index.tsx +++ b/web/app/components/app/configuration/base/operation-btn/index.tsx @@ -4,7 +4,7 @@ import { RiAddLine, RiEditLine, } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useTranslation } from 'react-i18next' import { cn } from '@/utils/classnames' diff --git a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx index c5c63279f6..bc94f87838 100644 --- a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx +++ b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx @@ -4,7 +4,7 @@ import type { ExternalDataTool } from '@/models/common' import type { PromptVariable } from '@/models/debug' import type { GenRes } from '@/service/debug' import { useBoolean } from 'ahooks' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { produce } from 'immer' import * as React from 'react' import { useState } from 'react' diff --git a/web/app/components/app/configuration/config/agent/prompt-editor.tsx b/web/app/components/app/configuration/config/agent/prompt-editor.tsx index 49a80aed6d..e9e3b60859 100644 --- a/web/app/components/app/configuration/config/agent/prompt-editor.tsx +++ b/web/app/components/app/configuration/config/agent/prompt-editor.tsx @@ -2,7 +2,7 @@ import type { FC } from 'react' import type { ExternalDataTool } from '@/models/common' import copy from 'copy-to-clipboard' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' diff --git a/web/app/components/app/configuration/dataset-config/params-config/weighted-score.tsx b/web/app/components/app/configuration/dataset-config/params-config/weighted-score.tsx index 9522d1263a..40beef52e8 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/weighted-score.tsx +++ b/web/app/components/app/configuration/dataset-config/params-config/weighted-score.tsx @@ -1,4 +1,4 @@ -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { memo } from 'react' import { useTranslation } from 'react-i18next' import Slider from '@/app/components/base/slider' diff --git a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx index 557eef3ed5..32ca4f99cd 100644 --- a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx +++ b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx @@ -3,7 +3,7 @@ import type { Member } from '@/models/common' import type { DataSet } from '@/models/datasets' import type { RetrievalConfig } from '@/types/app' import { RiCloseLine } from '@remixicon/react' -import { isEqual } from 'es-toolkit/compat' +import { isEqual } from 'es-toolkit/predicate' import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' diff --git a/web/app/components/app/configuration/debug/debug-with-multiple-model/context.tsx b/web/app/components/app/configuration/debug/debug-with-multiple-model/context.tsx index 3f0381074f..38f803f8ab 100644 --- a/web/app/components/app/configuration/debug/debug-with-multiple-model/context.tsx +++ b/web/app/components/app/configuration/debug/debug-with-multiple-model/context.tsx @@ -1,7 +1,7 @@ 'use client' import type { ModelAndParameter } from '../types' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { createContext, useContext } from 'use-context-selector' export type DebugWithMultipleModelContextType = { diff --git a/web/app/components/app/configuration/debug/debug-with-multiple-model/text-generation-item.tsx b/web/app/components/app/configuration/debug/debug-with-multiple-model/text-generation-item.tsx index 9113f782d9..d7918e7ad6 100644 --- a/web/app/components/app/configuration/debug/debug-with-multiple-model/text-generation-item.tsx +++ b/web/app/components/app/configuration/debug/debug-with-multiple-model/text-generation-item.tsx @@ -4,7 +4,8 @@ import type { OnSend, TextGenerationConfig, } from '@/app/components/base/text-generation/types' -import { cloneDeep, noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' +import { cloneDeep } from 'es-toolkit/object' import { memo } from 'react' import TextGeneration from '@/app/components/app/text-generate/item' import { TransferMethod } from '@/app/components/base/chat/types' diff --git a/web/app/components/app/configuration/debug/hooks.tsx b/web/app/components/app/configuration/debug/hooks.tsx index e66185e284..e5dba3640d 100644 --- a/web/app/components/app/configuration/debug/hooks.tsx +++ b/web/app/components/app/configuration/debug/hooks.tsx @@ -6,7 +6,7 @@ import type { ChatConfig, ChatItem, } from '@/app/components/base/chat/types' -import { cloneDeep } from 'es-toolkit/compat' +import { cloneDeep } from 'es-toolkit/object' import { useCallback, useRef, diff --git a/web/app/components/app/configuration/debug/index.tsx b/web/app/components/app/configuration/debug/index.tsx index 7144d38470..b97bd68c5d 100644 --- a/web/app/components/app/configuration/debug/index.tsx +++ b/web/app/components/app/configuration/debug/index.tsx @@ -11,7 +11,8 @@ import { RiSparklingFill, } from '@remixicon/react' import { useBoolean } from 'ahooks' -import { cloneDeep, noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' +import { cloneDeep } from 'es-toolkit/object' import { produce, setAutoFreeze } from 'immer' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' diff --git a/web/app/components/app/configuration/hooks/use-advanced-prompt-config.ts b/web/app/components/app/configuration/hooks/use-advanced-prompt-config.ts index 3e8f7c5b3a..55b44653c9 100644 --- a/web/app/components/app/configuration/hooks/use-advanced-prompt-config.ts +++ b/web/app/components/app/configuration/hooks/use-advanced-prompt-config.ts @@ -1,6 +1,6 @@ import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { ChatPromptConfig, CompletionPromptConfig, ConversationHistoriesRole, PromptItem } from '@/models/debug' -import { clone } from 'es-toolkit/compat' +import { clone } from 'es-toolkit/object' import { produce } from 'immer' import { useState } from 'react' import { checkHasContextBlock, checkHasHistoryBlock, checkHasQueryBlock, PRE_PROMPT_PLACEHOLDER_TEXT } from '@/app/components/base/prompt-editor/constants' diff --git a/web/app/components/app/configuration/index.tsx b/web/app/components/app/configuration/index.tsx index 8a53d9b328..919b7c355a 100644 --- a/web/app/components/app/configuration/index.tsx +++ b/web/app/components/app/configuration/index.tsx @@ -20,7 +20,8 @@ import type { import type { ModelConfig as BackendModelConfig, UserInputFormItem, VisionSettings } from '@/types/app' import { CodeBracketIcon } from '@heroicons/react/20/solid' import { useBoolean, useGetState } from 'ahooks' -import { clone, isEqual } from 'es-toolkit/compat' +import { clone } from 'es-toolkit/object' +import { isEqual } from 'es-toolkit/predicate' import { produce } from 'immer' import { usePathname } from 'next/navigation' import * as React from 'react' diff --git a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx index 57145cc223..71827c4e0d 100644 --- a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx +++ b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx @@ -3,7 +3,7 @@ import type { CodeBasedExtensionItem, ExternalDataTool, } from '@/models/common' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useState } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' diff --git a/web/app/components/app/create-from-dsl-modal/index.tsx b/web/app/components/app/create-from-dsl-modal/index.tsx index 05873b85a7..838e9cc03f 100644 --- a/web/app/components/app/create-from-dsl-modal/index.tsx +++ b/web/app/components/app/create-from-dsl-modal/index.tsx @@ -3,7 +3,7 @@ import type { MouseEventHandler } from 'react' import { RiCloseLine, RiCommandLine, RiCornerDownLeftLine } from '@remixicon/react' import { useDebounceFn, useKeyPress } from 'ahooks' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useRouter } from 'next/navigation' import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/app/duplicate-modal/index.tsx b/web/app/components/app/duplicate-modal/index.tsx index 315d871178..7d5b122f69 100644 --- a/web/app/components/app/duplicate-modal/index.tsx +++ b/web/app/components/app/duplicate-modal/index.tsx @@ -1,7 +1,7 @@ 'use client' import type { AppIconType } from '@/types/app' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/app/log/index.tsx b/web/app/components/app/log/index.tsx index d282d61d4e..e96c9ce0c9 100644 --- a/web/app/components/app/log/index.tsx +++ b/web/app/components/app/log/index.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react' import type { App } from '@/types/app' import { useDebounce } from 'ahooks' import dayjs from 'dayjs' -import { omit } from 'es-toolkit/compat' +import { omit } from 'es-toolkit/object' import { usePathname, useRouter, useSearchParams } from 'next/navigation' import * as React from 'react' import { useCallback, useEffect, useState } from 'react' diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index c260568582..a17177bf7e 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -12,7 +12,8 @@ import { RiCloseLine, RiEditFill } from '@remixicon/react' import dayjs from 'dayjs' import timezone from 'dayjs/plugin/timezone' import utc from 'dayjs/plugin/utc' -import { get, noop } from 'es-toolkit/compat' +import { get } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { usePathname, useRouter, useSearchParams } from 'next/navigation' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' diff --git a/web/app/components/app/overview/apikey-info-panel/apikey-info-panel.test-utils.tsx b/web/app/components/app/overview/apikey-info-panel/apikey-info-panel.test-utils.tsx index 5cffa1143b..17857ec702 100644 --- a/web/app/components/app/overview/apikey-info-panel/apikey-info-panel.test-utils.tsx +++ b/web/app/components/app/overview/apikey-info-panel/apikey-info-panel.test-utils.tsx @@ -2,7 +2,7 @@ import type { RenderOptions } from '@testing-library/react' import type { Mock, MockedFunction } from 'vitest' import type { ModalContextState } from '@/context/modal-context' import { fireEvent, render } from '@testing-library/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { defaultPlan } from '@/app/components/billing/config' import { useModalContext as actualUseModalContext } from '@/context/modal-context' diff --git a/web/app/components/app/switch-app-modal/index.tsx b/web/app/components/app/switch-app-modal/index.tsx index 259f8da0b0..30d7877ed0 100644 --- a/web/app/components/app/switch-app-modal/index.tsx +++ b/web/app/components/app/switch-app-modal/index.tsx @@ -2,7 +2,7 @@ import type { App } from '@/types/app' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useRouter } from 'next/navigation' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/app/workflow-log/index.tsx b/web/app/components/app/workflow-log/index.tsx index ae7b08320e..79ae2ed83c 100644 --- a/web/app/components/app/workflow-log/index.tsx +++ b/web/app/components/app/workflow-log/index.tsx @@ -5,7 +5,7 @@ import { useDebounce } from 'ahooks' import dayjs from 'dayjs' import timezone from 'dayjs/plugin/timezone' import utc from 'dayjs/plugin/utc' -import { omit } from 'es-toolkit/compat' +import { omit } from 'es-toolkit/object' import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/base/agent-log-modal/detail.tsx b/web/app/components/base/agent-log-modal/detail.tsx index fff2c3920d..a82a3207b1 100644 --- a/web/app/components/base/agent-log-modal/detail.tsx +++ b/web/app/components/base/agent-log-modal/detail.tsx @@ -2,7 +2,8 @@ import type { FC } from 'react' import type { IChatItem } from '@/app/components/base/chat/chat/type' import type { AgentIteration, AgentLogDetailResponse } from '@/models/log' -import { flatten, uniq } from 'es-toolkit/compat' +import { uniq } from 'es-toolkit/array' +import { flatten } from 'es-toolkit/compat' import * as React from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/base/app-icon-picker/index.tsx b/web/app/components/base/app-icon-picker/index.tsx index 9b9c642d51..4dfad1f6eb 100644 --- a/web/app/components/base/app-icon-picker/index.tsx +++ b/web/app/components/base/app-icon-picker/index.tsx @@ -3,7 +3,7 @@ import type { Area } from 'react-easy-crop' import type { OnImageInput } from './ImageInput' import type { AppIconType, ImageFile } from '@/types/app' import { RiImageCircleAiLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config' 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 d1496f8278..49dd06ca52 100644 --- a/web/app/components/base/chat/chat-with-history/context.tsx +++ b/web/app/components/base/chat/chat-with-history/context.tsx @@ -14,7 +14,7 @@ import type { AppMeta, ConversationItem, } from '@/models/share' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { createContext, useContext } from 'use-context-selector' export type ChatWithHistoryContextValue = { 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 154729ded0..5ff8e61ff6 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -10,7 +10,7 @@ import type { ConversationItem, } from '@/models/share' import { useLocalStorageState } from 'ahooks' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { produce } from 'immer' import { useCallback, diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts index 50704d9a0d..9b8a9b11dc 100644 --- a/web/app/components/base/chat/chat/hooks.ts +++ b/web/app/components/base/chat/chat/hooks.ts @@ -8,7 +8,8 @@ import type { InputForm } from './type' import type AudioPlayer from '@/app/components/base/audio-btn/audio' import type { FileEntity } from '@/app/components/base/file-uploader/types' import type { Annotation } from '@/models/log' -import { noop, uniqBy } from 'es-toolkit/compat' +import { uniqBy } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { produce, setAutoFreeze } from 'immer' import { useParams, usePathname } from 'next/navigation' import { diff --git a/web/app/components/base/chat/embedded-chatbot/context.tsx b/web/app/components/base/chat/embedded-chatbot/context.tsx index 97d3dd53cf..d690c28dd3 100644 --- a/web/app/components/base/chat/embedded-chatbot/context.tsx +++ b/web/app/components/base/chat/embedded-chatbot/context.tsx @@ -13,7 +13,7 @@ import type { AppMeta, ConversationItem, } from '@/models/share' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { createContext, useContext } from 'use-context-selector' export type EmbeddedChatbotContextValue = { diff --git a/web/app/components/base/chat/embedded-chatbot/hooks.tsx b/web/app/components/base/chat/embedded-chatbot/hooks.tsx index 7a7cf4ffd3..803e905837 100644 --- a/web/app/components/base/chat/embedded-chatbot/hooks.tsx +++ b/web/app/components/base/chat/embedded-chatbot/hooks.tsx @@ -9,7 +9,7 @@ import type { ConversationItem, } from '@/models/share' import { useLocalStorageState } from 'ahooks' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { produce } from 'immer' import { useCallback, diff --git a/web/app/components/base/emoji-picker/index.tsx b/web/app/components/base/emoji-picker/index.tsx index 99e1cfb7b4..9356efbfeb 100644 --- a/web/app/components/base/emoji-picker/index.tsx +++ b/web/app/components/base/emoji-picker/index.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' 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 6eb31d7476..79520134a4 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 @@ -3,7 +3,7 @@ import type { InputVar } from '@/app/components/workflow/types' import type { PromptVariable } from '@/models/debug' import { RiAddLine, RiAsterisk, RiCloseLine, RiDeleteBinLine, RiDraggable } from '@remixicon/react' import { useBoolean } from 'ahooks' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { produce } from 'immer' import * as React from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' diff --git a/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx b/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx index c352913e30..59b62d0bfd 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx @@ -2,7 +2,7 @@ import type { ChangeEvent, FC } from 'react' import type { CodeBasedExtensionItem } from '@/models/common' import type { ModerationConfig, ModerationContentConfig } from '@/models/debug' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useState } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' diff --git a/web/app/components/base/file-uploader/hooks.ts b/web/app/components/base/file-uploader/hooks.ts index 95a775f3bb..14e46548d8 100644 --- a/web/app/components/base/file-uploader/hooks.ts +++ b/web/app/components/base/file-uploader/hooks.ts @@ -2,7 +2,7 @@ import type { ClipboardEvent } from 'react' import type { FileEntity } from './types' import type { FileUpload } from '@/app/components/base/features/types' import type { FileUploadConfigResponse } from '@/models/common' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { produce } from 'immer' import { useParams } from 'next/navigation' import { diff --git a/web/app/components/base/file-uploader/pdf-preview.tsx b/web/app/components/base/file-uploader/pdf-preview.tsx index 25644d024e..aab8bcd9d1 100644 --- a/web/app/components/base/file-uploader/pdf-preview.tsx +++ b/web/app/components/base/file-uploader/pdf-preview.tsx @@ -1,6 +1,6 @@ import type { FC } from 'react' import { RiCloseLine, RiZoomInLine, RiZoomOutLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { t } from 'i18next' import * as React from 'react' import { useState } from 'react' diff --git a/web/app/components/base/file-uploader/store.tsx b/web/app/components/base/file-uploader/store.tsx index 2172733f20..24015df5cf 100644 --- a/web/app/components/base/file-uploader/store.tsx +++ b/web/app/components/base/file-uploader/store.tsx @@ -1,7 +1,7 @@ import type { FileEntity, } from './types' -import { isEqual } from 'es-toolkit/compat' +import { isEqual } from 'es-toolkit/predicate' import { createContext, useContext, diff --git a/web/app/components/base/fullscreen-modal/index.tsx b/web/app/components/base/fullscreen-modal/index.tsx index cad91b2452..fb2d2fa79b 100644 --- a/web/app/components/base/fullscreen-modal/index.tsx +++ b/web/app/components/base/fullscreen-modal/index.tsx @@ -1,6 +1,6 @@ import { Dialog, DialogPanel, Transition, TransitionChild } from '@headlessui/react' import { RiCloseLargeLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { cn } from '@/utils/classnames' type IModal = { diff --git a/web/app/components/base/image-uploader/image-preview.tsx b/web/app/components/base/image-uploader/image-preview.tsx index 794152804e..b6a07c60aa 100644 --- a/web/app/components/base/image-uploader/image-preview.tsx +++ b/web/app/components/base/image-uploader/image-preview.tsx @@ -1,6 +1,6 @@ import type { FC } from 'react' import { RiAddBoxLine, RiCloseLine, RiDownloadCloud2Line, RiFileCopyLine, RiZoomInLine, RiZoomOutLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { t } from 'i18next' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' diff --git a/web/app/components/base/input/index.tsx b/web/app/components/base/input/index.tsx index b529702a65..27adbb3973 100644 --- a/web/app/components/base/input/index.tsx +++ b/web/app/components/base/input/index.tsx @@ -2,7 +2,7 @@ import type { VariantProps } from 'class-variance-authority' import type { ChangeEventHandler, CSSProperties, FocusEventHandler } from 'react' import { RiCloseCircleFill, RiErrorWarningLine, RiSearchLine } from '@remixicon/react' import { cva } from 'class-variance-authority' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useTranslation } from 'react-i18next' import { cn } from '@/utils/classnames' diff --git a/web/app/components/base/modal/index.tsx b/web/app/components/base/modal/index.tsx index 7270af1c77..e38254b27a 100644 --- a/web/app/components/base/modal/index.tsx +++ b/web/app/components/base/modal/index.tsx @@ -1,6 +1,6 @@ import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { Fragment } from 'react' import { cn } from '@/utils/classnames' // https://headlessui.com/react/dialog diff --git a/web/app/components/base/modal/modal.tsx b/web/app/components/base/modal/modal.tsx index 1e606f125a..a24ca1765e 100644 --- a/web/app/components/base/modal/modal.tsx +++ b/web/app/components/base/modal/modal.tsx @@ -1,6 +1,6 @@ import type { ButtonProps } from '@/app/components/base/button' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { memo } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' diff --git a/web/app/components/base/pagination/pagination.tsx b/web/app/components/base/pagination/pagination.tsx index dafe0e4ab9..0eb06b594c 100644 --- a/web/app/components/base/pagination/pagination.tsx +++ b/web/app/components/base/pagination/pagination.tsx @@ -4,7 +4,7 @@ import type { IPaginationProps, PageButtonProps, } from './type' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { cn } from '@/utils/classnames' import usePagination from './hook' diff --git a/web/app/components/base/prompt-editor/plugins/context-block/context-block-replacement-block.tsx b/web/app/components/base/prompt-editor/plugins/context-block/context-block-replacement-block.tsx index c4a246c40d..36e0d7f17b 100644 --- a/web/app/components/base/prompt-editor/plugins/context-block/context-block-replacement-block.tsx +++ b/web/app/components/base/prompt-editor/plugins/context-block/context-block-replacement-block.tsx @@ -1,7 +1,7 @@ import type { ContextBlockType } from '../../types' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { mergeRegister } from '@lexical/utils' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { $applyNodeReplacement } from 'lexical' import { memo, diff --git a/web/app/components/base/prompt-editor/plugins/context-block/index.tsx b/web/app/components/base/prompt-editor/plugins/context-block/index.tsx index ce3ed4c210..e3382c011d 100644 --- a/web/app/components/base/prompt-editor/plugins/context-block/index.tsx +++ b/web/app/components/base/prompt-editor/plugins/context-block/index.tsx @@ -1,7 +1,7 @@ import type { ContextBlockType } from '../../types' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { mergeRegister } from '@lexical/utils' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { $insertNodes, COMMAND_PRIORITY_EDITOR, diff --git a/web/app/components/base/prompt-editor/plugins/history-block/history-block-replacement-block.tsx b/web/app/components/base/prompt-editor/plugins/history-block/history-block-replacement-block.tsx index f62fb6886b..759e654c02 100644 --- a/web/app/components/base/prompt-editor/plugins/history-block/history-block-replacement-block.tsx +++ b/web/app/components/base/prompt-editor/plugins/history-block/history-block-replacement-block.tsx @@ -1,7 +1,7 @@ import type { HistoryBlockType } from '../../types' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { mergeRegister } from '@lexical/utils' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { $applyNodeReplacement } from 'lexical' import { useCallback, diff --git a/web/app/components/base/prompt-editor/plugins/history-block/index.tsx b/web/app/components/base/prompt-editor/plugins/history-block/index.tsx index dc75fc230d..a1d788c8cd 100644 --- a/web/app/components/base/prompt-editor/plugins/history-block/index.tsx +++ b/web/app/components/base/prompt-editor/plugins/history-block/index.tsx @@ -1,7 +1,7 @@ import type { HistoryBlockType } from '../../types' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { mergeRegister } from '@lexical/utils' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { $insertNodes, COMMAND_PRIORITY_EDITOR, diff --git a/web/app/components/base/radio-card/index.tsx b/web/app/components/base/radio-card/index.tsx index 678d5b6dee..c349d08a96 100644 --- a/web/app/components/base/radio-card/index.tsx +++ b/web/app/components/base/radio-card/index.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { cn } from '@/utils/classnames' diff --git a/web/app/components/base/tag-management/panel.tsx b/web/app/components/base/tag-management/panel.tsx index 50f9298963..adf750580f 100644 --- a/web/app/components/base/tag-management/panel.tsx +++ b/web/app/components/base/tag-management/panel.tsx @@ -3,7 +3,7 @@ import type { HtmlContentProps } from '@/app/components/base/popover' import type { Tag } from '@/app/components/base/tag-management/constant' import { RiAddLine, RiPriceTag3Line } from '@remixicon/react' import { useUnmount } from 'ahooks' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/base/tag-management/tag-remove-modal.tsx b/web/app/components/base/tag-management/tag-remove-modal.tsx index a0fdf056d5..6ce52e9dd6 100644 --- a/web/app/components/base/tag-management/tag-remove-modal.tsx +++ b/web/app/components/base/tag-management/tag-remove-modal.tsx @@ -2,7 +2,7 @@ import type { Tag } from '@/app/components/base/tag-management/constant' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' diff --git a/web/app/components/base/toast/index.spec.tsx b/web/app/components/base/toast/index.spec.tsx index 59314063dd..cc5a1e7c6d 100644 --- a/web/app/components/base/toast/index.spec.tsx +++ b/web/app/components/base/toast/index.spec.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react' import { act, render, screen, waitFor } from '@testing-library/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import Toast, { ToastProvider, useToastContext } from '.' diff --git a/web/app/components/base/toast/index.tsx b/web/app/components/base/toast/index.tsx index cf9e1cd909..0ab636efc1 100644 --- a/web/app/components/base/toast/index.tsx +++ b/web/app/components/base/toast/index.tsx @@ -7,7 +7,7 @@ import { RiErrorWarningFill, RiInformation2Fill, } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useEffect, useState } from 'react' import { createRoot } from 'react-dom/client' diff --git a/web/app/components/base/with-input-validation/index.spec.tsx b/web/app/components/base/with-input-validation/index.spec.tsx index b2e67ce056..daf3fd9a74 100644 --- a/web/app/components/base/with-input-validation/index.spec.tsx +++ b/web/app/components/base/with-input-validation/index.spec.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { z } from 'zod' import withValidation from '.' diff --git a/web/app/components/datasets/common/document-status-with-action/index-failed.tsx b/web/app/components/datasets/common/document-status-with-action/index-failed.tsx index a97b74cf2e..b691f4e8c5 100644 --- a/web/app/components/datasets/common/document-status-with-action/index-failed.tsx +++ b/web/app/components/datasets/common/document-status-with-action/index-failed.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' import type { IndexingStatusResponse } from '@/models/datasets' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useEffect, useReducer } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/index.tsx b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/index.tsx index 38481f757f..2d187010b8 100644 --- a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/index.tsx +++ b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/index.tsx @@ -1,6 +1,6 @@ 'use client' import { useDebounceFn, useKeyPress } from 'ahooks' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useRouter } from 'next/navigation' import { useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/datasets/create/step-two/index.tsx b/web/app/components/datasets/create/step-two/index.tsx index ecc517ed48..51b5c15178 100644 --- a/web/app/components/datasets/create/step-two/index.tsx +++ b/web/app/components/datasets/create/step-two/index.tsx @@ -9,7 +9,7 @@ import { RiArrowLeftLine, RiSearchEyeLine, } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import Image from 'next/image' import Link from 'next/link' import { useCallback, useEffect, useMemo, useState } from 'react' diff --git a/web/app/components/datasets/documents/detail/batch-modal/index.tsx b/web/app/components/datasets/documents/detail/batch-modal/index.tsx index 080b631bae..8917d85ee7 100644 --- a/web/app/components/datasets/documents/detail/batch-modal/index.tsx +++ b/web/app/components/datasets/documents/detail/batch-modal/index.tsx @@ -2,7 +2,7 @@ import type { FC } from 'react' import type { ChunkingMode, FileItem } from '@/models/datasets' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/datasets/documents/detail/completed/common/full-screen-drawer.tsx b/web/app/components/datasets/documents/detail/completed/common/full-screen-drawer.tsx index 73b5cbdef9..de817b8d07 100644 --- a/web/app/components/datasets/documents/detail/completed/common/full-screen-drawer.tsx +++ b/web/app/components/datasets/documents/detail/completed/common/full-screen-drawer.tsx @@ -1,4 +1,4 @@ -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { cn } from '@/utils/classnames' import Drawer from './drawer' diff --git a/web/app/components/datasets/documents/detail/completed/common/regeneration-modal.tsx b/web/app/components/datasets/documents/detail/completed/common/regeneration-modal.tsx index 11f8b9fc65..23c1e826b7 100644 --- a/web/app/components/datasets/documents/detail/completed/common/regeneration-modal.tsx +++ b/web/app/components/datasets/documents/detail/completed/common/regeneration-modal.tsx @@ -1,7 +1,7 @@ import type { FC } from 'react' import { RiLoader2Line } from '@remixicon/react' import { useCountDown } from 'ahooks' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useRef, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/datasets/documents/detail/completed/index.tsx b/web/app/components/datasets/documents/detail/completed/index.tsx index 531603a1cf..40c70e34f6 100644 --- a/web/app/components/datasets/documents/detail/completed/index.tsx +++ b/web/app/components/datasets/documents/detail/completed/index.tsx @@ -4,7 +4,7 @@ import type { Item } from '@/app/components/base/select' import type { FileEntity } from '@/app/components/datasets/common/image-uploader/types' import type { ChildChunkDetail, SegmentDetailModel, SegmentUpdater } from '@/models/datasets' import { useDebounceFn } from 'ahooks' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { usePathname } from 'next/navigation' import * as React from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' diff --git a/web/app/components/datasets/documents/detail/settings/pipeline-settings/index.tsx b/web/app/components/datasets/documents/detail/settings/pipeline-settings/index.tsx index 76c9e2da96..08e13765e5 100644 --- a/web/app/components/datasets/documents/detail/settings/pipeline-settings/index.tsx +++ b/web/app/components/datasets/documents/detail/settings/pipeline-settings/index.tsx @@ -1,7 +1,7 @@ import type { NotionPage } from '@/models/common' import type { CrawlResultItem, CustomFile, FileIndexingEstimateResponse } from '@/models/datasets' import type { OnlineDriveFile, PublishedPipelineRunPreviewResponse } from '@/models/pipeline' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useRouter } from 'next/navigation' import { useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/datasets/documents/list.tsx b/web/app/components/datasets/documents/list.tsx index 0e039bba1a..5fd6cd3a70 100644 --- a/web/app/components/datasets/documents/list.tsx +++ b/web/app/components/datasets/documents/list.tsx @@ -9,7 +9,8 @@ import { RiGlobalLine, } from '@remixicon/react' import { useBoolean } from 'ahooks' -import { pick, uniq } from 'es-toolkit/compat' +import { uniq } from 'es-toolkit/array' +import { pick } from 'es-toolkit/object' import { useRouter } from 'next/navigation' import * as React from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' diff --git a/web/app/components/datasets/documents/operations.tsx b/web/app/components/datasets/documents/operations.tsx index a873fcd96d..93afec6f8e 100644 --- a/web/app/components/datasets/documents/operations.tsx +++ b/web/app/components/datasets/documents/operations.tsx @@ -11,7 +11,7 @@ import { RiPlayCircleLine, } from '@remixicon/react' import { useBoolean, useDebounceFn } from 'ahooks' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useRouter } from 'next/navigation' import * as React from 'react' import { useCallback, useState } from 'react' diff --git a/web/app/components/datasets/metadata/metadata-dataset/create-content.tsx b/web/app/components/datasets/metadata/metadata-dataset/create-content.tsx index b130267236..ee1b9cbcdc 100644 --- a/web/app/components/datasets/metadata/metadata-dataset/create-content.tsx +++ b/web/app/components/datasets/metadata/metadata-dataset/create-content.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' import { RiArrowLeftLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/datasets/rename-modal/index.tsx b/web/app/components/datasets/rename-modal/index.tsx index 589b5a918e..106e35c4ac 100644 --- a/web/app/components/datasets/rename-modal/index.tsx +++ b/web/app/components/datasets/rename-modal/index.tsx @@ -4,7 +4,7 @@ import type { MouseEventHandler } from 'react' import type { AppIconSelection } from '../../base/app-icon-picker' import type { DataSet } from '@/models/datasets' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' diff --git a/web/app/components/explore/create-app-modal/index.tsx b/web/app/components/explore/create-app-modal/index.tsx index 1137ae9e74..9bffcc6c69 100644 --- a/web/app/components/explore/create-app-modal/index.tsx +++ b/web/app/components/explore/create-app-modal/index.tsx @@ -2,7 +2,7 @@ import type { AppIconType } from '@/types/app' import { RiCloseLine, RiCommandLine, RiCornerDownLeftLine } from '@remixicon/react' import { useDebounceFn, useKeyPress } from 'ahooks' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/header/account-setting/api-based-extension-page/modal.tsx b/web/app/components/header/account-setting/api-based-extension-page/modal.tsx index e12ecbe756..d857b5fe1f 100644 --- a/web/app/components/header/account-setting/api-based-extension-page/modal.tsx +++ b/web/app/components/header/account-setting/api-based-extension-page/modal.tsx @@ -1,6 +1,6 @@ import type { FC } from 'react' import type { ApiBasedExtension } from '@/models/common' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useState } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' diff --git a/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx b/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx index 8f6120c826..0959383f29 100644 --- a/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx +++ b/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' import type { DataSourceNotion as TDataSourceNotion } from '@/models/common' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/header/account-setting/data-source-page/panel/config-item.tsx b/web/app/components/header/account-setting/data-source-page/panel/config-item.tsx index 06c311ef56..f62c5e147d 100644 --- a/web/app/components/header/account-setting/data-source-page/panel/config-item.tsx +++ b/web/app/components/header/account-setting/data-source-page/panel/config-item.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react' import { RiDeleteBinLine, } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useTranslation } from 'react-i18next' import { cn } from '@/utils/classnames' diff --git a/web/app/components/header/account-setting/members-page/edit-workspace-modal/index.tsx b/web/app/components/header/account-setting/members-page/edit-workspace-modal/index.tsx index 411c437a75..76f04382bd 100644 --- a/web/app/components/header/account-setting/members-page/edit-workspace-modal/index.tsx +++ b/web/app/components/header/account-setting/members-page/edit-workspace-modal/index.tsx @@ -1,6 +1,6 @@ 'use client' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' diff --git a/web/app/components/header/account-setting/members-page/invite-modal/index.tsx b/web/app/components/header/account-setting/members-page/invite-modal/index.tsx index 964d25e1cb..2d8d138af5 100644 --- a/web/app/components/header/account-setting/members-page/invite-modal/index.tsx +++ b/web/app/components/header/account-setting/members-page/invite-modal/index.tsx @@ -3,7 +3,7 @@ import type { RoleKey } from './role-selector' import type { InvitationResult } from '@/models/common' import { RiCloseLine, RiErrorWarningFill } from '@remixicon/react' import { useBoolean } from 'ahooks' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { ReactMultiEmail } from 'react-multi-email' diff --git a/web/app/components/header/account-setting/members-page/invited-modal/index.tsx b/web/app/components/header/account-setting/members-page/invited-modal/index.tsx index 3f756ab10d..389db4a42d 100644 --- a/web/app/components/header/account-setting/members-page/invited-modal/index.tsx +++ b/web/app/components/header/account-setting/members-page/invited-modal/index.tsx @@ -2,7 +2,7 @@ import type { InvitationResult } from '@/models/common' import { XMarkIcon } from '@heroicons/react/24/outline' import { CheckCircleIcon } from '@heroicons/react/24/solid' import { RiQuestionLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' diff --git a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx index 1d54167458..be7220da5e 100644 --- a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx +++ b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx @@ -1,5 +1,5 @@ import { RiCloseLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useState } from 'react' import { Trans, useTranslation } from 'react-i18next' diff --git a/web/app/components/header/account-setting/menu-dialog.tsx b/web/app/components/header/account-setting/menu-dialog.tsx index cc5adbc18f..847634ed83 100644 --- a/web/app/components/header/account-setting/menu-dialog.tsx +++ b/web/app/components/header/account-setting/menu-dialog.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react' import { Dialog, DialogPanel, Transition, TransitionChild } from '@headlessui/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { Fragment, useCallback, useEffect } from 'react' import { cn } from '@/utils/classnames' diff --git a/web/app/components/header/app-selector/index.tsx b/web/app/components/header/app-selector/index.tsx index c1b9cd1b83..13677ef7ab 100644 --- a/web/app/components/header/app-selector/index.tsx +++ b/web/app/components/header/app-selector/index.tsx @@ -2,7 +2,7 @@ import type { AppDetailResponse } from '@/models/app' import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react' import { ChevronDownIcon, PlusIcon } from '@heroicons/react/24/solid' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useRouter } from 'next/navigation' import { Fragment, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/plugins/base/deprecation-notice.tsx b/web/app/components/plugins/base/deprecation-notice.tsx index 7e32133045..c2ddfa6975 100644 --- a/web/app/components/plugins/base/deprecation-notice.tsx +++ b/web/app/components/plugins/base/deprecation-notice.tsx @@ -1,6 +1,6 @@ import type { FC } from 'react' import { RiAlertFill } from '@remixicon/react' -import { camelCase } from 'es-toolkit/compat' +import { camelCase } from 'es-toolkit/string' import Link from 'next/link' import * as React from 'react' import { useMemo } from 'react' diff --git a/web/app/components/plugins/marketplace/context.tsx b/web/app/components/plugins/marketplace/context.tsx index 97144630a6..31b6a7f592 100644 --- a/web/app/components/plugins/marketplace/context.tsx +++ b/web/app/components/plugins/marketplace/context.tsx @@ -11,7 +11,8 @@ import type { SearchParams, SearchParamsFromCollection, } from './types' -import { debounce, noop } from 'es-toolkit/compat' +import { debounce } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useCallback, useEffect, diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx index 18896b1f50..a4093ed00b 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx @@ -2,7 +2,7 @@ import type { FormRefObject, FormSchema } from '@/app/components/base/form/types' import type { ParametersSchema, PluginDetail } from '@/app/components/plugins/types' import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types' -import { isEqual } from 'es-toolkit/compat' +import { isEqual } from 'es-toolkit/predicate' import { useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { EncryptedBottom } from '@/app/components/base/encrypted-bottom' diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx index 75ffff781f..262235e6ed 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx @@ -2,7 +2,7 @@ import type { FormRefObject, FormSchema } from '@/app/components/base/form/types' import type { ParametersSchema, PluginDetail } from '@/app/components/plugins/types' import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types' -import { isEqual } from 'es-toolkit/compat' +import { isEqual } from 'es-toolkit/predicate' import { useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { BaseForm } from '@/app/components/base/form/components/base' diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx index 3332cd6b03..e57b9c0151 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx @@ -2,7 +2,7 @@ import type { FormRefObject, FormSchema } from '@/app/components/base/form/types' import type { ParametersSchema, PluginDetail } from '@/app/components/plugins/types' import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types' -import { isEqual } from 'es-toolkit/compat' +import { isEqual } from 'es-toolkit/predicate' import { useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { BaseForm } from '@/app/components/base/form/components/base' diff --git a/web/app/components/plugins/plugin-page/context.tsx b/web/app/components/plugins/plugin-page/context.tsx index 3d420ca1ab..fea78ae181 100644 --- a/web/app/components/plugins/plugin-page/context.tsx +++ b/web/app/components/plugins/plugin-page/context.tsx @@ -2,7 +2,7 @@ import type { ReactNode, RefObject } from 'react' import type { FilterState } from './filter-management' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useQueryState } from 'nuqs' import { useMemo, diff --git a/web/app/components/plugins/plugin-page/empty/index.tsx b/web/app/components/plugins/plugin-page/empty/index.tsx index 019dc9ec24..7149423d5f 100644 --- a/web/app/components/plugins/plugin-page/empty/index.tsx +++ b/web/app/components/plugins/plugin-page/empty/index.tsx @@ -1,5 +1,5 @@ 'use client' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/plugins/plugin-page/index.tsx b/web/app/components/plugins/plugin-page/index.tsx index 6d8542f5c9..b8fc891254 100644 --- a/web/app/components/plugins/plugin-page/index.tsx +++ b/web/app/components/plugins/plugin-page/index.tsx @@ -7,7 +7,7 @@ import { RiEqualizer2Line, } from '@remixicon/react' import { useBoolean } from 'ahooks' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import Link from 'next/link' import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx b/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx index 7dbd3e3026..322591a363 100644 --- a/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx +++ b/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx @@ -1,7 +1,7 @@ 'use client' import { RiAddLine, RiArrowDownSLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' diff --git a/web/app/components/rag-pipeline/components/panel/input-field/field-list/field-list-container.tsx b/web/app/components/rag-pipeline/components/panel/input-field/field-list/field-list-container.tsx index 108f3a642f..a14af1f704 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/field-list/field-list-container.tsx +++ b/web/app/components/rag-pipeline/components/panel/input-field/field-list/field-list-container.tsx @@ -1,6 +1,6 @@ import type { SortableItem } from './types' import type { InputVar } from '@/models/pipeline' -import { isEqual } from 'es-toolkit/compat' +import { isEqual } from 'es-toolkit/predicate' import { memo, useCallback, diff --git a/web/app/components/rag-pipeline/components/publish-as-knowledge-pipeline-modal.tsx b/web/app/components/rag-pipeline/components/publish-as-knowledge-pipeline-modal.tsx index 4c607810bb..8d8c7f1088 100644 --- a/web/app/components/rag-pipeline/components/publish-as-knowledge-pipeline-modal.tsx +++ b/web/app/components/rag-pipeline/components/publish-as-knowledge-pipeline-modal.tsx @@ -2,7 +2,7 @@ import type { AppIconSelection } from '@/app/components/base/app-icon-picker' import type { IconInfo } from '@/models/datasets' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' diff --git a/web/app/components/tools/labels/selector.tsx b/web/app/components/tools/labels/selector.tsx index f95ebed72e..08c1216700 100644 --- a/web/app/components/tools/labels/selector.tsx +++ b/web/app/components/tools/labels/selector.tsx @@ -2,7 +2,7 @@ import type { FC } from 'react' import type { Label } from '@/app/components/tools/labels/constant' import { RiArrowDownSLine } from '@remixicon/react' import { useDebounceFn } from 'ahooks' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Checkbox from '@/app/components/base/checkbox' diff --git a/web/app/components/tools/mcp/modal.tsx b/web/app/components/tools/mcp/modal.tsx index 9bf4b351b4..413a2d3948 100644 --- a/web/app/components/tools/mcp/modal.tsx +++ b/web/app/components/tools/mcp/modal.tsx @@ -5,7 +5,7 @@ import type { ToolWithProvider } from '@/app/components/workflow/types' import type { AppIconType } from '@/types/app' import { RiCloseLine, RiEditLine } from '@remixicon/react' import { useHover } from 'ahooks' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/tools/setting/build-in/config-credentials.tsx b/web/app/components/tools/setting/build-in/config-credentials.tsx index cb11c5cf16..863b3ba352 100644 --- a/web/app/components/tools/setting/build-in/config-credentials.tsx +++ b/web/app/components/tools/setting/build-in/config-credentials.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' import type { Collection } from '../../types' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/tools/workflow-tool/confirm-modal/index.tsx b/web/app/components/tools/workflow-tool/confirm-modal/index.tsx index 2abee055bb..0c7e083a56 100644 --- a/web/app/components/tools/workflow-tool/confirm-modal/index.tsx +++ b/web/app/components/tools/workflow-tool/confirm-modal/index.tsx @@ -1,7 +1,7 @@ 'use client' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' 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 031e2949cb..49c5d20dde 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-run.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-run.ts @@ -2,7 +2,7 @@ import type AudioPlayer from '@/app/components/base/audio-btn/audio' import type { Node } from '@/app/components/workflow/types' import type { IOtherOptions } from '@/service/base' import type { VersionHistory } from '@/types/workflow' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { produce } from 'immer' import { usePathname } from 'next/navigation' import { useCallback, useRef } from 'react' diff --git a/web/app/components/workflow/block-selector/market-place-plugin/list.tsx b/web/app/components/workflow/block-selector/market-place-plugin/list.tsx index 9927df500d..29f1e77e14 100644 --- a/web/app/components/workflow/block-selector/market-place-plugin/list.tsx +++ b/web/app/components/workflow/block-selector/market-place-plugin/list.tsx @@ -2,7 +2,7 @@ import type { RefObject } from 'react' import type { Plugin, PluginCategoryEnum } from '@/app/components/plugins/types' import { RiArrowRightUpLine, RiSearchLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import Link from 'next/link' import { useEffect, useImperativeHandle, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/custom-edge.tsx b/web/app/components/workflow/custom-edge.tsx index 6427520d81..0440ca0c3e 100644 --- a/web/app/components/workflow/custom-edge.tsx +++ b/web/app/components/workflow/custom-edge.tsx @@ -3,7 +3,7 @@ import type { Edge, OnSelectBlock, } from './types' -import { intersection } from 'es-toolkit/compat' +import { intersection } from 'es-toolkit/array' import { memo, useCallback, diff --git a/web/app/components/workflow/dsl-export-confirm-modal.tsx b/web/app/components/workflow/dsl-export-confirm-modal.tsx index 72f8c5857d..e698de722e 100644 --- a/web/app/components/workflow/dsl-export-confirm-modal.tsx +++ b/web/app/components/workflow/dsl-export-confirm-modal.tsx @@ -1,7 +1,7 @@ 'use client' import type { EnvironmentVariable } from '@/app/components/workflow/types' import { RiCloseLine, RiLock2Line } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/hooks-store/store.ts b/web/app/components/workflow/hooks-store/store.ts index 3aa4ba3d91..44014fc0d7 100644 --- a/web/app/components/workflow/hooks-store/store.ts +++ b/web/app/components/workflow/hooks-store/store.ts @@ -10,9 +10,7 @@ import type { IOtherOptions } from '@/service/base' import type { SchemaTypeDefinition } from '@/service/use-common' import type { FlowType } from '@/types/common' import type { VarInInspect } from '@/types/workflow' -import { - noop, -} from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useContext } from 'react' import { useStore as useZustandStore, diff --git a/web/app/components/workflow/hooks/use-nodes-layout.ts b/web/app/components/workflow/hooks/use-nodes-layout.ts index 2ad89ff100..0738703791 100644 --- a/web/app/components/workflow/hooks/use-nodes-layout.ts +++ b/web/app/components/workflow/hooks/use-nodes-layout.ts @@ -3,7 +3,7 @@ import type { Node, } from '../types' import ELK from 'elkjs/lib/elk.bundled.js' -import { cloneDeep } from 'es-toolkit/compat' +import { cloneDeep } from 'es-toolkit/object' import { useCallback } from 'react' import { useReactFlow, diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 8eeab43d7e..1543bce714 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -13,7 +13,7 @@ import type { VarInInspect } from '@/types/workflow' import { useEventListener, } from 'ahooks' -import { isEqual } from 'es-toolkit/compat' +import { isEqual } from 'es-toolkit/predicate' import { setAutoFreeze } from 'immer' import dynamic from 'next/dynamic' import { diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx index 8ea786ffb4..cc58176fb6 100644 --- a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx +++ b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx @@ -4,7 +4,7 @@ import type { NodeOutPutVar } from '../../../types' import type { ToolVarInputs } from '../../tool/types' import type { CredentialFormSchema, CredentialFormSchemaNumberInput, CredentialFormSchemaTextInput } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { PluginMeta } from '@/app/components/plugins/types' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import Link from 'next/link' import { memo } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx b/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx index ad5410986c..4714139541 100644 --- a/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' import Editor, { loader } from '@monaco-editor/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useEffect, useMemo, useRef, useState } from 'react' import { diff --git a/web/app/components/workflow/nodes/_base/components/file-type-item.tsx b/web/app/components/workflow/nodes/_base/components/file-type-item.tsx index cef2f76da6..aa73260af6 100644 --- a/web/app/components/workflow/nodes/_base/components/file-type-item.tsx +++ b/web/app/components/workflow/nodes/_base/components/file-type-item.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx b/web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx index 5a1c467be5..8880aedf80 100644 --- a/web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx +++ b/web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx @@ -5,7 +5,7 @@ import type { NodeOutPutVar, } from '@/app/components/workflow/types' import { useBoolean } from 'ahooks' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useEffect } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/nodes/_base/components/next-step/index.tsx b/web/app/components/workflow/nodes/_base/components/next-step/index.tsx index 686c2a17c6..0b22d44aac 100644 --- a/web/app/components/workflow/nodes/_base/components/next-step/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/next-step/index.tsx @@ -1,7 +1,7 @@ import type { Node, } from '../../../../types' -import { isEqual } from 'es-toolkit/compat' +import { isEqual } from 'es-toolkit/predicate' import { memo, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { diff --git a/web/app/components/workflow/nodes/_base/components/next-step/operator.tsx b/web/app/components/workflow/nodes/_base/components/next-step/operator.tsx index 9b4de96f9f..8484028868 100644 --- a/web/app/components/workflow/nodes/_base/components/next-step/operator.tsx +++ b/web/app/components/workflow/nodes/_base/components/next-step/operator.tsx @@ -3,7 +3,7 @@ import type { OnSelectBlock, } from '@/app/components/workflow/types' import { RiMoreFill } from '@remixicon/react' -import { intersection } from 'es-toolkit/compat' +import { intersection } from 'es-toolkit/array' import { useCallback, } from 'react' diff --git a/web/app/components/workflow/nodes/_base/components/panel-operator/change-block.tsx b/web/app/components/workflow/nodes/_base/components/panel-operator/change-block.tsx index 6652ce58ec..32f9e9a174 100644 --- a/web/app/components/workflow/nodes/_base/components/panel-operator/change-block.tsx +++ b/web/app/components/workflow/nodes/_base/components/panel-operator/change-block.tsx @@ -2,7 +2,7 @@ import type { Node, OnSelectBlock, } from '@/app/components/workflow/types' -import { intersection } from 'es-toolkit/compat' +import { intersection } from 'es-toolkit/array' import { memo, useCallback, diff --git a/web/app/components/workflow/nodes/_base/components/variable/utils.ts b/web/app/components/workflow/nodes/_base/components/variable/utils.ts index 9f033f9b27..9f77be0ce2 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/utils.ts +++ b/web/app/components/workflow/nodes/_base/components/variable/utils.ts @@ -33,7 +33,8 @@ import type { import type { PromptItem } from '@/models/debug' import type { RAGPipelineVariable } from '@/models/pipeline' import type { SchemaTypeDefinition } from '@/service/use-common' -import { isArray, uniq } from 'es-toolkit/compat' +import { uniq } from 'es-toolkit/array' +import { isArray } from 'es-toolkit/compat' import { produce } from 'immer' import { AGENT_OUTPUT_STRUCT, diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx index 86ec5c3c09..6dfcbaf4d8 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx @@ -11,7 +11,7 @@ import { RiLoader4Line, RiMoreLine, } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { produce } from 'immer' import * as React from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx index a8c1073d93..d44f560e08 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx @@ -4,7 +4,7 @@ import type { StructuredOutput } from '../../../llm/types' import type { Field } from '@/app/components/workflow/nodes/llm/types' import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' import { useHover } from 'ahooks' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-label.tsx b/web/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-label.tsx index 828f7e5ebe..55d6a14d37 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-label.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-label.tsx @@ -3,7 +3,7 @@ import { RiErrorWarningFill, RiMoreLine, } from '@remixicon/react' -import { capitalize } from 'es-toolkit/compat' +import { capitalize } from 'es-toolkit/string' import { memo } from 'react' import Tooltip from '@/app/components/base/tooltip' import { cn } from '@/utils/classnames' diff --git a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts index d8fd4d2c57..d2d7b6b6d9 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts @@ -1,7 +1,8 @@ import type { CommonNodeType, InputVar, TriggerNodeType, ValueSelector, Var, Variable } from '@/app/components/workflow/types' import type { FlowType } from '@/types/common' import type { NodeRunResult, NodeTracing } from '@/types/workflow' -import { noop, unionBy } from 'es-toolkit/compat' +import { unionBy } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { produce } from 'immer' import { useCallback, useEffect, useRef, useState } from 'react' diff --git a/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx b/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx index 0fd735abbb..202d7469aa 100644 --- a/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx +++ b/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react' import type { AssignerNodeOperation } from '../../types' import type { ValueSelector, Var } from '@/app/components/workflow/types' import { RiDeleteBinLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { produce } from 'immer' import * as React from 'react' import { useCallback } from 'react' diff --git a/web/app/components/workflow/nodes/if-else/components/condition-number-input.tsx b/web/app/components/workflow/nodes/if-else/components/condition-number-input.tsx index ee81d70106..ed538618e3 100644 --- a/web/app/components/workflow/nodes/if-else/components/condition-number-input.tsx +++ b/web/app/components/workflow/nodes/if-else/components/condition-number-input.tsx @@ -4,7 +4,7 @@ import type { } from '@/app/components/workflow/types' import { RiArrowDownSLine } from '@remixicon/react' import { useBoolean } from 'ahooks' -import { capitalize } from 'es-toolkit/compat' +import { capitalize } from 'es-toolkit/string' import { memo, useCallback, diff --git a/web/app/components/workflow/nodes/if-else/components/condition-wrap.tsx b/web/app/components/workflow/nodes/if-else/components/condition-wrap.tsx index 5554696480..7ac76dd936 100644 --- a/web/app/components/workflow/nodes/if-else/components/condition-wrap.tsx +++ b/web/app/components/workflow/nodes/if-else/components/condition-wrap.tsx @@ -7,7 +7,7 @@ import { RiDeleteBinLine, RiDraggable, } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/nodes/iteration/use-config.ts b/web/app/components/workflow/nodes/iteration/use-config.ts index 3106577085..79df409474 100644 --- a/web/app/components/workflow/nodes/iteration/use-config.ts +++ b/web/app/components/workflow/nodes/iteration/use-config.ts @@ -2,7 +2,7 @@ import type { ErrorHandleMode, ValueSelector, Var } from '../../types' import type { IterationNodeType } from './types' import type { Item } from '@/app/components/base/select' import type { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' -import { isEqual } from 'es-toolkit/compat' +import { isEqual } from 'es-toolkit/predicate' import { produce } from 'immer' import { useCallback } from 'react' import { diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-value-method.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-value-method.tsx index 574501a27d..08a316c82a 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-value-method.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-value-method.tsx @@ -1,5 +1,5 @@ import { RiArrowDownSLine } from '@remixicon/react' -import { capitalize } from 'es-toolkit/compat' +import { capitalize } from 'es-toolkit/string' import { useState } from 'react' import Button from '@/app/components/base/button' import { diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/index.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/index.tsx index 3394c1f7a7..88b7ff303c 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/index.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/index.tsx @@ -1,5 +1,5 @@ import type { MetadataShape } from '@/app/components/workflow/nodes/knowledge-retrieval/types' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useCallback, useState, diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts b/web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts index 5208200b25..a1662a1e1c 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts +++ b/web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts @@ -9,7 +9,7 @@ import type { MultipleRetrievalConfig, } from './types' import type { DataSet } from '@/models/datasets' -import { isEqual } from 'es-toolkit/compat' +import { isEqual } from 'es-toolkit/predicate' import { produce } from 'immer' import { useCallback, diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/utils.ts b/web/app/components/workflow/nodes/knowledge-retrieval/utils.ts index d6cd69b39a..a30ec8e735 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/utils.ts +++ b/web/app/components/workflow/nodes/knowledge-retrieval/utils.ts @@ -3,10 +3,8 @@ import type { DataSet, SelectedDatasetsMode, } from '@/models/datasets' -import { - uniq, - xorBy, -} from 'es-toolkit/compat' +import { uniq } from 'es-toolkit/array' +import { xorBy } from 'es-toolkit/compat' import { DATASET_DEFAULT } from '@/config' import { DEFAULT_WEIGHTED_SCORE, diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context.tsx index 557cddfb61..82a3c27a98 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context.tsx @@ -1,4 +1,4 @@ -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { createContext, useContext, diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts index 1673c80f4f..6159028c21 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts @@ -1,7 +1,7 @@ import type { VisualEditorProps } from '.' import type { Field } from '../../../types' import type { EditData } from './edit-card' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { produce } from 'immer' import Toast from '@/app/components/base/toast' import { ArrayType, Type } from '../../../types' diff --git a/web/app/components/workflow/nodes/llm/use-single-run-form-params.ts b/web/app/components/workflow/nodes/llm/use-single-run-form-params.ts index 580a5d44ad..e0c4c97fad 100644 --- a/web/app/components/workflow/nodes/llm/use-single-run-form-params.ts +++ b/web/app/components/workflow/nodes/llm/use-single-run-form-params.ts @@ -2,7 +2,7 @@ import type { RefObject } from 'react' import type { LLMNodeType } from './types' import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' import type { InputVar, PromptItem, Var, Variable } from '@/app/components/workflow/types' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { InputVarType, VarType } from '@/app/components/workflow/types' diff --git a/web/app/components/workflow/nodes/loop/components/condition-number-input.tsx b/web/app/components/workflow/nodes/loop/components/condition-number-input.tsx index ee81d70106..ed538618e3 100644 --- a/web/app/components/workflow/nodes/loop/components/condition-number-input.tsx +++ b/web/app/components/workflow/nodes/loop/components/condition-number-input.tsx @@ -4,7 +4,7 @@ import type { } from '@/app/components/workflow/types' import { RiArrowDownSLine } from '@remixicon/react' import { useBoolean } from 'ahooks' -import { capitalize } from 'es-toolkit/compat' +import { capitalize } from 'es-toolkit/string' import { memo, useCallback, diff --git a/web/app/components/workflow/nodes/parameter-extractor/use-single-run-form-params.ts b/web/app/components/workflow/nodes/parameter-extractor/use-single-run-form-params.ts index e736ee6e79..52a4462153 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/use-single-run-form-params.ts +++ b/web/app/components/workflow/nodes/parameter-extractor/use-single-run-form-params.ts @@ -2,7 +2,7 @@ import type { RefObject } from 'react' import type { ParameterExtractorNodeType } from './types' import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' import type { InputVar, Var, Variable } from '@/app/components/workflow/types' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { InputVarType, VarType } from '@/app/components/workflow/types' diff --git a/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx b/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx index 724de571b6..5e41b5ee1c 100644 --- a/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx +++ b/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react' import type { Topic } from '@/app/components/workflow/nodes/question-classifier/types' import type { ValueSelector, Var } from '@/app/components/workflow/types' import { RiDraggable } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { produce } from 'immer' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' diff --git a/web/app/components/workflow/nodes/question-classifier/use-single-run-form-params.ts b/web/app/components/workflow/nodes/question-classifier/use-single-run-form-params.ts index 2bea1f8318..9b4b05367e 100644 --- a/web/app/components/workflow/nodes/question-classifier/use-single-run-form-params.ts +++ b/web/app/components/workflow/nodes/question-classifier/use-single-run-form-params.ts @@ -2,7 +2,7 @@ import type { RefObject } from 'react' import type { QuestionClassifierNodeType } from './types' import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' import type { InputVar, Var, Variable } from '@/app/components/workflow/types' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { InputVarType, VarType } from '@/app/components/workflow/types' diff --git a/web/app/components/workflow/nodes/start/components/var-item.tsx b/web/app/components/workflow/nodes/start/components/var-item.tsx index fdb07d0be9..64fd1804b0 100644 --- a/web/app/components/workflow/nodes/start/components/var-item.tsx +++ b/web/app/components/workflow/nodes/start/components/var-item.tsx @@ -5,7 +5,7 @@ import { RiDeleteBinLine, } from '@remixicon/react' import { useBoolean, useHover } from 'ahooks' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import * as React from 'react' import { useCallback, useRef } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/nodes/tool/components/input-var-list.tsx b/web/app/components/workflow/nodes/tool/components/input-var-list.tsx index 9bd01ac74e..96a878b506 100644 --- a/web/app/components/workflow/nodes/tool/components/input-var-list.tsx +++ b/web/app/components/workflow/nodes/tool/components/input-var-list.tsx @@ -4,7 +4,7 @@ import type { ToolVarInputs } from '../types' import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { Tool } from '@/app/components/tools/types' import type { ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { produce } from 'immer' import * as React from 'react' import { useCallback, useState } from 'react' diff --git a/web/app/components/workflow/nodes/variable-assigner/components/var-list/index.tsx b/web/app/components/workflow/nodes/variable-assigner/components/var-list/index.tsx index c2f289b7d1..1f45a8a0e1 100644 --- a/web/app/components/workflow/nodes/variable-assigner/components/var-list/index.tsx +++ b/web/app/components/workflow/nodes/variable-assigner/components/var-list/index.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' import type { ValueSelector, Var } from '@/app/components/workflow/types' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { produce } from 'immer' import * as React from 'react' import { useCallback } from 'react' diff --git a/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/component.tsx b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/component.tsx index 20027f37a1..b66827da45 100644 --- a/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/component.tsx +++ b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/component.tsx @@ -11,7 +11,7 @@ import { RiLinkUnlinkM, } from '@remixicon/react' import { useClickAway } from 'ahooks' -import { escape } from 'es-toolkit/compat' +import { escape } from 'es-toolkit/string' import { memo, useEffect, diff --git a/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/hooks.ts b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/hooks.ts index 5248680d89..3a084ef0af 100644 --- a/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/hooks.ts +++ b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/hooks.ts @@ -5,7 +5,7 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext import { mergeRegister, } from '@lexical/utils' -import { escape } from 'es-toolkit/compat' +import { escape } from 'es-toolkit/string' import { CLICK_COMMAND, COMMAND_PRIORITY_LOW, diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/variable-item.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/variable-item.tsx index 9fec4f7d01..58548e3084 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/variable-item.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/variable-item.tsx @@ -1,6 +1,6 @@ import type { ConversationVariable } from '@/app/components/workflow/types' import { RiDeleteBinLine, RiEditLine } from '@remixicon/react' -import { capitalize } from 'es-toolkit/compat' +import { capitalize } from 'es-toolkit/string' import { memo, useState } from 'react' import { BubbleX } from '@/app/components/base/icons/src/vender/line/others' import { cn } from '@/utils/classnames' diff --git a/web/app/components/workflow/panel/debug-and-preview/conversation-variable-modal.tsx b/web/app/components/workflow/panel/debug-and-preview/conversation-variable-modal.tsx index 4e9b72f6e5..0f6a4c081d 100644 --- a/web/app/components/workflow/panel/debug-and-preview/conversation-variable-modal.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/conversation-variable-modal.tsx @@ -5,7 +5,8 @@ import type { import { RiCloseLine } from '@remixicon/react' import { useMount } from 'ahooks' import copy from 'copy-to-clipboard' -import { capitalize, noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' +import { capitalize } from 'es-toolkit/string' import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/panel/debug-and-preview/index.tsx b/web/app/components/workflow/panel/debug-and-preview/index.tsx index b42d4205fa..bc5116bc65 100644 --- a/web/app/components/workflow/panel/debug-and-preview/index.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/index.tsx @@ -1,7 +1,8 @@ import type { StartNodeType } from '../../nodes/start/types' import { RiCloseLine, RiEqualizer2Line } from '@remixicon/react' -import { debounce, noop } from 'es-toolkit/compat' +import { debounce } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { memo, useCallback, diff --git a/web/app/components/workflow/panel/env-panel/env-item.tsx b/web/app/components/workflow/panel/env-panel/env-item.tsx index 582539b85b..26696daad0 100644 --- a/web/app/components/workflow/panel/env-panel/env-item.tsx +++ b/web/app/components/workflow/panel/env-panel/env-item.tsx @@ -1,6 +1,6 @@ import type { EnvironmentVariable } from '@/app/components/workflow/types' import { RiDeleteBinLine, RiEditLine, RiLock2Line } from '@remixicon/react' -import { capitalize } from 'es-toolkit/compat' +import { capitalize } from 'es-toolkit/string' import { memo, useState } from 'react' import { Env } from '@/app/components/base/icons/src/vender/line/others' import { useStore } from '@/app/components/workflow/store' diff --git a/web/app/components/workflow/panel/global-variable-panel/item.tsx b/web/app/components/workflow/panel/global-variable-panel/item.tsx index 458dd27692..83c0df7758 100644 --- a/web/app/components/workflow/panel/global-variable-panel/item.tsx +++ b/web/app/components/workflow/panel/global-variable-panel/item.tsx @@ -1,5 +1,5 @@ import type { GlobalVariable } from '@/app/components/workflow/types' -import { capitalize } from 'es-toolkit/compat' +import { capitalize } from 'es-toolkit/string' import { memo } from 'react' import { GlobalVariable as GlobalVariableIcon } from '@/app/components/base/icons/src/vender/line/others' diff --git a/web/app/components/workflow/run/utils/format-log/agent/index.ts b/web/app/components/workflow/run/utils/format-log/agent/index.ts index f86e4b33bb..19acd8c120 100644 --- a/web/app/components/workflow/run/utils/format-log/agent/index.ts +++ b/web/app/components/workflow/run/utils/format-log/agent/index.ts @@ -1,5 +1,5 @@ import type { AgentLogItem, AgentLogItemWithChildren, NodeTracing } from '@/types/workflow' -import { cloneDeep } from 'es-toolkit/compat' +import { cloneDeep } from 'es-toolkit/object' import { BlockEnum } from '@/app/components/workflow/types' const supportedAgentLogNodes = [BlockEnum.Agent, BlockEnum.Tool] diff --git a/web/app/components/workflow/run/utils/format-log/index.ts b/web/app/components/workflow/run/utils/format-log/index.ts index 1dbe8f1682..c152a5156a 100644 --- a/web/app/components/workflow/run/utils/format-log/index.ts +++ b/web/app/components/workflow/run/utils/format-log/index.ts @@ -1,5 +1,5 @@ import type { NodeTracing } from '@/types/workflow' -import { cloneDeep } from 'es-toolkit/compat' +import { cloneDeep } from 'es-toolkit/object' import { BlockEnum } from '../../../types' import formatAgentNode from './agent' import { addChildrenToIterationNode } from './iteration' diff --git a/web/app/components/workflow/run/utils/format-log/iteration/index.spec.ts b/web/app/components/workflow/run/utils/format-log/iteration/index.spec.ts index 8b4416f529..f984dbea76 100644 --- a/web/app/components/workflow/run/utils/format-log/iteration/index.spec.ts +++ b/web/app/components/workflow/run/utils/format-log/iteration/index.spec.ts @@ -1,4 +1,4 @@ -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import format from '.' import graphToLogStruct from '../graph-to-log-struct' diff --git a/web/app/components/workflow/run/utils/format-log/loop/index.spec.ts b/web/app/components/workflow/run/utils/format-log/loop/index.spec.ts index 3d31e43ba3..d2a2fd24bb 100644 --- a/web/app/components/workflow/run/utils/format-log/loop/index.spec.ts +++ b/web/app/components/workflow/run/utils/format-log/loop/index.spec.ts @@ -1,4 +1,4 @@ -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import format from '.' import graphToLogStruct from '../graph-to-log-struct' diff --git a/web/app/components/workflow/utils/elk-layout.ts b/web/app/components/workflow/utils/elk-layout.ts index 1a4bbf2d50..c3b37c8f16 100644 --- a/web/app/components/workflow/utils/elk-layout.ts +++ b/web/app/components/workflow/utils/elk-layout.ts @@ -5,7 +5,7 @@ import type { Node, } from '@/app/components/workflow/types' import ELK from 'elkjs/lib/elk.bundled.js' -import { cloneDeep } from 'es-toolkit/compat' +import { cloneDeep } from 'es-toolkit/object' import { CUSTOM_NODE, NODE_LAYOUT_HORIZONTAL_PADDING, diff --git a/web/app/components/workflow/utils/workflow-init.ts b/web/app/components/workflow/utils/workflow-init.ts index fa211934e4..77a2ccefac 100644 --- a/web/app/components/workflow/utils/workflow-init.ts +++ b/web/app/components/workflow/utils/workflow-init.ts @@ -7,9 +7,7 @@ import type { Edge, Node, } from '../types' -import { - cloneDeep, -} from 'es-toolkit/compat' +import { cloneDeep } from 'es-toolkit/object' import { getConnectedEdges, } from 'reactflow' diff --git a/web/app/components/workflow/workflow-history-store.tsx b/web/app/components/workflow/workflow-history-store.tsx index 6729fe50e3..5d10f81b27 100644 --- a/web/app/components/workflow/workflow-history-store.tsx +++ b/web/app/components/workflow/workflow-history-store.tsx @@ -3,7 +3,7 @@ import type { TemporalState } from 'zundo' import type { StoreApi } from 'zustand' import type { WorkflowHistoryEventT } from './hooks' import type { Edge, Node } from './types' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import isDeepEqual from 'fast-deep-equal' import { createContext, useContext, useMemo, useState } from 'react' import { temporal } from 'zundo' diff --git a/web/app/education-apply/education-apply-page.tsx b/web/app/education-apply/education-apply-page.tsx index f9ab2b4646..0be88091dc 100644 --- a/web/app/education-apply/education-apply-page.tsx +++ b/web/app/education-apply/education-apply-page.tsx @@ -1,7 +1,7 @@ 'use client' import { RiExternalLinkLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useRouter, useSearchParams, diff --git a/web/app/reset-password/page.tsx b/web/app/reset-password/page.tsx index 6be429960c..9fdccdfd87 100644 --- a/web/app/reset-password/page.tsx +++ b/web/app/reset-password/page.tsx @@ -1,6 +1,6 @@ 'use client' import { RiArrowLeftLine, RiLockPasswordLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' import { useState } from 'react' diff --git a/web/app/signin/components/mail-and-password-auth.tsx b/web/app/signin/components/mail-and-password-auth.tsx index 4a18e884ad..101ddf559a 100644 --- a/web/app/signin/components/mail-and-password-auth.tsx +++ b/web/app/signin/components/mail-and-password-auth.tsx @@ -1,5 +1,5 @@ import type { ResponseError } from '@/service/fetch' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' import { useState } from 'react' diff --git a/web/app/signin/invite-settings/page.tsx b/web/app/signin/invite-settings/page.tsx index 360f305cbd..1e38638360 100644 --- a/web/app/signin/invite-settings/page.tsx +++ b/web/app/signin/invite-settings/page.tsx @@ -1,7 +1,7 @@ 'use client' import type { Locale } from '@/i18n-config' import { RiAccountCircleLine } from '@remixicon/react' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' import { useCallback, useState } from 'react' diff --git a/web/app/signup/components/input-mail.tsx b/web/app/signup/components/input-mail.tsx index a1730b90c9..6342e7909c 100644 --- a/web/app/signup/components/input-mail.tsx +++ b/web/app/signup/components/input-mail.tsx @@ -1,6 +1,6 @@ 'use client' import type { MailSendResponse } from '@/service/use-common' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import Link from 'next/link' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/context/app-context.tsx b/web/context/app-context.tsx index cb4cab65b7..335f96fcce 100644 --- a/web/context/app-context.tsx +++ b/web/context/app-context.tsx @@ -3,7 +3,7 @@ import type { FC, ReactNode } from 'react' import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common' import { useQueryClient } from '@tanstack/react-query' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useCallback, useEffect, useMemo } from 'react' import { createContext, useContext, useContextSelector } from 'use-context-selector' import { setUserId, setUserProperties } from '@/app/components/base/amplitude' diff --git a/web/context/datasets-context.tsx b/web/context/datasets-context.tsx index f35767bc21..d309a8ef3f 100644 --- a/web/context/datasets-context.tsx +++ b/web/context/datasets-context.tsx @@ -1,7 +1,7 @@ 'use client' import type { DataSet } from '@/models/datasets' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { createContext, useContext } from 'use-context-selector' export type DatasetsContextValue = { diff --git a/web/context/debug-configuration.ts b/web/context/debug-configuration.ts index 2518af6260..ba157e1bf7 100644 --- a/web/context/debug-configuration.ts +++ b/web/context/debug-configuration.ts @@ -22,7 +22,7 @@ import type { TextToSpeechConfig, } from '@/models/debug' import type { VisionSettings } from '@/types/app' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { createContext, useContext } from 'use-context-selector' import { ANNOTATION_DEFAULT, DEFAULT_AGENT_SETTING, DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config' import { PromptMode } from '@/models/debug' diff --git a/web/context/explore-context.ts b/web/context/explore-context.ts index 1a7b35a09b..fc446c0453 100644 --- a/web/context/explore-context.ts +++ b/web/context/explore-context.ts @@ -1,5 +1,5 @@ import type { InstalledApp } from '@/models/explore' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { createContext } from 'use-context-selector' type IExplore = { diff --git a/web/context/mitt-context.tsx b/web/context/mitt-context.tsx index 0fc160613a..4317fc5660 100644 --- a/web/context/mitt-context.tsx +++ b/web/context/mitt-context.tsx @@ -1,4 +1,4 @@ -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { createContext, useContext, useContextSelector } from 'use-context-selector' import { useMitt } from '@/hooks/use-mitt' diff --git a/web/context/modal-context.tsx b/web/context/modal-context.tsx index dce7b9f6e1..293970259a 100644 --- a/web/context/modal-context.tsx +++ b/web/context/modal-context.tsx @@ -22,7 +22,7 @@ import type { ExternalDataTool, } from '@/models/common' import type { ModerationConfig, PromptVariable } from '@/models/debug' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import dynamic from 'next/dynamic' import { useCallback, useEffect, useRef, useState } from 'react' import { createContext, useContext, useContextSelector } from 'use-context-selector' diff --git a/web/context/provider-context.tsx b/web/context/provider-context.tsx index e6a2a1c5af..7eca72e322 100644 --- a/web/context/provider-context.tsx +++ b/web/context/provider-context.tsx @@ -5,7 +5,7 @@ import type { Model, ModelProvider } from '@/app/components/header/account-setti import type { RETRIEVE_METHOD } from '@/types/app' import { useQueryClient } from '@tanstack/react-query' import dayjs from 'dayjs' -import { noop } from 'es-toolkit/compat' +import { noop } from 'es-toolkit/function' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { createContext, useContext, useContextSelector } from 'use-context-selector' diff --git a/web/i18n-config/i18next-config.ts b/web/i18n-config/i18next-config.ts index 107954a384..0997485967 100644 --- a/web/i18n-config/i18next-config.ts +++ b/web/i18n-config/i18next-config.ts @@ -1,6 +1,6 @@ 'use client' import type { Locale } from '.' -import { camelCase, kebabCase } from 'es-toolkit/compat' +import { camelCase, kebabCase } from 'es-toolkit/string' import i18n from 'i18next' import { initReactI18next } from 'react-i18next' import appAnnotation from '../i18n/en-US/app-annotation.json' diff --git a/web/package.json b/web/package.json index 300b9b450a..1d6812b820 100644 --- a/web/package.json +++ b/web/package.json @@ -206,7 +206,6 @@ "jsdom-testing-mocks": "^1.16.0", "knip": "^5.66.1", "lint-staged": "^15.5.2", - "lodash": "^4.17.21", "nock": "^14.0.10", "postcss": "^8.5.6", "react-scan": "^0.4.3", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 3c4f881fdf..d5cb16cd4c 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -529,9 +529,6 @@ importers: lint-staged: specifier: ^15.5.2 version: 15.5.2 - lodash: - specifier: ^4.17.21 - version: 4.17.21 nock: specifier: ^14.0.10 version: 14.0.10 diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index 32ea4f35fd..5c10bac5d2 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -33,7 +33,7 @@ import { useQuery, useQueryClient, } from '@tanstack/react-query' -import { cloneDeep } from 'es-toolkit/compat' +import { cloneDeep } from 'es-toolkit/object' import { useCallback, useEffect, useState } from 'react' import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list' import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils' diff --git a/web/utils/index.ts b/web/utils/index.ts index 5704e82d87..fe6463c18e 100644 --- a/web/utils/index.ts +++ b/web/utils/index.ts @@ -1,4 +1,4 @@ -import { escape } from 'es-toolkit/compat' +import { escape } from 'es-toolkit/string' export const sleep = (ms: number) => { return new Promise(resolve => setTimeout(resolve, ms)) From 0421387672d4fded149d1219d08540903d535710 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 Dec 2025 11:59:39 +0800 Subject: [PATCH 11/37] chore(deps): bump qs from 6.14.0 to 6.14.1 in /web (#30409) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/package.json | 2 +- web/pnpm-lock.yaml | 31 ++++++++++++++++++++----------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/web/package.json b/web/package.json index 1d6812b820..a78575304c 100644 --- a/web/package.json +++ b/web/package.json @@ -112,7 +112,7 @@ "nuqs": "^2.8.6", "pinyin-pro": "^3.27.0", "qrcode.react": "^4.2.0", - "qs": "^6.14.0", + "qs": "^6.14.1", "react": "19.2.3", "react-18-input-autosize": "^3.0.0", "react-dom": "19.2.3", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index d5cb16cd4c..fe9032d248 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -253,8 +253,8 @@ importers: specifier: ^4.2.0 version: 4.2.0(react@19.2.3) qs: - specifier: ^6.14.0 - version: 6.14.0 + specifier: ^6.14.1 + version: 6.14.1 react: specifier: 19.2.3 version: 19.2.3 @@ -3698,6 +3698,9 @@ packages: '@types/node@20.19.26': resolution: {integrity: sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==} + '@types/node@20.19.27': + resolution: {integrity: sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==} + '@types/papaparse@5.5.1': resolution: {integrity: sha512-esEO+VISsLIyE+JZBmb89NzsYYbpwV8lmv2rPo6oX5y9KhBaIP7hhHgjuTut54qjdKVMufTEcrh5fUl9+58huw==} @@ -6372,8 +6375,8 @@ packages: lexical@0.38.2: resolution: {integrity: sha512-JJmfsG3c4gwBHzUGffbV7ifMNkKAWMCnYE3xJl87gty7hjyV5f3xq7eqTjP5HFYvO4XpjJvvWO2/djHp5S10tw==} - lib0@0.2.115: - resolution: {integrity: sha512-noaW4yNp6hCjOgDnWWxW0vGXE3kZQI5Kqiwz+jIWXavI9J9WyfJ9zjsbQlQlgjIbHBrvlA/x3TSIXBUJj+0L6g==} + lib0@0.2.117: + resolution: {integrity: sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==} engines: {node: '>=16'} hasBin: true @@ -7348,8 +7351,8 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - qs@6.14.0: - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} engines: {node: '>=0.6'} quansync@0.2.11: @@ -8776,6 +8779,7 @@ packages: whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} @@ -12408,6 +12412,11 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@20.19.27': + dependencies: + undici-types: 6.21.0 + optional: true + '@types/papaparse@5.5.1': dependencies: '@types/node': 18.15.0 @@ -14884,7 +14893,7 @@ snapshots: happy-dom@20.0.11: dependencies: - '@types/node': 20.19.26 + '@types/node': 20.19.27 '@types/whatwg-mimetype': 3.0.2 whatwg-mimetype: 3.0.0 optional: true @@ -15508,7 +15517,7 @@ snapshots: lexical@0.38.2: {} - lib0@0.2.115: + lib0@0.2.117: dependencies: isomorphic.js: 0.2.5 @@ -16814,7 +16823,7 @@ snapshots: dependencies: react: 19.2.3 - qs@6.14.0: + qs@6.14.1: dependencies: side-channel: '@nolyfill/side-channel@1.0.44' @@ -18106,7 +18115,7 @@ snapshots: url@0.11.4: dependencies: punycode: 1.4.1 - qs: 6.14.0 + qs: 6.14.1 use-callback-ref@1.3.3(@types/react@19.2.7)(react@19.2.3): dependencies: @@ -18585,7 +18594,7 @@ snapshots: yjs@13.6.27: dependencies: - lib0: 0.2.115 + lib0: 0.2.117 yocto-queue@0.1.0: {} From 1b8e80a722d731e1142f0e33c42e35a70584637f Mon Sep 17 00:00:00 2001 From: DevByteAI <161969603+devbyteai@users.noreply.github.com> Date: Wed, 31 Dec 2025 07:28:25 +0200 Subject: [PATCH 12/37] fix: Ensure chat history refreshes when switching back to conversations (#30389) --- web/service/use-share.spec.tsx | 45 ++++++++++++++++++++++++++++++++++ web/service/use-share.ts | 4 +++ 2 files changed, 49 insertions(+) diff --git a/web/service/use-share.spec.tsx b/web/service/use-share.spec.tsx index d0202ed140..db20329767 100644 --- a/web/service/use-share.spec.tsx +++ b/web/service/use-share.spec.tsx @@ -160,6 +160,51 @@ describe('useShareChatList', () => { }) expect(mockFetchChatList).not.toHaveBeenCalled() }) + + it('should always consider data stale to ensure fresh data on conversation switch (GitHub #30378)', async () => { + // This test verifies that chat list data is always considered stale (staleTime: 0) + // which ensures fresh data is fetched when switching back to a conversation. + // Without this, users would see outdated messages until double-switching. + const queryClient = createQueryClient() + const wrapper = createWrapper(queryClient) + const params = { + conversationId: 'conversation-1', + isInstalledApp: false, + appId: undefined, + } + const initialResponse = { data: [{ id: '1', content: 'initial' }] } + const updatedResponse = { data: [{ id: '1', content: 'initial' }, { id: '2', content: 'new message' }] } + + // First fetch + mockFetchChatList.mockResolvedValueOnce(initialResponse) + const { result, unmount } = renderHook(() => useShareChatList(params), { wrapper }) + + await waitFor(() => { + expect(result.current.data).toEqual(initialResponse) + }) + expect(mockFetchChatList).toHaveBeenCalledTimes(1) + + // Unmount (simulates switching away from conversation) + unmount() + + // Remount with same params (simulates switching back) + // With staleTime: 0, this should trigger a background refetch + mockFetchChatList.mockResolvedValueOnce(updatedResponse) + const { result: result2 } = renderHook(() => useShareChatList(params), { wrapper }) + + // Should immediately return cached data + expect(result2.current.data).toEqual(initialResponse) + + // Should trigger background refetch due to staleTime: 0 + await waitFor(() => { + expect(mockFetchChatList).toHaveBeenCalledTimes(2) + }) + + // Should update with fresh data + await waitFor(() => { + expect(result2.current.data).toEqual(updatedResponse) + }) + }) }) // Scenario: conversation name queries follow enabled flags and installation constraints. diff --git a/web/service/use-share.ts b/web/service/use-share.ts index 4dd43e06aa..eef61ccc29 100644 --- a/web/service/use-share.ts +++ b/web/service/use-share.ts @@ -122,6 +122,10 @@ export const useShareChatList = (params: ShareChatListParams, options: ShareQuer enabled: isEnabled, refetchOnReconnect, refetchOnWindowFocus, + // Always consider chat list data stale to ensure fresh data when switching + // back to a conversation. This fixes issue where recent messages don't appear + // until switching away and back again (GitHub issue #30378). + staleTime: 0, }) } From 8129b04143ef283afc2d4f54617ec5c418813319 Mon Sep 17 00:00:00 2001 From: QuantumGhost Date: Wed, 31 Dec 2025 13:38:16 +0800 Subject: [PATCH 13/37] fix(web): enable JSON_OBJECT type support in console UI (#30412) Co-authored-by: zhsama --- .../config-var/config-modal/config.ts | 27 ++++---- .../config-var/config-modal/index.tsx | 63 +++++++++++++++---- web/i18n/ar-TN/app-debug.json | 2 + web/i18n/de-DE/app-debug.json | 2 + web/i18n/en-US/app-debug.json | 2 + web/i18n/es-ES/app-debug.json | 2 + web/i18n/fa-IR/app-debug.json | 2 + web/i18n/fr-FR/app-debug.json | 2 + web/i18n/hi-IN/app-debug.json | 2 + web/i18n/id-ID/app-debug.json | 2 + web/i18n/it-IT/app-debug.json | 2 + web/i18n/ja-JP/app-debug.json | 2 + web/i18n/ko-KR/app-debug.json | 2 + web/i18n/pl-PL/app-debug.json | 2 + web/i18n/pt-BR/app-debug.json | 2 + web/i18n/ro-RO/app-debug.json | 2 + web/i18n/ru-RU/app-debug.json | 2 + web/i18n/sl-SI/app-debug.json | 2 + web/i18n/th-TH/app-debug.json | 2 + web/i18n/tr-TR/app-debug.json | 2 + web/i18n/uk-UA/app-debug.json | 2 + web/i18n/vi-VN/app-debug.json | 2 + web/i18n/zh-Hans/app-debug.json | 2 + web/i18n/zh-Hant/app-debug.json | 2 + 24 files changed, 110 insertions(+), 24 deletions(-) diff --git a/web/app/components/app/configuration/config-var/config-modal/config.ts b/web/app/components/app/configuration/config-var/config-modal/config.ts index 94a3cfae58..6586c2fd54 100644 --- a/web/app/components/app/configuration/config-var/config-modal/config.ts +++ b/web/app/components/app/configuration/config-var/config-modal/config.ts @@ -7,19 +7,24 @@ export const jsonObjectWrap = { export const jsonConfigPlaceHolder = JSON.stringify( { - foo: { - type: 'string', - }, - bar: { - type: 'object', - properties: { - sub: { - type: 'number', - }, + type: 'object', + properties: { + foo: { + type: 'string', + }, + bar: { + type: 'object', + properties: { + sub: { + type: 'number', + }, + }, + required: [], + additionalProperties: true, }, - required: [], - additionalProperties: true, }, + required: [], + additionalProperties: true, }, null, 2, diff --git a/web/app/components/app/configuration/config-var/config-modal/index.tsx b/web/app/components/app/configuration/config-var/config-modal/index.tsx index df07231465..782744882e 100644 --- a/web/app/components/app/configuration/config-var/config-modal/index.tsx +++ b/web/app/components/app/configuration/config-var/config-modal/index.tsx @@ -28,7 +28,7 @@ import { checkKeys, getNewVarInWorkflow, replaceSpaceWithUnderscoreInVarNameInpu import ConfigSelect from '../config-select' import ConfigString from '../config-string' import ModalFoot from '../modal-foot' -import { jsonConfigPlaceHolder, jsonObjectWrap } from './config' +import { jsonConfigPlaceHolder } from './config' import Field from './field' import TypeSelector from './type-select' @@ -78,13 +78,12 @@ const ConfigModal: FC = ({ const modalRef = useRef(null) const appDetail = useAppStore(state => state.appDetail) const isBasicApp = appDetail?.mode !== AppModeEnum.ADVANCED_CHAT && appDetail?.mode !== AppModeEnum.WORKFLOW - const isSupportJSON = false const jsonSchemaStr = useMemo(() => { const isJsonObject = type === InputVarType.jsonObject if (!isJsonObject || !tempPayload.json_schema) return '' try { - return JSON.stringify(JSON.parse(tempPayload.json_schema).properties, null, 2) + return JSON.stringify(JSON.parse(tempPayload.json_schema), null, 2) } catch { return '' @@ -129,13 +128,14 @@ const ConfigModal: FC = ({ }, []) const handleJSONSchemaChange = useCallback((value: string) => { + const isEmpty = value == null || value.trim() === '' + if (isEmpty) { + handlePayloadChange('json_schema')(undefined) + return null + } try { const v = JSON.parse(value) - const res = { - ...jsonObjectWrap, - properties: v, - } - handlePayloadChange('json_schema')(JSON.stringify(res, null, 2)) + handlePayloadChange('json_schema')(JSON.stringify(v, null, 2)) } catch { return null @@ -175,7 +175,7 @@ const ConfigModal: FC = ({ }, ] : []), - ...((!isBasicApp && isSupportJSON) + ...((!isBasicApp) ? [{ name: t('variableConfig.json', { ns: 'appDebug' }), value: InputVarType.jsonObject, @@ -233,7 +233,28 @@ const ConfigModal: FC = ({ const checkboxDefaultSelectValue = useMemo(() => getCheckboxDefaultSelectValue(tempPayload.default), [tempPayload.default]) + const isJsonSchemaEmpty = (value: InputVar['json_schema']) => { + if (value === null || value === undefined) { + return true + } + if (typeof value !== 'string') { + return false + } + const trimmed = value.trim() + return trimmed === '' + } + const handleConfirm = () => { + const jsonSchemaValue = tempPayload.json_schema + const isSchemaEmpty = isJsonSchemaEmpty(jsonSchemaValue) + const normalizedJsonSchema = isSchemaEmpty ? undefined : jsonSchemaValue + + // if the input type is jsonObject and the schema is empty as determined by `isJsonSchemaEmpty`, + // remove the `json_schema` field from the payload by setting its value to `undefined`. + const payloadToSave = tempPayload.type === InputVarType.jsonObject && isSchemaEmpty + ? { ...tempPayload, json_schema: undefined } + : tempPayload + const moreInfo = tempPayload.variable === payload?.variable ? undefined : { @@ -250,7 +271,7 @@ const ConfigModal: FC = ({ return } if (isStringInput || type === InputVarType.number) { - onConfirm(tempPayload, moreInfo) + onConfirm(payloadToSave, moreInfo) } else if (type === InputVarType.select) { if (options?.length === 0) { @@ -270,7 +291,7 @@ const ConfigModal: FC = ({ Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.optionRepeat', { ns: 'appDebug' }) }) return } - onConfirm(tempPayload, moreInfo) + onConfirm(payloadToSave, moreInfo) } else if ([InputVarType.singleFile, InputVarType.multiFiles].includes(type)) { if (tempPayload.allowed_file_types?.length === 0) { @@ -283,10 +304,26 @@ const ConfigModal: FC = ({ Toast.notify({ type: 'error', message: errorMessages }) return } - onConfirm(tempPayload, moreInfo) + onConfirm(payloadToSave, moreInfo) + } + else if (type === InputVarType.jsonObject) { + if (!isSchemaEmpty && typeof normalizedJsonSchema === 'string') { + try { + const schema = JSON.parse(normalizedJsonSchema) + if (schema?.type !== 'object') { + Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.jsonSchemaMustBeObject', { ns: 'appDebug' }) }) + return + } + } + catch { + Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.jsonSchemaInvalid', { ns: 'appDebug' }) }) + return + } + } + onConfirm(payloadToSave, moreInfo) } else { - onConfirm(tempPayload, moreInfo) + onConfirm(payloadToSave, moreInfo) } } diff --git a/web/i18n/ar-TN/app-debug.json b/web/i18n/ar-TN/app-debug.json index 192f1b49cf..c35983397a 100644 --- a/web/i18n/ar-TN/app-debug.json +++ b/web/i18n/ar-TN/app-debug.json @@ -306,6 +306,8 @@ "variableConfig.displayName": "اسم العرض", "variableConfig.editModalTitle": "تعديل حقل إدخال", "variableConfig.errorMsg.atLeastOneOption": "خيار واحد على الأقل مطلوب", + "variableConfig.errorMsg.jsonSchemaInvalid": "مخطط JSON ليس JSON صالحًا", + "variableConfig.errorMsg.jsonSchemaMustBeObject": "يجب أن يكون نوع مخطط JSON \"object\"", "variableConfig.errorMsg.labelNameRequired": "اسم التسمية مطلوب", "variableConfig.errorMsg.optionRepeat": "يوجد خيارات مكررة", "variableConfig.errorMsg.varNameCanBeRepeat": "اسم المتغير لا يمكن تكراره", diff --git a/web/i18n/de-DE/app-debug.json b/web/i18n/de-DE/app-debug.json index 2ab907ef2d..d038320cf8 100644 --- a/web/i18n/de-DE/app-debug.json +++ b/web/i18n/de-DE/app-debug.json @@ -306,6 +306,8 @@ "variableConfig.displayName": "Anzeigename", "variableConfig.editModalTitle": "Eingabefeld bearbeiten", "variableConfig.errorMsg.atLeastOneOption": "Mindestens eine Option ist erforderlich", + "variableConfig.errorMsg.jsonSchemaInvalid": "JSON-Schema ist kein gültiges JSON", + "variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON-Schema muss den Typ \"object\" haben", "variableConfig.errorMsg.labelNameRequired": "Labelname ist erforderlich", "variableConfig.errorMsg.optionRepeat": "Hat Wiederholungsoptionen", "variableConfig.errorMsg.varNameCanBeRepeat": "Variablenname kann nicht wiederholt werden", diff --git a/web/i18n/en-US/app-debug.json b/web/i18n/en-US/app-debug.json index 266f0e5483..cc53891912 100644 --- a/web/i18n/en-US/app-debug.json +++ b/web/i18n/en-US/app-debug.json @@ -306,6 +306,8 @@ "variableConfig.displayName": "Display Name", "variableConfig.editModalTitle": "Edit Input Field", "variableConfig.errorMsg.atLeastOneOption": "At least one option is required", + "variableConfig.errorMsg.jsonSchemaInvalid": "JSON Schema is not valid JSON", + "variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON Schema must have type \"object\"", "variableConfig.errorMsg.labelNameRequired": "Label name is required", "variableConfig.errorMsg.optionRepeat": "Has repeat options", "variableConfig.errorMsg.varNameCanBeRepeat": "Variable name can not be repeated", diff --git a/web/i18n/es-ES/app-debug.json b/web/i18n/es-ES/app-debug.json index a1dfd8ad53..8245f2b325 100644 --- a/web/i18n/es-ES/app-debug.json +++ b/web/i18n/es-ES/app-debug.json @@ -306,6 +306,8 @@ "variableConfig.displayName": "Nombre para mostrar", "variableConfig.editModalTitle": "Editar Campo de Entrada", "variableConfig.errorMsg.atLeastOneOption": "Se requiere al menos una opción", + "variableConfig.errorMsg.jsonSchemaInvalid": "El esquema JSON no es un JSON válido", + "variableConfig.errorMsg.jsonSchemaMustBeObject": "El esquema JSON debe tener el tipo \"object\"", "variableConfig.errorMsg.labelNameRequired": "Nombre de la etiqueta es requerido", "variableConfig.errorMsg.optionRepeat": "Hay opciones repetidas", "variableConfig.errorMsg.varNameCanBeRepeat": "El nombre de la variable no puede repetirse", diff --git a/web/i18n/fa-IR/app-debug.json b/web/i18n/fa-IR/app-debug.json index c6e1b588e5..5427ebb72a 100644 --- a/web/i18n/fa-IR/app-debug.json +++ b/web/i18n/fa-IR/app-debug.json @@ -306,6 +306,8 @@ "variableConfig.displayName": "نام نمایشی", "variableConfig.editModalTitle": "ویرایش فیلد ورودی", "variableConfig.errorMsg.atLeastOneOption": "حداقل یک گزینه مورد نیاز است", + "variableConfig.errorMsg.jsonSchemaInvalid": "JSON Schema یک JSON معتبر نیست", + "variableConfig.errorMsg.jsonSchemaMustBeObject": "نوع JSON Schema باید \"object\" باشد", "variableConfig.errorMsg.labelNameRequired": "نام برچسب الزامی است", "variableConfig.errorMsg.optionRepeat": "دارای گزینه های تکرار", "variableConfig.errorMsg.varNameCanBeRepeat": "نام متغیر را نمی توان تکرار کرد", diff --git a/web/i18n/fr-FR/app-debug.json b/web/i18n/fr-FR/app-debug.json index 2add116cd3..88f75f5136 100644 --- a/web/i18n/fr-FR/app-debug.json +++ b/web/i18n/fr-FR/app-debug.json @@ -306,6 +306,8 @@ "variableConfig.displayName": "Nom d’affichage", "variableConfig.editModalTitle": "Edit Input Field", "variableConfig.errorMsg.atLeastOneOption": "At least one option is required", + "variableConfig.errorMsg.jsonSchemaInvalid": "Le schéma JSON n’est pas un JSON valide", + "variableConfig.errorMsg.jsonSchemaMustBeObject": "Le schéma JSON doit avoir le type \"object\"", "variableConfig.errorMsg.labelNameRequired": "Label name is required", "variableConfig.errorMsg.optionRepeat": "Has repeat options", "variableConfig.errorMsg.varNameCanBeRepeat": "Variable name can not be repeated", diff --git a/web/i18n/hi-IN/app-debug.json b/web/i18n/hi-IN/app-debug.json index df891f60ec..97e733e167 100644 --- a/web/i18n/hi-IN/app-debug.json +++ b/web/i18n/hi-IN/app-debug.json @@ -306,6 +306,8 @@ "variableConfig.displayName": "प्रदर्शन नाम", "variableConfig.editModalTitle": "इनपुट फ़ील्ड संपादित करें", "variableConfig.errorMsg.atLeastOneOption": "कम से कम एक विकल्प आवश्यक है", + "variableConfig.errorMsg.jsonSchemaInvalid": "JSON स्कीमा मान्य JSON नहीं है", + "variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON स्कीमा का प्रकार \"object\" होना चाहिए", "variableConfig.errorMsg.labelNameRequired": "लेबल नाम आवश्यक है", "variableConfig.errorMsg.optionRepeat": "विकल्प दोहराए गए हैं", "variableConfig.errorMsg.varNameCanBeRepeat": "वेरिएबल नाम दोहराया नहीं जा सकता", diff --git a/web/i18n/id-ID/app-debug.json b/web/i18n/id-ID/app-debug.json index dfce0159a5..21651349ba 100644 --- a/web/i18n/id-ID/app-debug.json +++ b/web/i18n/id-ID/app-debug.json @@ -306,6 +306,8 @@ "variableConfig.displayName": "Nama Tampilan", "variableConfig.editModalTitle": "Edit Bidang Input", "variableConfig.errorMsg.atLeastOneOption": "Setidaknya satu opsi diperlukan", + "variableConfig.errorMsg.jsonSchemaInvalid": "JSON Schema bukan JSON yang valid", + "variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON Schema harus bertipe \"object\"", "variableConfig.errorMsg.labelNameRequired": "Nama label diperlukan", "variableConfig.errorMsg.optionRepeat": "Memiliki opsi pengulangan", "variableConfig.errorMsg.varNameCanBeRepeat": "Nama variabel tidak dapat diulang", diff --git a/web/i18n/it-IT/app-debug.json b/web/i18n/it-IT/app-debug.json index fce5620eef..94e4c00894 100644 --- a/web/i18n/it-IT/app-debug.json +++ b/web/i18n/it-IT/app-debug.json @@ -306,6 +306,8 @@ "variableConfig.displayName": "Nome visualizzato", "variableConfig.editModalTitle": "Modifica Campo Input", "variableConfig.errorMsg.atLeastOneOption": "È richiesta almeno un'opzione", + "variableConfig.errorMsg.jsonSchemaInvalid": "Lo schema JSON non è un JSON valido", + "variableConfig.errorMsg.jsonSchemaMustBeObject": "Lo schema JSON deve avere tipo \"object\"", "variableConfig.errorMsg.labelNameRequired": "Il nome dell'etichetta è richiesto", "variableConfig.errorMsg.optionRepeat": "Ci sono opzioni ripetute", "variableConfig.errorMsg.varNameCanBeRepeat": "Il nome della variabile non può essere ripetuto", diff --git a/web/i18n/ja-JP/app-debug.json b/web/i18n/ja-JP/app-debug.json index cf44ccff94..4b18745e5f 100644 --- a/web/i18n/ja-JP/app-debug.json +++ b/web/i18n/ja-JP/app-debug.json @@ -306,6 +306,8 @@ "variableConfig.displayName": "表示名", "variableConfig.editModalTitle": "入力フィールドを編集", "variableConfig.errorMsg.atLeastOneOption": "少なくとも 1 つのオプションが必要です", + "variableConfig.errorMsg.jsonSchemaInvalid": "JSONスキーマが有効なJSONではありません", + "variableConfig.errorMsg.jsonSchemaMustBeObject": "JSONスキーマのtypeは\"object\"である必要があります", "variableConfig.errorMsg.labelNameRequired": "ラベル名は必須です", "variableConfig.errorMsg.optionRepeat": "繰り返しオプションがあります", "variableConfig.errorMsg.varNameCanBeRepeat": "変数名は繰り返すことができません", diff --git a/web/i18n/ko-KR/app-debug.json b/web/i18n/ko-KR/app-debug.json index 600062464d..3cb295a3f7 100644 --- a/web/i18n/ko-KR/app-debug.json +++ b/web/i18n/ko-KR/app-debug.json @@ -306,6 +306,8 @@ "variableConfig.displayName": "표시 이름", "variableConfig.editModalTitle": "입력 필드 편집", "variableConfig.errorMsg.atLeastOneOption": "적어도 하나의 옵션이 필요합니다", + "variableConfig.errorMsg.jsonSchemaInvalid": "JSON 스키마가 올바른 JSON이 아닙니다", + "variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON 스키마의 type은 \"object\"이어야 합니다", "variableConfig.errorMsg.labelNameRequired": "레이블명은 필수입니다", "variableConfig.errorMsg.optionRepeat": "옵션이 중복되어 있습니다", "variableConfig.errorMsg.varNameCanBeRepeat": "변수명은 중복될 수 없습니다", diff --git a/web/i18n/pl-PL/app-debug.json b/web/i18n/pl-PL/app-debug.json index ece00eb9ad..0a9da0fcce 100644 --- a/web/i18n/pl-PL/app-debug.json +++ b/web/i18n/pl-PL/app-debug.json @@ -306,6 +306,8 @@ "variableConfig.displayName": "Nazwa wyświetlana", "variableConfig.editModalTitle": "Edytuj Pole Wejściowe", "variableConfig.errorMsg.atLeastOneOption": "Wymagana jest co najmniej jedna opcja", + "variableConfig.errorMsg.jsonSchemaInvalid": "Schemat JSON nie jest prawidłowym JSON-em", + "variableConfig.errorMsg.jsonSchemaMustBeObject": "Schemat JSON musi mieć typ \"object\"", "variableConfig.errorMsg.labelNameRequired": "Wymagana nazwa etykiety", "variableConfig.errorMsg.optionRepeat": "Powtarzają się opcje", "variableConfig.errorMsg.varNameCanBeRepeat": "Nazwa zmiennej nie może się powtarzać", diff --git a/web/i18n/pt-BR/app-debug.json b/web/i18n/pt-BR/app-debug.json index 98ad27f69a..6059787894 100644 --- a/web/i18n/pt-BR/app-debug.json +++ b/web/i18n/pt-BR/app-debug.json @@ -306,6 +306,8 @@ "variableConfig.displayName": "Nome de exibição", "variableConfig.editModalTitle": "Editar Campo de Entrada", "variableConfig.errorMsg.atLeastOneOption": "Pelo menos uma opção é obrigatória", + "variableConfig.errorMsg.jsonSchemaInvalid": "O JSON Schema não é um JSON válido", + "variableConfig.errorMsg.jsonSchemaMustBeObject": "O JSON Schema deve ter o tipo \"object\"", "variableConfig.errorMsg.labelNameRequired": "O nome do rótulo é obrigatório", "variableConfig.errorMsg.optionRepeat": "Tem opções repetidas", "variableConfig.errorMsg.varNameCanBeRepeat": "O nome da variável não pode ser repetido", diff --git a/web/i18n/ro-RO/app-debug.json b/web/i18n/ro-RO/app-debug.json index 83990992cc..6245ca680d 100644 --- a/web/i18n/ro-RO/app-debug.json +++ b/web/i18n/ro-RO/app-debug.json @@ -306,6 +306,8 @@ "variableConfig.displayName": "Nume afișat", "variableConfig.editModalTitle": "Editați câmpul de intrare", "variableConfig.errorMsg.atLeastOneOption": "Este necesară cel puțin o opțiune", + "variableConfig.errorMsg.jsonSchemaInvalid": "Schema JSON nu este un JSON valid", + "variableConfig.errorMsg.jsonSchemaMustBeObject": "Schema JSON trebuie să aibă tipul \"object\"", "variableConfig.errorMsg.labelNameRequired": "Numele etichetei este obligatoriu", "variableConfig.errorMsg.optionRepeat": "Există opțiuni repetate", "variableConfig.errorMsg.varNameCanBeRepeat": "Numele variabilei nu poate fi repetat", diff --git a/web/i18n/ru-RU/app-debug.json b/web/i18n/ru-RU/app-debug.json index c3075a5b01..fdc797da2f 100644 --- a/web/i18n/ru-RU/app-debug.json +++ b/web/i18n/ru-RU/app-debug.json @@ -306,6 +306,8 @@ "variableConfig.displayName": "Отображаемое имя", "variableConfig.editModalTitle": "Редактировать поле ввода", "variableConfig.errorMsg.atLeastOneOption": "Требуется хотя бы один вариант", + "variableConfig.errorMsg.jsonSchemaInvalid": "JSON Schema не является корректным JSON", + "variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON Schema должна иметь тип \"object\"", "variableConfig.errorMsg.labelNameRequired": "Имя метки обязательно", "variableConfig.errorMsg.optionRepeat": "Есть повторяющиеся варианты", "variableConfig.errorMsg.varNameCanBeRepeat": "Имя переменной не может повторяться", diff --git a/web/i18n/sl-SI/app-debug.json b/web/i18n/sl-SI/app-debug.json index 28094186bf..948e3dbc67 100644 --- a/web/i18n/sl-SI/app-debug.json +++ b/web/i18n/sl-SI/app-debug.json @@ -306,6 +306,8 @@ "variableConfig.displayName": "Prikazno ime", "variableConfig.editModalTitle": "Uredi vnosno polje", "variableConfig.errorMsg.atLeastOneOption": "Potrebna je vsaj ena možnost", + "variableConfig.errorMsg.jsonSchemaInvalid": "Shema JSON ni veljaven JSON", + "variableConfig.errorMsg.jsonSchemaMustBeObject": "Shema JSON mora imeti tip \"object\"", "variableConfig.errorMsg.labelNameRequired": "Ime nalepke je obvezno", "variableConfig.errorMsg.optionRepeat": "Ima možnosti ponavljanja", "variableConfig.errorMsg.varNameCanBeRepeat": "Imena spremenljivke ni mogoče ponoviti", diff --git a/web/i18n/th-TH/app-debug.json b/web/i18n/th-TH/app-debug.json index c0dd099f08..53dc629a7f 100644 --- a/web/i18n/th-TH/app-debug.json +++ b/web/i18n/th-TH/app-debug.json @@ -306,6 +306,8 @@ "variableConfig.displayName": "ชื่อที่แสดง", "variableConfig.editModalTitle": "แก้ไขฟิลด์อินพุต", "variableConfig.errorMsg.atLeastOneOption": "จําเป็นต้องมีอย่างน้อยหนึ่งตัวเลือก", + "variableConfig.errorMsg.jsonSchemaInvalid": "JSON Schema ไม่ใช่ JSON ที่ถูกต้อง", + "variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON Schema ต้องมีชนิดเป็น \"object\"", "variableConfig.errorMsg.labelNameRequired": "ต้องมีชื่อฉลาก", "variableConfig.errorMsg.optionRepeat": "มีตัวเลือกการทําซ้ํา", "variableConfig.errorMsg.varNameCanBeRepeat": "ไม่สามารถทําซ้ําชื่อตัวแปรได้", diff --git a/web/i18n/tr-TR/app-debug.json b/web/i18n/tr-TR/app-debug.json index f04a8fd64e..1ae01dca6c 100644 --- a/web/i18n/tr-TR/app-debug.json +++ b/web/i18n/tr-TR/app-debug.json @@ -306,6 +306,8 @@ "variableConfig.displayName": "Görünen Ad", "variableConfig.editModalTitle": "Giriş Alanı Düzenle", "variableConfig.errorMsg.atLeastOneOption": "En az bir seçenek gereklidir", + "variableConfig.errorMsg.jsonSchemaInvalid": "JSON Şeması geçerli bir JSON değil", + "variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON Şeması’nın türü \"object\" olmalı", "variableConfig.errorMsg.labelNameRequired": "Etiket adı gereklidir", "variableConfig.errorMsg.optionRepeat": "Yinelenen seçenekler var", "variableConfig.errorMsg.varNameCanBeRepeat": "Değişken adı tekrar edemez", diff --git a/web/i18n/uk-UA/app-debug.json b/web/i18n/uk-UA/app-debug.json index 6d7ecd92a7..74dcc72efd 100644 --- a/web/i18n/uk-UA/app-debug.json +++ b/web/i18n/uk-UA/app-debug.json @@ -306,6 +306,8 @@ "variableConfig.displayName": "Відображуване ім'я", "variableConfig.editModalTitle": "Редагувати Поле Введення", "variableConfig.errorMsg.atLeastOneOption": "Потрібно щонайменше одну опцію", + "variableConfig.errorMsg.jsonSchemaInvalid": "JSON Schema не є коректним JSON", + "variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON Schema має мати тип \"object\"", "variableConfig.errorMsg.labelNameRequired": "Потрібно вказати назву мітки", "variableConfig.errorMsg.optionRepeat": "Є повторні опції", "variableConfig.errorMsg.varNameCanBeRepeat": "Назва змінної не може повторюватися", diff --git a/web/i18n/vi-VN/app-debug.json b/web/i18n/vi-VN/app-debug.json index e94e442626..f13df6c43d 100644 --- a/web/i18n/vi-VN/app-debug.json +++ b/web/i18n/vi-VN/app-debug.json @@ -306,6 +306,8 @@ "variableConfig.displayName": "Tên hiển thị", "variableConfig.editModalTitle": "Chỉnh sửa trường nhập", "variableConfig.errorMsg.atLeastOneOption": "Cần ít nhất một tùy chọn", + "variableConfig.errorMsg.jsonSchemaInvalid": "JSON Schema không phải là JSON hợp lệ", + "variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON Schema phải có kiểu \"object\"", "variableConfig.errorMsg.labelNameRequired": "Tên nhãn là bắt buộc", "variableConfig.errorMsg.optionRepeat": "Có các tùy chọn trùng lặp", "variableConfig.errorMsg.varNameCanBeRepeat": "Tên biến không được trùng lặp", diff --git a/web/i18n/zh-Hans/app-debug.json b/web/i18n/zh-Hans/app-debug.json index 7965400dd7..7559bf2b56 100644 --- a/web/i18n/zh-Hans/app-debug.json +++ b/web/i18n/zh-Hans/app-debug.json @@ -306,6 +306,8 @@ "variableConfig.displayName": "显示名称", "variableConfig.editModalTitle": "编辑变量", "variableConfig.errorMsg.atLeastOneOption": "至少需要一个选项", + "variableConfig.errorMsg.jsonSchemaInvalid": "JSON Schema 不是合法的 JSON", + "variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON Schema 的 type 必须为 \"object\"", "variableConfig.errorMsg.labelNameRequired": "显示名称必填", "variableConfig.errorMsg.optionRepeat": "选项不能重复", "variableConfig.errorMsg.varNameCanBeRepeat": "变量名称不能重复", diff --git a/web/i18n/zh-Hant/app-debug.json b/web/i18n/zh-Hant/app-debug.json index eabcebd8e0..ab7286691f 100644 --- a/web/i18n/zh-Hant/app-debug.json +++ b/web/i18n/zh-Hant/app-debug.json @@ -306,6 +306,8 @@ "variableConfig.displayName": "顯示名稱", "variableConfig.editModalTitle": "編輯變數", "variableConfig.errorMsg.atLeastOneOption": "至少需要一個選項", + "variableConfig.errorMsg.jsonSchemaInvalid": "JSON Schema 不是合法的 JSON", + "variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON Schema 的 type 必須為「object」", "variableConfig.errorMsg.labelNameRequired": "顯示名稱必填", "variableConfig.errorMsg.optionRepeat": "選項不能重複", "variableConfig.errorMsg.varNameCanBeRepeat": "變數名稱不能重複", From f28a08a69642e43dc556d3c33b3ba75dc9eb528a Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Wed, 31 Dec 2025 13:51:05 +0800 Subject: [PATCH 14/37] fix: correct useEducationStatus query cache configuration (#30416) --- web/context/provider-context.tsx | 8 ++++---- web/service/use-education.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web/context/provider-context.tsx b/web/context/provider-context.tsx index 7eca72e322..d350d08b4a 100644 --- a/web/context/provider-context.tsx +++ b/web/context/provider-context.tsx @@ -134,7 +134,7 @@ export const ProviderContextProvider = ({ const [enableEducationPlan, setEnableEducationPlan] = useState(false) const [isEducationWorkspace, setIsEducationWorkspace] = useState(false) - const { data: educationAccountInfo, isLoading: isLoadingEducationAccountInfo, isFetching: isFetchingEducationAccountInfo } = useEducationStatus(!enableEducationPlan) + const { data: educationAccountInfo, isLoading: isLoadingEducationAccountInfo, isFetching: isFetchingEducationAccountInfo, isFetchedAfterMount: isEducationDataFetchedAfterMount } = useEducationStatus(!enableEducationPlan) const [isAllowTransferWorkspace, setIsAllowTransferWorkspace] = useState(false) const [isAllowPublishAsCustomKnowledgePipelineTemplate, setIsAllowPublishAsCustomKnowledgePipelineTemplate] = useState(false) @@ -240,9 +240,9 @@ export const ProviderContextProvider = ({ datasetOperatorEnabled, enableEducationPlan, isEducationWorkspace, - isEducationAccount: educationAccountInfo?.is_student || false, - allowRefreshEducationVerify: educationAccountInfo?.allow_refresh || false, - educationAccountExpireAt: educationAccountInfo?.expire_at || null, + isEducationAccount: isEducationDataFetchedAfterMount ? (educationAccountInfo?.is_student ?? false) : false, + allowRefreshEducationVerify: isEducationDataFetchedAfterMount ? (educationAccountInfo?.allow_refresh ?? false) : false, + educationAccountExpireAt: isEducationDataFetchedAfterMount ? (educationAccountInfo?.expire_at ?? null) : null, isLoadingEducationAccountInfo, isFetchingEducationAccountInfo, webappCopyrightEnabled, diff --git a/web/service/use-education.ts b/web/service/use-education.ts index 34b08e59c6..a75ec6f3c7 100644 --- a/web/service/use-education.ts +++ b/web/service/use-education.ts @@ -59,7 +59,7 @@ export const useEducationStatus = (disable?: boolean) => { return get<{ is_student: boolean, allow_refresh: boolean, expire_at: number | null }>('/account/education') }, retry: false, - gcTime: 0, // No cache. Prevent switch account caused stale data + staleTime: 0, // Data expires immediately, ensuring fresh data on refetch }) } From fa69cce1e79f9703c6c16b3b318295d6cbe6efe8 Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Wed, 31 Dec 2025 14:57:39 +0800 Subject: [PATCH 15/37] fix: fix create app xss issue (#30305) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- api/controllers/console/app/app.py | 58 ++++ .../console/app/test_xss_prevention.py | 254 ++++++++++++++++++ 2 files changed, 312 insertions(+) create mode 100644 api/tests/unit_tests/controllers/console/app/test_xss_prevention.py diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 62e997dae2..44cf89d6a9 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -1,3 +1,4 @@ +import re import uuid from typing import Literal @@ -73,6 +74,48 @@ class AppListQuery(BaseModel): raise ValueError("Invalid UUID format in tag_ids.") from exc +# XSS prevention: patterns that could lead to XSS attacks +# Includes: script tags, iframe tags, javascript: protocol, SVG with onload, etc. +_XSS_PATTERNS = [ + r"]*>.*?", # Script tags + r"]*?(?:/>|>.*?)", # Iframe tags (including self-closing) + r"javascript:", # JavaScript protocol + r"]*?\s+onload\s*=[^>]*>", # SVG with onload handler (attribute-aware, flexible whitespace) + r"<.*?on\s*\w+\s*=", # Event handlers like onclick, onerror, etc. + r"]*(?:\s*/>|>.*?)", # Object tags (opening tag) + r"]*>", # Embed tags (self-closing) + r"]*>", # Link tags with javascript +] + + +def _validate_xss_safe(value: str | None, field_name: str = "Field") -> str | None: + """ + Validate that a string value doesn't contain potential XSS payloads. + + Args: + value: The string value to validate + field_name: Name of the field for error messages + + Returns: + The original value if safe + + Raises: + ValueError: If the value contains XSS patterns + """ + if value is None: + return None + + value_lower = value.lower() + for pattern in _XSS_PATTERNS: + if re.search(pattern, value_lower, re.DOTALL | re.IGNORECASE): + raise ValueError( + f"{field_name} contains invalid characters or patterns. " + "HTML tags, JavaScript, and other potentially dangerous content are not allowed." + ) + + return value + + class CreateAppPayload(BaseModel): name: str = Field(..., min_length=1, description="App name") description: str | None = Field(default=None, description="App description (max 400 chars)", max_length=400) @@ -81,6 +124,11 @@ class CreateAppPayload(BaseModel): icon: str | None = Field(default=None, description="Icon") icon_background: str | None = Field(default=None, description="Icon background color") + @field_validator("name", "description", mode="before") + @classmethod + def validate_xss_safe(cls, value: str | None, info) -> str | None: + return _validate_xss_safe(value, info.field_name) + class UpdateAppPayload(BaseModel): name: str = Field(..., min_length=1, description="App name") @@ -91,6 +139,11 @@ class UpdateAppPayload(BaseModel): use_icon_as_answer_icon: bool | None = Field(default=None, description="Use icon as answer icon") max_active_requests: int | None = Field(default=None, description="Maximum active requests") + @field_validator("name", "description", mode="before") + @classmethod + def validate_xss_safe(cls, value: str | None, info) -> str | None: + return _validate_xss_safe(value, info.field_name) + class CopyAppPayload(BaseModel): name: str | None = Field(default=None, description="Name for the copied app") @@ -99,6 +152,11 @@ class CopyAppPayload(BaseModel): icon: str | None = Field(default=None, description="Icon") icon_background: str | None = Field(default=None, description="Icon background color") + @field_validator("name", "description", mode="before") + @classmethod + def validate_xss_safe(cls, value: str | None, info) -> str | None: + return _validate_xss_safe(value, info.field_name) + class AppExportQuery(BaseModel): include_secret: bool = Field(default=False, description="Include secrets in export") diff --git a/api/tests/unit_tests/controllers/console/app/test_xss_prevention.py b/api/tests/unit_tests/controllers/console/app/test_xss_prevention.py new file mode 100644 index 0000000000..313818547b --- /dev/null +++ b/api/tests/unit_tests/controllers/console/app/test_xss_prevention.py @@ -0,0 +1,254 @@ +""" +Unit tests for XSS prevention in App payloads. + +This test module validates that HTML tags, JavaScript, and other potentially +dangerous content are rejected in App names and descriptions. +""" + +import pytest + +from controllers.console.app.app import CopyAppPayload, CreateAppPayload, UpdateAppPayload + + +class TestXSSPreventionUnit: + """Unit tests for XSS prevention in App payloads.""" + + def test_create_app_valid_names(self): + """Test CreateAppPayload with valid app names.""" + # Normal app names should be valid + valid_names = [ + "My App", + "Test App 123", + "App with - dash", + "App with _ underscore", + "App with + plus", + "App with () parentheses", + "App with [] brackets", + "App with {} braces", + "App with ! exclamation", + "App with @ at", + "App with # hash", + "App with $ dollar", + "App with % percent", + "App with ^ caret", + "App with & ampersand", + "App with * asterisk", + "Unicode: 测试应用", + "Emoji: 🤖", + "Mixed: Test 测试 123", + ] + + for name in valid_names: + payload = CreateAppPayload( + name=name, + mode="chat", + ) + assert payload.name == name + + def test_create_app_xss_script_tags(self): + """Test CreateAppPayload rejects script tags.""" + xss_payloads = [ + "", + "", + "", + "", + "", + "", + ] + + for name in xss_payloads: + with pytest.raises(ValueError) as exc_info: + CreateAppPayload(name=name, mode="chat") + assert "invalid characters or patterns" in str(exc_info.value).lower() + + def test_create_app_xss_iframe_tags(self): + """Test CreateAppPayload rejects iframe tags.""" + xss_payloads = [ + "", + "", + "", + ] + + for name in xss_payloads: + with pytest.raises(ValueError) as exc_info: + CreateAppPayload(name=name, mode="chat") + assert "invalid characters or patterns" in str(exc_info.value).lower() + + def test_create_app_xss_javascript_protocol(self): + """Test CreateAppPayload rejects javascript: protocol.""" + xss_payloads = [ + "javascript:alert(1)", + "JAVASCRIPT:alert(1)", + "JavaScript:alert(document.cookie)", + "javascript:void(0)", + "javascript://comment%0Aalert(1)", + ] + + for name in xss_payloads: + with pytest.raises(ValueError) as exc_info: + CreateAppPayload(name=name, mode="chat") + assert "invalid characters or patterns" in str(exc_info.value).lower() + + def test_create_app_xss_svg_onload(self): + """Test CreateAppPayload rejects SVG with onload.""" + xss_payloads = [ + "", + "", + "", + ] + + for name in xss_payloads: + with pytest.raises(ValueError) as exc_info: + CreateAppPayload(name=name, mode="chat") + assert "invalid characters or patterns" in str(exc_info.value).lower() + + def test_create_app_xss_event_handlers(self): + """Test CreateAppPayload rejects HTML event handlers.""" + xss_payloads = [ + "
", + "", + "", + "", + "", + "
", + "", + ] + + for name in xss_payloads: + with pytest.raises(ValueError) as exc_info: + CreateAppPayload(name=name, mode="chat") + assert "invalid characters or patterns" in str(exc_info.value).lower() + + def test_create_app_xss_object_embed(self): + """Test CreateAppPayload rejects object and embed tags.""" + xss_payloads = [ + "", + "", + "", + ] + + for name in xss_payloads: + with pytest.raises(ValueError) as exc_info: + CreateAppPayload(name=name, mode="chat") + assert "invalid characters or patterns" in str(exc_info.value).lower() + + def test_create_app_xss_link_javascript(self): + """Test CreateAppPayload rejects link tags with javascript.""" + xss_payloads = [ + "", + "", + ] + + for name in xss_payloads: + with pytest.raises(ValueError) as exc_info: + CreateAppPayload(name=name, mode="chat") + assert "invalid characters or patterns" in str(exc_info.value).lower() + + def test_create_app_xss_in_description(self): + """Test CreateAppPayload rejects XSS in description.""" + xss_descriptions = [ + "", + "javascript:alert(1)", + "", + ] + + for description in xss_descriptions: + with pytest.raises(ValueError) as exc_info: + CreateAppPayload( + name="Valid Name", + mode="chat", + description=description, + ) + assert "invalid characters or patterns" in str(exc_info.value).lower() + + def test_create_app_valid_descriptions(self): + """Test CreateAppPayload with valid descriptions.""" + valid_descriptions = [ + "A simple description", + "Description with < and > symbols", + "Description with & ampersand", + "Description with 'quotes' and \"double quotes\"", + "Description with / slashes", + "Description with \\ backslashes", + "Description with ; semicolons", + "Unicode: 这是一个描述", + "Emoji: 🎉🚀", + ] + + for description in valid_descriptions: + payload = CreateAppPayload( + name="Valid App Name", + mode="chat", + description=description, + ) + assert payload.description == description + + def test_create_app_none_description(self): + """Test CreateAppPayload with None description.""" + payload = CreateAppPayload( + name="Valid App Name", + mode="chat", + description=None, + ) + assert payload.description is None + + def test_update_app_xss_prevention(self): + """Test UpdateAppPayload also prevents XSS.""" + xss_names = [ + "", + "javascript:alert(1)", + "", + ] + + for name in xss_names: + with pytest.raises(ValueError) as exc_info: + UpdateAppPayload(name=name) + assert "invalid characters or patterns" in str(exc_info.value).lower() + + def test_update_app_valid_names(self): + """Test UpdateAppPayload with valid names.""" + payload = UpdateAppPayload(name="Valid Updated Name") + assert payload.name == "Valid Updated Name" + + def test_copy_app_xss_prevention(self): + """Test CopyAppPayload also prevents XSS.""" + xss_names = [ + "", + "javascript:alert(1)", + "", + ] + + for name in xss_names: + with pytest.raises(ValueError) as exc_info: + CopyAppPayload(name=name) + assert "invalid characters or patterns" in str(exc_info.value).lower() + + def test_copy_app_valid_names(self): + """Test CopyAppPayload with valid names.""" + payload = CopyAppPayload(name="Valid Copy Name") + assert payload.name == "Valid Copy Name" + + def test_copy_app_none_name(self): + """Test CopyAppPayload with None name (should be allowed).""" + payload = CopyAppPayload(name=None) + assert payload.name is None + + def test_edge_case_angle_brackets_content(self): + """Test that angle brackets with actual content are rejected.""" + # Angle brackets without valid HTML-like patterns should be checked + # The regex pattern <.*?on\w+\s*= should catch event handlers + # But let's verify other patterns too + + # Valid: angle brackets used as symbols (not matched by our patterns) + # Our patterns specifically look for dangerous constructs + + # Invalid: actual HTML tags with event handlers + invalid_names = [ + "
", + "", + ] + + for name in invalid_names: + with pytest.raises(ValueError) as exc_info: + CreateAppPayload(name=name, mode="chat") + assert "invalid characters or patterns" in str(exc_info.value).lower() From 27be89c9848d39b2f53c2e5bebc1e4e870db8489 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Wed, 31 Dec 2025 15:31:11 +0800 Subject: [PATCH 16/37] chore: lint for react compiler (#30417) --- web/eslint.config.mjs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index 89f6d292cd..574dbb091e 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -13,6 +13,24 @@ export default antfu( 'react/no-forward-ref': 'off', 'react/no-use-context': 'off', 'react/prefer-namespace-import': 'error', + + // React Compiler rules + // Set to warn for gradual adoption + 'react-hooks/config': 'warn', + 'react-hooks/error-boundaries': 'warn', + 'react-hooks/component-hook-factories': 'warn', + 'react-hooks/gating': 'warn', + 'react-hooks/globals': 'warn', + 'react-hooks/immutability': 'warn', + 'react-hooks/preserve-manual-memoization': 'warn', + 'react-hooks/purity': 'warn', + 'react-hooks/refs': 'warn', + 'react-hooks/set-state-in-effect': 'warn', + 'react-hooks/set-state-in-render': 'warn', + 'react-hooks/static-components': 'warn', + 'react-hooks/unsupported-syntax': 'warn', + 'react-hooks/use-memo': 'warn', + 'react-hooks/incompatible-library': 'warn', }, }, nextjs: true, From e856287b65fc479a250ef2e2a9afd06b155f90e0 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Wed, 31 Dec 2025 15:38:07 +0800 Subject: [PATCH 17/37] chore: update knip config and include in CI (#30410) --- .github/workflows/style.yml | 5 + web/eslint-rules/rules/no-as-any-in-t.js | 5 - .../rules/no-legacy-namespace-prefix.js | 25 -- web/eslint-rules/rules/require-ns-option.js | 5 - web/knip.config.ts | 266 +----------------- web/package.json | 6 +- web/pnpm-lock.yaml | 29 +- .../script.mjs => scripts/gen-icons.mjs} | 13 +- 8 files changed, 30 insertions(+), 324 deletions(-) rename web/{app/components/base/icons/script.mjs => scripts/gen-icons.mjs} (91%) diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index d463349686..39b559f4ca 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -110,6 +110,11 @@ jobs: working-directory: ./web run: pnpm run type-check:tsgo + - name: Web dead code check + if: steps.changed-files.outputs.any_changed == 'true' + working-directory: ./web + run: pnpm run knip + superlinter: name: SuperLinter runs-on: ubuntu-latest diff --git a/web/eslint-rules/rules/no-as-any-in-t.js b/web/eslint-rules/rules/no-as-any-in-t.js index 0eb134a3cf..5e4ffc8c1c 100644 --- a/web/eslint-rules/rules/no-as-any-in-t.js +++ b/web/eslint-rules/rules/no-as-any-in-t.js @@ -29,11 +29,6 @@ export default { const options = context.options[0] || {} const mode = options.mode || 'any' - /** - * Check if this is a t() function call - * @param {import('estree').CallExpression} node - * @returns {boolean} - */ function isTCall(node) { // Direct t() call if (node.callee.type === 'Identifier' && node.callee.name === 't') diff --git a/web/eslint-rules/rules/no-legacy-namespace-prefix.js b/web/eslint-rules/rules/no-legacy-namespace-prefix.js index dc268c9b65..023e6b73d3 100644 --- a/web/eslint-rules/rules/no-legacy-namespace-prefix.js +++ b/web/eslint-rules/rules/no-legacy-namespace-prefix.js @@ -19,26 +19,11 @@ export default { create(context) { const sourceCode = context.sourceCode - // Track all t() calls to fix - /** @type {Array<{ node: import('estree').CallExpression }>} */ const tCallsToFix = [] - - // Track variables with namespace prefix - /** @type {Map} */ const variablesToFix = new Map() - - // Track all namespaces used in the file (from legacy prefix detection) - /** @type {Set} */ const namespacesUsed = new Set() - - // Track variable values for template literal analysis - /** @type {Map} */ const variableValues = new Map() - /** - * Analyze a template literal and extract namespace info - * @param {import('estree').TemplateLiteral} node - */ function analyzeTemplateLiteral(node) { const quasis = node.quasis const expressions = node.expressions @@ -78,11 +63,6 @@ export default { return { ns: null, canFix: false, fixedQuasis: null, variableToUpdate: null } } - /** - * Build a fixed template literal string - * @param {string[]} quasis - * @param {import('estree').Expression[]} expressions - */ function buildTemplateLiteral(quasis, expressions) { let result = '`' for (let i = 0; i < quasis.length; i++) { @@ -95,11 +75,6 @@ export default { return result } - /** - * Check if a t() call already has ns in its second argument - * @param {import('estree').CallExpression} node - * @returns {boolean} - */ function hasNsArgument(node) { if (node.arguments.length < 2) return false diff --git a/web/eslint-rules/rules/require-ns-option.js b/web/eslint-rules/rules/require-ns-option.js index df8f7ec2e8..74621596fd 100644 --- a/web/eslint-rules/rules/require-ns-option.js +++ b/web/eslint-rules/rules/require-ns-option.js @@ -12,11 +12,6 @@ export default { }, }, create(context) { - /** - * Check if a t() call has ns in its second argument - * @param {import('estree').CallExpression} node - * @returns {boolean} - */ function hasNsOption(node) { if (node.arguments.length < 2) return false diff --git a/web/knip.config.ts b/web/knip.config.ts index 975a85b997..414b00fb7f 100644 --- a/web/knip.config.ts +++ b/web/knip.config.ts @@ -1,277 +1,33 @@ import type { KnipConfig } from 'knip' /** - * Knip Configuration for Dead Code Detection - * - * This configuration helps identify unused files, exports, and dependencies - * in the Dify web application (Next.js 15 + TypeScript + React 19). - * - * ⚠️ SAFETY FIRST: This configuration is designed to be conservative and - * avoid false positives that could lead to deleting actively used code. - * * @see https://knip.dev/reference/configuration */ const config: KnipConfig = { - // ============================================================================ - // Next.js Framework Configuration - // ============================================================================ - // Configure entry points specific to Next.js application structure. - // These files are automatically treated as entry points by the framework. - next: { - entry: [ - // Next.js App Router pages (must exist for routing) - 'app/**/page.tsx', - 'app/**/layout.tsx', - 'app/**/loading.tsx', - 'app/**/error.tsx', - 'app/**/not-found.tsx', - 'app/**/template.tsx', - 'app/**/default.tsx', - - // Middleware (runs before every route) - 'middleware.ts', - - // Configuration files - 'next.config.js', - 'tailwind.config.js', - 'tailwind-common-config.ts', - 'postcss.config.js', - - // Linting configuration - 'eslint.config.mjs', - ], - }, - - // ============================================================================ - // Global Entry Points - // ============================================================================ - // Files that serve as entry points for the application. - // The '!' suffix means these patterns take precedence and are always included. entry: [ - // Next.js App Router patterns (high priority) - 'app/**/page.tsx!', - 'app/**/layout.tsx!', - 'app/**/loading.tsx!', - 'app/**/error.tsx!', - 'app/**/not-found.tsx!', - 'app/**/template.tsx!', - 'app/**/default.tsx!', - - // Core configuration files - 'middleware.ts!', - 'next.config.js!', - 'tailwind.config.js!', - 'tailwind-common-config.ts!', - 'postcss.config.js!', - - // Linting setup - 'eslint.config.mjs!', - - // ======================================================================== - // 🔒 CRITICAL: Global Initializers and Providers - // ======================================================================== - // These files are imported by root layout.tsx and provide global functionality. - // Even if not directly imported elsewhere, they are essential for app initialization. - - // Browser initialization (runs on client startup) - 'app/components/browser-initializer.tsx!', - 'app/components/sentry-initializer.tsx!', - 'app/components/app-initializer.tsx!', - - // i18n initialization (server and client) - 'app/components/i18n.tsx!', - 'app/components/i18n-server.tsx!', - - // Route prefix handling (used in root layout) - 'app/routePrefixHandle.tsx!', - - // ======================================================================== - // 🔒 CRITICAL: Context Providers - // ======================================================================== - // Context providers might be used via React Context API and imported dynamically. - // Protecting all context files to prevent breaking the provider chain. - 'context/**/*.ts?(x)!', - - // Component-level contexts (also used via React.useContext) - 'app/components/**/*.context.ts?(x)!', - - // ======================================================================== - // 🔒 CRITICAL: State Management Stores - // ======================================================================== - // Zustand stores might be imported dynamically or via hooks. - // These are often imported at module level, so they should be protected. - 'app/components/**/*.store.ts?(x)!', - 'context/**/*.store.ts?(x)!', - - // ======================================================================== - // 🔒 CRITICAL: Provider Components - // ======================================================================== - // Provider components wrap the app and provide global state/functionality - 'app/components/**/*.provider.ts?(x)!', - 'context/**/*.provider.ts?(x)!', - - // ======================================================================== - // Development tools - // ======================================================================== - // Storybook configuration - '.storybook/**/*', + 'scripts/**/*.{js,ts,mjs}', + 'bin/**/*.{js,ts,mjs}', ], - - // ============================================================================ - // Project Files to Analyze - // ============================================================================ - // Glob patterns for files that should be analyzed for unused code. - // Excludes test files to avoid false positives. - project: [ - '**/*.{js,jsx,ts,tsx,mjs,cjs}', - ], - - // ============================================================================ - // Ignored Files and Directories - // ============================================================================ - // Files and directories that should be completely excluded from analysis. - // These typically contain: - // - Test files - // - Internationalization files (loaded dynamically) - // - Static assets - // - Build outputs - // - External libraries ignore: [ - // Test files and directories - '**/__tests__/**', - '**/*.spec.{ts,tsx}', - '**/*.test.{ts,tsx}', - - // ======================================================================== - // 🔒 CRITICAL: i18n Files (Dynamically Loaded) - // ======================================================================== - // Internationalization files are loaded dynamically at runtime via i18next. - // Pattern: import(`@/i18n/${locale}/messages`) - // These will NEVER show up in static analysis but are essential! 'i18n/**', - - // ======================================================================== - // 🔒 CRITICAL: Static Assets - // ======================================================================== - // Static assets are referenced by URL in the browser, not via imports. - // Examples: /logo.png, /icons/*, /embed.js 'public/**', - - // Build outputs and caches - 'node_modules/**', - '.next/**', - 'coverage/**', - - // Development tools - '**/*.stories.{ts,tsx}', - - // ======================================================================== - // 🔒 Utility scripts (not part of application runtime) - // ======================================================================== - // These scripts are run manually (e.g., pnpm gen-icons, pnpm i18n:check) - // and are not imported by the application code. - 'scripts/**', - 'bin/**', - 'i18n-config/**', - - // Icon generation script (generates components, not used in runtime) - 'app/components/base/icons/script.mjs', ], - - // ============================================================================ - // Ignored Dependencies - // ============================================================================ - // Dependencies that are used but not directly imported in code. - // These are typically: - // - Build tools - // - Plugins loaded by configuration files - // - CLI tools - ignoreDependencies: [ - // ======================================================================== - // Next.js plugins (loaded by next.config.js) - // ======================================================================== - 'next-pwa', - '@next/bundle-analyzer', - '@next/mdx', - - // ======================================================================== - // Build tools (used by webpack/next.js build process) - // ======================================================================== - 'code-inspector-plugin', - - // ======================================================================== - // Development and translation tools (used by scripts) - // ======================================================================== - 'bing-translate-api', - 'uglify-js', - ], - - // ============================================================================ - // Export Analysis Configuration - // ============================================================================ - // Configure how exports are analyzed - - // Ignore exports that are only used within the same file - // (e.g., helper functions used internally in the same module) - ignoreExportsUsedInFile: true, - - // ⚠️ SAFETY: Include exports from entry files in the analysis - // This helps find unused public APIs, but be careful with: - // - Context exports (useContext hooks) - // - Store exports (useStore hooks) - // - Type exports (might be used in other files) - includeEntryExports: true, - - // ============================================================================ - // Ignored Binaries - // ============================================================================ - // Binary executables that are used but not listed in package.json ignoreBinaries: [ - 'only-allow', // Used in preinstall script to enforce pnpm usage + 'only-allow', ], - - // ============================================================================ - // Reporting Rules - // ============================================================================ - // Configure what types of issues to report and at what severity level rules: { - // ======================================================================== - // Unused files are ERRORS - // ======================================================================== - // These should definitely be removed or used. - // However, always manually verify before deleting! - files: 'error', - - // ======================================================================== - // Unused dependencies are WARNINGS - // ======================================================================== - // Dependencies might be: - // - Used in production builds but not in dev - // - Peer dependencies - // - Used by other tools + files: 'warn', dependencies: 'warn', devDependencies: 'warn', - - // ======================================================================== - // Unlisted imports are ERRORS - // ======================================================================== - // Missing from package.json - will break in production! - unlisted: 'error', - - // ======================================================================== - // Unused exports are WARNINGS (not errors!) - // ======================================================================== - // Exports might be: - // - Part of public API for future use - // - Used by external tools - // - Exported for type inference - // ⚠️ ALWAYS manually verify before removing exports! + optionalPeerDependencies: 'warn', + unlisted: 'warn', + unresolved: 'warn', exports: 'warn', - - // Unused types are warnings (might be part of type definitions) + nsExports: 'warn', + classMembers: 'warn', types: 'warn', - - // Duplicate exports are warnings (could cause confusion but not breaking) + nsTypes: 'warn', + enumMembers: 'warn', duplicates: 'warn', }, } diff --git a/web/package.json b/web/package.json index a78575304c..0c6821ce86 100644 --- a/web/package.json +++ b/web/package.json @@ -31,7 +31,7 @@ "type-check": "tsc --noEmit", "type-check:tsgo": "tsgo --noEmit", "prepare": "cd ../ && node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || husky ./web/.husky", - "gen-icons": "node ./app/components/base/icons/script.mjs", + "gen-icons": "node ./scripts/gen-icons.mjs && eslint --fix app/components/base/icons/src/", "uglify-embed": "node ./bin/uglify-embed", "i18n:check": "tsx ./scripts/check-i18n.js", "i18n:gen": "tsx ./scripts/auto-gen-i18n.js", @@ -190,7 +190,6 @@ "@vitejs/plugin-react": "^5.1.2", "@vitest/coverage-v8": "4.0.16", "autoprefixer": "^10.4.21", - "babel-loader": "^10.0.0", "bing-translate-api": "^4.1.0", "code-inspector-plugin": "1.2.9", "cross-env": "^10.1.0", @@ -201,10 +200,9 @@ "eslint-plugin-storybook": "^10.1.10", "eslint-plugin-tailwindcss": "^3.18.2", "husky": "^9.1.7", - "istanbul-lib-coverage": "^3.2.2", "jsdom": "^27.3.0", "jsdom-testing-mocks": "^1.16.0", - "knip": "^5.66.1", + "knip": "^5.78.0", "lint-staged": "^15.5.2", "nock": "^14.0.10", "postcss": "^8.5.6", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index fe9032d248..373e2e4020 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -481,9 +481,6 @@ importers: autoprefixer: specifier: ^10.4.21 version: 10.4.22(postcss@8.5.6) - babel-loader: - specifier: ^10.0.0 - version: 10.0.0(@babel/core@7.28.5)(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)) bing-translate-api: specifier: ^4.1.0 version: 4.2.0 @@ -514,9 +511,6 @@ importers: husky: specifier: ^9.1.7 version: 9.1.7 - istanbul-lib-coverage: - specifier: ^3.2.2 - version: 3.2.2 jsdom: specifier: ^27.3.0 version: 27.3.0(canvas@3.2.0) @@ -524,8 +518,8 @@ importers: specifier: ^1.16.0 version: 1.16.0 knip: - specifier: ^5.66.1 - version: 5.72.0(@types/node@18.15.0)(typescript@5.9.3) + specifier: ^5.78.0 + version: 5.78.0(@types/node@18.15.0)(typescript@5.9.3) lint-staged: specifier: ^15.5.2 version: 15.5.2 @@ -4271,13 +4265,6 @@ packages: peerDependencies: postcss: ^8.1.0 - babel-loader@10.0.0: - resolution: {integrity: sha512-z8jt+EdS61AMw22nSfoNJAZ0vrtmhPRVi6ghL3rCeRZI8cdNYFiV5xeV3HbE7rlZZNmGH8BVccwWt8/ED0QOHA==} - engines: {node: ^18.20.0 || ^20.10.0 || >=22.0.0} - peerDependencies: - '@babel/core': ^7.12.0 - webpack: '>=5.61.0' - babel-loader@8.4.1: resolution: {integrity: sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA==} engines: {node: '>= 8.9'} @@ -6336,8 +6323,8 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} - knip@5.72.0: - resolution: {integrity: sha512-rlyoXI8FcggNtM/QXd/GW0sbsYvNuA/zPXt7bsuVi6kVQogY2PDCr81bPpzNnl0CP8AkFm2Z2plVeL5QQSis2w==} + knip@5.78.0: + resolution: {integrity: sha512-nB7i/fgiJl7WVxdv5lX4ZPfDt9/zrw/lOgZtyioy988xtFhKuFJCRdHWT1Zg9Avc0yaojvnmEuAXU8SeMblKww==} engines: {node: '>=18.18.0'} hasBin: true peerDependencies: @@ -13093,12 +13080,6 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 - babel-loader@10.0.0(@babel/core@7.28.5)(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)): - dependencies: - '@babel/core': 7.28.5 - find-up: 5.0.0 - webpack: 5.103.0(esbuild@0.25.0)(uglify-js@3.19.3) - babel-loader@8.4.1(@babel/core@7.28.5)(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)): dependencies: '@babel/core': 7.28.5 @@ -15468,7 +15449,7 @@ snapshots: kleur@4.1.5: {} - knip@5.72.0(@types/node@18.15.0)(typescript@5.9.3): + knip@5.78.0(@types/node@18.15.0)(typescript@5.9.3): dependencies: '@nodelib/fs.walk': 1.2.8 '@types/node': 18.15.0 diff --git a/web/app/components/base/icons/script.mjs b/web/scripts/gen-icons.mjs similarity index 91% rename from web/app/components/base/icons/script.mjs rename to web/scripts/gen-icons.mjs index 81566cc4cf..f681d65759 100644 --- a/web/app/components/base/icons/script.mjs +++ b/web/scripts/gen-icons.mjs @@ -5,6 +5,7 @@ import { parseXml } from '@rgrove/parse-xml' import { camelCase, template } from 'es-toolkit/compat' const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const iconsDir = path.resolve(__dirname, '../app/components/base/icons') const generateDir = async (currentPath) => { try { @@ -32,7 +33,7 @@ const processSvgStructure = (svgStructure, replaceFillOrStrokeColor) => { } } const generateSvgComponent = async (fileHandle, entry, pathList, replaceFillOrStrokeColor) => { - const currentPath = path.resolve(__dirname, 'src', ...pathList.slice(2)) + const currentPath = path.resolve(iconsDir, 'src', ...pathList.slice(2)) try { await access(currentPath) @@ -86,7 +87,7 @@ export { default as <%= svgName %> } from './<%= svgName %>' } const generateImageComponent = async (entry, pathList) => { - const currentPath = path.resolve(__dirname, 'src', ...pathList.slice(2)) + const currentPath = path.resolve(iconsDir, 'src', ...pathList.slice(2)) try { await access(currentPath) @@ -167,8 +168,8 @@ const walk = async (entry, pathList, replaceFillOrStrokeColor) => { } (async () => { - await rm(path.resolve(__dirname, 'src'), { recursive: true, force: true }) - await walk('public', [__dirname, 'assets']) - await walk('vender', [__dirname, 'assets'], true) - await walk('image', [__dirname, 'assets']) + await rm(path.resolve(iconsDir, 'src'), { recursive: true, force: true }) + await walk('public', [iconsDir, 'assets']) + await walk('vender', [iconsDir, 'assets'], true) + await walk('image', [iconsDir, 'assets']) })() From cad7101534c251f5d0c12218d6d000bab39365d1 Mon Sep 17 00:00:00 2001 From: Zhiqiang Yang Date: Wed, 31 Dec 2025 15:49:06 +0800 Subject: [PATCH 18/37] feat: support image extraction in PDF RAG extractor (#30399) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/core/rag/extractor/extract_processor.py | 4 +- api/core/rag/extractor/pdf_extractor.py | 122 +++++++++++- .../core/rag/extractor/test_pdf_extractor.py | 186 ++++++++++++++++++ 3 files changed, 304 insertions(+), 8 deletions(-) create mode 100644 api/tests/unit_tests/core/rag/extractor/test_pdf_extractor.py diff --git a/api/core/rag/extractor/extract_processor.py b/api/core/rag/extractor/extract_processor.py index 013c287248..6d28ce25bc 100644 --- a/api/core/rag/extractor/extract_processor.py +++ b/api/core/rag/extractor/extract_processor.py @@ -112,7 +112,7 @@ class ExtractProcessor: if file_extension in {".xlsx", ".xls"}: extractor = ExcelExtractor(file_path) elif file_extension == ".pdf": - extractor = PdfExtractor(file_path) + extractor = PdfExtractor(file_path, upload_file.tenant_id, upload_file.created_by) elif file_extension in {".md", ".markdown", ".mdx"}: extractor = ( UnstructuredMarkdownExtractor(file_path, unstructured_api_url, unstructured_api_key) @@ -148,7 +148,7 @@ class ExtractProcessor: if file_extension in {".xlsx", ".xls"}: extractor = ExcelExtractor(file_path) elif file_extension == ".pdf": - extractor = PdfExtractor(file_path) + extractor = PdfExtractor(file_path, upload_file.tenant_id, upload_file.created_by) elif file_extension in {".md", ".markdown", ".mdx"}: extractor = MarkdownExtractor(file_path, autodetect_encoding=True) elif file_extension in {".htm", ".html"}: diff --git a/api/core/rag/extractor/pdf_extractor.py b/api/core/rag/extractor/pdf_extractor.py index 80530d99a6..6aabcac704 100644 --- a/api/core/rag/extractor/pdf_extractor.py +++ b/api/core/rag/extractor/pdf_extractor.py @@ -1,25 +1,57 @@ """Abstract interface for document loader implementations.""" import contextlib +import io +import logging +import uuid from collections.abc import Iterator +import pypdfium2 +import pypdfium2.raw as pdfium_c + +from configs import dify_config from core.rag.extractor.blob.blob import Blob from core.rag.extractor.extractor_base import BaseExtractor from core.rag.models.document import Document +from extensions.ext_database import db from extensions.ext_storage import storage +from libs.datetime_utils import naive_utc_now +from models.enums import CreatorUserRole +from models.model import UploadFile + +logger = logging.getLogger(__name__) class PdfExtractor(BaseExtractor): - """Load pdf files. - + """ + PdfExtractor is used to extract text and images from PDF files. Args: - file_path: Path to the file to load. + file_path: Path to the PDF file. + tenant_id: Workspace ID. + user_id: ID of the user performing the extraction. + file_cache_key: Optional cache key for the extracted text. """ - def __init__(self, file_path: str, file_cache_key: str | None = None): - """Initialize with file path.""" + # Magic bytes for image format detection: (magic_bytes, extension, mime_type) + IMAGE_FORMATS = [ + (b"\xff\xd8\xff", "jpg", "image/jpeg"), + (b"\x89PNG\r\n\x1a\n", "png", "image/png"), + (b"\x00\x00\x00\x0c\x6a\x50\x20\x20\x0d\x0a\x87\x0a", "jp2", "image/jp2"), + (b"GIF8", "gif", "image/gif"), + (b"BM", "bmp", "image/bmp"), + (b"II*\x00", "tiff", "image/tiff"), + (b"MM\x00*", "tiff", "image/tiff"), + (b"II+\x00", "tiff", "image/tiff"), + (b"MM\x00+", "tiff", "image/tiff"), + ] + MAX_MAGIC_LEN = max(len(m) for m, _, _ in IMAGE_FORMATS) + + def __init__(self, file_path: str, tenant_id: str, user_id: str, file_cache_key: str | None = None): + """Initialize PdfExtractor.""" self._file_path = file_path + self._tenant_id = tenant_id + self._user_id = user_id self._file_cache_key = file_cache_key def extract(self) -> list[Document]: @@ -50,7 +82,6 @@ class PdfExtractor(BaseExtractor): def parse(self, blob: Blob) -> Iterator[Document]: """Lazily parse the blob.""" - import pypdfium2 # type: ignore with blob.as_bytes_io() as file_path: pdf_reader = pypdfium2.PdfDocument(file_path, autoclose=True) @@ -59,8 +90,87 @@ class PdfExtractor(BaseExtractor): text_page = page.get_textpage() content = text_page.get_text_range() text_page.close() + + image_content = self._extract_images(page) + if image_content: + content += "\n" + image_content + page.close() metadata = {"source": blob.source, "page": page_number} yield Document(page_content=content, metadata=metadata) finally: pdf_reader.close() + + def _extract_images(self, page) -> str: + """ + Extract images from a PDF page, save them to storage and database, + and return markdown image links. + + Args: + page: pypdfium2 page object. + + Returns: + Markdown string containing links to the extracted images. + """ + image_content = [] + upload_files = [] + base_url = dify_config.INTERNAL_FILES_URL or dify_config.FILES_URL + + try: + image_objects = page.get_objects(filter=(pdfium_c.FPDF_PAGEOBJ_IMAGE,)) + for obj in image_objects: + try: + # Extract image bytes + img_byte_arr = io.BytesIO() + # Extract DCTDecode (JPEG) and JPXDecode (JPEG 2000) images directly + # Fallback to png for other formats + obj.extract(img_byte_arr, fb_format="png") + img_bytes = img_byte_arr.getvalue() + + if not img_bytes: + continue + + header = img_bytes[: self.MAX_MAGIC_LEN] + image_ext = None + mime_type = None + for magic, ext, mime in self.IMAGE_FORMATS: + if header.startswith(magic): + image_ext = ext + mime_type = mime + break + + if not image_ext or not mime_type: + continue + + file_uuid = str(uuid.uuid4()) + file_key = "image_files/" + self._tenant_id + "/" + file_uuid + "." + image_ext + + storage.save(file_key, img_bytes) + + # save file to db + upload_file = UploadFile( + tenant_id=self._tenant_id, + storage_type=dify_config.STORAGE_TYPE, + key=file_key, + name=file_key, + size=len(img_bytes), + extension=image_ext, + mime_type=mime_type, + created_by=self._user_id, + created_by_role=CreatorUserRole.ACCOUNT, + created_at=naive_utc_now(), + used=True, + used_by=self._user_id, + used_at=naive_utc_now(), + ) + upload_files.append(upload_file) + image_content.append(f"![image]({base_url}/files/{upload_file.id}/file-preview)") + except Exception as e: + logger.warning("Failed to extract image from PDF: %s", e) + continue + except Exception as e: + logger.warning("Failed to get objects from PDF page: %s", e) + if upload_files: + db.session.add_all(upload_files) + db.session.commit() + return "\n".join(image_content) diff --git a/api/tests/unit_tests/core/rag/extractor/test_pdf_extractor.py b/api/tests/unit_tests/core/rag/extractor/test_pdf_extractor.py new file mode 100644 index 0000000000..3167a9a301 --- /dev/null +++ b/api/tests/unit_tests/core/rag/extractor/test_pdf_extractor.py @@ -0,0 +1,186 @@ +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +import pytest + +import core.rag.extractor.pdf_extractor as pe + + +@pytest.fixture +def mock_dependencies(monkeypatch): + # Mock storage + saves = [] + + def save(key, data): + saves.append((key, data)) + + monkeypatch.setattr(pe, "storage", SimpleNamespace(save=save)) + + # Mock db + class DummySession: + def __init__(self): + self.added = [] + self.committed = False + + def add(self, obj): + self.added.append(obj) + + def add_all(self, objs): + self.added.extend(objs) + + def commit(self): + self.committed = True + + db_stub = SimpleNamespace(session=DummySession()) + monkeypatch.setattr(pe, "db", db_stub) + + # Mock UploadFile + class FakeUploadFile: + DEFAULT_ID = "test_file_id" + + def __init__(self, **kwargs): + # Assign id from DEFAULT_ID, allow override via kwargs if needed + self.id = self.DEFAULT_ID + for k, v in kwargs.items(): + setattr(self, k, v) + + monkeypatch.setattr(pe, "UploadFile", FakeUploadFile) + + # Mock config + monkeypatch.setattr(pe.dify_config, "FILES_URL", "http://files.local") + monkeypatch.setattr(pe.dify_config, "INTERNAL_FILES_URL", None) + monkeypatch.setattr(pe.dify_config, "STORAGE_TYPE", "local") + + return SimpleNamespace(saves=saves, db=db_stub, UploadFile=FakeUploadFile) + + +@pytest.mark.parametrize( + ("image_bytes", "expected_mime", "expected_ext", "file_id"), + [ + (b"\xff\xd8\xff some jpeg", "image/jpeg", "jpg", "test_file_id_jpeg"), + (b"\x89PNG\r\n\x1a\n some png", "image/png", "png", "test_file_id_png"), + ], +) +def test_extract_images_formats(mock_dependencies, monkeypatch, image_bytes, expected_mime, expected_ext, file_id): + saves = mock_dependencies.saves + db_stub = mock_dependencies.db + + # Customize FakeUploadFile id for this test case. + # Using monkeypatch ensures the class attribute is reset between parameter sets. + monkeypatch.setattr(mock_dependencies.UploadFile, "DEFAULT_ID", file_id) + + # Mock page and image objects + mock_page = MagicMock() + mock_image_obj = MagicMock() + + def mock_extract(buf, fb_format=None): + buf.write(image_bytes) + + mock_image_obj.extract.side_effect = mock_extract + + mock_page.get_objects.return_value = [mock_image_obj] + + extractor = pe.PdfExtractor(file_path="test.pdf", tenant_id="t1", user_id="u1") + + # We need to handle the import inside _extract_images + with patch("pypdfium2.raw") as mock_raw: + mock_raw.FPDF_PAGEOBJ_IMAGE = 1 + result = extractor._extract_images(mock_page) + + assert f"![image](http://files.local/files/{file_id}/file-preview)" in result + assert len(saves) == 1 + assert saves[0][1] == image_bytes + assert len(db_stub.session.added) == 1 + assert db_stub.session.added[0].tenant_id == "t1" + assert db_stub.session.added[0].size == len(image_bytes) + assert db_stub.session.added[0].mime_type == expected_mime + assert db_stub.session.added[0].extension == expected_ext + assert db_stub.session.committed is True + + +@pytest.mark.parametrize( + ("get_objects_side_effect", "get_objects_return_value"), + [ + (None, []), # Empty list + (None, None), # None returned + (Exception("Failed to get objects"), None), # Exception raised + ], +) +def test_extract_images_get_objects_scenarios(mock_dependencies, get_objects_side_effect, get_objects_return_value): + mock_page = MagicMock() + if get_objects_side_effect: + mock_page.get_objects.side_effect = get_objects_side_effect + else: + mock_page.get_objects.return_value = get_objects_return_value + + extractor = pe.PdfExtractor(file_path="test.pdf", tenant_id="t1", user_id="u1") + + with patch("pypdfium2.raw") as mock_raw: + mock_raw.FPDF_PAGEOBJ_IMAGE = 1 + result = extractor._extract_images(mock_page) + + assert result == "" + + +def test_extract_calls_extract_images(mock_dependencies, monkeypatch): + # Mock pypdfium2 + mock_pdf_doc = MagicMock() + mock_page = MagicMock() + mock_pdf_doc.__iter__.return_value = [mock_page] + + # Mock text extraction + mock_text_page = MagicMock() + mock_text_page.get_text_range.return_value = "Page text content" + mock_page.get_textpage.return_value = mock_text_page + + with patch("pypdfium2.PdfDocument", return_value=mock_pdf_doc): + # Mock Blob + mock_blob = MagicMock() + mock_blob.source = "test.pdf" + with patch("core.rag.extractor.pdf_extractor.Blob.from_path", return_value=mock_blob): + extractor = pe.PdfExtractor(file_path="test.pdf", tenant_id="t1", user_id="u1") + + # Mock _extract_images to return a known string + monkeypatch.setattr(extractor, "_extract_images", lambda p: "![image](img_url)") + + documents = list(extractor.extract()) + + assert len(documents) == 1 + assert "Page text content" in documents[0].page_content + assert "![image](img_url)" in documents[0].page_content + assert documents[0].metadata["page"] == 0 + + +def test_extract_images_failures(mock_dependencies): + saves = mock_dependencies.saves + db_stub = mock_dependencies.db + + # Mock page and image objects + mock_page = MagicMock() + mock_image_obj_fail = MagicMock() + mock_image_obj_ok = MagicMock() + + # First image raises exception + mock_image_obj_fail.extract.side_effect = Exception("Extraction failure") + + # Second image is OK (JPEG) + jpeg_bytes = b"\xff\xd8\xff some image data" + + def mock_extract(buf, fb_format=None): + buf.write(jpeg_bytes) + + mock_image_obj_ok.extract.side_effect = mock_extract + + mock_page.get_objects.return_value = [mock_image_obj_fail, mock_image_obj_ok] + + extractor = pe.PdfExtractor(file_path="test.pdf", tenant_id="t1", user_id="u1") + + with patch("pypdfium2.raw") as mock_raw: + mock_raw.FPDF_PAGEOBJ_IMAGE = 1 + result = extractor._extract_images(mock_page) + + # Should have one success + assert "![image](http://files.local/files/test_file_id/file-preview)" in result + assert len(saves) == 1 + assert saves[0][1] == jpeg_bytes + assert db_stub.session.committed is True From 2bb1e24fb4593220f7278605ab0154aa43f4e28e Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Wed, 31 Dec 2025 15:53:33 +0800 Subject: [PATCH 19/37] test: unify i18next mocks into centralized helpers (#30376) Co-authored-by: Claude Opus 4.5 Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: yyh --- .../assets/component-test.template.tsx | 17 ++-- .../frontend-testing/references/mocking.md | 28 ++++--- .../config/agent-setting-button.spec.tsx | 9 --- .../config/config-audio.spec.tsx | 9 --- .../base/inline-delete-confirm/index.spec.tsx | 26 ++---- .../base/input-with-copy/index.spec.tsx | 23 ++---- web/app/components/base/input/index.spec.tsx | 19 ++--- .../billing/pricing/footer.spec.tsx | 18 ----- .../components/datasets/create/index.spec.tsx | 10 --- .../processing/index.spec.tsx | 10 --- .../components/plugins/card/index.spec.tsx | 27 ------- .../steps/install.spec.tsx | 34 ++++---- .../steps/uploading.spec.tsx | 15 ---- .../plugins/marketplace/index.spec.tsx | 46 +++++------ .../create/common-modal.spec.tsx | 11 --- .../create/oauth-client.spec.tsx | 11 --- .../subscription-list/edit/index.spec.tsx | 10 --- .../plugin-mutation-model/index.spec.tsx | 22 ------ web/test/i18n-mock.ts | 79 +++++++++++++++++++ web/testing/testing.md | 27 ++++--- web/vitest.setup.ts | 20 +---- 21 files changed, 178 insertions(+), 293 deletions(-) create mode 100644 web/test/i18n-mock.ts diff --git a/.claude/skills/frontend-testing/assets/component-test.template.tsx b/.claude/skills/frontend-testing/assets/component-test.template.tsx index c39baff916..6b7803bd4b 100644 --- a/.claude/skills/frontend-testing/assets/component-test.template.tsx +++ b/.claude/skills/frontend-testing/assets/component-test.template.tsx @@ -28,17 +28,14 @@ import userEvent from '@testing-library/user-event' // i18n (automatically mocked) // WHY: Global mock in web/vitest.setup.ts is auto-loaded by Vitest setup -// No explicit mock needed - it returns translation keys as-is +// The global mock provides: useTranslation, Trans, useMixedTranslation, useGetLanguage +// No explicit mock needed for most tests +// // Override only if custom translations are required: -// vi.mock('react-i18next', () => ({ -// useTranslation: () => ({ -// t: (key: string) => { -// const customTranslations: Record = { -// 'my.custom.key': 'Custom Translation', -// } -// return customTranslations[key] || key -// }, -// }), +// import { createReactI18nextMock } from '@/test/i18n-mock' +// vi.mock('react-i18next', () => createReactI18nextMock({ +// 'my.custom.key': 'Custom Translation', +// 'button.save': 'Save', // })) // Router (if component uses useRouter, usePathname, useSearchParams) diff --git a/.claude/skills/frontend-testing/references/mocking.md b/.claude/skills/frontend-testing/references/mocking.md index 23889c8d3d..c70bcf0ae5 100644 --- a/.claude/skills/frontend-testing/references/mocking.md +++ b/.claude/skills/frontend-testing/references/mocking.md @@ -52,23 +52,29 @@ Modules are not mocked automatically. Use `vi.mock` in test files, or add global ### 1. i18n (Auto-loaded via Global Mock) A global mock is defined in `web/vitest.setup.ts` and is auto-loaded by Vitest setup. -**No explicit mock needed** for most tests - it returns translation keys as-is. -For tests requiring custom translations, override the mock: +The global mock provides: + +- `useTranslation` - returns translation keys with namespace prefix +- `Trans` component - renders i18nKey and components +- `useMixedTranslation` (from `@/app/components/plugins/marketplace/hooks`) +- `useGetLanguage` (from `@/context/i18n`) - returns `'en-US'` + +**Default behavior**: Most tests should use the global mock (no local override needed). + +**For custom translations**: Use the helper function from `@/test/i18n-mock`: ```typescript -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => { - const translations: Record = { - 'my.custom.key': 'Custom translation', - } - return translations[key] || key - }, - }), +import { createReactI18nextMock } from '@/test/i18n-mock' + +vi.mock('react-i18next', () => createReactI18nextMock({ + 'my.custom.key': 'Custom translation', + 'button.save': 'Save', })) ``` +**Avoid**: Manually defining `useTranslation` mocks that just return the key - the global mock already does this. + ### 2. Next.js Router ```typescript diff --git a/web/app/components/app/configuration/config/agent-setting-button.spec.tsx b/web/app/components/app/configuration/config/agent-setting-button.spec.tsx index 1874a3cccf..492b3b104c 100644 --- a/web/app/components/app/configuration/config/agent-setting-button.spec.tsx +++ b/web/app/components/app/configuration/config/agent-setting-button.spec.tsx @@ -5,15 +5,6 @@ import * as React from 'react' import { AgentStrategy } from '@/types/app' import AgentSettingButton from './agent-setting-button' -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string, options?: { ns?: string }) => { - const prefix = options?.ns ? `${options.ns}.` : '' - return `${prefix}${key}` - }, - }), -})) - let latestAgentSettingProps: any vi.mock('./agent/agent-setting', () => ({ default: (props: any) => { diff --git a/web/app/components/app/configuration/config/config-audio.spec.tsx b/web/app/components/app/configuration/config/config-audio.spec.tsx index 41219fd1fa..a3e5c7c149 100644 --- a/web/app/components/app/configuration/config/config-audio.spec.tsx +++ b/web/app/components/app/configuration/config/config-audio.spec.tsx @@ -15,15 +15,6 @@ vi.mock('use-context-selector', async (importOriginal) => { } }) -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string, options?: { ns?: string }) => { - const prefix = options?.ns ? `${options.ns}.` : '' - return `${prefix}${key}` - }, - }), -})) - const mockUseFeatures = vi.fn() const mockUseFeaturesStore = vi.fn() vi.mock('@/app/components/base/features/hooks', () => ({ diff --git a/web/app/components/base/inline-delete-confirm/index.spec.tsx b/web/app/components/base/inline-delete-confirm/index.spec.tsx index 0de8c0844f..b770fccc88 100644 --- a/web/app/components/base/inline-delete-confirm/index.spec.tsx +++ b/web/app/components/base/inline-delete-confirm/index.spec.tsx @@ -1,26 +1,14 @@ import { cleanup, fireEvent, render } from '@testing-library/react' import * as React from 'react' +import { createReactI18nextMock } from '@/test/i18n-mock' import InlineDeleteConfirm from './index' -// Mock react-i18next -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string, defaultValueOrOptions?: string | { ns?: string }) => { - const translations: Record = { - 'operation.deleteConfirmTitle': 'Delete?', - 'operation.yes': 'Yes', - 'operation.no': 'No', - 'operation.confirmAction': 'Please confirm your action.', - } - if (translations[key]) - return translations[key] - // Handle case where second arg is default value string - if (typeof defaultValueOrOptions === 'string') - return defaultValueOrOptions - const prefix = defaultValueOrOptions?.ns ? `${defaultValueOrOptions.ns}.` : '' - return `${prefix}${key}` - }, - }), +// Mock react-i18next with custom translations for test assertions +vi.mock('react-i18next', () => createReactI18nextMock({ + 'operation.deleteConfirmTitle': 'Delete?', + 'operation.yes': 'Yes', + 'operation.no': 'No', + 'operation.confirmAction': 'Please confirm your action.', })) afterEach(cleanup) diff --git a/web/app/components/base/input-with-copy/index.spec.tsx b/web/app/components/base/input-with-copy/index.spec.tsx index 5a4ca7c97e..438e72d142 100644 --- a/web/app/components/base/input-with-copy/index.spec.tsx +++ b/web/app/components/base/input-with-copy/index.spec.tsx @@ -1,5 +1,6 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' +import { createReactI18nextMock } from '@/test/i18n-mock' import InputWithCopy from './index' // Create a mock function that we can track using vi.hoisted @@ -10,22 +11,12 @@ vi.mock('copy-to-clipboard', () => ({ default: mockCopyToClipboard, })) -// Mock the i18n hook -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string, options?: { ns?: string }) => { - const translations: Record = { - 'operation.copy': 'Copy', - 'operation.copied': 'Copied', - 'overview.appInfo.embedded.copy': 'Copy', - 'overview.appInfo.embedded.copied': 'Copied', - } - if (translations[key]) - return translations[key] - const prefix = options?.ns ? `${options.ns}.` : '' - return `${prefix}${key}` - }, - }), +// Mock the i18n hook with custom translations for test assertions +vi.mock('react-i18next', () => createReactI18nextMock({ + 'operation.copy': 'Copy', + 'operation.copied': 'Copied', + 'overview.appInfo.embedded.copy': 'Copy', + 'overview.appInfo.embedded.copied': 'Copied', })) // Mock es-toolkit/compat debounce diff --git a/web/app/components/base/input/index.spec.tsx b/web/app/components/base/input/index.spec.tsx index a0de3c4ca4..65589ddcdf 100644 --- a/web/app/components/base/input/index.spec.tsx +++ b/web/app/components/base/input/index.spec.tsx @@ -1,21 +1,12 @@ import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' +import { createReactI18nextMock } from '@/test/i18n-mock' import Input, { inputVariants } from './index' -// Mock the i18n hook -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string, options?: { ns?: string }) => { - const translations: Record = { - 'operation.search': 'Search', - 'placeholder.input': 'Please input', - } - if (translations[key]) - return translations[key] - const prefix = options?.ns ? `${options.ns}.` : '' - return `${prefix}${key}` - }, - }), +// Mock the i18n hook with custom translations for test assertions +vi.mock('react-i18next', () => createReactI18nextMock({ + 'operation.search': 'Search', + 'placeholder.input': 'Please input', })) describe('Input component', () => { diff --git a/web/app/components/billing/pricing/footer.spec.tsx b/web/app/components/billing/pricing/footer.spec.tsx index 0bbc38224e..85bd72c247 100644 --- a/web/app/components/billing/pricing/footer.spec.tsx +++ b/web/app/components/billing/pricing/footer.spec.tsx @@ -3,8 +3,6 @@ import * as React from 'react' import { CategoryEnum } from '.' import Footer from './footer' -let mockTranslations: Record = {} - vi.mock('next/link', () => ({ default: ({ children, href, className, target }: { children: React.ReactNode, href: string, className?: string, target?: string }) => ( @@ -13,25 +11,9 @@ vi.mock('next/link', () => ({ ), })) -vi.mock('react-i18next', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - useTranslation: () => ({ - t: (key: string, options?: { ns?: string }) => { - if (mockTranslations[key]) - return mockTranslations[key] - const prefix = options?.ns ? `${options.ns}.` : '' - return `${prefix}${key}` - }, - }), - } -}) - describe('Footer', () => { beforeEach(() => { vi.clearAllMocks() - mockTranslations = {} }) // Rendering behavior diff --git a/web/app/components/datasets/create/index.spec.tsx b/web/app/components/datasets/create/index.spec.tsx index 1cf24e6f21..7e20e4bc1c 100644 --- a/web/app/components/datasets/create/index.spec.tsx +++ b/web/app/components/datasets/create/index.spec.tsx @@ -18,16 +18,6 @@ const IndexingTypeValues = { // Mock External Dependencies // ========================================== -// Mock react-i18next (handled by global mock in web/vitest.setup.ts but we override for custom messages) -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string, options?: { ns?: string }) => { - const prefix = options?.ns ? `${options.ns}.` : '' - return `${prefix}${key}` - }, - }), -})) - // Mock next/link vi.mock('next/link', () => { return function MockLink({ children, href }: { children: React.ReactNode, href: string }) { diff --git a/web/app/components/datasets/documents/create-from-pipeline/processing/index.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/processing/index.spec.tsx index 875adb2779..d9fea93446 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/processing/index.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/processing/index.spec.tsx @@ -9,16 +9,6 @@ import Processing from './index' // Mock External Dependencies // ========================================== -// Mock react-i18next (handled by global mock in web/vitest.setup.ts but we override for custom messages) -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string, options?: { ns?: string }) => { - const prefix = options?.ns ? `${options.ns}.` : '' - return `${prefix}${key}` - }, - }), -})) - // Mock useDocLink - returns a function that generates doc URLs // Strips leading slash from path to match actual implementation behavior vi.mock('@/context/i18n', () => ({ diff --git a/web/app/components/plugins/card/index.spec.tsx b/web/app/components/plugins/card/index.spec.tsx index d32aafff57..4a3e5a587b 100644 --- a/web/app/components/plugins/card/index.spec.tsx +++ b/web/app/components/plugins/card/index.spec.tsx @@ -21,33 +21,6 @@ import Card from './index' // Mock External Dependencies Only // ================================ -// Mock react-i18next (translation hook) -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - -// Mock useMixedTranslation hook -vi.mock('../marketplace/hooks', () => ({ - useMixedTranslation: (_locale?: string) => ({ - t: (key: string, options?: { ns?: string }) => { - const fullKey = options?.ns ? `${options.ns}.${key}` : key - const translations: Record = { - 'plugin.marketplace.partnerTip': 'Partner plugin', - 'plugin.marketplace.verifiedTip': 'Verified plugin', - 'plugin.installModal.installWarning': 'Install warning message', - } - return translations[fullKey] || key - }, - }), -})) - -// Mock useGetLanguage context -vi.mock('@/context/i18n', () => ({ - useGetLanguage: () => 'en-US', -})) - // Mock useTheme hook vi.mock('@/hooks/use-theme', () => ({ default: () => ({ theme: 'light' }), diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.spec.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.spec.tsx index 4e3a3307df..7f95eb0b35 100644 --- a/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.spec.tsx +++ b/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.spec.tsx @@ -64,26 +64,20 @@ vi.mock('@/context/app-context', () => ({ }), })) -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string, options?: { ns?: string } & Record) => { - // Build full key with namespace prefix if provided - const fullKey = options?.ns ? `${options.ns}.${key}` : key - // Handle interpolation params (excluding ns) - const { ns: _ns, ...params } = options || {} - if (Object.keys(params).length > 0) { - return `${fullKey}:${JSON.stringify(params)}` - } - return fullKey - }, - }), - Trans: ({ i18nKey, components }: { i18nKey: string, components?: Record }) => ( - - {i18nKey} - {components?.trustSource} - - ), -})) +vi.mock('react-i18next', async (importOriginal) => { + const actual = await importOriginal() + const { createReactI18nextMock } = await import('@/test/i18n-mock') + return { + ...actual, + ...createReactI18nextMock(), + Trans: ({ i18nKey, components }: { i18nKey: string, components?: Record }) => ( + + {i18nKey} + {components?.trustSource} + + ), + } +}) vi.mock('../../../card', () => ({ default: ({ payload, titleLeft }: { diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/steps/uploading.spec.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/steps/uploading.spec.tsx index c1d7e8cefe..35256b6633 100644 --- a/web/app/components/plugins/install-plugin/install-from-local-package/steps/uploading.spec.tsx +++ b/web/app/components/plugins/install-plugin/install-from-local-package/steps/uploading.spec.tsx @@ -48,21 +48,6 @@ vi.mock('@/service/plugins', () => ({ uploadFile: (...args: unknown[]) => mockUploadFile(...args), })) -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string, options?: { ns?: string } & Record) => { - // Build full key with namespace prefix if provided - const fullKey = options?.ns ? `${options.ns}.${key}` : key - // Handle interpolation params (excluding ns) - const { ns: _ns, ...params } = options || {} - if (Object.keys(params).length > 0) { - return `${fullKey}:${JSON.stringify(params)}` - } - return fullKey - }, - }), -})) - vi.mock('../../../card', () => ({ default: ({ payload, isLoading, loadingFileName }: { payload: { name: string } diff --git a/web/app/components/plugins/marketplace/index.spec.tsx b/web/app/components/plugins/marketplace/index.spec.tsx index 6047afe950..3073897ba1 100644 --- a/web/app/components/plugins/marketplace/index.spec.tsx +++ b/web/app/components/plugins/marketplace/index.spec.tsx @@ -27,17 +27,17 @@ import { // Mock External Dependencies Only // ================================ -// Mock react-i18next -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - // Mock i18next-config vi.mock('@/i18n-config/i18next-config', () => ({ default: { - getFixedT: (_locale: string) => (key: string) => key, + getFixedT: (_locale: string) => (key: string, options?: Record) => { + if (options && options.ns) { + return `${options.ns}.${key}` + } + else { + return key + } + }, }, })) @@ -617,8 +617,8 @@ describe('hooks', () => { it('should return translation key when no translation found', () => { const { result } = renderHook(() => useMixedTranslation()) - // The mock returns key as-is - expect(result.current.t('category.all', { ns: 'plugin' })).toBe('category.all') + // The global mock returns key with namespace prefix + expect(result.current.t('category.all', { ns: 'plugin' })).toBe('plugin.category.all') }) it('should use locale from outer when provided', () => { @@ -638,8 +638,8 @@ describe('hooks', () => { it('should use getFixedT when localeFromOuter is provided', () => { const { result } = renderHook(() => useMixedTranslation('fr-FR')) - // Should still return a function - expect(result.current.t('search', { ns: 'plugin' })).toBe('search') + // The global mock returns key with namespace prefix + expect(result.current.t('search', { ns: 'plugin' })).toBe('plugin.search') }) }) }) @@ -2756,15 +2756,15 @@ describe('PluginTypeSwitch Component', () => { , ) - // Note: The mock returns the key without namespace prefix - expect(screen.getByText('category.all')).toBeInTheDocument() - expect(screen.getByText('category.models')).toBeInTheDocument() - expect(screen.getByText('category.tools')).toBeInTheDocument() - expect(screen.getByText('category.datasources')).toBeInTheDocument() - expect(screen.getByText('category.triggers')).toBeInTheDocument() - expect(screen.getByText('category.agents')).toBeInTheDocument() - expect(screen.getByText('category.extensions')).toBeInTheDocument() - expect(screen.getByText('category.bundles')).toBeInTheDocument() + // Note: The global mock returns the key with namespace prefix (plugin.) + expect(screen.getByText('plugin.category.all')).toBeInTheDocument() + expect(screen.getByText('plugin.category.models')).toBeInTheDocument() + expect(screen.getByText('plugin.category.tools')).toBeInTheDocument() + expect(screen.getByText('plugin.category.datasources')).toBeInTheDocument() + expect(screen.getByText('plugin.category.triggers')).toBeInTheDocument() + expect(screen.getByText('plugin.category.agents')).toBeInTheDocument() + expect(screen.getByText('plugin.category.extensions')).toBeInTheDocument() + expect(screen.getByText('plugin.category.bundles')).toBeInTheDocument() }) it('should apply className prop', () => { @@ -2794,7 +2794,7 @@ describe('PluginTypeSwitch Component', () => { , ) - fireEvent.click(screen.getByText('category.tools')) + fireEvent.click(screen.getByText('plugin.category.tools')) expect(screen.getByTestId('active-type-display')).toHaveTextContent('tool') }) @@ -2816,7 +2816,7 @@ describe('PluginTypeSwitch Component', () => { ) fireEvent.click(screen.getByTestId('set-model')) - const modelOption = screen.getByText('category.models').closest('div') + const modelOption = screen.getByText('plugin.category.models').closest('div') expect(modelOption).toHaveClass('shadow-xs') }) }) diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.spec.tsx index 33cb93013d..c87fc1e4da 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.spec.tsx @@ -78,17 +78,6 @@ function createMockLogData(logs: TriggerLogEntity[] = []): { logs: TriggerLogEnt // Mock Setup // ============================================================================ -const mockTranslate = vi.fn((key: string, options?: { ns?: string }) => { - // Build full key with namespace prefix if provided - const fullKey = options?.ns ? `${options.ns}.${key}` : key - return fullKey -}) -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: mockTranslate, - }), -})) - // Mock plugin store const mockPluginDetail = createMockPluginDetail() const mockUsePluginStore = vi.fn(() => mockPluginDetail) diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.spec.tsx index 74599a13c5..f1cb7a65ae 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.spec.tsx @@ -68,17 +68,6 @@ function createMockSubscriptionBuilder(overrides: Partial { - // Build full key with namespace prefix if provided - const fullKey = options?.ns ? `${options.ns}.${key}` : key - return fullKey -}) -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: mockTranslate, - }), -})) - // Mock plugin store const mockPluginDetail = createMockPluginDetail() const mockUsePluginStore = vi.fn(() => mockPluginDetail) diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/index.spec.tsx index 4ce1841b05..b7988c916b 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/index.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/index.spec.tsx @@ -12,16 +12,6 @@ import { OAuthEditModal } from './oauth-edit-modal' // ==================== Mock Setup ==================== -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string, options?: { ns?: string }) => { - // Build full key with namespace prefix if provided - const fullKey = options?.ns ? `${options.ns}.${key}` : key - return fullKey - }, - }), -})) - const mockToastNotify = vi.fn() vi.mock('@/app/components/base/toast', () => ({ default: { notify: (params: unknown) => mockToastNotify(params) }, diff --git a/web/app/components/plugins/plugin-mutation-model/index.spec.tsx b/web/app/components/plugins/plugin-mutation-model/index.spec.tsx index f007c32ef1..95c9db3c97 100644 --- a/web/app/components/plugins/plugin-mutation-model/index.spec.tsx +++ b/web/app/components/plugins/plugin-mutation-model/index.spec.tsx @@ -9,28 +9,6 @@ import PluginMutationModal from './index' // Mock External Dependencies Only // ================================ -// Mock react-i18next (translation hook) -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - -// Mock useMixedTranslation hook -vi.mock('../marketplace/hooks', () => ({ - useMixedTranslation: (_locale?: string) => ({ - t: (key: string, options?: { ns?: string }) => { - const fullKey = options?.ns ? `${options.ns}.${key}` : key - return fullKey - }, - }), -})) - -// Mock useGetLanguage context -vi.mock('@/context/i18n', () => ({ - useGetLanguage: () => 'en-US', -})) - // Mock useTheme hook vi.mock('@/hooks/use-theme', () => ({ default: () => ({ theme: 'light' }), diff --git a/web/test/i18n-mock.ts b/web/test/i18n-mock.ts new file mode 100644 index 0000000000..20e7a22eef --- /dev/null +++ b/web/test/i18n-mock.ts @@ -0,0 +1,79 @@ +import * as React from 'react' +import { vi } from 'vitest' + +type TranslationMap = Record + +/** + * Create a t function with optional custom translations + * Checks translations[key] first, then translations[ns.key], then returns ns.key as fallback + */ +export function createTFunction(translations: TranslationMap, defaultNs?: string) { + return (key: string, options?: Record) => { + // Check custom translations first (without namespace) + if (translations[key] !== undefined) + return translations[key] + + const ns = (options?.ns as string | undefined) ?? defaultNs + const fullKey = ns ? `${ns}.${key}` : key + + // Check custom translations with namespace + if (translations[fullKey] !== undefined) + return translations[fullKey] + + // Serialize params (excluding ns) for test assertions + const params = { ...options } + delete params.ns + const suffix = Object.keys(params).length > 0 ? `:${JSON.stringify(params)}` : '' + return `${fullKey}${suffix}` + } +} + +/** + * Create useTranslation mock with optional custom translations + * + * @example + * vi.mock('react-i18next', () => createUseTranslationMock({ + * 'operation.confirm': 'Confirm', + * })) + */ +export function createUseTranslationMock(translations: TranslationMap = {}) { + return { + useTranslation: (defaultNs?: string) => ({ + t: createTFunction(translations, defaultNs), + i18n: { + language: 'en', + changeLanguage: vi.fn(), + }, + }), + } +} + +/** + * Create Trans component mock with optional custom translations + */ +export function createTransMock(translations: TranslationMap = {}) { + return { + Trans: ({ i18nKey, children }: { + i18nKey: string + children?: React.ReactNode + }) => { + const text = translations[i18nKey] ?? i18nKey + return React.createElement('span', { 'data-i18n-key': i18nKey }, children ?? text) + }, + } +} + +/** + * Create complete react-i18next mock (useTranslation + Trans) + * + * @example + * vi.mock('react-i18next', () => createReactI18nextMock({ + * 'modal.title': 'My Modal', + * })) + */ +export function createReactI18nextMock(translations: TranslationMap = {}) { + return { + ...createUseTranslationMock(translations), + ...createTransMock(translations), + } +} diff --git a/web/testing/testing.md b/web/testing/testing.md index 1d578ae634..47341e445e 100644 --- a/web/testing/testing.md +++ b/web/testing/testing.md @@ -329,21 +329,28 @@ describe('ComponentName', () => { 1. **i18n**: Uses global mock in `web/vitest.setup.ts` (auto-loaded by Vitest setup) - The global mock returns translation keys as-is. For custom translations, override: + The global mock provides: + + - `useTranslation` - returns translation keys with namespace prefix + - `Trans` component - renders i18nKey and components + - `useMixedTranslation` (from `@/app/components/plugins/marketplace/hooks`) + - `useGetLanguage` (from `@/context/i18n`) - returns `'en-US'` + + **Default behavior**: Most tests should use the global mock (no local override needed). + + **For custom translations**: Use the helper function from `@/test/i18n-mock`: ```typescript - vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => { - const translations: Record = { - 'my.custom.key': 'Custom translation', - } - return translations[key] || key - }, - }), + import { createReactI18nextMock } from '@/test/i18n-mock' + + vi.mock('react-i18next', () => createReactI18nextMock({ + 'my.custom.key': 'Custom translation', + 'button.save': 'Save', })) ``` + **Avoid**: Manually defining `useTranslation` mocks that just return the key - the global mock already does this. + 1. **Forms**: Test validation logic thoroughly 1. **Example - Correct mock with conditional rendering**: diff --git a/web/vitest.setup.ts b/web/vitest.setup.ts index 551a22475b..26dc25bbcf 100644 --- a/web/vitest.setup.ts +++ b/web/vitest.setup.ts @@ -88,26 +88,10 @@ vi.mock('next/image') // mock react-i18next vi.mock('react-i18next', async () => { const actual = await vi.importActual('react-i18next') + const { createReactI18nextMock } = await import('./test/i18n-mock') return { ...actual, - useTranslation: (defaultNs?: string) => ({ - t: (key: string, options?: Record) => { - if (options?.returnObjects) - return [`${key}-feature-1`, `${key}-feature-2`] - const ns = options?.ns ?? defaultNs - if (options || ns) { - const { ns: _ns, ...rest } = options ?? {} - const prefix = ns ? `${ns}.` : '' - const suffix = Object.keys(rest).length > 0 ? `:${JSON.stringify(rest)}` : '' - return `${prefix}${key}${suffix}` - } - return key - }, - i18n: { - language: 'en', - changeLanguage: vi.fn(), - }, - }), + ...createReactI18nextMock(), } }) From 3015e9be73e1781d860699cedf6fe2210fb3c8ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Wed, 31 Dec 2025 16:14:46 +0800 Subject: [PATCH 20/37] feat: add archive storage client and env config (#30422) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/.env.example | 9 + api/configs/extra/__init__.py | 2 + api/configs/extra/archive_config.py | 43 +++ api/libs/archive_storage.py | 347 ++++++++++++++++++ .../unit_tests/libs/test_archive_storage.py | 272 ++++++++++++++ docker/.env.example | 9 + docker/docker-compose.yaml | 7 + 7 files changed, 689 insertions(+) create mode 100644 api/configs/extra/archive_config.py create mode 100644 api/libs/archive_storage.py create mode 100644 api/tests/unit_tests/libs/test_archive_storage.py diff --git a/api/.env.example b/api/.env.example index 99cd2ba558..5f8d369ec4 100644 --- a/api/.env.example +++ b/api/.env.example @@ -101,6 +101,15 @@ S3_ACCESS_KEY=your-access-key S3_SECRET_KEY=your-secret-key S3_REGION=your-region +# Workflow run and Conversation archive storage (S3-compatible) +ARCHIVE_STORAGE_ENABLED=false +ARCHIVE_STORAGE_ENDPOINT= +ARCHIVE_STORAGE_ARCHIVE_BUCKET= +ARCHIVE_STORAGE_EXPORT_BUCKET= +ARCHIVE_STORAGE_ACCESS_KEY= +ARCHIVE_STORAGE_SECRET_KEY= +ARCHIVE_STORAGE_REGION=auto + # Azure Blob Storage configuration AZURE_BLOB_ACCOUNT_NAME=your-account-name AZURE_BLOB_ACCOUNT_KEY=your-account-key diff --git a/api/configs/extra/__init__.py b/api/configs/extra/__init__.py index 4543b5389d..de97adfc0e 100644 --- a/api/configs/extra/__init__.py +++ b/api/configs/extra/__init__.py @@ -1,9 +1,11 @@ +from configs.extra.archive_config import ArchiveStorageConfig from configs.extra.notion_config import NotionConfig from configs.extra.sentry_config import SentryConfig class ExtraServiceConfig( # place the configs in alphabet order + ArchiveStorageConfig, NotionConfig, SentryConfig, ): diff --git a/api/configs/extra/archive_config.py b/api/configs/extra/archive_config.py new file mode 100644 index 0000000000..a85628fa61 --- /dev/null +++ b/api/configs/extra/archive_config.py @@ -0,0 +1,43 @@ +from pydantic import Field +from pydantic_settings import BaseSettings + + +class ArchiveStorageConfig(BaseSettings): + """ + Configuration settings for workflow run logs archiving storage. + """ + + ARCHIVE_STORAGE_ENABLED: bool = Field( + description="Enable workflow run logs archiving to S3-compatible storage", + default=False, + ) + + ARCHIVE_STORAGE_ENDPOINT: str | None = Field( + description="URL of the S3-compatible storage endpoint (e.g., 'https://storage.example.com')", + default=None, + ) + + ARCHIVE_STORAGE_ARCHIVE_BUCKET: str | None = Field( + description="Name of the bucket to store archived workflow logs", + default=None, + ) + + ARCHIVE_STORAGE_EXPORT_BUCKET: str | None = Field( + description="Name of the bucket to store exported workflow runs", + default=None, + ) + + ARCHIVE_STORAGE_ACCESS_KEY: str | None = Field( + description="Access key ID for authenticating with storage", + default=None, + ) + + ARCHIVE_STORAGE_SECRET_KEY: str | None = Field( + description="Secret access key for authenticating with storage", + default=None, + ) + + ARCHIVE_STORAGE_REGION: str = Field( + description="Region for storage (use 'auto' if the provider supports it)", + default="auto", + ) diff --git a/api/libs/archive_storage.py b/api/libs/archive_storage.py new file mode 100644 index 0000000000..f84d226447 --- /dev/null +++ b/api/libs/archive_storage.py @@ -0,0 +1,347 @@ +""" +Archive Storage Client for S3-compatible storage. + +This module provides a dedicated storage client for archiving or exporting logs +to S3-compatible object storage. +""" + +import base64 +import datetime +import gzip +import hashlib +import logging +from collections.abc import Generator +from typing import Any, cast + +import boto3 +import orjson +from botocore.client import Config +from botocore.exceptions import ClientError + +from configs import dify_config + +logger = logging.getLogger(__name__) + + +class ArchiveStorageError(Exception): + """Base exception for archive storage operations.""" + + pass + + +class ArchiveStorageNotConfiguredError(ArchiveStorageError): + """Raised when archive storage is not properly configured.""" + + pass + + +class ArchiveStorage: + """ + S3-compatible storage client for archiving or exporting. + + This client provides methods for storing and retrieving archived data in JSONL+gzip format. + """ + + def __init__(self, bucket: str): + if not dify_config.ARCHIVE_STORAGE_ENABLED: + raise ArchiveStorageNotConfiguredError("Archive storage is not enabled") + + if not bucket: + raise ArchiveStorageNotConfiguredError("Archive storage bucket is not configured") + if not all( + [ + dify_config.ARCHIVE_STORAGE_ENDPOINT, + bucket, + dify_config.ARCHIVE_STORAGE_ACCESS_KEY, + dify_config.ARCHIVE_STORAGE_SECRET_KEY, + ] + ): + raise ArchiveStorageNotConfiguredError( + "Archive storage configuration is incomplete. " + "Required: ARCHIVE_STORAGE_ENDPOINT, ARCHIVE_STORAGE_ACCESS_KEY, " + "ARCHIVE_STORAGE_SECRET_KEY, and a bucket name" + ) + + self.bucket = bucket + self.client = boto3.client( + "s3", + endpoint_url=dify_config.ARCHIVE_STORAGE_ENDPOINT, + aws_access_key_id=dify_config.ARCHIVE_STORAGE_ACCESS_KEY, + aws_secret_access_key=dify_config.ARCHIVE_STORAGE_SECRET_KEY, + region_name=dify_config.ARCHIVE_STORAGE_REGION, + config=Config(s3={"addressing_style": "path"}), + ) + + # Verify bucket accessibility + try: + self.client.head_bucket(Bucket=self.bucket) + except ClientError as e: + error_code = e.response.get("Error", {}).get("Code") + if error_code == "404": + raise ArchiveStorageNotConfiguredError(f"Archive bucket '{self.bucket}' does not exist") + elif error_code == "403": + raise ArchiveStorageNotConfiguredError(f"Access denied to archive bucket '{self.bucket}'") + else: + raise ArchiveStorageError(f"Failed to access archive bucket: {e}") + + def put_object(self, key: str, data: bytes) -> str: + """ + Upload an object to the archive storage. + + Args: + key: Object key (path) within the bucket + data: Binary data to upload + + Returns: + MD5 checksum of the uploaded data + + Raises: + ArchiveStorageError: If upload fails + """ + checksum = hashlib.md5(data).hexdigest() + try: + self.client.put_object( + Bucket=self.bucket, + Key=key, + Body=data, + ContentMD5=self._content_md5(data), + ) + logger.debug("Uploaded object: %s (size=%d, checksum=%s)", key, len(data), checksum) + return checksum + except ClientError as e: + raise ArchiveStorageError(f"Failed to upload object '{key}': {e}") + + def get_object(self, key: str) -> bytes: + """ + Download an object from the archive storage. + + Args: + key: Object key (path) within the bucket + + Returns: + Binary data of the object + + Raises: + ArchiveStorageError: If download fails + FileNotFoundError: If object does not exist + """ + try: + response = self.client.get_object(Bucket=self.bucket, Key=key) + return response["Body"].read() + except ClientError as e: + error_code = e.response.get("Error", {}).get("Code") + if error_code == "NoSuchKey": + raise FileNotFoundError(f"Archive object not found: {key}") + raise ArchiveStorageError(f"Failed to download object '{key}': {e}") + + def get_object_stream(self, key: str) -> Generator[bytes, None, None]: + """ + Stream an object from the archive storage. + + Args: + key: Object key (path) within the bucket + + Yields: + Chunks of binary data + + Raises: + ArchiveStorageError: If download fails + FileNotFoundError: If object does not exist + """ + try: + response = self.client.get_object(Bucket=self.bucket, Key=key) + yield from response["Body"].iter_chunks() + except ClientError as e: + error_code = e.response.get("Error", {}).get("Code") + if error_code == "NoSuchKey": + raise FileNotFoundError(f"Archive object not found: {key}") + raise ArchiveStorageError(f"Failed to stream object '{key}': {e}") + + def object_exists(self, key: str) -> bool: + """ + Check if an object exists in the archive storage. + + Args: + key: Object key (path) within the bucket + + Returns: + True if object exists, False otherwise + """ + try: + self.client.head_object(Bucket=self.bucket, Key=key) + return True + except ClientError: + return False + + def delete_object(self, key: str) -> None: + """ + Delete an object from the archive storage. + + Args: + key: Object key (path) within the bucket + + Raises: + ArchiveStorageError: If deletion fails + """ + try: + self.client.delete_object(Bucket=self.bucket, Key=key) + logger.debug("Deleted object: %s", key) + except ClientError as e: + raise ArchiveStorageError(f"Failed to delete object '{key}': {e}") + + def generate_presigned_url(self, key: str, expires_in: int = 3600) -> str: + """ + Generate a pre-signed URL for downloading an object. + + Args: + key: Object key (path) within the bucket + expires_in: URL validity duration in seconds (default: 1 hour) + + Returns: + Pre-signed URL string. + + Raises: + ArchiveStorageError: If generation fails + """ + try: + return self.client.generate_presigned_url( + ClientMethod="get_object", + Params={"Bucket": self.bucket, "Key": key}, + ExpiresIn=expires_in, + ) + except ClientError as e: + raise ArchiveStorageError(f"Failed to generate pre-signed URL for '{key}': {e}") + + def list_objects(self, prefix: str) -> list[str]: + """ + List objects under a given prefix. + + Args: + prefix: Object key prefix to filter by + + Returns: + List of object keys matching the prefix + """ + keys = [] + paginator = self.client.get_paginator("list_objects_v2") + + try: + for page in paginator.paginate(Bucket=self.bucket, Prefix=prefix): + for obj in page.get("Contents", []): + keys.append(obj["Key"]) + except ClientError as e: + raise ArchiveStorageError(f"Failed to list objects with prefix '{prefix}': {e}") + + return keys + + @staticmethod + def _content_md5(data: bytes) -> str: + """Calculate base64-encoded MD5 for Content-MD5 header.""" + return base64.b64encode(hashlib.md5(data).digest()).decode() + + @staticmethod + def serialize_to_jsonl_gz(records: list[dict[str, Any]]) -> bytes: + """ + Serialize records to gzipped JSONL format. + + Args: + records: List of dictionaries to serialize + + Returns: + Gzipped JSONL bytes + """ + lines = [] + for record in records: + # Convert datetime objects to ISO format strings + serialized = ArchiveStorage._serialize_record(record) + lines.append(orjson.dumps(serialized)) + + jsonl_content = b"\n".join(lines) + if jsonl_content: + jsonl_content += b"\n" + + return gzip.compress(jsonl_content) + + @staticmethod + def deserialize_from_jsonl_gz(data: bytes) -> list[dict[str, Any]]: + """ + Deserialize gzipped JSONL data to records. + + Args: + data: Gzipped JSONL bytes + + Returns: + List of dictionaries + """ + jsonl_content = gzip.decompress(data) + records = [] + + for line in jsonl_content.splitlines(): + if line: + records.append(orjson.loads(line)) + + return records + + @staticmethod + def _serialize_record(record: dict[str, Any]) -> dict[str, Any]: + """Serialize a single record, converting special types.""" + + def _serialize(item: Any) -> Any: + if isinstance(item, datetime.datetime): + return item.isoformat() + if isinstance(item, dict): + return {key: _serialize(value) for key, value in item.items()} + if isinstance(item, list): + return [_serialize(value) for value in item] + return item + + return cast(dict[str, Any], _serialize(record)) + + @staticmethod + def compute_checksum(data: bytes) -> str: + """Compute MD5 checksum of data.""" + return hashlib.md5(data).hexdigest() + + +# Singleton instance (lazy initialization) +_archive_storage: ArchiveStorage | None = None +_export_storage: ArchiveStorage | None = None + + +def get_archive_storage() -> ArchiveStorage: + """ + Get the archive storage singleton instance. + + Returns: + ArchiveStorage instance + + Raises: + ArchiveStorageNotConfiguredError: If archive storage is not configured + """ + global _archive_storage + if _archive_storage is None: + archive_bucket = dify_config.ARCHIVE_STORAGE_ARCHIVE_BUCKET + if not archive_bucket: + raise ArchiveStorageNotConfiguredError( + "Archive storage bucket is not configured. Required: ARCHIVE_STORAGE_ARCHIVE_BUCKET" + ) + _archive_storage = ArchiveStorage(bucket=archive_bucket) + return _archive_storage + + +def get_export_storage() -> ArchiveStorage: + """ + Get the export storage singleton instance. + + Returns: + ArchiveStorage instance + """ + global _export_storage + if _export_storage is None: + export_bucket = dify_config.ARCHIVE_STORAGE_EXPORT_BUCKET + if not export_bucket: + raise ArchiveStorageNotConfiguredError( + "Archive export bucket is not configured. Required: ARCHIVE_STORAGE_EXPORT_BUCKET" + ) + _export_storage = ArchiveStorage(bucket=export_bucket) + return _export_storage diff --git a/api/tests/unit_tests/libs/test_archive_storage.py b/api/tests/unit_tests/libs/test_archive_storage.py new file mode 100644 index 0000000000..697760e33a --- /dev/null +++ b/api/tests/unit_tests/libs/test_archive_storage.py @@ -0,0 +1,272 @@ +import base64 +import hashlib +from datetime import datetime +from unittest.mock import ANY, MagicMock + +import pytest +from botocore.exceptions import ClientError + +from libs import archive_storage as storage_module +from libs.archive_storage import ( + ArchiveStorage, + ArchiveStorageError, + ArchiveStorageNotConfiguredError, +) + +BUCKET_NAME = "archive-bucket" + + +def _configure_storage(monkeypatch, **overrides): + defaults = { + "ARCHIVE_STORAGE_ENABLED": True, + "ARCHIVE_STORAGE_ENDPOINT": "https://storage.example.com", + "ARCHIVE_STORAGE_ARCHIVE_BUCKET": BUCKET_NAME, + "ARCHIVE_STORAGE_ACCESS_KEY": "access", + "ARCHIVE_STORAGE_SECRET_KEY": "secret", + "ARCHIVE_STORAGE_REGION": "auto", + } + defaults.update(overrides) + for key, value in defaults.items(): + monkeypatch.setattr(storage_module.dify_config, key, value, raising=False) + + +def _client_error(code: str) -> ClientError: + return ClientError({"Error": {"Code": code}}, "Operation") + + +def _mock_client(monkeypatch): + client = MagicMock() + client.head_bucket.return_value = None + boto_client = MagicMock(return_value=client) + monkeypatch.setattr(storage_module.boto3, "client", boto_client) + return client, boto_client + + +def test_init_disabled(monkeypatch): + _configure_storage(monkeypatch, ARCHIVE_STORAGE_ENABLED=False) + with pytest.raises(ArchiveStorageNotConfiguredError, match="not enabled"): + ArchiveStorage(bucket=BUCKET_NAME) + + +def test_init_missing_config(monkeypatch): + _configure_storage(monkeypatch, ARCHIVE_STORAGE_ENDPOINT=None) + with pytest.raises(ArchiveStorageNotConfiguredError, match="incomplete"): + ArchiveStorage(bucket=BUCKET_NAME) + + +def test_init_bucket_not_found(monkeypatch): + _configure_storage(monkeypatch) + client, _ = _mock_client(monkeypatch) + client.head_bucket.side_effect = _client_error("404") + + with pytest.raises(ArchiveStorageNotConfiguredError, match="does not exist"): + ArchiveStorage(bucket=BUCKET_NAME) + + +def test_init_bucket_access_denied(monkeypatch): + _configure_storage(monkeypatch) + client, _ = _mock_client(monkeypatch) + client.head_bucket.side_effect = _client_error("403") + + with pytest.raises(ArchiveStorageNotConfiguredError, match="Access denied"): + ArchiveStorage(bucket=BUCKET_NAME) + + +def test_init_bucket_other_error(monkeypatch): + _configure_storage(monkeypatch) + client, _ = _mock_client(monkeypatch) + client.head_bucket.side_effect = _client_error("500") + + with pytest.raises(ArchiveStorageError, match="Failed to access archive bucket"): + ArchiveStorage(bucket=BUCKET_NAME) + + +def test_init_sets_client(monkeypatch): + _configure_storage(monkeypatch) + client, boto_client = _mock_client(monkeypatch) + + storage = ArchiveStorage(bucket=BUCKET_NAME) + + boto_client.assert_called_once_with( + "s3", + endpoint_url="https://storage.example.com", + aws_access_key_id="access", + aws_secret_access_key="secret", + region_name="auto", + config=ANY, + ) + assert storage.client is client + assert storage.bucket == BUCKET_NAME + + +def test_put_object_returns_checksum(monkeypatch): + _configure_storage(monkeypatch) + client, _ = _mock_client(monkeypatch) + storage = ArchiveStorage(bucket=BUCKET_NAME) + + data = b"hello" + checksum = storage.put_object("key", data) + + expected_md5 = hashlib.md5(data).hexdigest() + expected_content_md5 = base64.b64encode(hashlib.md5(data).digest()).decode() + client.put_object.assert_called_once_with( + Bucket="archive-bucket", + Key="key", + Body=data, + ContentMD5=expected_content_md5, + ) + assert checksum == expected_md5 + + +def test_put_object_raises_on_error(monkeypatch): + _configure_storage(monkeypatch) + client, _ = _mock_client(monkeypatch) + storage = ArchiveStorage(bucket=BUCKET_NAME) + client.put_object.side_effect = _client_error("500") + + with pytest.raises(ArchiveStorageError, match="Failed to upload object"): + storage.put_object("key", b"data") + + +def test_get_object_returns_bytes(monkeypatch): + _configure_storage(monkeypatch) + client, _ = _mock_client(monkeypatch) + body = MagicMock() + body.read.return_value = b"payload" + client.get_object.return_value = {"Body": body} + storage = ArchiveStorage(bucket=BUCKET_NAME) + + assert storage.get_object("key") == b"payload" + + +def test_get_object_missing(monkeypatch): + _configure_storage(monkeypatch) + client, _ = _mock_client(monkeypatch) + client.get_object.side_effect = _client_error("NoSuchKey") + storage = ArchiveStorage(bucket=BUCKET_NAME) + + with pytest.raises(FileNotFoundError, match="Archive object not found"): + storage.get_object("missing") + + +def test_get_object_stream(monkeypatch): + _configure_storage(monkeypatch) + client, _ = _mock_client(monkeypatch) + body = MagicMock() + body.iter_chunks.return_value = [b"a", b"b"] + client.get_object.return_value = {"Body": body} + storage = ArchiveStorage(bucket=BUCKET_NAME) + + assert list(storage.get_object_stream("key")) == [b"a", b"b"] + + +def test_get_object_stream_missing(monkeypatch): + _configure_storage(monkeypatch) + client, _ = _mock_client(monkeypatch) + client.get_object.side_effect = _client_error("NoSuchKey") + storage = ArchiveStorage(bucket=BUCKET_NAME) + + with pytest.raises(FileNotFoundError, match="Archive object not found"): + list(storage.get_object_stream("missing")) + + +def test_object_exists(monkeypatch): + _configure_storage(monkeypatch) + client, _ = _mock_client(monkeypatch) + storage = ArchiveStorage(bucket=BUCKET_NAME) + + assert storage.object_exists("key") is True + client.head_object.side_effect = _client_error("404") + assert storage.object_exists("missing") is False + + +def test_delete_object_error(monkeypatch): + _configure_storage(monkeypatch) + client, _ = _mock_client(monkeypatch) + client.delete_object.side_effect = _client_error("500") + storage = ArchiveStorage(bucket=BUCKET_NAME) + + with pytest.raises(ArchiveStorageError, match="Failed to delete object"): + storage.delete_object("key") + + +def test_list_objects(monkeypatch): + _configure_storage(monkeypatch) + client, _ = _mock_client(monkeypatch) + paginator = MagicMock() + paginator.paginate.return_value = [ + {"Contents": [{"Key": "a"}, {"Key": "b"}]}, + {"Contents": [{"Key": "c"}]}, + ] + client.get_paginator.return_value = paginator + storage = ArchiveStorage(bucket=BUCKET_NAME) + + assert storage.list_objects("prefix") == ["a", "b", "c"] + paginator.paginate.assert_called_once_with(Bucket="archive-bucket", Prefix="prefix") + + +def test_list_objects_error(monkeypatch): + _configure_storage(monkeypatch) + client, _ = _mock_client(monkeypatch) + paginator = MagicMock() + paginator.paginate.side_effect = _client_error("500") + client.get_paginator.return_value = paginator + storage = ArchiveStorage(bucket=BUCKET_NAME) + + with pytest.raises(ArchiveStorageError, match="Failed to list objects"): + storage.list_objects("prefix") + + +def test_generate_presigned_url(monkeypatch): + _configure_storage(monkeypatch) + client, _ = _mock_client(monkeypatch) + client.generate_presigned_url.return_value = "http://signed-url" + storage = ArchiveStorage(bucket=BUCKET_NAME) + + url = storage.generate_presigned_url("key", expires_in=123) + + client.generate_presigned_url.assert_called_once_with( + ClientMethod="get_object", + Params={"Bucket": "archive-bucket", "Key": "key"}, + ExpiresIn=123, + ) + assert url == "http://signed-url" + + +def test_generate_presigned_url_error(monkeypatch): + _configure_storage(monkeypatch) + client, _ = _mock_client(monkeypatch) + client.generate_presigned_url.side_effect = _client_error("500") + storage = ArchiveStorage(bucket=BUCKET_NAME) + + with pytest.raises(ArchiveStorageError, match="Failed to generate pre-signed URL"): + storage.generate_presigned_url("key") + + +def test_serialization_roundtrip(): + records = [ + { + "id": "1", + "created_at": datetime(2024, 1, 1, 12, 0, 0), + "payload": {"nested": "value"}, + "items": [{"name": "a"}], + }, + {"id": "2", "value": 123}, + ] + + data = ArchiveStorage.serialize_to_jsonl_gz(records) + decoded = ArchiveStorage.deserialize_from_jsonl_gz(data) + + assert decoded[0]["id"] == "1" + assert decoded[0]["payload"]["nested"] == "value" + assert decoded[0]["items"][0]["name"] == "a" + assert "2024-01-01T12:00:00" in decoded[0]["created_at"] + assert decoded[1]["value"] == 123 + + +def test_content_md5_matches_checksum(): + data = b"checksum" + expected = base64.b64encode(hashlib.md5(data).digest()).decode() + + assert ArchiveStorage._content_md5(data) == expected + assert ArchiveStorage.compute_checksum(data) == hashlib.md5(data).hexdigest() diff --git a/docker/.env.example b/docker/.env.example index 0e09d6869d..5c1089408c 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -447,6 +447,15 @@ S3_SECRET_KEY= # If set to false, the access key and secret key must be provided. S3_USE_AWS_MANAGED_IAM=false +# Workflow run and Conversation archive storage (S3-compatible) +ARCHIVE_STORAGE_ENABLED=false +ARCHIVE_STORAGE_ENDPOINT= +ARCHIVE_STORAGE_ARCHIVE_BUCKET= +ARCHIVE_STORAGE_EXPORT_BUCKET= +ARCHIVE_STORAGE_ACCESS_KEY= +ARCHIVE_STORAGE_SECRET_KEY= +ARCHIVE_STORAGE_REGION=auto + # Azure Blob Configuration # AZURE_BLOB_ACCOUNT_NAME=difyai diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 1c8d8d03e3..9910c95a84 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -122,6 +122,13 @@ x-shared-env: &shared-api-worker-env S3_ACCESS_KEY: ${S3_ACCESS_KEY:-} S3_SECRET_KEY: ${S3_SECRET_KEY:-} S3_USE_AWS_MANAGED_IAM: ${S3_USE_AWS_MANAGED_IAM:-false} + ARCHIVE_STORAGE_ENABLED: ${ARCHIVE_STORAGE_ENABLED:-false} + ARCHIVE_STORAGE_ENDPOINT: ${ARCHIVE_STORAGE_ENDPOINT:-} + ARCHIVE_STORAGE_ARCHIVE_BUCKET: ${ARCHIVE_STORAGE_ARCHIVE_BUCKET:-} + ARCHIVE_STORAGE_EXPORT_BUCKET: ${ARCHIVE_STORAGE_EXPORT_BUCKET:-} + ARCHIVE_STORAGE_ACCESS_KEY: ${ARCHIVE_STORAGE_ACCESS_KEY:-} + ARCHIVE_STORAGE_SECRET_KEY: ${ARCHIVE_STORAGE_SECRET_KEY:-} + ARCHIVE_STORAGE_REGION: ${ARCHIVE_STORAGE_REGION:-auto} AZURE_BLOB_ACCOUNT_NAME: ${AZURE_BLOB_ACCOUNT_NAME:-difyai} AZURE_BLOB_ACCOUNT_KEY: ${AZURE_BLOB_ACCOUNT_KEY:-difyai} AZURE_BLOB_CONTAINER_NAME: ${AZURE_BLOB_CONTAINER_NAME:-difyai-container} From 184077c37c7ddbaff4c16f0d9bb417ea1493d5c1 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Wed, 31 Dec 2025 16:41:43 +0800 Subject: [PATCH 21/37] build: bring back babel-loader, add build check (#30427) --- .github/workflows/style.yml | 5 +++++ web/knip.config.ts | 4 ++++ web/package.json | 1 + web/pnpm-lock.yaml | 16 ++++++++++++++++ 4 files changed, 26 insertions(+) diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index 39b559f4ca..462ece303e 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -115,6 +115,11 @@ jobs: working-directory: ./web run: pnpm run knip + - name: Web build check + if: steps.changed-files.outputs.any_changed == 'true' + working-directory: ./web + run: pnpm run build + superlinter: name: SuperLinter runs-on: ubuntu-latest diff --git a/web/knip.config.ts b/web/knip.config.ts index 414b00fb7f..6ffda0316a 100644 --- a/web/knip.config.ts +++ b/web/knip.config.ts @@ -15,6 +15,10 @@ const config: KnipConfig = { ignoreBinaries: [ 'only-allow', ], + ignoreDependencies: [ + // required by next-pwa + 'babel-loader', + ], rules: { files: 'warn', dependencies: 'warn', diff --git a/web/package.json b/web/package.json index 0c6821ce86..7ee2325dbc 100644 --- a/web/package.json +++ b/web/package.json @@ -190,6 +190,7 @@ "@vitejs/plugin-react": "^5.1.2", "@vitest/coverage-v8": "4.0.16", "autoprefixer": "^10.4.21", + "babel-loader": "^10.0.0", "bing-translate-api": "^4.1.0", "code-inspector-plugin": "1.2.9", "cross-env": "^10.1.0", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 373e2e4020..cdd194da37 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -481,6 +481,9 @@ importers: autoprefixer: specifier: ^10.4.21 version: 10.4.22(postcss@8.5.6) + babel-loader: + specifier: ^10.0.0 + version: 10.0.0(@babel/core@7.28.5)(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)) bing-translate-api: specifier: ^4.1.0 version: 4.2.0 @@ -4265,6 +4268,13 @@ packages: peerDependencies: postcss: ^8.1.0 + babel-loader@10.0.0: + resolution: {integrity: sha512-z8jt+EdS61AMw22nSfoNJAZ0vrtmhPRVi6ghL3rCeRZI8cdNYFiV5xeV3HbE7rlZZNmGH8BVccwWt8/ED0QOHA==} + engines: {node: ^18.20.0 || ^20.10.0 || >=22.0.0} + peerDependencies: + '@babel/core': ^7.12.0 + webpack: '>=5.61.0' + babel-loader@8.4.1: resolution: {integrity: sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA==} engines: {node: '>= 8.9'} @@ -13080,6 +13090,12 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 + babel-loader@10.0.0(@babel/core@7.28.5)(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)): + dependencies: + '@babel/core': 7.28.5 + find-up: 5.0.0 + webpack: 5.103.0(esbuild@0.25.0)(uglify-js@3.19.3) + babel-loader@8.4.1(@babel/core@7.28.5)(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)): dependencies: '@babel/core': 7.28.5 From ee1d0df927b8b64baf835b9df1e7aaba62c2cb2c Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Wed, 31 Dec 2025 17:55:25 +0800 Subject: [PATCH 22/37] chore: add jotai store (#30432) Signed-off-by: yyh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: yyh --- web/app/layout.tsx | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/web/app/layout.tsx b/web/app/layout.tsx index fa1f7d48b5..8fc5f8abcc 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -1,4 +1,5 @@ import type { Viewport } from 'next' +import { Provider as JotaiProvider } from 'jotai' import { ThemeProvider } from 'next-themes' import { Instrument_Serif } from 'next/font/google' import { NuqsAdapter } from 'nuqs/adapters/next/app' @@ -91,27 +92,29 @@ const LocaleLayout = async ({ {...datasetMap} > - - - - - - - - {children} - - - - - - - + + + + + + + + + {children} + + + + + + + + From e3ef33366db303add95c46a863635485864fa644 Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Thu, 1 Jan 2026 00:36:18 +0800 Subject: [PATCH 23/37] fix(web): stop thinking timer when user clicks stop button (#30442) --- .../base/markdown-blocks/think-block.spec.tsx | 248 ++++++++++++++++++ .../base/markdown-blocks/think-block.tsx | 6 +- 2 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 web/app/components/base/markdown-blocks/think-block.spec.tsx diff --git a/web/app/components/base/markdown-blocks/think-block.spec.tsx b/web/app/components/base/markdown-blocks/think-block.spec.tsx new file mode 100644 index 0000000000..a155b240b9 --- /dev/null +++ b/web/app/components/base/markdown-blocks/think-block.spec.tsx @@ -0,0 +1,248 @@ +import { act, render, screen } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { ChatContextProvider } from '@/app/components/base/chat/chat/context' +import ThinkBlock from './think-block' + +// Mock react-i18next +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => { + const translations: Record = { + 'chat.thinking': 'Thinking...', + 'chat.thought': 'Thought', + } + return translations[key] || key + }, + }), +})) + +// Helper to wrap component with ChatContextProvider +const renderWithContext = ( + children: React.ReactNode, + isResponding: boolean = true, +) => { + return render( + + {children} + , + ) +} + +describe('ThinkBlock', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + describe('Rendering', () => { + it('should render regular details element when data-think is false', () => { + render( + +

Regular content

+
, + ) + + expect(screen.getByText('Regular content')).toBeInTheDocument() + }) + + it('should render think block with thinking state when data-think is true', () => { + renderWithContext( + +

Thinking content

+
, + true, + ) + + expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument() + expect(screen.getByText('Thinking content')).toBeInTheDocument() + }) + + it('should render thought state when content has ENDTHINKFLAG', () => { + renderWithContext( + +

Completed thinking[ENDTHINKFLAG]

+
, + true, + ) + + expect(screen.getByText(/Thought/)).toBeInTheDocument() + }) + }) + + describe('Timer behavior', () => { + it('should update elapsed time while thinking', () => { + renderWithContext( + +

Thinking...

+
, + true, + ) + + // Initial state should show 0.0s + expect(screen.getByText(/\(0\.0s\)/)).toBeInTheDocument() + + // Advance timer by 500ms and run pending timers + act(() => { + vi.advanceTimersByTime(500) + }) + + // Should show approximately 0.5s + expect(screen.getByText(/\(0\.5s\)/)).toBeInTheDocument() + }) + + it('should stop timer when isResponding becomes false', () => { + const { rerender } = render( + + +

Thinking content

+
+
, + ) + + // Verify initial thinking state + expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument() + + // Advance timer + act(() => { + vi.advanceTimersByTime(1000) + }) + + // Simulate user clicking stop (isResponding becomes false) + rerender( + + +

Thinking content

+
+
, + ) + + // Should now show "Thought" instead of "Thinking..." + expect(screen.getByText(/Thought/)).toBeInTheDocument() + }) + + it('should NOT stop timer when isResponding is undefined (outside ChatContextProvider)', () => { + // Render without ChatContextProvider + render( + +

Content without ENDTHINKFLAG

+
, + ) + + // Initial state should show "Thinking..." + expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument() + + // Advance timer + act(() => { + vi.advanceTimersByTime(2000) + }) + + // Timer should still be running (showing "Thinking..." not "Thought") + expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument() + expect(screen.getByText(/\(2\.0s\)/)).toBeInTheDocument() + }) + }) + + describe('ENDTHINKFLAG handling', () => { + it('should remove ENDTHINKFLAG from displayed content', () => { + renderWithContext( + +

Content[ENDTHINKFLAG]

+
, + true, + ) + + expect(screen.getByText('Content')).toBeInTheDocument() + expect(screen.queryByText('[ENDTHINKFLAG]')).not.toBeInTheDocument() + }) + + it('should detect ENDTHINKFLAG in nested children', () => { + renderWithContext( + +
+ Nested content[ENDTHINKFLAG] +
+
, + true, + ) + + // Should show "Thought" since ENDTHINKFLAG is present + expect(screen.getByText(/Thought/)).toBeInTheDocument() + }) + + it('should detect ENDTHINKFLAG in array children', () => { + renderWithContext( + + {['Part 1', 'Part 2[ENDTHINKFLAG]']} + , + true, + ) + + expect(screen.getByText(/Thought/)).toBeInTheDocument() + }) + }) + + describe('Edge cases', () => { + it('should handle empty children', () => { + renderWithContext( + , + true, + ) + + expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument() + }) + + it('should handle null children gracefully', () => { + renderWithContext( + + {null} + , + true, + ) + + expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument() + }) + }) +}) diff --git a/web/app/components/base/markdown-blocks/think-block.tsx b/web/app/components/base/markdown-blocks/think-block.tsx index 697fd096cc..f920218152 100644 --- a/web/app/components/base/markdown-blocks/think-block.tsx +++ b/web/app/components/base/markdown-blocks/think-block.tsx @@ -59,7 +59,11 @@ const useThinkTimer = (children: any) => { }, [startTime, isComplete]) useEffect(() => { - if (hasEndThink(children) || !isResponding) + // Stop timer when: + // 1. Content has [ENDTHINKFLAG] marker (normal completion) + // 2. isResponding is explicitly false (user clicked stop button) + // Note: Don't stop when isResponding is undefined (component used outside ChatContextProvider) + if (hasEndThink(children) || isResponding === false) setIsComplete(true) }, [children, isResponding]) From 5b02e5dcb6f6770e30a733bfa17724939193442f Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Thu, 1 Jan 2026 01:38:12 +0900 Subject: [PATCH 24/37] refactor: migrate some ns.model to BaseModel (#30388) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- api/controllers/common/fields.py | 101 +++++++++--------- api/controllers/console/explore/parameter.py | 6 +- api/controllers/service_api/app/annotation.py | 4 +- api/controllers/service_api/app/app.py | 6 +- api/controllers/service_api/app/site.py | 5 +- api/controllers/service_api/app/workflow.py | 4 +- api/controllers/web/app.py | 6 +- api/fields/annotation_fields.py | 4 +- api/fields/conversation_fields.py | 10 +- api/fields/conversation_variable_fields.py | 6 +- api/fields/end_user_fields.py | 4 +- api/fields/file_fields.py | 10 +- api/fields/member_fields.py | 4 +- api/fields/message_fields.py | 6 +- api/fields/tag_fields.py | 4 +- api/fields/workflow_app_log_fields.py | 6 +- api/fields/workflow_run_fields.py | 4 +- api/models/account.py | 8 +- .../controllers/common/test_fields.py | 69 ++++++++++++ 19 files changed, 168 insertions(+), 99 deletions(-) create mode 100644 api/tests/unit_tests/controllers/common/test_fields.py diff --git a/api/controllers/common/fields.py b/api/controllers/common/fields.py index df9de825de..c16a23fac8 100644 --- a/api/controllers/common/fields.py +++ b/api/controllers/common/fields.py @@ -1,62 +1,59 @@ -from flask_restx import Api, Namespace, fields +from __future__ import annotations -from libs.helper import AppIconUrlField +from typing import Any, TypeAlias -parameters__system_parameters = { - "image_file_size_limit": fields.Integer, - "video_file_size_limit": fields.Integer, - "audio_file_size_limit": fields.Integer, - "file_size_limit": fields.Integer, - "workflow_file_upload_limit": fields.Integer, -} +from pydantic import BaseModel, ConfigDict, computed_field + +from core.file import helpers as file_helpers +from models.model import IconType + +JSONValue: TypeAlias = str | int | float | bool | None | dict[str, Any] | list[Any] +JSONObject: TypeAlias = dict[str, Any] -def build_system_parameters_model(api_or_ns: Api | Namespace): - """Build the system parameters model for the API or Namespace.""" - return api_or_ns.model("SystemParameters", parameters__system_parameters) +class SystemParameters(BaseModel): + image_file_size_limit: int + video_file_size_limit: int + audio_file_size_limit: int + file_size_limit: int + workflow_file_upload_limit: int -parameters_fields = { - "opening_statement": fields.String, - "suggested_questions": fields.Raw, - "suggested_questions_after_answer": fields.Raw, - "speech_to_text": fields.Raw, - "text_to_speech": fields.Raw, - "retriever_resource": fields.Raw, - "annotation_reply": fields.Raw, - "more_like_this": fields.Raw, - "user_input_form": fields.Raw, - "sensitive_word_avoidance": fields.Raw, - "file_upload": fields.Raw, - "system_parameters": fields.Nested(parameters__system_parameters), -} +class Parameters(BaseModel): + opening_statement: str | None = None + suggested_questions: list[str] + suggested_questions_after_answer: JSONObject + speech_to_text: JSONObject + text_to_speech: JSONObject + retriever_resource: JSONObject + annotation_reply: JSONObject + more_like_this: JSONObject + user_input_form: list[JSONObject] + sensitive_word_avoidance: JSONObject + file_upload: JSONObject + system_parameters: SystemParameters -def build_parameters_model(api_or_ns: Api | Namespace): - """Build the parameters model for the API or Namespace.""" - copied_fields = parameters_fields.copy() - copied_fields["system_parameters"] = fields.Nested(build_system_parameters_model(api_or_ns)) - return api_or_ns.model("Parameters", copied_fields) +class Site(BaseModel): + model_config = ConfigDict(from_attributes=True) + title: str + chat_color_theme: str | None = None + chat_color_theme_inverted: bool + icon_type: str | None = None + icon: str | None = None + icon_background: str | None = None + description: str | None = None + copyright: str | None = None + privacy_policy: str | None = None + custom_disclaimer: str | None = None + default_language: str + show_workflow_steps: bool + use_icon_as_answer_icon: bool -site_fields = { - "title": fields.String, - "chat_color_theme": fields.String, - "chat_color_theme_inverted": fields.Boolean, - "icon_type": fields.String, - "icon": fields.String, - "icon_background": fields.String, - "icon_url": AppIconUrlField, - "description": fields.String, - "copyright": fields.String, - "privacy_policy": fields.String, - "custom_disclaimer": fields.String, - "default_language": fields.String, - "show_workflow_steps": fields.Boolean, - "use_icon_as_answer_icon": fields.Boolean, -} - - -def build_site_model(api_or_ns: Api | Namespace): - """Build the site model for the API or Namespace.""" - return api_or_ns.model("Site", site_fields) + @computed_field(return_type=str | None) # type: ignore + @property + def icon_url(self) -> str | None: + if self.icon and self.icon_type == IconType.IMAGE: + return file_helpers.get_signed_file_url(self.icon) + return None diff --git a/api/controllers/console/explore/parameter.py b/api/controllers/console/explore/parameter.py index 9c6b2aedfb..660a4d5aea 100644 --- a/api/controllers/console/explore/parameter.py +++ b/api/controllers/console/explore/parameter.py @@ -1,5 +1,3 @@ -from flask_restx import marshal_with - from controllers.common import fields from controllers.console import console_ns from controllers.console.app.error import AppUnavailableError @@ -13,7 +11,6 @@ from services.app_service import AppService class AppParameterApi(InstalledAppResource): """Resource for app variables.""" - @marshal_with(fields.parameters_fields) def get(self, installed_app: InstalledApp): """Retrieve app parameters.""" app_model = installed_app.app @@ -37,7 +34,8 @@ class AppParameterApi(InstalledAppResource): user_input_form = features_dict.get("user_input_form", []) - return get_parameters_from_feature_dict(features_dict=features_dict, user_input_form=user_input_form) + parameters = get_parameters_from_feature_dict(features_dict=features_dict, user_input_form=user_input_form) + return fields.Parameters.model_validate(parameters).model_dump(mode="json") @console_ns.route("/installed-apps//meta", endpoint="installed_app_meta") diff --git a/api/controllers/service_api/app/annotation.py b/api/controllers/service_api/app/annotation.py index 63c373b50f..85ac9336d6 100644 --- a/api/controllers/service_api/app/annotation.py +++ b/api/controllers/service_api/app/annotation.py @@ -1,7 +1,7 @@ from typing import Literal from flask import request -from flask_restx import Api, Namespace, Resource, fields +from flask_restx import Namespace, Resource, fields from flask_restx.api import HTTPStatus from pydantic import BaseModel, Field @@ -92,7 +92,7 @@ annotation_list_fields = { } -def build_annotation_list_model(api_or_ns: Api | Namespace): +def build_annotation_list_model(api_or_ns: Namespace): """Build the annotation list model for the API or Namespace.""" copied_annotation_list_fields = annotation_list_fields.copy() copied_annotation_list_fields["data"] = fields.List(fields.Nested(build_annotation_model(api_or_ns))) diff --git a/api/controllers/service_api/app/app.py b/api/controllers/service_api/app/app.py index 25d7ccccec..562f5e33cc 100644 --- a/api/controllers/service_api/app/app.py +++ b/api/controllers/service_api/app/app.py @@ -1,6 +1,6 @@ from flask_restx import Resource -from controllers.common.fields import build_parameters_model +from controllers.common.fields import Parameters from controllers.service_api import service_api_ns from controllers.service_api.app.error import AppUnavailableError from controllers.service_api.wraps import validate_app_token @@ -23,7 +23,6 @@ class AppParameterApi(Resource): } ) @validate_app_token - @service_api_ns.marshal_with(build_parameters_model(service_api_ns)) def get(self, app_model: App): """Retrieve app parameters. @@ -45,7 +44,8 @@ class AppParameterApi(Resource): user_input_form = features_dict.get("user_input_form", []) - return get_parameters_from_feature_dict(features_dict=features_dict, user_input_form=user_input_form) + parameters = get_parameters_from_feature_dict(features_dict=features_dict, user_input_form=user_input_form) + return Parameters.model_validate(parameters).model_dump(mode="json") @service_api_ns.route("/meta") diff --git a/api/controllers/service_api/app/site.py b/api/controllers/service_api/app/site.py index 9f8324a84e..8b47a887bb 100644 --- a/api/controllers/service_api/app/site.py +++ b/api/controllers/service_api/app/site.py @@ -1,7 +1,7 @@ from flask_restx import Resource from werkzeug.exceptions import Forbidden -from controllers.common.fields import build_site_model +from controllers.common.fields import Site as SiteResponse from controllers.service_api import service_api_ns from controllers.service_api.wraps import validate_app_token from extensions.ext_database import db @@ -23,7 +23,6 @@ class AppSiteApi(Resource): } ) @validate_app_token - @service_api_ns.marshal_with(build_site_model(service_api_ns)) def get(self, app_model: App): """Retrieve app site info. @@ -38,4 +37,4 @@ class AppSiteApi(Resource): if app_model.tenant.status == TenantStatus.ARCHIVE: raise Forbidden() - return site + return SiteResponse.model_validate(site).model_dump(mode="json") diff --git a/api/controllers/service_api/app/workflow.py b/api/controllers/service_api/app/workflow.py index 4964888fd6..6a549fc926 100644 --- a/api/controllers/service_api/app/workflow.py +++ b/api/controllers/service_api/app/workflow.py @@ -3,7 +3,7 @@ from typing import Any, Literal from dateutil.parser import isoparse from flask import request -from flask_restx import Api, Namespace, Resource, fields +from flask_restx import Namespace, Resource, fields from pydantic import BaseModel, Field from sqlalchemy.orm import Session, sessionmaker from werkzeug.exceptions import BadRequest, InternalServerError, NotFound @@ -78,7 +78,7 @@ workflow_run_fields = { } -def build_workflow_run_model(api_or_ns: Api | Namespace): +def build_workflow_run_model(api_or_ns: Namespace): """Build the workflow run model for the API or Namespace.""" return api_or_ns.model("WorkflowRun", workflow_run_fields) diff --git a/api/controllers/web/app.py b/api/controllers/web/app.py index db3b93a4dc..62ea532eac 100644 --- a/api/controllers/web/app.py +++ b/api/controllers/web/app.py @@ -1,7 +1,7 @@ import logging from flask import request -from flask_restx import Resource, marshal_with +from flask_restx import Resource from pydantic import BaseModel, ConfigDict, Field from werkzeug.exceptions import Unauthorized @@ -50,7 +50,6 @@ class AppParameterApi(WebApiResource): 500: "Internal Server Error", } ) - @marshal_with(fields.parameters_fields) def get(self, app_model: App, end_user): """Retrieve app parameters.""" if app_model.mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}: @@ -69,7 +68,8 @@ class AppParameterApi(WebApiResource): user_input_form = features_dict.get("user_input_form", []) - return get_parameters_from_feature_dict(features_dict=features_dict, user_input_form=user_input_form) + parameters = get_parameters_from_feature_dict(features_dict=features_dict, user_input_form=user_input_form) + return fields.Parameters.model_validate(parameters).model_dump(mode="json") @web_ns.route("/meta") diff --git a/api/fields/annotation_fields.py b/api/fields/annotation_fields.py index 38835d5ac7..e69306dcb2 100644 --- a/api/fields/annotation_fields.py +++ b/api/fields/annotation_fields.py @@ -1,4 +1,4 @@ -from flask_restx import Api, Namespace, fields +from flask_restx import Namespace, fields from libs.helper import TimestampField @@ -12,7 +12,7 @@ annotation_fields = { } -def build_annotation_model(api_or_ns: Api | Namespace): +def build_annotation_model(api_or_ns: Namespace): """Build the annotation model for the API or Namespace.""" return api_or_ns.model("Annotation", annotation_fields) diff --git a/api/fields/conversation_fields.py b/api/fields/conversation_fields.py index ecc267cf38..e4ca2e7a42 100644 --- a/api/fields/conversation_fields.py +++ b/api/fields/conversation_fields.py @@ -1,4 +1,4 @@ -from flask_restx import Api, Namespace, fields +from flask_restx import Namespace, fields from fields.member_fields import simple_account_fields from libs.helper import TimestampField @@ -46,7 +46,7 @@ message_file_fields = { } -def build_message_file_model(api_or_ns: Api | Namespace): +def build_message_file_model(api_or_ns: Namespace): """Build the message file fields for the API or Namespace.""" return api_or_ns.model("MessageFile", message_file_fields) @@ -217,7 +217,7 @@ conversation_infinite_scroll_pagination_fields = { } -def build_conversation_infinite_scroll_pagination_model(api_or_ns: Api | Namespace): +def build_conversation_infinite_scroll_pagination_model(api_or_ns: Namespace): """Build the conversation infinite scroll pagination model for the API or Namespace.""" simple_conversation_model = build_simple_conversation_model(api_or_ns) @@ -226,11 +226,11 @@ def build_conversation_infinite_scroll_pagination_model(api_or_ns: Api | Namespa return api_or_ns.model("ConversationInfiniteScrollPagination", copied_fields) -def build_conversation_delete_model(api_or_ns: Api | Namespace): +def build_conversation_delete_model(api_or_ns: Namespace): """Build the conversation delete model for the API or Namespace.""" return api_or_ns.model("ConversationDelete", conversation_delete_fields) -def build_simple_conversation_model(api_or_ns: Api | Namespace): +def build_simple_conversation_model(api_or_ns: Namespace): """Build the simple conversation model for the API or Namespace.""" return api_or_ns.model("SimpleConversation", simple_conversation_fields) diff --git a/api/fields/conversation_variable_fields.py b/api/fields/conversation_variable_fields.py index 7d5e311591..c55014a368 100644 --- a/api/fields/conversation_variable_fields.py +++ b/api/fields/conversation_variable_fields.py @@ -1,4 +1,4 @@ -from flask_restx import Api, Namespace, fields +from flask_restx import Namespace, fields from libs.helper import TimestampField @@ -29,12 +29,12 @@ conversation_variable_infinite_scroll_pagination_fields = { } -def build_conversation_variable_model(api_or_ns: Api | Namespace): +def build_conversation_variable_model(api_or_ns: Namespace): """Build the conversation variable model for the API or Namespace.""" return api_or_ns.model("ConversationVariable", conversation_variable_fields) -def build_conversation_variable_infinite_scroll_pagination_model(api_or_ns: Api | Namespace): +def build_conversation_variable_infinite_scroll_pagination_model(api_or_ns: Namespace): """Build the conversation variable infinite scroll pagination model for the API or Namespace.""" # Build the nested variable model first conversation_variable_model = build_conversation_variable_model(api_or_ns) diff --git a/api/fields/end_user_fields.py b/api/fields/end_user_fields.py index ea43e3b5fd..5389b0213a 100644 --- a/api/fields/end_user_fields.py +++ b/api/fields/end_user_fields.py @@ -1,4 +1,4 @@ -from flask_restx import Api, Namespace, fields +from flask_restx import Namespace, fields simple_end_user_fields = { "id": fields.String, @@ -8,5 +8,5 @@ simple_end_user_fields = { } -def build_simple_end_user_model(api_or_ns: Api | Namespace): +def build_simple_end_user_model(api_or_ns: Namespace): return api_or_ns.model("SimpleEndUser", simple_end_user_fields) diff --git a/api/fields/file_fields.py b/api/fields/file_fields.py index a707500445..70138404c7 100644 --- a/api/fields/file_fields.py +++ b/api/fields/file_fields.py @@ -1,4 +1,4 @@ -from flask_restx import Api, Namespace, fields +from flask_restx import Namespace, fields from libs.helper import TimestampField @@ -14,7 +14,7 @@ upload_config_fields = { } -def build_upload_config_model(api_or_ns: Api | Namespace): +def build_upload_config_model(api_or_ns: Namespace): """Build the upload config model for the API or Namespace. Args: @@ -39,7 +39,7 @@ file_fields = { } -def build_file_model(api_or_ns: Api | Namespace): +def build_file_model(api_or_ns: Namespace): """Build the file model for the API or Namespace. Args: @@ -57,7 +57,7 @@ remote_file_info_fields = { } -def build_remote_file_info_model(api_or_ns: Api | Namespace): +def build_remote_file_info_model(api_or_ns: Namespace): """Build the remote file info model for the API or Namespace. Args: @@ -81,7 +81,7 @@ file_fields_with_signed_url = { } -def build_file_with_signed_url_model(api_or_ns: Api | Namespace): +def build_file_with_signed_url_model(api_or_ns: Namespace): """Build the file with signed URL model for the API or Namespace. Args: diff --git a/api/fields/member_fields.py b/api/fields/member_fields.py index 08e38a6931..25160927e6 100644 --- a/api/fields/member_fields.py +++ b/api/fields/member_fields.py @@ -1,4 +1,4 @@ -from flask_restx import Api, Namespace, fields +from flask_restx import Namespace, fields from libs.helper import AvatarUrlField, TimestampField @@ -9,7 +9,7 @@ simple_account_fields = { } -def build_simple_account_model(api_or_ns: Api | Namespace): +def build_simple_account_model(api_or_ns: Namespace): return api_or_ns.model("SimpleAccount", simple_account_fields) diff --git a/api/fields/message_fields.py b/api/fields/message_fields.py index a419da2e18..151ff6f826 100644 --- a/api/fields/message_fields.py +++ b/api/fields/message_fields.py @@ -1,4 +1,4 @@ -from flask_restx import Api, Namespace, fields +from flask_restx import Namespace, fields from fields.conversation_fields import message_file_fields from libs.helper import TimestampField @@ -10,7 +10,7 @@ feedback_fields = { } -def build_feedback_model(api_or_ns: Api | Namespace): +def build_feedback_model(api_or_ns: Namespace): """Build the feedback model for the API or Namespace.""" return api_or_ns.model("Feedback", feedback_fields) @@ -30,7 +30,7 @@ agent_thought_fields = { } -def build_agent_thought_model(api_or_ns: Api | Namespace): +def build_agent_thought_model(api_or_ns: Namespace): """Build the agent thought model for the API or Namespace.""" return api_or_ns.model("AgentThought", agent_thought_fields) diff --git a/api/fields/tag_fields.py b/api/fields/tag_fields.py index d5b7c86a04..e359a4408c 100644 --- a/api/fields/tag_fields.py +++ b/api/fields/tag_fields.py @@ -1,4 +1,4 @@ -from flask_restx import Api, Namespace, fields +from flask_restx import Namespace, fields dataset_tag_fields = { "id": fields.String, @@ -8,5 +8,5 @@ dataset_tag_fields = { } -def build_dataset_tag_fields(api_or_ns: Api | Namespace): +def build_dataset_tag_fields(api_or_ns: Namespace): return api_or_ns.model("DataSetTag", dataset_tag_fields) diff --git a/api/fields/workflow_app_log_fields.py b/api/fields/workflow_app_log_fields.py index 4cbdf6f0ca..0ebc03a98c 100644 --- a/api/fields/workflow_app_log_fields.py +++ b/api/fields/workflow_app_log_fields.py @@ -1,4 +1,4 @@ -from flask_restx import Api, Namespace, fields +from flask_restx import Namespace, fields from fields.end_user_fields import build_simple_end_user_model, simple_end_user_fields from fields.member_fields import build_simple_account_model, simple_account_fields @@ -17,7 +17,7 @@ workflow_app_log_partial_fields = { } -def build_workflow_app_log_partial_model(api_or_ns: Api | Namespace): +def build_workflow_app_log_partial_model(api_or_ns: Namespace): """Build the workflow app log partial model for the API or Namespace.""" workflow_run_model = build_workflow_run_for_log_model(api_or_ns) simple_account_model = build_simple_account_model(api_or_ns) @@ -43,7 +43,7 @@ workflow_app_log_pagination_fields = { } -def build_workflow_app_log_pagination_model(api_or_ns: Api | Namespace): +def build_workflow_app_log_pagination_model(api_or_ns: Namespace): """Build the workflow app log pagination model for the API or Namespace.""" # Build the nested partial model first workflow_app_log_partial_model = build_workflow_app_log_partial_model(api_or_ns) diff --git a/api/fields/workflow_run_fields.py b/api/fields/workflow_run_fields.py index 821ce62ecc..476025064f 100644 --- a/api/fields/workflow_run_fields.py +++ b/api/fields/workflow_run_fields.py @@ -1,4 +1,4 @@ -from flask_restx import Api, Namespace, fields +from flask_restx import Namespace, fields from fields.end_user_fields import simple_end_user_fields from fields.member_fields import simple_account_fields @@ -19,7 +19,7 @@ workflow_run_for_log_fields = { } -def build_workflow_run_for_log_model(api_or_ns: Api | Namespace): +def build_workflow_run_for_log_model(api_or_ns: Namespace): return api_or_ns.model("WorkflowRunForLog", workflow_run_for_log_fields) diff --git a/api/models/account.py b/api/models/account.py index 420e6adc6c..f7a9c20026 100644 --- a/api/models/account.py +++ b/api/models/account.py @@ -8,7 +8,7 @@ from uuid import uuid4 import sqlalchemy as sa from flask_login import UserMixin from sqlalchemy import DateTime, String, func, select -from sqlalchemy.orm import Mapped, Session, mapped_column +from sqlalchemy.orm import Mapped, Session, mapped_column, validates from typing_extensions import deprecated from .base import TypeBase @@ -116,6 +116,12 @@ class Account(UserMixin, TypeBase): role: TenantAccountRole | None = field(default=None, init=False) _current_tenant: "Tenant | None" = field(default=None, init=False) + @validates("status") + def _normalize_status(self, _key: str, value: str | AccountStatus) -> str: + if isinstance(value, AccountStatus): + return value.value + return value + @property def is_password_set(self): return self.password is not None diff --git a/api/tests/unit_tests/controllers/common/test_fields.py b/api/tests/unit_tests/controllers/common/test_fields.py new file mode 100644 index 0000000000..d4dc13127d --- /dev/null +++ b/api/tests/unit_tests/controllers/common/test_fields.py @@ -0,0 +1,69 @@ +import builtins +from types import SimpleNamespace +from unittest.mock import patch + +from flask.views import MethodView as FlaskMethodView + +_NEEDS_METHOD_VIEW_CLEANUP = False +if not hasattr(builtins, "MethodView"): + builtins.MethodView = FlaskMethodView + _NEEDS_METHOD_VIEW_CLEANUP = True +from controllers.common.fields import Parameters, Site +from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict +from models.model import IconType + + +def test_parameters_model_round_trip(): + parameters = get_parameters_from_feature_dict(features_dict={}, user_input_form=[]) + + model = Parameters.model_validate(parameters) + + assert model.model_dump(mode="json") == parameters + + +def test_site_icon_url_uses_signed_url_for_image_icon(): + site = SimpleNamespace( + title="Example", + chat_color_theme=None, + chat_color_theme_inverted=False, + icon_type=IconType.IMAGE, + icon="file-id", + icon_background=None, + description=None, + copyright=None, + privacy_policy=None, + custom_disclaimer=None, + default_language="en-US", + show_workflow_steps=True, + use_icon_as_answer_icon=False, + ) + + with patch("controllers.common.fields.file_helpers.get_signed_file_url", return_value="signed") as mock_helper: + model = Site.model_validate(site) + + assert model.icon_url == "signed" + mock_helper.assert_called_once_with("file-id") + + +def test_site_icon_url_is_none_for_non_image_icon(): + site = SimpleNamespace( + title="Example", + chat_color_theme=None, + chat_color_theme_inverted=False, + icon_type=IconType.EMOJI, + icon="file-id", + icon_background=None, + description=None, + copyright=None, + privacy_policy=None, + custom_disclaimer=None, + default_language="en-US", + show_workflow_steps=True, + use_icon_as_answer_icon=False, + ) + + with patch("controllers.common.fields.file_helpers.get_signed_file_url") as mock_helper: + model = Site.model_validate(site) + + assert model.icon_url is None + mock_helper.assert_not_called() From ae43ad5cb6ca51ed630d3f1a8f2e827566af8bab Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Thu, 1 Jan 2026 00:40:21 +0800 Subject: [PATCH 25/37] fix: fix when vision is disabled delete the configs (#30420) --- web/app/components/workflow/hooks/use-config-vision.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/app/components/workflow/hooks/use-config-vision.ts b/web/app/components/workflow/hooks/use-config-vision.ts index 12a2ad38ad..f9c68243c0 100644 --- a/web/app/components/workflow/hooks/use-config-vision.ts +++ b/web/app/components/workflow/hooks/use-config-vision.ts @@ -49,6 +49,9 @@ const useConfigVision = (model: ModelConfig, { variable_selector: ['sys', 'files'], } } + else if (!enabled) { + delete draft.configs + } }) onChange(newPayload) }, [isChatMode, onChange, payload]) From 9b6b2f31950c50ec54a06985dadaf6577e295c83 Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Thu, 1 Jan 2026 00:40:54 +0800 Subject: [PATCH 26/37] feat: add AgentMaxIterationError exc (#30423) --- api/core/agent/cot_agent_runner.py | 6 ++++++ api/core/agent/fc_agent_runner.py | 5 +++++ api/core/workflow/nodes/agent/exc.py | 11 +++++++++++ 3 files changed, 22 insertions(+) diff --git a/api/core/agent/cot_agent_runner.py b/api/core/agent/cot_agent_runner.py index b32e35d0ca..a55f2d0f5f 100644 --- a/api/core/agent/cot_agent_runner.py +++ b/api/core/agent/cot_agent_runner.py @@ -22,6 +22,7 @@ from core.prompt.agent_history_prompt_transform import AgentHistoryPromptTransfo from core.tools.__base.tool import Tool from core.tools.entities.tool_entities import ToolInvokeMeta from core.tools.tool_engine import ToolEngine +from core.workflow.nodes.agent.exc import AgentMaxIterationError from models.model import Message logger = logging.getLogger(__name__) @@ -165,6 +166,11 @@ class CotAgentRunner(BaseAgentRunner, ABC): scratchpad.thought = scratchpad.thought.strip() or "I am thinking about how to help you" self._agent_scratchpad.append(scratchpad) + # Check if max iteration is reached and model still wants to call tools + if iteration_step == max_iteration_steps and scratchpad.action: + if scratchpad.action.action_name.lower() != "final answer": + raise AgentMaxIterationError(app_config.agent.max_iteration) + # get llm usage if "usage" in usage_dict: if usage_dict["usage"] is not None: diff --git a/api/core/agent/fc_agent_runner.py b/api/core/agent/fc_agent_runner.py index dcc1326b33..68d14ad027 100644 --- a/api/core/agent/fc_agent_runner.py +++ b/api/core/agent/fc_agent_runner.py @@ -25,6 +25,7 @@ from core.model_runtime.entities.message_entities import ImagePromptMessageConte from core.prompt.agent_history_prompt_transform import AgentHistoryPromptTransform from core.tools.entities.tool_entities import ToolInvokeMeta from core.tools.tool_engine import ToolEngine +from core.workflow.nodes.agent.exc import AgentMaxIterationError from models.model import Message logger = logging.getLogger(__name__) @@ -222,6 +223,10 @@ class FunctionCallAgentRunner(BaseAgentRunner): final_answer += response + "\n" + # Check if max iteration is reached and model still wants to call tools + if iteration_step == max_iteration_steps and tool_calls: + raise AgentMaxIterationError(app_config.agent.max_iteration) + # call tools tool_responses = [] for tool_call_id, tool_call_name, tool_call_args in tool_calls: diff --git a/api/core/workflow/nodes/agent/exc.py b/api/core/workflow/nodes/agent/exc.py index 944f5f0b20..ba2c83d8a6 100644 --- a/api/core/workflow/nodes/agent/exc.py +++ b/api/core/workflow/nodes/agent/exc.py @@ -119,3 +119,14 @@ class AgentVariableTypeError(AgentNodeError): self.expected_type = expected_type self.actual_type = actual_type super().__init__(message) + + +class AgentMaxIterationError(AgentNodeError): + """Exception raised when the agent exceeds the maximum iteration limit.""" + + def __init__(self, max_iteration: int): + self.max_iteration = max_iteration + super().__init__( + f"Agent exceeded the maximum iteration limit of {max_iteration}. " + f"The agent was unable to complete the task within the allowed number of iterations." + ) From 8f2aabf7bd9afaa2e84860af5d58ff2599e8e1a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Sat, 3 Jan 2026 01:34:17 +0800 Subject: [PATCH 27/37] chore: Standardized the OpenAI icon (#30471) --- .../base/icons/src/public/llm/OpenaiBlue.json | 37 ------------------- .../base/icons/src/public/llm/OpenaiBlue.tsx | 20 ---------- .../base/icons/src/public/llm/OpenaiTeal.json | 37 ------------------- .../base/icons/src/public/llm/OpenaiTeal.tsx | 20 ---------- .../icons/src/public/llm/OpenaiViolet.json | 37 ------------------- .../icons/src/public/llm/OpenaiViolet.tsx | 20 ---------- .../base/icons/src/public/llm/index.ts | 3 -- .../model-provider-page/model-icon/index.tsx | 8 +--- 8 files changed, 1 insertion(+), 181 deletions(-) delete mode 100644 web/app/components/base/icons/src/public/llm/OpenaiBlue.json delete mode 100644 web/app/components/base/icons/src/public/llm/OpenaiBlue.tsx delete mode 100644 web/app/components/base/icons/src/public/llm/OpenaiTeal.json delete mode 100644 web/app/components/base/icons/src/public/llm/OpenaiTeal.tsx delete mode 100644 web/app/components/base/icons/src/public/llm/OpenaiViolet.json delete mode 100644 web/app/components/base/icons/src/public/llm/OpenaiViolet.tsx diff --git a/web/app/components/base/icons/src/public/llm/OpenaiBlue.json b/web/app/components/base/icons/src/public/llm/OpenaiBlue.json deleted file mode 100644 index c5d4f974a2..0000000000 --- a/web/app/components/base/icons/src/public/llm/OpenaiBlue.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "icon": { - "type": "element", - "isRootNode": true, - "name": "svg", - "attributes": { - "width": "24", - "height": "24", - "viewBox": "0 0 24 24", - "fill": "none", - "xmlns": "http://www.w3.org/2000/svg" - }, - "children": [ - { - "type": "element", - "name": "rect", - "attributes": { - "width": "24", - "height": "24", - "rx": "6", - "fill": "#03A4EE" - }, - "children": [] - }, - { - "type": "element", - "name": "path", - "attributes": { - "d": "M19.7758 11.5959C19.9546 11.9948 20.0681 12.4213 20.1145 12.8563C20.1592 13.2913 20.1369 13.7315 20.044 14.1596C19.9529 14.5878 19.7947 14.9987 19.5746 15.377C19.4302 15.6298 19.2599 15.867 19.0639 16.0854C18.8696 16.3021 18.653 16.4981 18.4174 16.67C18.1801 16.842 17.9274 16.9864 17.6591 17.105C17.3926 17.222 17.1141 17.3114 16.8286 17.3698C16.6945 17.7859 16.4951 18.1797 16.2371 18.5339C15.9809 18.8881 15.6697 19.1993 15.3155 19.4555C14.9613 19.7134 14.5693 19.9129 14.1532 20.047C13.7371 20.1829 13.302 20.2499 12.8636 20.2499C12.573 20.2516 12.2807 20.2207 11.9953 20.1622C11.7116 20.102 11.433 20.0109 11.1665 19.8923C10.9 19.7736 10.6472 19.6258 10.4116 19.4538C10.1778 19.2819 9.96115 19.0841 9.76857 18.8658C9.33871 18.9586 8.89853 18.981 8.46351 18.9363C8.02849 18.8898 7.60207 18.7763 7.20143 18.5975C6.80252 18.4204 6.43284 18.1797 6.10786 17.8857C5.78289 17.5916 5.50606 17.2478 5.28769 16.8695C5.14153 16.6167 5.02117 16.3502 4.93004 16.0734C4.83891 15.7965 4.77873 15.5111 4.74778 15.2205C4.71683 14.9317 4.71855 14.6393 4.7495 14.3488C4.78045 14.0599 4.84407 13.7745 4.9352 13.4976C4.64289 13.1727 4.40217 12.803 4.22335 12.4041C4.04624 12.0034 3.93104 11.5787 3.88634 11.1437C3.83991 10.7087 3.86398 10.2685 3.95511 9.84036C4.04624 9.41222 4.20443 9.00127 4.42452 8.62299C4.56896 8.37023 4.73918 8.13123 4.93348 7.91458C5.12778 7.69793 5.34615 7.50191 5.58171 7.32997C5.81728 7.15802 6.07176 7.01187 6.33827 6.89495C6.6065 6.7763 6.88506 6.68861 7.17048 6.63015C7.3046 6.21232 7.50406 5.82029 7.76026 5.46608C8.01817 5.11188 8.32939 4.80066 8.6836 4.54274C9.03781 4.28654 9.42984 4.08708 9.84595 3.95125C10.2621 3.81713 10.6971 3.74835 11.1355 3.75007C11.4261 3.74835 11.7184 3.77758 12.0039 3.83776C12.2893 3.89794 12.5678 3.98736 12.8344 4.106C13.1009 4.22636 13.3536 4.37251 13.5892 4.54446C13.8248 4.71812 14.0414 4.91414 14.234 5.13251C14.6621 5.04138 15.1023 5.01903 15.5373 5.06373C15.9723 5.10844 16.3971 5.22364 16.7977 5.40074C17.1966 5.57957 17.5663 5.81857 17.8913 6.1126C18.2162 6.4049 18.4931 6.74707 18.7114 7.12707C18.8576 7.37811 18.9779 7.64463 19.0691 7.92318C19.1602 8.20001 19.2221 8.48544 19.2513 8.77602C19.2823 9.06661 19.2823 9.35892 19.2496 9.64951C19.2187 9.94009 19.155 10.2255 19.0639 10.5024C19.3579 10.8273 19.5969 11.1953 19.7758 11.5959ZM14.0466 18.9363C14.4214 18.7815 14.7619 18.5528 15.049 18.2657C15.3362 17.9785 15.5648 17.6381 15.7196 17.2615C15.8743 16.8867 15.9552 16.4843 15.9552 16.0785V12.2442C15.954 12.2407 15.9529 12.2367 15.9517 12.2321C15.9506 12.2287 15.9488 12.2252 15.9466 12.2218C15.9443 12.2184 15.9414 12.2155 15.938 12.2132C15.9345 12.2098 15.9311 12.2075 15.9276 12.2063L14.54 11.4051V16.0373C14.54 16.0837 14.5332 16.1318 14.5211 16.1765C14.5091 16.223 14.4919 16.2659 14.4678 16.3072C14.4438 16.3485 14.4162 16.3863 14.3819 16.419C14.3484 16.4523 14.3109 16.4812 14.2701 16.505L10.9842 18.4015C10.9567 18.4187 10.9103 18.4428 10.8862 18.4565C11.0221 18.5717 11.1699 18.6732 11.3247 18.7626C11.4811 18.852 11.6428 18.9277 11.8113 18.9896C11.9798 19.0497 12.1535 19.0962 12.3288 19.1271C12.5059 19.1581 12.6848 19.1735 12.8636 19.1735C13.2694 19.1735 13.6717 19.0927 14.0466 18.9363ZM6.22135 16.333C6.42596 16.6855 6.69592 16.9916 7.01745 17.2392C7.34071 17.4868 7.70695 17.6673 8.09899 17.7722C8.49102 17.8771 8.90025 17.9046 9.3026 17.8513C9.70495 17.798 10.0918 17.6673 10.4443 17.4644L13.7663 15.5472L13.7749 15.5386C13.7772 15.5363 13.7789 15.5329 13.78 15.5283C13.7823 15.5249 13.7841 15.5214 13.7852 15.518V13.9017L9.77545 16.2212C9.73418 16.2453 9.6912 16.2625 9.64649 16.2763C9.60007 16.2883 9.55364 16.2935 9.5055 16.2935C9.45907 16.2935 9.41265 16.2883 9.36622 16.2763C9.32152 16.2625 9.27681 16.2453 9.23554 16.2212L5.94967 14.323C5.92044 14.3058 5.87746 14.28 5.85339 14.2645C5.82244 14.4416 5.80696 14.6204 5.80696 14.7993C5.80696 14.9781 5.82415 15.1569 5.85511 15.334C5.88605 15.5094 5.9342 15.6831 5.99438 15.8516C6.05628 16.0201 6.13194 16.1817 6.22135 16.3364V16.333ZM5.35818 9.1629C5.15529 9.51539 5.02461 9.90398 4.97131 10.3063C4.918 10.7087 4.94552 11.1162 5.0504 11.51C5.15529 11.902 5.33583 12.2682 5.58343 12.5915C5.83103 12.913 6.13881 13.183 6.48958 13.3859L9.80984 15.3048C9.81328 15.3059 9.81729 15.3071 9.82188 15.3082H9.83391C9.8385 15.3082 9.84251 15.3071 9.84595 15.3048C9.84939 15.3036 9.85283 15.3019 9.85627 15.2996L11.249 14.4949L7.23926 12.1805C7.19971 12.1565 7.16189 12.1272 7.1275 12.0946C7.09418 12.0611 7.06529 12.0236 7.04153 11.9828C7.01917 11.9415 7.00026 11.8985 6.98822 11.8521C6.97619 11.8074 6.96931 11.761 6.97103 11.7128V7.80797C6.80252 7.86987 6.63917 7.94553 6.48442 8.03494C6.32967 8.12607 6.18352 8.22924 6.04596 8.34444C5.91013 8.45965 5.78289 8.58688 5.66769 8.72444C5.55248 8.86028 5.45103 9.00815 5.36162 9.1629H5.35818ZM16.7633 11.8177C16.8046 11.8418 16.8424 11.8693 16.8768 11.9037C16.9094 11.9364 16.9387 11.9742 16.9628 12.0155C16.9851 12.0567 17.004 12.1014 17.0161 12.1461C17.0264 12.1926 17.0332 12.239 17.0315 12.2871V16.192C17.5835 15.9891 18.0649 15.6332 18.4208 15.1655C18.7785 14.6978 18.9934 14.139 19.0433 13.5544C19.0931 12.9698 18.9762 12.3817 18.7046 11.8607C18.4329 11.3397 18.0185 10.9064 17.5095 10.6141L14.1893 8.69521C14.1858 8.69406 14.1818 8.69292 14.1772 8.69177H14.1652C14.1618 8.69292 14.1578 8.69406 14.1532 8.69521C14.1497 8.69636 14.1463 8.69808 14.1429 8.70037L12.757 9.50163L16.7667 11.8177H16.7633ZM18.1475 9.7372H18.1457V9.73892L18.1475 9.7372ZM18.1457 9.73548C18.2455 9.15774 18.1784 8.56281 17.9514 8.02119C17.7262 7.47956 17.3496 7.01359 16.8682 6.67658C16.3867 6.34128 15.8193 6.1487 15.233 6.12291C14.6449 6.09884 14.0638 6.24155 13.5548 6.53386L10.2345 8.45105C10.2311 8.45334 10.2282 8.45621 10.2259 8.45965L10.2191 8.46996C10.2179 8.4734 10.2168 8.47741 10.2156 8.482C10.2145 8.48544 10.2139 8.48945 10.2139 8.49403V10.0966L14.2237 7.78046C14.2649 7.75639 14.3096 7.7392 14.3543 7.72544C14.4008 7.7134 14.4472 7.70825 14.4936 7.70825C14.5418 7.70825 14.5882 7.7134 14.6346 7.72544C14.6793 7.7392 14.7223 7.75639 14.7636 7.78046L18.0494 9.67874C18.0787 9.69593 18.1217 9.72 18.1457 9.73548ZM9.45735 7.96101C9.45735 7.91458 9.46423 7.86816 9.47627 7.82173C9.4883 7.77702 9.5055 7.73232 9.52957 7.69105C9.55364 7.6515 9.58115 7.61368 9.61554 7.57929C9.64821 7.54662 9.68604 7.51739 9.72731 7.49503L13.0132 5.59848C13.0441 5.57957 13.0871 5.55549 13.1112 5.54346C12.6607 5.1669 12.1105 4.92618 11.5276 4.85224C10.9447 4.77658 10.3532 4.86943 9.82188 5.11875C9.28885 5.36807 8.83835 5.76527 8.52369 6.26047C8.20903 6.75739 8.04224 7.33169 8.04224 7.91974V11.7541C8.04339 11.7587 8.04454 11.7627 8.04568 11.7661C8.04683 11.7696 8.04855 11.773 8.05084 11.7765C8.05313 11.7799 8.056 11.7833 8.05944 11.7868C8.06173 11.7891 8.06517 11.7914 8.06976 11.7937L9.45735 12.5949V7.96101ZM10.2105 13.0282L11.997 14.0599L13.7835 13.0282V10.9666L11.9987 9.93493L10.2122 10.9666L10.2105 13.0282Z", - "fill": "white" - }, - "children": [] - } - ] - }, - "name": "OpenaiBlue" -} diff --git a/web/app/components/base/icons/src/public/llm/OpenaiBlue.tsx b/web/app/components/base/icons/src/public/llm/OpenaiBlue.tsx deleted file mode 100644 index 9934a77591..0000000000 --- a/web/app/components/base/icons/src/public/llm/OpenaiBlue.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import type { IconData } from '@/app/components/base/icons/IconBase' -import * as React from 'react' -import IconBase from '@/app/components/base/icons/IconBase' -import data from './OpenaiBlue.json' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject> - }, -) => - -Icon.displayName = 'OpenaiBlue' - -export default Icon diff --git a/web/app/components/base/icons/src/public/llm/OpenaiTeal.json b/web/app/components/base/icons/src/public/llm/OpenaiTeal.json deleted file mode 100644 index ffd0981512..0000000000 --- a/web/app/components/base/icons/src/public/llm/OpenaiTeal.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "icon": { - "type": "element", - "isRootNode": true, - "name": "svg", - "attributes": { - "width": "24", - "height": "24", - "viewBox": "0 0 24 24", - "fill": "none", - "xmlns": "http://www.w3.org/2000/svg" - }, - "children": [ - { - "type": "element", - "name": "rect", - "attributes": { - "width": "24", - "height": "24", - "rx": "6", - "fill": "#009688" - }, - "children": [] - }, - { - "type": "element", - "name": "path", - "attributes": { - "d": "M19.7758 11.5959C19.9546 11.9948 20.0681 12.4213 20.1145 12.8563C20.1592 13.2913 20.1369 13.7315 20.044 14.1596C19.9529 14.5878 19.7947 14.9987 19.5746 15.377C19.4302 15.6298 19.2599 15.867 19.0639 16.0854C18.8696 16.3021 18.653 16.4981 18.4174 16.67C18.1801 16.842 17.9274 16.9864 17.6591 17.105C17.3926 17.222 17.1141 17.3114 16.8286 17.3698C16.6945 17.7859 16.4951 18.1797 16.2371 18.5339C15.9809 18.8881 15.6697 19.1993 15.3155 19.4555C14.9613 19.7134 14.5693 19.9129 14.1532 20.047C13.7371 20.1829 13.302 20.2499 12.8636 20.2499C12.573 20.2516 12.2807 20.2207 11.9953 20.1622C11.7116 20.102 11.433 20.0109 11.1665 19.8923C10.9 19.7736 10.6472 19.6258 10.4116 19.4538C10.1778 19.2819 9.96115 19.0841 9.76857 18.8658C9.33871 18.9586 8.89853 18.981 8.46351 18.9363C8.02849 18.8898 7.60207 18.7763 7.20143 18.5975C6.80252 18.4204 6.43284 18.1797 6.10786 17.8857C5.78289 17.5916 5.50606 17.2478 5.28769 16.8695C5.14153 16.6167 5.02117 16.3502 4.93004 16.0734C4.83891 15.7965 4.77873 15.5111 4.74778 15.2205C4.71683 14.9317 4.71855 14.6393 4.7495 14.3488C4.78045 14.0599 4.84407 13.7745 4.9352 13.4976C4.64289 13.1727 4.40217 12.803 4.22335 12.4041C4.04624 12.0034 3.93104 11.5787 3.88634 11.1437C3.83991 10.7087 3.86398 10.2685 3.95511 9.84036C4.04624 9.41222 4.20443 9.00127 4.42452 8.62299C4.56896 8.37023 4.73918 8.13123 4.93348 7.91458C5.12778 7.69793 5.34615 7.50191 5.58171 7.32997C5.81728 7.15802 6.07176 7.01187 6.33827 6.89495C6.6065 6.7763 6.88506 6.68861 7.17048 6.63015C7.3046 6.21232 7.50406 5.82029 7.76026 5.46608C8.01817 5.11188 8.32939 4.80066 8.6836 4.54274C9.03781 4.28654 9.42984 4.08708 9.84595 3.95125C10.2621 3.81713 10.6971 3.74835 11.1355 3.75007C11.4261 3.74835 11.7184 3.77758 12.0039 3.83776C12.2893 3.89794 12.5678 3.98736 12.8344 4.106C13.1009 4.22636 13.3536 4.37251 13.5892 4.54446C13.8248 4.71812 14.0414 4.91414 14.234 5.13251C14.6621 5.04138 15.1023 5.01903 15.5373 5.06373C15.9723 5.10844 16.3971 5.22364 16.7977 5.40074C17.1966 5.57957 17.5663 5.81857 17.8913 6.1126C18.2162 6.4049 18.4931 6.74707 18.7114 7.12707C18.8576 7.37811 18.9779 7.64463 19.0691 7.92318C19.1602 8.20001 19.2221 8.48544 19.2513 8.77602C19.2823 9.06661 19.2823 9.35892 19.2496 9.64951C19.2187 9.94009 19.155 10.2255 19.0639 10.5024C19.3579 10.8273 19.5969 11.1953 19.7758 11.5959ZM14.0466 18.9363C14.4214 18.7815 14.7619 18.5528 15.049 18.2657C15.3362 17.9785 15.5648 17.6381 15.7196 17.2615C15.8743 16.8867 15.9552 16.4843 15.9552 16.0785V12.2442C15.954 12.2407 15.9529 12.2367 15.9517 12.2321C15.9506 12.2287 15.9488 12.2252 15.9466 12.2218C15.9443 12.2184 15.9414 12.2155 15.938 12.2132C15.9345 12.2098 15.9311 12.2075 15.9276 12.2063L14.54 11.4051V16.0373C14.54 16.0837 14.5332 16.1318 14.5211 16.1765C14.5091 16.223 14.4919 16.2659 14.4678 16.3072C14.4438 16.3485 14.4162 16.3863 14.3819 16.419C14.3484 16.4523 14.3109 16.4812 14.2701 16.505L10.9842 18.4015C10.9567 18.4187 10.9103 18.4428 10.8862 18.4565C11.0221 18.5717 11.1699 18.6732 11.3247 18.7626C11.4811 18.852 11.6428 18.9277 11.8113 18.9896C11.9798 19.0497 12.1535 19.0962 12.3288 19.1271C12.5059 19.1581 12.6848 19.1735 12.8636 19.1735C13.2694 19.1735 13.6717 19.0927 14.0466 18.9363ZM6.22135 16.333C6.42596 16.6855 6.69592 16.9916 7.01745 17.2392C7.34071 17.4868 7.70695 17.6673 8.09899 17.7722C8.49102 17.8771 8.90025 17.9046 9.3026 17.8513C9.70495 17.798 10.0918 17.6673 10.4443 17.4644L13.7663 15.5472L13.7749 15.5386C13.7772 15.5363 13.7789 15.5329 13.78 15.5283C13.7823 15.5249 13.7841 15.5214 13.7852 15.518V13.9017L9.77545 16.2212C9.73418 16.2453 9.6912 16.2625 9.64649 16.2763C9.60007 16.2883 9.55364 16.2935 9.5055 16.2935C9.45907 16.2935 9.41265 16.2883 9.36622 16.2763C9.32152 16.2625 9.27681 16.2453 9.23554 16.2212L5.94967 14.323C5.92044 14.3058 5.87746 14.28 5.85339 14.2645C5.82244 14.4416 5.80696 14.6204 5.80696 14.7993C5.80696 14.9781 5.82415 15.1569 5.85511 15.334C5.88605 15.5094 5.9342 15.6831 5.99438 15.8516C6.05628 16.0201 6.13194 16.1817 6.22135 16.3364V16.333ZM5.35818 9.1629C5.15529 9.51539 5.02461 9.90398 4.97131 10.3063C4.918 10.7087 4.94552 11.1162 5.0504 11.51C5.15529 11.902 5.33583 12.2682 5.58343 12.5915C5.83103 12.913 6.13881 13.183 6.48958 13.3859L9.80984 15.3048C9.81328 15.3059 9.81729 15.3071 9.82188 15.3082H9.83391C9.8385 15.3082 9.84251 15.3071 9.84595 15.3048C9.84939 15.3036 9.85283 15.3019 9.85627 15.2996L11.249 14.4949L7.23926 12.1805C7.19971 12.1565 7.16189 12.1272 7.1275 12.0946C7.09418 12.0611 7.06529 12.0236 7.04153 11.9828C7.01917 11.9415 7.00026 11.8985 6.98822 11.8521C6.97619 11.8074 6.96931 11.761 6.97103 11.7128V7.80797C6.80252 7.86987 6.63917 7.94553 6.48442 8.03494C6.32967 8.12607 6.18352 8.22924 6.04596 8.34444C5.91013 8.45965 5.78289 8.58688 5.66769 8.72444C5.55248 8.86028 5.45103 9.00815 5.36162 9.1629H5.35818ZM16.7633 11.8177C16.8046 11.8418 16.8424 11.8693 16.8768 11.9037C16.9094 11.9364 16.9387 11.9742 16.9628 12.0155C16.9851 12.0567 17.004 12.1014 17.0161 12.1461C17.0264 12.1926 17.0332 12.239 17.0315 12.2871V16.192C17.5835 15.9891 18.0649 15.6332 18.4208 15.1655C18.7785 14.6978 18.9934 14.139 19.0433 13.5544C19.0931 12.9698 18.9762 12.3817 18.7046 11.8607C18.4329 11.3397 18.0185 10.9064 17.5095 10.6141L14.1893 8.69521C14.1858 8.69406 14.1818 8.69292 14.1772 8.69177H14.1652C14.1618 8.69292 14.1578 8.69406 14.1532 8.69521C14.1497 8.69636 14.1463 8.69808 14.1429 8.70037L12.757 9.50163L16.7667 11.8177H16.7633ZM18.1475 9.7372H18.1457V9.73892L18.1475 9.7372ZM18.1457 9.73548C18.2455 9.15774 18.1784 8.56281 17.9514 8.02119C17.7262 7.47956 17.3496 7.01359 16.8682 6.67658C16.3867 6.34128 15.8193 6.1487 15.233 6.12291C14.6449 6.09884 14.0638 6.24155 13.5548 6.53386L10.2345 8.45105C10.2311 8.45334 10.2282 8.45621 10.2259 8.45965L10.2191 8.46996C10.2179 8.4734 10.2168 8.47741 10.2156 8.482C10.2145 8.48544 10.2139 8.48945 10.2139 8.49403V10.0966L14.2237 7.78046C14.2649 7.75639 14.3096 7.7392 14.3543 7.72544C14.4008 7.7134 14.4472 7.70825 14.4936 7.70825C14.5418 7.70825 14.5882 7.7134 14.6346 7.72544C14.6793 7.7392 14.7223 7.75639 14.7636 7.78046L18.0494 9.67874C18.0787 9.69593 18.1217 9.72 18.1457 9.73548ZM9.45735 7.96101C9.45735 7.91458 9.46423 7.86816 9.47627 7.82173C9.4883 7.77702 9.5055 7.73232 9.52957 7.69105C9.55364 7.6515 9.58115 7.61368 9.61554 7.57929C9.64821 7.54662 9.68604 7.51739 9.72731 7.49503L13.0132 5.59848C13.0441 5.57957 13.0871 5.55549 13.1112 5.54346C12.6607 5.1669 12.1105 4.92618 11.5276 4.85224C10.9447 4.77658 10.3532 4.86943 9.82188 5.11875C9.28885 5.36807 8.83835 5.76527 8.52369 6.26047C8.20903 6.75739 8.04224 7.33169 8.04224 7.91974V11.7541C8.04339 11.7587 8.04454 11.7627 8.04568 11.7661C8.04683 11.7696 8.04855 11.773 8.05084 11.7765C8.05313 11.7799 8.056 11.7833 8.05944 11.7868C8.06173 11.7891 8.06517 11.7914 8.06976 11.7937L9.45735 12.5949V7.96101ZM10.2105 13.0282L11.997 14.0599L13.7835 13.0282V10.9666L11.9987 9.93493L10.2122 10.9666L10.2105 13.0282Z", - "fill": "white" - }, - "children": [] - } - ] - }, - "name": "OpenaiTeal" -} diff --git a/web/app/components/base/icons/src/public/llm/OpenaiTeal.tsx b/web/app/components/base/icons/src/public/llm/OpenaiTeal.tsx deleted file mode 100644 index ef803ea52f..0000000000 --- a/web/app/components/base/icons/src/public/llm/OpenaiTeal.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import type { IconData } from '@/app/components/base/icons/IconBase' -import * as React from 'react' -import IconBase from '@/app/components/base/icons/IconBase' -import data from './OpenaiTeal.json' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject> - }, -) => - -Icon.displayName = 'OpenaiTeal' - -export default Icon diff --git a/web/app/components/base/icons/src/public/llm/OpenaiViolet.json b/web/app/components/base/icons/src/public/llm/OpenaiViolet.json deleted file mode 100644 index e80a85507e..0000000000 --- a/web/app/components/base/icons/src/public/llm/OpenaiViolet.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "icon": { - "type": "element", - "isRootNode": true, - "name": "svg", - "attributes": { - "width": "24", - "height": "24", - "viewBox": "0 0 24 24", - "fill": "none", - "xmlns": "http://www.w3.org/2000/svg" - }, - "children": [ - { - "type": "element", - "name": "rect", - "attributes": { - "width": "24", - "height": "24", - "rx": "6", - "fill": "#AB68FF" - }, - "children": [] - }, - { - "type": "element", - "name": "path", - "attributes": { - "d": "M19.7758 11.5959C19.9546 11.9948 20.0681 12.4213 20.1145 12.8563C20.1592 13.2913 20.1369 13.7315 20.044 14.1596C19.9529 14.5878 19.7947 14.9987 19.5746 15.377C19.4302 15.6298 19.2599 15.867 19.0639 16.0854C18.8696 16.3021 18.653 16.4981 18.4174 16.67C18.1801 16.842 17.9274 16.9864 17.6591 17.105C17.3926 17.222 17.1141 17.3114 16.8286 17.3698C16.6945 17.7859 16.4951 18.1797 16.2371 18.5339C15.9809 18.8881 15.6697 19.1993 15.3155 19.4555C14.9613 19.7134 14.5693 19.9129 14.1532 20.047C13.7371 20.1829 13.302 20.2499 12.8636 20.2499C12.573 20.2516 12.2807 20.2207 11.9953 20.1622C11.7116 20.102 11.433 20.0109 11.1665 19.8923C10.9 19.7736 10.6472 19.6258 10.4116 19.4538C10.1778 19.2819 9.96115 19.0841 9.76857 18.8658C9.33871 18.9586 8.89853 18.981 8.46351 18.9363C8.02849 18.8898 7.60207 18.7763 7.20143 18.5975C6.80252 18.4204 6.43284 18.1797 6.10786 17.8857C5.78289 17.5916 5.50606 17.2478 5.28769 16.8695C5.14153 16.6167 5.02117 16.3502 4.93004 16.0734C4.83891 15.7965 4.77873 15.5111 4.74778 15.2205C4.71683 14.9317 4.71855 14.6393 4.7495 14.3488C4.78045 14.0599 4.84407 13.7745 4.9352 13.4976C4.64289 13.1727 4.40217 12.803 4.22335 12.4041C4.04624 12.0034 3.93104 11.5787 3.88634 11.1437C3.83991 10.7087 3.86398 10.2685 3.95511 9.84036C4.04624 9.41222 4.20443 9.00127 4.42452 8.62299C4.56896 8.37023 4.73918 8.13123 4.93348 7.91458C5.12778 7.69793 5.34615 7.50191 5.58171 7.32997C5.81728 7.15802 6.07176 7.01187 6.33827 6.89495C6.6065 6.7763 6.88506 6.68861 7.17048 6.63015C7.3046 6.21232 7.50406 5.82029 7.76026 5.46608C8.01817 5.11188 8.32939 4.80066 8.6836 4.54274C9.03781 4.28654 9.42984 4.08708 9.84595 3.95125C10.2621 3.81713 10.6971 3.74835 11.1355 3.75007C11.4261 3.74835 11.7184 3.77758 12.0039 3.83776C12.2893 3.89794 12.5678 3.98736 12.8344 4.106C13.1009 4.22636 13.3536 4.37251 13.5892 4.54446C13.8248 4.71812 14.0414 4.91414 14.234 5.13251C14.6621 5.04138 15.1023 5.01903 15.5373 5.06373C15.9723 5.10844 16.3971 5.22364 16.7977 5.40074C17.1966 5.57957 17.5663 5.81857 17.8913 6.1126C18.2162 6.4049 18.4931 6.74707 18.7114 7.12707C18.8576 7.37811 18.9779 7.64463 19.0691 7.92318C19.1602 8.20001 19.2221 8.48544 19.2513 8.77602C19.2823 9.06661 19.2823 9.35892 19.2496 9.64951C19.2187 9.94009 19.155 10.2255 19.0639 10.5024C19.3579 10.8273 19.5969 11.1953 19.7758 11.5959ZM14.0466 18.9363C14.4214 18.7815 14.7619 18.5528 15.049 18.2657C15.3362 17.9785 15.5648 17.6381 15.7196 17.2615C15.8743 16.8867 15.9552 16.4843 15.9552 16.0785V12.2442C15.954 12.2407 15.9529 12.2367 15.9517 12.2321C15.9506 12.2287 15.9488 12.2252 15.9466 12.2218C15.9443 12.2184 15.9414 12.2155 15.938 12.2132C15.9345 12.2098 15.9311 12.2075 15.9276 12.2063L14.54 11.4051V16.0373C14.54 16.0837 14.5332 16.1318 14.5211 16.1765C14.5091 16.223 14.4919 16.2659 14.4678 16.3072C14.4438 16.3485 14.4162 16.3863 14.3819 16.419C14.3484 16.4523 14.3109 16.4812 14.2701 16.505L10.9842 18.4015C10.9567 18.4187 10.9103 18.4428 10.8862 18.4565C11.0221 18.5717 11.1699 18.6732 11.3247 18.7626C11.4811 18.852 11.6428 18.9277 11.8113 18.9896C11.9798 19.0497 12.1535 19.0962 12.3288 19.1271C12.5059 19.1581 12.6848 19.1735 12.8636 19.1735C13.2694 19.1735 13.6717 19.0927 14.0466 18.9363ZM6.22135 16.333C6.42596 16.6855 6.69592 16.9916 7.01745 17.2392C7.34071 17.4868 7.70695 17.6673 8.09899 17.7722C8.49102 17.8771 8.90025 17.9046 9.3026 17.8513C9.70495 17.798 10.0918 17.6673 10.4443 17.4644L13.7663 15.5472L13.7749 15.5386C13.7772 15.5363 13.7789 15.5329 13.78 15.5283C13.7823 15.5249 13.7841 15.5214 13.7852 15.518V13.9017L9.77545 16.2212C9.73418 16.2453 9.6912 16.2625 9.64649 16.2763C9.60007 16.2883 9.55364 16.2935 9.5055 16.2935C9.45907 16.2935 9.41265 16.2883 9.36622 16.2763C9.32152 16.2625 9.27681 16.2453 9.23554 16.2212L5.94967 14.323C5.92044 14.3058 5.87746 14.28 5.85339 14.2645C5.82244 14.4416 5.80696 14.6204 5.80696 14.7993C5.80696 14.9781 5.82415 15.1569 5.85511 15.334C5.88605 15.5094 5.9342 15.6831 5.99438 15.8516C6.05628 16.0201 6.13194 16.1817 6.22135 16.3364V16.333ZM5.35818 9.1629C5.15529 9.51539 5.02461 9.90398 4.97131 10.3063C4.918 10.7087 4.94552 11.1162 5.0504 11.51C5.15529 11.902 5.33583 12.2682 5.58343 12.5915C5.83103 12.913 6.13881 13.183 6.48958 13.3859L9.80984 15.3048C9.81328 15.3059 9.81729 15.3071 9.82188 15.3082H9.83391C9.8385 15.3082 9.84251 15.3071 9.84595 15.3048C9.84939 15.3036 9.85283 15.3019 9.85627 15.2996L11.249 14.4949L7.23926 12.1805C7.19971 12.1565 7.16189 12.1272 7.1275 12.0946C7.09418 12.0611 7.06529 12.0236 7.04153 11.9828C7.01917 11.9415 7.00026 11.8985 6.98822 11.8521C6.97619 11.8074 6.96931 11.761 6.97103 11.7128V7.80797C6.80252 7.86987 6.63917 7.94553 6.48442 8.03494C6.32967 8.12607 6.18352 8.22924 6.04596 8.34444C5.91013 8.45965 5.78289 8.58688 5.66769 8.72444C5.55248 8.86028 5.45103 9.00815 5.36162 9.1629H5.35818ZM16.7633 11.8177C16.8046 11.8418 16.8424 11.8693 16.8768 11.9037C16.9094 11.9364 16.9387 11.9742 16.9628 12.0155C16.9851 12.0567 17.004 12.1014 17.0161 12.1461C17.0264 12.1926 17.0332 12.239 17.0315 12.2871V16.192C17.5835 15.9891 18.0649 15.6332 18.4208 15.1655C18.7785 14.6978 18.9934 14.139 19.0433 13.5544C19.0931 12.9698 18.9762 12.3817 18.7046 11.8607C18.4329 11.3397 18.0185 10.9064 17.5095 10.6141L14.1893 8.69521C14.1858 8.69406 14.1818 8.69292 14.1772 8.69177H14.1652C14.1618 8.69292 14.1578 8.69406 14.1532 8.69521C14.1497 8.69636 14.1463 8.69808 14.1429 8.70037L12.757 9.50163L16.7667 11.8177H16.7633ZM18.1475 9.7372H18.1457V9.73892L18.1475 9.7372ZM18.1457 9.73548C18.2455 9.15774 18.1784 8.56281 17.9514 8.02119C17.7262 7.47956 17.3496 7.01359 16.8682 6.67658C16.3867 6.34128 15.8193 6.1487 15.233 6.12291C14.6449 6.09884 14.0638 6.24155 13.5548 6.53386L10.2345 8.45105C10.2311 8.45334 10.2282 8.45621 10.2259 8.45965L10.2191 8.46996C10.2179 8.4734 10.2168 8.47741 10.2156 8.482C10.2145 8.48544 10.2139 8.48945 10.2139 8.49403V10.0966L14.2237 7.78046C14.2649 7.75639 14.3096 7.7392 14.3543 7.72544C14.4008 7.7134 14.4472 7.70825 14.4936 7.70825C14.5418 7.70825 14.5882 7.7134 14.6346 7.72544C14.6793 7.7392 14.7223 7.75639 14.7636 7.78046L18.0494 9.67874C18.0787 9.69593 18.1217 9.72 18.1457 9.73548ZM9.45735 7.96101C9.45735 7.91458 9.46423 7.86816 9.47627 7.82173C9.4883 7.77702 9.5055 7.73232 9.52957 7.69105C9.55364 7.6515 9.58115 7.61368 9.61554 7.57929C9.64821 7.54662 9.68604 7.51739 9.72731 7.49503L13.0132 5.59848C13.0441 5.57957 13.0871 5.55549 13.1112 5.54346C12.6607 5.1669 12.1105 4.92618 11.5276 4.85224C10.9447 4.77658 10.3532 4.86943 9.82188 5.11875C9.28885 5.36807 8.83835 5.76527 8.52369 6.26047C8.20903 6.75739 8.04224 7.33169 8.04224 7.91974V11.7541C8.04339 11.7587 8.04454 11.7627 8.04568 11.7661C8.04683 11.7696 8.04855 11.773 8.05084 11.7765C8.05313 11.7799 8.056 11.7833 8.05944 11.7868C8.06173 11.7891 8.06517 11.7914 8.06976 11.7937L9.45735 12.5949V7.96101ZM10.2105 13.0282L11.997 14.0599L13.7835 13.0282V10.9666L11.9987 9.93493L10.2122 10.9666L10.2105 13.0282Z", - "fill": "white" - }, - "children": [] - } - ] - }, - "name": "OpenaiViolet" -} diff --git a/web/app/components/base/icons/src/public/llm/OpenaiViolet.tsx b/web/app/components/base/icons/src/public/llm/OpenaiViolet.tsx deleted file mode 100644 index 9aa08c0f3b..0000000000 --- a/web/app/components/base/icons/src/public/llm/OpenaiViolet.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import type { IconData } from '@/app/components/base/icons/IconBase' -import * as React from 'react' -import IconBase from '@/app/components/base/icons/IconBase' -import data from './OpenaiViolet.json' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject> - }, -) => - -Icon.displayName = 'OpenaiViolet' - -export default Icon diff --git a/web/app/components/base/icons/src/public/llm/index.ts b/web/app/components/base/icons/src/public/llm/index.ts index dd76c9593b..3a4306391e 100644 --- a/web/app/components/base/icons/src/public/llm/index.ts +++ b/web/app/components/base/icons/src/public/llm/index.ts @@ -26,12 +26,9 @@ export { default as Localai } from './Localai' export { default as LocalaiText } from './LocalaiText' export { default as Microsoft } from './Microsoft' export { default as OpenaiBlack } from './OpenaiBlack' -export { default as OpenaiBlue } from './OpenaiBlue' export { default as OpenaiGreen } from './OpenaiGreen' -export { default as OpenaiTeal } from './OpenaiTeal' export { default as OpenaiText } from './OpenaiText' export { default as OpenaiTransparent } from './OpenaiTransparent' -export { default as OpenaiViolet } from './OpenaiViolet' export { default as OpenaiYellow } from './OpenaiYellow' export { default as Openllm } from './Openllm' export { default as OpenllmText } from './OpenllmText' diff --git a/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx index fb047b3983..997b3aa818 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx @@ -3,7 +3,7 @@ import type { Model, ModelProvider, } from '../declarations' -import { OpenaiBlue, OpenaiTeal, OpenaiViolet, OpenaiYellow } from '@/app/components/base/icons/src/public/llm' +import { OpenaiYellow } from '@/app/components/base/icons/src/public/llm' import { Group } from '@/app/components/base/icons/src/vender/other' import useTheme from '@/hooks/use-theme' import { renderI18nObject } from '@/i18n-config' @@ -29,12 +29,6 @@ const ModelIcon: FC = ({ const language = useLanguage() if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.startsWith('o')) return
- if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.includes('gpt-4.1')) - return
- if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.includes('gpt-4o')) - return
- if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.startsWith('gpt-4')) - return
if (provider?.icon_small) { return ( From c1bb310183d89de08a4e335b9f214288479b9cdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Sat, 3 Jan 2026 01:35:17 +0800 Subject: [PATCH 28/37] chore: remove icon_large of models (#30466) Co-authored-by: zhsama --- api/core/entities/model_entities.py | 3 --- .../model_runtime/entities/provider_entities.py | 3 --- .../model_providers/model_provider_factory.py | 10 ++-------- api/core/provider_manager.py | 1 - .../entities/model_provider_entities.py | 17 ----------------- api/services/model_provider_service.py | 5 +---- .../model_runtime/__mock/plugin_model.py | 4 ---- .../services/test_model_provider_service.py | 8 -------- .../core/test_provider_configuration.py | 1 - .../test_model_provider_service_sanitization.py | 1 - .../debug-with-single-model/index.spec.tsx | 5 ----- .../model-provider-page/declarations.ts | 3 --- .../model-selector/index.spec.tsx | 1 - 13 files changed, 3 insertions(+), 59 deletions(-) diff --git a/api/core/entities/model_entities.py b/api/core/entities/model_entities.py index 12431976f0..a123fb0321 100644 --- a/api/core/entities/model_entities.py +++ b/api/core/entities/model_entities.py @@ -30,7 +30,6 @@ class SimpleModelProviderEntity(BaseModel): label: I18nObject icon_small: I18nObject | None = None icon_small_dark: I18nObject | None = None - icon_large: I18nObject | None = None supported_model_types: list[ModelType] def __init__(self, provider_entity: ProviderEntity): @@ -44,7 +43,6 @@ class SimpleModelProviderEntity(BaseModel): label=provider_entity.label, icon_small=provider_entity.icon_small, icon_small_dark=provider_entity.icon_small_dark, - icon_large=provider_entity.icon_large, supported_model_types=provider_entity.supported_model_types, ) @@ -94,7 +92,6 @@ class DefaultModelProviderEntity(BaseModel): provider: str label: I18nObject icon_small: I18nObject | None = None - icon_large: I18nObject | None = None supported_model_types: Sequence[ModelType] = [] diff --git a/api/core/model_runtime/entities/provider_entities.py b/api/core/model_runtime/entities/provider_entities.py index 648b209ef1..2d88751668 100644 --- a/api/core/model_runtime/entities/provider_entities.py +++ b/api/core/model_runtime/entities/provider_entities.py @@ -100,7 +100,6 @@ class SimpleProviderEntity(BaseModel): label: I18nObject icon_small: I18nObject | None = None icon_small_dark: I18nObject | None = None - icon_large: I18nObject | None = None supported_model_types: Sequence[ModelType] models: list[AIModelEntity] = [] @@ -123,7 +122,6 @@ class ProviderEntity(BaseModel): label: I18nObject description: I18nObject | None = None icon_small: I18nObject | None = None - icon_large: I18nObject | None = None icon_small_dark: I18nObject | None = None background: str | None = None help: ProviderHelpEntity | None = None @@ -157,7 +155,6 @@ class ProviderEntity(BaseModel): provider=self.provider, label=self.label, icon_small=self.icon_small, - icon_large=self.icon_large, supported_model_types=self.supported_model_types, models=self.models, ) diff --git a/api/core/model_runtime/model_providers/model_provider_factory.py b/api/core/model_runtime/model_providers/model_provider_factory.py index b8704ef4ed..12a202ce64 100644 --- a/api/core/model_runtime/model_providers/model_provider_factory.py +++ b/api/core/model_runtime/model_providers/model_provider_factory.py @@ -285,7 +285,7 @@ class ModelProviderFactory: """ Get provider icon :param provider: provider name - :param icon_type: icon type (icon_small or icon_large) + :param icon_type: icon type (icon_small or icon_small_dark) :param lang: language (zh_Hans or en_US) :return: provider icon """ @@ -309,13 +309,7 @@ class ModelProviderFactory: else: file_name = provider_schema.icon_small_dark.en_US else: - if not provider_schema.icon_large: - raise ValueError(f"Provider {provider} does not have large icon.") - - if lang.lower() == "zh_hans": - file_name = provider_schema.icon_large.zh_Hans - else: - file_name = provider_schema.icon_large.en_US + raise ValueError(f"Unsupported icon type: {icon_type}.") if not file_name: raise ValueError(f"Provider {provider} does not have icon.") diff --git a/api/core/provider_manager.py b/api/core/provider_manager.py index 6c818bdc8b..10d86d1762 100644 --- a/api/core/provider_manager.py +++ b/api/core/provider_manager.py @@ -331,7 +331,6 @@ class ProviderManager: provider=provider_schema.provider, label=provider_schema.label, icon_small=provider_schema.icon_small, - icon_large=provider_schema.icon_large, supported_model_types=provider_schema.supported_model_types, ), ) diff --git a/api/services/entities/model_provider_entities.py b/api/services/entities/model_provider_entities.py index f405546909..a29d848ac5 100644 --- a/api/services/entities/model_provider_entities.py +++ b/api/services/entities/model_provider_entities.py @@ -70,7 +70,6 @@ class ProviderResponse(BaseModel): description: I18nObject | None = None icon_small: I18nObject | None = None icon_small_dark: I18nObject | None = None - icon_large: I18nObject | None = None background: str | None = None help: ProviderHelpEntity | None = None supported_model_types: Sequence[ModelType] @@ -98,11 +97,6 @@ class ProviderResponse(BaseModel): en_US=f"{url_prefix}/icon_small_dark/en_US", zh_Hans=f"{url_prefix}/icon_small_dark/zh_Hans", ) - - if self.icon_large is not None: - self.icon_large = I18nObject( - en_US=f"{url_prefix}/icon_large/en_US", zh_Hans=f"{url_prefix}/icon_large/zh_Hans" - ) return self @@ -116,7 +110,6 @@ class ProviderWithModelsResponse(BaseModel): label: I18nObject icon_small: I18nObject | None = None icon_small_dark: I18nObject | None = None - icon_large: I18nObject | None = None status: CustomConfigurationStatus models: list[ProviderModelWithStatusEntity] @@ -134,11 +127,6 @@ class ProviderWithModelsResponse(BaseModel): self.icon_small_dark = I18nObject( en_US=f"{url_prefix}/icon_small_dark/en_US", zh_Hans=f"{url_prefix}/icon_small_dark/zh_Hans" ) - - if self.icon_large is not None: - self.icon_large = I18nObject( - en_US=f"{url_prefix}/icon_large/en_US", zh_Hans=f"{url_prefix}/icon_large/zh_Hans" - ) return self @@ -163,11 +151,6 @@ class SimpleProviderEntityResponse(SimpleProviderEntity): self.icon_small_dark = I18nObject( en_US=f"{url_prefix}/icon_small_dark/en_US", zh_Hans=f"{url_prefix}/icon_small_dark/zh_Hans" ) - - if self.icon_large is not None: - self.icon_large = I18nObject( - en_US=f"{url_prefix}/icon_large/en_US", zh_Hans=f"{url_prefix}/icon_large/zh_Hans" - ) return self diff --git a/api/services/model_provider_service.py b/api/services/model_provider_service.py index eea382febe..edd1004b82 100644 --- a/api/services/model_provider_service.py +++ b/api/services/model_provider_service.py @@ -99,7 +99,6 @@ class ModelProviderService: description=provider_configuration.provider.description, icon_small=provider_configuration.provider.icon_small, icon_small_dark=provider_configuration.provider.icon_small_dark, - icon_large=provider_configuration.provider.icon_large, background=provider_configuration.provider.background, help=provider_configuration.provider.help, supported_model_types=provider_configuration.provider.supported_model_types, @@ -423,7 +422,6 @@ class ModelProviderService: label=first_model.provider.label, icon_small=first_model.provider.icon_small, icon_small_dark=first_model.provider.icon_small_dark, - icon_large=first_model.provider.icon_large, status=CustomConfigurationStatus.ACTIVE, models=[ ProviderModelWithStatusEntity( @@ -488,7 +486,6 @@ class ModelProviderService: provider=result.provider.provider, label=result.provider.label, icon_small=result.provider.icon_small, - icon_large=result.provider.icon_large, supported_model_types=result.provider.supported_model_types, ), ) @@ -522,7 +519,7 @@ class ModelProviderService: :param tenant_id: workspace id :param provider: provider name - :param icon_type: icon type (icon_small or icon_large) + :param icon_type: icon type (icon_small or icon_small_dark) :param lang: language (zh_Hans or en_US) :return: """ diff --git a/api/tests/integration_tests/model_runtime/__mock/plugin_model.py b/api/tests/integration_tests/model_runtime/__mock/plugin_model.py index d59d5dc0fe..5012defdad 100644 --- a/api/tests/integration_tests/model_runtime/__mock/plugin_model.py +++ b/api/tests/integration_tests/model_runtime/__mock/plugin_model.py @@ -48,10 +48,6 @@ class MockModelClass(PluginModelClient): en_US="https://example.com/icon_small.png", zh_Hans="https://example.com/icon_small.png", ), - icon_large=I18nObject( - en_US="https://example.com/icon_large.png", - zh_Hans="https://example.com/icon_large.png", - ), supported_model_types=[ModelType.LLM], configurate_methods=[ConfigurateMethod.PREDEFINED_MODEL], models=[ diff --git a/api/tests/test_containers_integration_tests/services/test_model_provider_service.py b/api/tests/test_containers_integration_tests/services/test_model_provider_service.py index 612210ef86..d57ab7428b 100644 --- a/api/tests/test_containers_integration_tests/services/test_model_provider_service.py +++ b/api/tests/test_containers_integration_tests/services/test_model_provider_service.py @@ -228,7 +228,6 @@ class TestModelProviderService: mock_provider_entity.description = {"en_US": "OpenAI provider", "zh_Hans": "OpenAI 提供商"} mock_provider_entity.icon_small = {"en_US": "icon_small.png", "zh_Hans": "icon_small.png"} mock_provider_entity.icon_small_dark = None - mock_provider_entity.icon_large = {"en_US": "icon_large.png", "zh_Hans": "icon_large.png"} mock_provider_entity.background = "#FF6B6B" mock_provider_entity.help = None mock_provider_entity.supported_model_types = [ModelType.LLM, ModelType.TEXT_EMBEDDING] @@ -302,7 +301,6 @@ class TestModelProviderService: mock_provider_entity_llm.description = {"en_US": "OpenAI provider", "zh_Hans": "OpenAI 提供商"} mock_provider_entity_llm.icon_small = {"en_US": "icon_small.png", "zh_Hans": "icon_small.png"} mock_provider_entity_llm.icon_small_dark = None - mock_provider_entity_llm.icon_large = {"en_US": "icon_large.png", "zh_Hans": "icon_large.png"} mock_provider_entity_llm.background = "#FF6B6B" mock_provider_entity_llm.help = None mock_provider_entity_llm.supported_model_types = [ModelType.LLM] @@ -316,7 +314,6 @@ class TestModelProviderService: mock_provider_entity_embedding.description = {"en_US": "Cohere provider", "zh_Hans": "Cohere 提供商"} mock_provider_entity_embedding.icon_small = {"en_US": "icon_small.png", "zh_Hans": "icon_small.png"} mock_provider_entity_embedding.icon_small_dark = None - mock_provider_entity_embedding.icon_large = {"en_US": "icon_large.png", "zh_Hans": "icon_large.png"} mock_provider_entity_embedding.background = "#4ECDC4" mock_provider_entity_embedding.help = None mock_provider_entity_embedding.supported_model_types = [ModelType.TEXT_EMBEDDING] @@ -419,7 +416,6 @@ class TestModelProviderService: provider="openai", label=I18nObject(en_US="OpenAI", zh_Hans="OpenAI"), icon_small=I18nObject(en_US="icon_small.png", zh_Hans="icon_small.png"), - icon_large=I18nObject(en_US="icon_large.png", zh_Hans="icon_large.png"), supported_model_types=[ModelType.LLM], configurate_methods=[], models=[], @@ -431,7 +427,6 @@ class TestModelProviderService: provider="openai", label=I18nObject(en_US="OpenAI", zh_Hans="OpenAI"), icon_small=I18nObject(en_US="icon_small.png", zh_Hans="icon_small.png"), - icon_large=I18nObject(en_US="icon_large.png", zh_Hans="icon_large.png"), supported_model_types=[ModelType.LLM], configurate_methods=[], models=[], @@ -655,7 +650,6 @@ class TestModelProviderService: provider="openai", label=I18nObject(en_US="OpenAI", zh_Hans="OpenAI"), icon_small=I18nObject(en_US="icon_small.png", zh_Hans="icon_small.png"), - icon_large=I18nObject(en_US="icon_large.png", zh_Hans="icon_large.png"), supported_model_types=[ModelType.LLM], ), ) @@ -1027,7 +1021,6 @@ class TestModelProviderService: label={"en_US": "OpenAI", "zh_Hans": "OpenAI"}, icon_small={"en_US": "icon_small.png", "zh_Hans": "icon_small.png"}, icon_small_dark=None, - icon_large={"en_US": "icon_large.png", "zh_Hans": "icon_large.png"}, ), model="gpt-3.5-turbo", model_type=ModelType.LLM, @@ -1045,7 +1038,6 @@ class TestModelProviderService: label={"en_US": "OpenAI", "zh_Hans": "OpenAI"}, icon_small={"en_US": "icon_small.png", "zh_Hans": "icon_small.png"}, icon_small_dark=None, - icon_large={"en_US": "icon_large.png", "zh_Hans": "icon_large.png"}, ), model="gpt-4", model_type=ModelType.LLM, diff --git a/api/tests/unit_tests/core/test_provider_configuration.py b/api/tests/unit_tests/core/test_provider_configuration.py index 9060cf7b6c..636fac7a40 100644 --- a/api/tests/unit_tests/core/test_provider_configuration.py +++ b/api/tests/unit_tests/core/test_provider_configuration.py @@ -32,7 +32,6 @@ def mock_provider_entity(): label=I18nObject(en_US="OpenAI", zh_Hans="OpenAI"), description=I18nObject(en_US="OpenAI provider", zh_Hans="OpenAI 提供商"), icon_small=I18nObject(en_US="icon.png", zh_Hans="icon.png"), - icon_large=I18nObject(en_US="icon.png", zh_Hans="icon.png"), background="background.png", help=None, supported_model_types=[ModelType.LLM], diff --git a/api/tests/unit_tests/services/test_model_provider_service_sanitization.py b/api/tests/unit_tests/services/test_model_provider_service_sanitization.py index 9a107da1c7..e2360b116d 100644 --- a/api/tests/unit_tests/services/test_model_provider_service_sanitization.py +++ b/api/tests/unit_tests/services/test_model_provider_service_sanitization.py @@ -27,7 +27,6 @@ def service_with_fake_configurations(): description=None, icon_small=None, icon_small_dark=None, - icon_large=None, background=None, help=None, supported_model_types=[ModelType.LLM], diff --git a/web/app/components/app/configuration/debug/debug-with-single-model/index.spec.tsx b/web/app/components/app/configuration/debug/debug-with-single-model/index.spec.tsx index c1793e33ca..151038d787 100644 --- a/web/app/components/app/configuration/debug/debug-with-single-model/index.spec.tsx +++ b/web/app/components/app/configuration/debug/debug-with-single-model/index.spec.tsx @@ -93,7 +93,6 @@ function createMockProviderContext(overrides: Partial = {} provider: 'openai', label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' }, icon_small: { en_US: 'icon', zh_Hans: 'icon' }, - icon_large: { en_US: 'icon', zh_Hans: 'icon' }, status: ModelStatusEnum.active, models: [ { @@ -711,7 +710,6 @@ describe('DebugWithSingleModel', () => { provider: 'openai', label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' }, icon_small: { en_US: 'icon', zh_Hans: 'icon' }, - icon_large: { en_US: 'icon', zh_Hans: 'icon' }, status: ModelStatusEnum.active, models: [ { @@ -742,7 +740,6 @@ describe('DebugWithSingleModel', () => { provider: 'different-provider', label: { en_US: 'Different Provider', zh_Hans: '不同提供商' }, icon_small: { en_US: 'icon', zh_Hans: 'icon' }, - icon_large: { en_US: 'icon', zh_Hans: 'icon' }, status: ModelStatusEnum.active, models: [], }, @@ -925,7 +922,6 @@ describe('DebugWithSingleModel', () => { provider: 'openai', label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' }, icon_small: { en_US: 'icon', zh_Hans: 'icon' }, - icon_large: { en_US: 'icon', zh_Hans: 'icon' }, status: ModelStatusEnum.active, models: [ { @@ -975,7 +971,6 @@ describe('DebugWithSingleModel', () => { provider: 'openai', label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' }, icon_small: { en_US: 'icon', zh_Hans: 'icon' }, - icon_large: { en_US: 'icon', zh_Hans: 'icon' }, status: ModelStatusEnum.active, models: [ { diff --git a/web/app/components/header/account-setting/model-provider-page/declarations.ts b/web/app/components/header/account-setting/model-provider-page/declarations.ts index a5dd46b59f..6c961637cd 100644 --- a/web/app/components/header/account-setting/model-provider-page/declarations.ts +++ b/web/app/components/header/account-setting/model-provider-page/declarations.ts @@ -218,7 +218,6 @@ export type ModelProvider = { } icon_small: TypeWithI18N icon_small_dark?: TypeWithI18N - icon_large: TypeWithI18N background?: string supported_model_types: ModelTypeEnum[] configurate_methods: ConfigurationMethodEnum[] @@ -254,7 +253,6 @@ export type ModelProvider = { export type Model = { provider: string - icon_large: TypeWithI18N icon_small: TypeWithI18N icon_small_dark?: TypeWithI18N label: TypeWithI18N @@ -267,7 +265,6 @@ export type DefaultModelResponse = { model_type: ModelTypeEnum provider: { provider: string - icon_large: TypeWithI18N icon_small: TypeWithI18N } } diff --git a/web/app/components/plugins/plugin-detail-panel/model-selector/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/model-selector/index.spec.tsx index ea7a9dca8b..91c978ad7d 100644 --- a/web/app/components/plugins/plugin-detail-panel/model-selector/index.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/model-selector/index.spec.tsx @@ -219,7 +219,6 @@ const createModelItem = (overrides: Partial = {}): ModelItem => ({ */ const createModel = (overrides: Partial = {}): Model => ({ provider: 'openai', - icon_large: { en_US: 'icon-large.png', zh_Hans: 'icon-large.png' }, icon_small: { en_US: 'icon-small.png', zh_Hans: 'icon-small.png' }, label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' }, models: [createModelItem()], From 9a22baf57d69785938fcf7be4a85e37286619c6c Mon Sep 17 00:00:00 2001 From: longbingljw Date: Sat, 3 Jan 2026 20:33:20 +0800 Subject: [PATCH 29/37] feat: optimize for migration versions (#28787) Co-authored-by: -LAN- --- ...ef91f18_rename_api_provider_description.py | 29 ++--- ...84c228_remove_tool_id_from_model_invoke.py | 14 +-- ...c1af8d_add_dataset_permission_tenant_id.py | 16 +-- ...-6af6a521a53e_update_retrieval_resource.py | 73 ++++--------- ...3f6769a94a3_add_upload_files_source_url.py | 1 - ...pdate_type_of_custom_disclaimer_to_text.py | 100 ++++++------------ ...9b_update_workflows_graph_features_and_.py | 66 ++++-------- ...5fa_add_provider_model_multi_credential.py | 83 +++++---------- ...47-8d289573e1da_add_oauth_provider_apps.py | 1 - ...20211f18133_add_headers_to_mcp_provider.py | 9 +- ...68519ad5cd18_knowledge_pipeline_migrate.py | 12 ++- ...662b25d9bc_remove_builtin_template_user.py | 16 +-- ...11-03f8dcbc611e_add_workflowpause_model.py | 1 - ..._30_1518-669ffd70119c_introduce_trigger.py | 6 ++ ...9d_add_message_files_into_agent_thought.py | 13 +-- .../246ba09cbbdb_add_app_anntation_setting.py | 10 +- .../versions/2a3aebbbf4bb_add_app_tracing.py | 13 +-- ...2e9819ca5b28_add_tenant_id_in_api_token.py | 36 ++----- ...5564d_conversation_columns_set_nullable.py | 72 ++++--------- ...fee_change_message_chain_id_to_nullable.py | 42 ++------ ...nable_tool_file_without_conversation_id.py | 36 ++----- ...fb077b04_add_dataset_collection_binding.py | 9 +- ...39_add_anntation_history_match_response.py | 16 +-- ...3755c_add_app_config_retriever_resource.py | 13 +-- .../7ce5a52e4eee_add_tool_providers.py | 11 +- ...8072f0caa04_add_custom_config_in_tenant.py | 13 +-- api/migrations/versions/89c7899ca936_.py | 41 ++----- ...6f3c800_rename_api_provider_credentails.py | 13 +-- .../8fe468ba0ca5_add_gpt4v_supports.py | 9 +- .../9f4e3427ea84_add_created_by_role.py | 2 - ...56fb053ef_app_config_add_speech_to_text.py | 13 +-- ...e_add_external_data_tools_in_app_model_.py | 13 +-- api/migrations/versions/b24be59fbb04_.py | 13 +-- ...09c049e8e_add_advanced_prompt_templates.py | 22 +--- ...9_remove_app_model_config_trace_config_.py | 1 - .../e1901f623fd0_add_annotation_reply.py | 68 ++++-------- ...85e260_add_anntation_history_message_id.py | 16 +-- 37 files changed, 244 insertions(+), 678 deletions(-) diff --git a/api/migrations/versions/00bacef91f18_rename_api_provider_description.py b/api/migrations/versions/00bacef91f18_rename_api_provider_description.py index 17ed067d81..657d28f896 100644 --- a/api/migrations/versions/00bacef91f18_rename_api_provider_description.py +++ b/api/migrations/versions/00bacef91f18_rename_api_provider_description.py @@ -11,9 +11,6 @@ from alembic import op import models.types -def _is_pg(conn): - return conn.dialect.name == "postgresql" - # revision identifiers, used by Alembic. revision = '00bacef91f18' down_revision = '8ec536f3c800' @@ -23,31 +20,17 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('tool_api_providers', schema=None) as batch_op: - batch_op.add_column(sa.Column('description', sa.Text(), nullable=False)) - batch_op.drop_column('description_str') - else: - with op.batch_alter_table('tool_api_providers', schema=None) as batch_op: - batch_op.add_column(sa.Column('description', models.types.LongText(), nullable=False)) - batch_op.drop_column('description_str') + with op.batch_alter_table('tool_api_providers', schema=None) as batch_op: + batch_op.add_column(sa.Column('description', models.types.LongText(), nullable=False)) + batch_op.drop_column('description_str') # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('tool_api_providers', schema=None) as batch_op: - batch_op.add_column(sa.Column('description_str', sa.TEXT(), autoincrement=False, nullable=False)) - batch_op.drop_column('description') - else: - with op.batch_alter_table('tool_api_providers', schema=None) as batch_op: - batch_op.add_column(sa.Column('description_str', models.types.LongText(), autoincrement=False, nullable=False)) - batch_op.drop_column('description') + with op.batch_alter_table('tool_api_providers', schema=None) as batch_op: + batch_op.add_column(sa.Column('description_str', models.types.LongText(), autoincrement=False, nullable=False)) + batch_op.drop_column('description') # ### end Alembic commands ### diff --git a/api/migrations/versions/114eed84c228_remove_tool_id_from_model_invoke.py b/api/migrations/versions/114eed84c228_remove_tool_id_from_model_invoke.py index ed70bf5d08..912d9dbfa4 100644 --- a/api/migrations/versions/114eed84c228_remove_tool_id_from_model_invoke.py +++ b/api/migrations/versions/114eed84c228_remove_tool_id_from_model_invoke.py @@ -7,14 +7,10 @@ Create Date: 2024-01-10 04:40:57.257824 """ import sqlalchemy as sa from alembic import op -from sqlalchemy.dialects import postgresql import models.types -def _is_pg(conn): - return conn.dialect.name == "postgresql" - # revision identifiers, used by Alembic. revision = '114eed84c228' down_revision = 'c71211c8f604' @@ -32,13 +28,7 @@ def upgrade(): def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('tool_model_invokes', schema=None) as batch_op: - batch_op.add_column(sa.Column('tool_id', postgresql.UUID(), autoincrement=False, nullable=False)) - else: - with op.batch_alter_table('tool_model_invokes', schema=None) as batch_op: - batch_op.add_column(sa.Column('tool_id', models.types.StringUUID(), autoincrement=False, nullable=False)) + with op.batch_alter_table('tool_model_invokes', schema=None) as batch_op: + batch_op.add_column(sa.Column('tool_id', models.types.StringUUID(), autoincrement=False, nullable=False)) # ### end Alembic commands ### diff --git a/api/migrations/versions/161cadc1af8d_add_dataset_permission_tenant_id.py b/api/migrations/versions/161cadc1af8d_add_dataset_permission_tenant_id.py index 509bd5d0e8..0ca905129d 100644 --- a/api/migrations/versions/161cadc1af8d_add_dataset_permission_tenant_id.py +++ b/api/migrations/versions/161cadc1af8d_add_dataset_permission_tenant_id.py @@ -11,9 +11,6 @@ from alembic import op import models.types -def _is_pg(conn): - return conn.dialect.name == "postgresql" - # revision identifiers, used by Alembic. revision = '161cadc1af8d' down_revision = '7e6a8693e07a' @@ -23,16 +20,9 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('dataset_permissions', schema=None) as batch_op: - # Step 1: Add column without NOT NULL constraint - op.add_column('dataset_permissions', sa.Column('tenant_id', sa.UUID(), nullable=False)) - else: - with op.batch_alter_table('dataset_permissions', schema=None) as batch_op: - # Step 1: Add column without NOT NULL constraint - op.add_column('dataset_permissions', sa.Column('tenant_id', models.types.StringUUID(), nullable=False)) + with op.batch_alter_table('dataset_permissions', schema=None) as batch_op: + # Step 1: Add column without NOT NULL constraint + op.add_column('dataset_permissions', sa.Column('tenant_id', models.types.StringUUID(), nullable=False)) # ### end Alembic commands ### diff --git a/api/migrations/versions/2024_09_24_0922-6af6a521a53e_update_retrieval_resource.py b/api/migrations/versions/2024_09_24_0922-6af6a521a53e_update_retrieval_resource.py index 0767b725f6..be1b42f883 100644 --- a/api/migrations/versions/2024_09_24_0922-6af6a521a53e_update_retrieval_resource.py +++ b/api/migrations/versions/2024_09_24_0922-6af6a521a53e_update_retrieval_resource.py @@ -9,11 +9,6 @@ from alembic import op import models.types -def _is_pg(conn): - return conn.dialect.name == "postgresql" -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - # revision identifiers, used by Alembic. revision = '6af6a521a53e' down_revision = 'd57ba9ebb251' @@ -23,58 +18,30 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('dataset_retriever_resources', schema=None) as batch_op: - batch_op.alter_column('document_id', - existing_type=sa.UUID(), - nullable=True) - batch_op.alter_column('data_source_type', - existing_type=sa.TEXT(), - nullable=True) - batch_op.alter_column('segment_id', - existing_type=sa.UUID(), - nullable=True) - else: - with op.batch_alter_table('dataset_retriever_resources', schema=None) as batch_op: - batch_op.alter_column('document_id', - existing_type=models.types.StringUUID(), - nullable=True) - batch_op.alter_column('data_source_type', - existing_type=models.types.LongText(), - nullable=True) - batch_op.alter_column('segment_id', - existing_type=models.types.StringUUID(), - nullable=True) + with op.batch_alter_table('dataset_retriever_resources', schema=None) as batch_op: + batch_op.alter_column('document_id', + existing_type=models.types.StringUUID(), + nullable=True) + batch_op.alter_column('data_source_type', + existing_type=models.types.LongText(), + nullable=True) + batch_op.alter_column('segment_id', + existing_type=models.types.StringUUID(), + nullable=True) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('dataset_retriever_resources', schema=None) as batch_op: - batch_op.alter_column('segment_id', - existing_type=sa.UUID(), - nullable=False) - batch_op.alter_column('data_source_type', - existing_type=sa.TEXT(), - nullable=False) - batch_op.alter_column('document_id', - existing_type=sa.UUID(), - nullable=False) - else: - with op.batch_alter_table('dataset_retriever_resources', schema=None) as batch_op: - batch_op.alter_column('segment_id', - existing_type=models.types.StringUUID(), - nullable=False) - batch_op.alter_column('data_source_type', - existing_type=models.types.LongText(), - nullable=False) - batch_op.alter_column('document_id', - existing_type=models.types.StringUUID(), - nullable=False) + with op.batch_alter_table('dataset_retriever_resources', schema=None) as batch_op: + batch_op.alter_column('segment_id', + existing_type=models.types.StringUUID(), + nullable=False) + batch_op.alter_column('data_source_type', + existing_type=models.types.LongText(), + nullable=False) + batch_op.alter_column('document_id', + existing_type=models.types.StringUUID(), + nullable=False) # ### end Alembic commands ### diff --git a/api/migrations/versions/2024_11_01_0434-d3f6769a94a3_add_upload_files_source_url.py b/api/migrations/versions/2024_11_01_0434-d3f6769a94a3_add_upload_files_source_url.py index a749c8bddf..5d12419bf7 100644 --- a/api/migrations/versions/2024_11_01_0434-d3f6769a94a3_add_upload_files_source_url.py +++ b/api/migrations/versions/2024_11_01_0434-d3f6769a94a3_add_upload_files_source_url.py @@ -8,7 +8,6 @@ Create Date: 2024-11-01 04:34:23.816198 from alembic import op import models as models import sqlalchemy as sa -from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. revision = 'd3f6769a94a3' diff --git a/api/migrations/versions/2024_11_01_0622-d07474999927_update_type_of_custom_disclaimer_to_text.py b/api/migrations/versions/2024_11_01_0622-d07474999927_update_type_of_custom_disclaimer_to_text.py index 45842295ea..a49d6a52f6 100644 --- a/api/migrations/versions/2024_11_01_0622-d07474999927_update_type_of_custom_disclaimer_to_text.py +++ b/api/migrations/versions/2024_11_01_0622-d07474999927_update_type_of_custom_disclaimer_to_text.py @@ -28,85 +28,45 @@ def upgrade(): op.execute("UPDATE sites SET custom_disclaimer = '' WHERE custom_disclaimer IS NULL") op.execute("UPDATE tool_api_providers SET custom_disclaimer = '' WHERE custom_disclaimer IS NULL") - if _is_pg(conn): - with op.batch_alter_table('recommended_apps', schema=None) as batch_op: - batch_op.alter_column('custom_disclaimer', - existing_type=sa.VARCHAR(length=255), - type_=sa.TEXT(), - nullable=False) + with op.batch_alter_table('recommended_apps', schema=None) as batch_op: + batch_op.alter_column('custom_disclaimer', + existing_type=sa.VARCHAR(length=255), + type_=models.types.LongText(), + nullable=False) - with op.batch_alter_table('sites', schema=None) as batch_op: - batch_op.alter_column('custom_disclaimer', - existing_type=sa.VARCHAR(length=255), - type_=sa.TEXT(), - nullable=False) + with op.batch_alter_table('sites', schema=None) as batch_op: + batch_op.alter_column('custom_disclaimer', + existing_type=sa.VARCHAR(length=255), + type_=models.types.LongText(), + nullable=False) - with op.batch_alter_table('tool_api_providers', schema=None) as batch_op: - batch_op.alter_column('custom_disclaimer', - existing_type=sa.VARCHAR(length=255), - type_=sa.TEXT(), - nullable=False) - else: - with op.batch_alter_table('recommended_apps', schema=None) as batch_op: - batch_op.alter_column('custom_disclaimer', - existing_type=sa.VARCHAR(length=255), - type_=models.types.LongText(), - nullable=False) - - with op.batch_alter_table('sites', schema=None) as batch_op: - batch_op.alter_column('custom_disclaimer', - existing_type=sa.VARCHAR(length=255), - type_=models.types.LongText(), - nullable=False) - - with op.batch_alter_table('tool_api_providers', schema=None) as batch_op: - batch_op.alter_column('custom_disclaimer', - existing_type=sa.VARCHAR(length=255), - type_=models.types.LongText(), - nullable=False) + with op.batch_alter_table('tool_api_providers', schema=None) as batch_op: + batch_op.alter_column('custom_disclaimer', + existing_type=sa.VARCHAR(length=255), + type_=models.types.LongText(), + nullable=False) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('tool_api_providers', schema=None) as batch_op: - batch_op.alter_column('custom_disclaimer', - existing_type=sa.TEXT(), - type_=sa.VARCHAR(length=255), - nullable=True) + with op.batch_alter_table('tool_api_providers', schema=None) as batch_op: + batch_op.alter_column('custom_disclaimer', + existing_type=models.types.LongText(), + type_=sa.VARCHAR(length=255), + nullable=True) - with op.batch_alter_table('sites', schema=None) as batch_op: - batch_op.alter_column('custom_disclaimer', - existing_type=sa.TEXT(), - type_=sa.VARCHAR(length=255), - nullable=True) + with op.batch_alter_table('sites', schema=None) as batch_op: + batch_op.alter_column('custom_disclaimer', + existing_type=models.types.LongText(), + type_=sa.VARCHAR(length=255), + nullable=True) - with op.batch_alter_table('recommended_apps', schema=None) as batch_op: - batch_op.alter_column('custom_disclaimer', - existing_type=sa.TEXT(), - type_=sa.VARCHAR(length=255), - nullable=True) - else: - with op.batch_alter_table('tool_api_providers', schema=None) as batch_op: - batch_op.alter_column('custom_disclaimer', - existing_type=models.types.LongText(), - type_=sa.VARCHAR(length=255), - nullable=True) - - with op.batch_alter_table('sites', schema=None) as batch_op: - batch_op.alter_column('custom_disclaimer', - existing_type=models.types.LongText(), - type_=sa.VARCHAR(length=255), - nullable=True) - - with op.batch_alter_table('recommended_apps', schema=None) as batch_op: - batch_op.alter_column('custom_disclaimer', - existing_type=models.types.LongText(), - type_=sa.VARCHAR(length=255), - nullable=True) + with op.batch_alter_table('recommended_apps', schema=None) as batch_op: + batch_op.alter_column('custom_disclaimer', + existing_type=models.types.LongText(), + type_=sa.VARCHAR(length=255), + nullable=True) # ### end Alembic commands ### diff --git a/api/migrations/versions/2024_11_01_0623-09a8d1878d9b_update_workflows_graph_features_and_.py b/api/migrations/versions/2024_11_01_0623-09a8d1878d9b_update_workflows_graph_features_and_.py index fdd8984029..8a36c9c4a5 100644 --- a/api/migrations/versions/2024_11_01_0623-09a8d1878d9b_update_workflows_graph_features_and_.py +++ b/api/migrations/versions/2024_11_01_0623-09a8d1878d9b_update_workflows_graph_features_and_.py @@ -49,57 +49,33 @@ def upgrade(): op.execute("UPDATE workflows SET updated_at = created_at WHERE updated_at IS NULL") op.execute("UPDATE workflows SET graph = '' WHERE graph IS NULL") op.execute("UPDATE workflows SET features = '' WHERE features IS NULL") - if _is_pg(conn): - with op.batch_alter_table('workflows', schema=None) as batch_op: - batch_op.alter_column('graph', - existing_type=sa.TEXT(), - nullable=False) - batch_op.alter_column('features', - existing_type=sa.TEXT(), - nullable=False) - batch_op.alter_column('updated_at', - existing_type=postgresql.TIMESTAMP(), - nullable=False) - else: - with op.batch_alter_table('workflows', schema=None) as batch_op: - batch_op.alter_column('graph', - existing_type=models.types.LongText(), - nullable=False) - batch_op.alter_column('features', - existing_type=models.types.LongText(), - nullable=False) - batch_op.alter_column('updated_at', - existing_type=sa.TIMESTAMP(), - nullable=False) + + with op.batch_alter_table('workflows', schema=None) as batch_op: + batch_op.alter_column('graph', + existing_type=models.types.LongText(), + nullable=False) + batch_op.alter_column('features', + existing_type=models.types.LongText(), + nullable=False) + batch_op.alter_column('updated_at', + existing_type=sa.TIMESTAMP(), + nullable=False) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('workflows', schema=None) as batch_op: - batch_op.alter_column('updated_at', - existing_type=postgresql.TIMESTAMP(), - nullable=True) - batch_op.alter_column('features', - existing_type=sa.TEXT(), - nullable=True) - batch_op.alter_column('graph', - existing_type=sa.TEXT(), - nullable=True) - else: - with op.batch_alter_table('workflows', schema=None) as batch_op: - batch_op.alter_column('updated_at', - existing_type=sa.TIMESTAMP(), - nullable=True) - batch_op.alter_column('features', - existing_type=models.types.LongText(), - nullable=True) - batch_op.alter_column('graph', - existing_type=models.types.LongText(), - nullable=True) + with op.batch_alter_table('workflows', schema=None) as batch_op: + batch_op.alter_column('updated_at', + existing_type=sa.TIMESTAMP(), + nullable=True) + batch_op.alter_column('features', + existing_type=models.types.LongText(), + nullable=True) + batch_op.alter_column('graph', + existing_type=models.types.LongText(), + nullable=True) if _is_pg(conn): with op.batch_alter_table('messages', schema=None) as batch_op: diff --git a/api/migrations/versions/2025_08_13_1605-0e154742a5fa_add_provider_model_multi_credential.py b/api/migrations/versions/2025_08_13_1605-0e154742a5fa_add_provider_model_multi_credential.py index 16ca902726..1fc4a64df1 100644 --- a/api/migrations/versions/2025_08_13_1605-0e154742a5fa_add_provider_model_multi_credential.py +++ b/api/migrations/versions/2025_08_13_1605-0e154742a5fa_add_provider_model_multi_credential.py @@ -86,57 +86,30 @@ def upgrade(): def migrate_existing_provider_models_data(): """migrate provider_models table data to provider_model_credentials""" - conn = op.get_bind() - # Define table structure for data manipulation - if _is_pg(conn): - provider_models_table = table('provider_models', - column('id', models.types.StringUUID()), - column('tenant_id', models.types.StringUUID()), - column('provider_name', sa.String()), - column('model_name', sa.String()), - column('model_type', sa.String()), - column('encrypted_config', sa.Text()), - column('created_at', sa.DateTime()), - column('updated_at', sa.DateTime()), - column('credential_id', models.types.StringUUID()), - ) - else: - provider_models_table = table('provider_models', - column('id', models.types.StringUUID()), - column('tenant_id', models.types.StringUUID()), - column('provider_name', sa.String()), - column('model_name', sa.String()), - column('model_type', sa.String()), - column('encrypted_config', models.types.LongText()), - column('created_at', sa.DateTime()), - column('updated_at', sa.DateTime()), - column('credential_id', models.types.StringUUID()), - ) + # Define table structure for data manipulatio + provider_models_table = table('provider_models', + column('id', models.types.StringUUID()), + column('tenant_id', models.types.StringUUID()), + column('provider_name', sa.String()), + column('model_name', sa.String()), + column('model_type', sa.String()), + column('encrypted_config', models.types.LongText()), + column('created_at', sa.DateTime()), + column('updated_at', sa.DateTime()), + column('credential_id', models.types.StringUUID()), + ) - if _is_pg(conn): - provider_model_credentials_table = table('provider_model_credentials', - column('id', models.types.StringUUID()), - column('tenant_id', models.types.StringUUID()), - column('provider_name', sa.String()), - column('model_name', sa.String()), - column('model_type', sa.String()), - column('credential_name', sa.String()), - column('encrypted_config', sa.Text()), - column('created_at', sa.DateTime()), - column('updated_at', sa.DateTime()) - ) - else: - provider_model_credentials_table = table('provider_model_credentials', - column('id', models.types.StringUUID()), - column('tenant_id', models.types.StringUUID()), - column('provider_name', sa.String()), - column('model_name', sa.String()), - column('model_type', sa.String()), - column('credential_name', sa.String()), - column('encrypted_config', models.types.LongText()), - column('created_at', sa.DateTime()), - column('updated_at', sa.DateTime()) - ) + provider_model_credentials_table = table('provider_model_credentials', + column('id', models.types.StringUUID()), + column('tenant_id', models.types.StringUUID()), + column('provider_name', sa.String()), + column('model_name', sa.String()), + column('model_type', sa.String()), + column('credential_name', sa.String()), + column('encrypted_config', models.types.LongText()), + column('created_at', sa.DateTime()), + column('updated_at', sa.DateTime()) + ) # Get database connection @@ -183,14 +156,8 @@ def migrate_existing_provider_models_data(): def downgrade(): # Re-add encrypted_config column to provider_models table - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('provider_models', schema=None) as batch_op: - batch_op.add_column(sa.Column('encrypted_config', sa.Text(), nullable=True)) - else: - with op.batch_alter_table('provider_models', schema=None) as batch_op: - batch_op.add_column(sa.Column('encrypted_config', models.types.LongText(), nullable=True)) + with op.batch_alter_table('provider_models', schema=None) as batch_op: + batch_op.add_column(sa.Column('encrypted_config', models.types.LongText(), nullable=True)) if not context.is_offline_mode(): # Migrate data back from provider_model_credentials to provider_models diff --git a/api/migrations/versions/2025_08_20_1747-8d289573e1da_add_oauth_provider_apps.py b/api/migrations/versions/2025_08_20_1747-8d289573e1da_add_oauth_provider_apps.py index 75b4d61173..79fe9d9bba 100644 --- a/api/migrations/versions/2025_08_20_1747-8d289573e1da_add_oauth_provider_apps.py +++ b/api/migrations/versions/2025_08_20_1747-8d289573e1da_add_oauth_provider_apps.py @@ -8,7 +8,6 @@ Create Date: 2025-08-20 17:47:17.015695 from alembic import op import models as models import sqlalchemy as sa -from libs.uuid_utils import uuidv7 def _is_pg(conn): diff --git a/api/migrations/versions/2025_09_08_1007-c20211f18133_add_headers_to_mcp_provider.py b/api/migrations/versions/2025_09_08_1007-c20211f18133_add_headers_to_mcp_provider.py index 4f472fe4b4..cf2b973d2d 100644 --- a/api/migrations/versions/2025_09_08_1007-c20211f18133_add_headers_to_mcp_provider.py +++ b/api/migrations/versions/2025_09_08_1007-c20211f18133_add_headers_to_mcp_provider.py @@ -9,8 +9,6 @@ from alembic import op import models as models -def _is_pg(conn): - return conn.dialect.name == "postgresql" import sqlalchemy as sa @@ -23,12 +21,7 @@ depends_on = None def upgrade(): # Add encrypted_headers column to tool_mcp_providers table - conn = op.get_bind() - - if _is_pg(conn): - op.add_column('tool_mcp_providers', sa.Column('encrypted_headers', sa.Text(), nullable=True)) - else: - op.add_column('tool_mcp_providers', sa.Column('encrypted_headers', models.types.LongText(), nullable=True)) + op.add_column('tool_mcp_providers', sa.Column('encrypted_headers', models.types.LongText(), nullable=True)) def downgrade(): diff --git a/api/migrations/versions/2025_09_17_1515-68519ad5cd18_knowledge_pipeline_migrate.py b/api/migrations/versions/2025_09_17_1515-68519ad5cd18_knowledge_pipeline_migrate.py index 8eac0dee10..bad516dcac 100644 --- a/api/migrations/versions/2025_09_17_1515-68519ad5cd18_knowledge_pipeline_migrate.py +++ b/api/migrations/versions/2025_09_17_1515-68519ad5cd18_knowledge_pipeline_migrate.py @@ -44,6 +44,7 @@ def upgrade(): sa.PrimaryKeyConstraint('id', name='datasource_oauth_config_pkey'), sa.UniqueConstraint('plugin_id', 'provider', name='datasource_oauth_config_datasource_id_provider_idx') ) + if _is_pg(conn): op.create_table('datasource_oauth_tenant_params', sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False), @@ -70,6 +71,7 @@ def upgrade(): sa.PrimaryKeyConstraint('id', name='datasource_oauth_tenant_config_pkey'), sa.UniqueConstraint('tenant_id', 'plugin_id', 'provider', name='datasource_oauth_tenant_config_unique') ) + if _is_pg(conn): op.create_table('datasource_providers', sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False), @@ -104,6 +106,7 @@ def upgrade(): sa.PrimaryKeyConstraint('id', name='datasource_provider_pkey'), sa.UniqueConstraint('tenant_id', 'plugin_id', 'provider', 'name', name='datasource_provider_unique_name') ) + with op.batch_alter_table('datasource_providers', schema=None) as batch_op: batch_op.create_index('datasource_provider_auth_type_provider_idx', ['tenant_id', 'plugin_id', 'provider'], unique=False) @@ -133,6 +136,7 @@ def upgrade(): sa.Column('created_at', sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False), sa.PrimaryKeyConstraint('id', name='document_pipeline_execution_log_pkey') ) + with op.batch_alter_table('document_pipeline_execution_logs', schema=None) as batch_op: batch_op.create_index('document_pipeline_execution_logs_document_id_idx', ['document_id'], unique=False) @@ -174,6 +178,7 @@ def upgrade(): sa.Column('updated_by', models.types.StringUUID(), nullable=True), sa.PrimaryKeyConstraint('id', name='pipeline_built_in_template_pkey') ) + if _is_pg(conn): op.create_table('pipeline_customized_templates', sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False), @@ -193,7 +198,6 @@ def upgrade(): sa.PrimaryKeyConstraint('id', name='pipeline_customized_template_pkey') ) else: - # MySQL: Use compatible syntax op.create_table('pipeline_customized_templates', sa.Column('id', models.types.StringUUID(), nullable=False), sa.Column('tenant_id', models.types.StringUUID(), nullable=False), @@ -211,6 +215,7 @@ def upgrade(): sa.Column('updated_at', sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False), sa.PrimaryKeyConstraint('id', name='pipeline_customized_template_pkey') ) + with op.batch_alter_table('pipeline_customized_templates', schema=None) as batch_op: batch_op.create_index('pipeline_customized_template_tenant_idx', ['tenant_id'], unique=False) @@ -236,6 +241,7 @@ def upgrade(): sa.Column('updated_at', sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False), sa.PrimaryKeyConstraint('id', name='pipeline_recommended_plugin_pkey') ) + if _is_pg(conn): op.create_table('pipelines', sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False), @@ -266,6 +272,7 @@ def upgrade(): sa.Column('updated_at', sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False), sa.PrimaryKeyConstraint('id', name='pipeline_pkey') ) + if _is_pg(conn): op.create_table('workflow_draft_variable_files', sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False), @@ -292,6 +299,7 @@ def upgrade(): sa.Column('value_type', sa.String(20), nullable=False), sa.PrimaryKeyConstraint('id', name=op.f('workflow_draft_variable_files_pkey')) ) + if _is_pg(conn): op.create_table('workflow_node_execution_offload', sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False), @@ -316,6 +324,7 @@ def upgrade(): sa.PrimaryKeyConstraint('id', name=op.f('workflow_node_execution_offload_pkey')), sa.UniqueConstraint('node_execution_id', 'type', name=op.f('workflow_node_execution_offload_node_execution_id_key')) ) + if _is_pg(conn): with op.batch_alter_table('datasets', schema=None) as batch_op: batch_op.add_column(sa.Column('keyword_number', sa.Integer(), server_default=sa.text('10'), nullable=True)) @@ -342,6 +351,7 @@ def upgrade(): comment='Indicates whether the current value is the default for a conversation variable. Always `FALSE` for other types of variables.',) ) batch_op.create_index('workflow_draft_variable_file_id_idx', ['file_id'], unique=False) + if _is_pg(conn): with op.batch_alter_table('workflows', schema=None) as batch_op: batch_op.add_column(sa.Column('rag_pipeline_variables', sa.Text(), server_default='{}', nullable=False)) diff --git a/api/migrations/versions/2025_10_21_1430-ae662b25d9bc_remove_builtin_template_user.py b/api/migrations/versions/2025_10_21_1430-ae662b25d9bc_remove_builtin_template_user.py index 0776ab0818..ec0cfbd11d 100644 --- a/api/migrations/versions/2025_10_21_1430-ae662b25d9bc_remove_builtin_template_user.py +++ b/api/migrations/versions/2025_10_21_1430-ae662b25d9bc_remove_builtin_template_user.py @@ -9,8 +9,6 @@ from alembic import op import models as models -def _is_pg(conn): - return conn.dialect.name == "postgresql" import sqlalchemy as sa @@ -33,15 +31,9 @@ def upgrade(): def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('pipeline_built_in_templates', schema=None) as batch_op: - batch_op.add_column(sa.Column('created_by', sa.UUID(), autoincrement=False, nullable=False)) - batch_op.add_column(sa.Column('updated_by', sa.UUID(), autoincrement=False, nullable=True)) - else: - with op.batch_alter_table('pipeline_built_in_templates', schema=None) as batch_op: - batch_op.add_column(sa.Column('created_by', models.types.StringUUID(), autoincrement=False, nullable=False)) - batch_op.add_column(sa.Column('updated_by', models.types.StringUUID(), autoincrement=False, nullable=True)) + + with op.batch_alter_table('pipeline_built_in_templates', schema=None) as batch_op: + batch_op.add_column(sa.Column('created_by', models.types.StringUUID(), autoincrement=False, nullable=False)) + batch_op.add_column(sa.Column('updated_by', models.types.StringUUID(), autoincrement=False, nullable=True)) # ### end Alembic commands ### diff --git a/api/migrations/versions/2025_10_22_1611-03f8dcbc611e_add_workflowpause_model.py b/api/migrations/versions/2025_10_22_1611-03f8dcbc611e_add_workflowpause_model.py index 627219cc4b..12905b3674 100644 --- a/api/migrations/versions/2025_10_22_1611-03f8dcbc611e_add_workflowpause_model.py +++ b/api/migrations/versions/2025_10_22_1611-03f8dcbc611e_add_workflowpause_model.py @@ -9,7 +9,6 @@ Create Date: 2025-10-22 16:11:31.805407 from alembic import op import models as models import sqlalchemy as sa -from libs.uuid_utils import uuidv7 def _is_pg(conn): return conn.dialect.name == "postgresql" diff --git a/api/migrations/versions/2025_10_30_1518-669ffd70119c_introduce_trigger.py b/api/migrations/versions/2025_10_30_1518-669ffd70119c_introduce_trigger.py index 9641a15c89..c27c1058d1 100644 --- a/api/migrations/versions/2025_10_30_1518-669ffd70119c_introduce_trigger.py +++ b/api/migrations/versions/2025_10_30_1518-669ffd70119c_introduce_trigger.py @@ -105,6 +105,7 @@ def upgrade(): sa.PrimaryKeyConstraint('id', name='trigger_oauth_tenant_client_pkey'), sa.UniqueConstraint('tenant_id', 'plugin_id', 'provider', name='unique_trigger_oauth_tenant_client') ) + if _is_pg(conn): op.create_table('trigger_subscriptions', sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), @@ -143,6 +144,7 @@ def upgrade(): sa.PrimaryKeyConstraint('id', name='trigger_provider_pkey'), sa.UniqueConstraint('tenant_id', 'provider_id', 'name', name='unique_trigger_provider') ) + with op.batch_alter_table('trigger_subscriptions', schema=None) as batch_op: batch_op.create_index('idx_trigger_providers_endpoint', ['endpoint_id'], unique=True) batch_op.create_index('idx_trigger_providers_tenant_endpoint', ['tenant_id', 'endpoint_id'], unique=False) @@ -176,6 +178,7 @@ def upgrade(): sa.PrimaryKeyConstraint('id', name='workflow_plugin_trigger_pkey'), sa.UniqueConstraint('app_id', 'node_id', name='uniq_app_node_subscription') ) + with op.batch_alter_table('workflow_plugin_triggers', schema=None) as batch_op: batch_op.create_index('workflow_plugin_trigger_tenant_subscription_idx', ['tenant_id', 'subscription_id', 'event_name'], unique=False) @@ -207,6 +210,7 @@ def upgrade(): sa.PrimaryKeyConstraint('id', name='workflow_schedule_plan_pkey'), sa.UniqueConstraint('app_id', 'node_id', name='uniq_app_node') ) + with op.batch_alter_table('workflow_schedule_plans', schema=None) as batch_op: batch_op.create_index('workflow_schedule_plan_next_idx', ['next_run_at'], unique=False) @@ -264,6 +268,7 @@ def upgrade(): sa.Column('finished_at', sa.DateTime(), nullable=True), sa.PrimaryKeyConstraint('id', name='workflow_trigger_log_pkey') ) + with op.batch_alter_table('workflow_trigger_logs', schema=None) as batch_op: batch_op.create_index('workflow_trigger_log_created_at_idx', ['created_at'], unique=False) batch_op.create_index('workflow_trigger_log_status_idx', ['status'], unique=False) @@ -299,6 +304,7 @@ def upgrade(): sa.UniqueConstraint('app_id', 'node_id', name='uniq_node'), sa.UniqueConstraint('webhook_id', name='uniq_webhook_id') ) + with op.batch_alter_table('workflow_webhook_triggers', schema=None) as batch_op: batch_op.create_index('workflow_webhook_trigger_tenant_idx', ['tenant_id'], unique=False) diff --git a/api/migrations/versions/23db93619b9d_add_message_files_into_agent_thought.py b/api/migrations/versions/23db93619b9d_add_message_files_into_agent_thought.py index fae506906b..127ffd5599 100644 --- a/api/migrations/versions/23db93619b9d_add_message_files_into_agent_thought.py +++ b/api/migrations/versions/23db93619b9d_add_message_files_into_agent_thought.py @@ -11,9 +11,6 @@ from alembic import op import models.types -def _is_pg(conn): - return conn.dialect.name == "postgresql" - # revision identifiers, used by Alembic. revision = '23db93619b9d' down_revision = '8ae9bc661daa' @@ -23,14 +20,8 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('message_agent_thoughts', schema=None) as batch_op: - batch_op.add_column(sa.Column('message_files', sa.Text(), nullable=True)) - else: - with op.batch_alter_table('message_agent_thoughts', schema=None) as batch_op: - batch_op.add_column(sa.Column('message_files', models.types.LongText(), nullable=True)) + with op.batch_alter_table('message_agent_thoughts', schema=None) as batch_op: + batch_op.add_column(sa.Column('message_files', models.types.LongText(), nullable=True)) # ### end Alembic commands ### diff --git a/api/migrations/versions/246ba09cbbdb_add_app_anntation_setting.py b/api/migrations/versions/246ba09cbbdb_add_app_anntation_setting.py index 2676ef0b94..31829d8e58 100644 --- a/api/migrations/versions/246ba09cbbdb_add_app_anntation_setting.py +++ b/api/migrations/versions/246ba09cbbdb_add_app_anntation_setting.py @@ -62,14 +62,8 @@ def upgrade(): def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('app_model_configs', schema=None) as batch_op: - batch_op.add_column(sa.Column('annotation_reply', sa.TEXT(), autoincrement=False, nullable=True)) - else: - with op.batch_alter_table('app_model_configs', schema=None) as batch_op: - batch_op.add_column(sa.Column('annotation_reply', models.types.LongText(), autoincrement=False, nullable=True)) + with op.batch_alter_table('app_model_configs', schema=None) as batch_op: + batch_op.add_column(sa.Column('annotation_reply', models.types.LongText(), autoincrement=False, nullable=True)) with op.batch_alter_table('app_annotation_settings', schema=None) as batch_op: batch_op.drop_index('app_annotation_settings_app_idx') diff --git a/api/migrations/versions/2a3aebbbf4bb_add_app_tracing.py b/api/migrations/versions/2a3aebbbf4bb_add_app_tracing.py index 3362a3a09f..07a8cd86b1 100644 --- a/api/migrations/versions/2a3aebbbf4bb_add_app_tracing.py +++ b/api/migrations/versions/2a3aebbbf4bb_add_app_tracing.py @@ -11,9 +11,6 @@ from alembic import op import models as models -def _is_pg(conn): - return conn.dialect.name == "postgresql" - # revision identifiers, used by Alembic. revision = '2a3aebbbf4bb' down_revision = 'c031d46af369' @@ -23,14 +20,8 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('apps', schema=None) as batch_op: - batch_op.add_column(sa.Column('tracing', sa.Text(), nullable=True)) - else: - with op.batch_alter_table('apps', schema=None) as batch_op: - batch_op.add_column(sa.Column('tracing', models.types.LongText(), nullable=True)) + with op.batch_alter_table('apps', schema=None) as batch_op: + batch_op.add_column(sa.Column('tracing', models.types.LongText(), nullable=True)) # ### end Alembic commands ### diff --git a/api/migrations/versions/2e9819ca5b28_add_tenant_id_in_api_token.py b/api/migrations/versions/2e9819ca5b28_add_tenant_id_in_api_token.py index 40bd727f66..211b2d8882 100644 --- a/api/migrations/versions/2e9819ca5b28_add_tenant_id_in_api_token.py +++ b/api/migrations/versions/2e9819ca5b28_add_tenant_id_in_api_token.py @@ -7,14 +7,10 @@ Create Date: 2023-09-22 15:41:01.243183 """ import sqlalchemy as sa from alembic import op -from sqlalchemy.dialects import postgresql import models.types -def _is_pg(conn): - return conn.dialect.name == "postgresql" - # revision identifiers, used by Alembic. revision = '2e9819ca5b28' down_revision = 'ab23c11305d4' @@ -24,35 +20,19 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('api_tokens', schema=None) as batch_op: - batch_op.add_column(sa.Column('tenant_id', postgresql.UUID(), nullable=True)) - batch_op.create_index('api_token_tenant_idx', ['tenant_id', 'type'], unique=False) - batch_op.drop_column('dataset_id') - else: - with op.batch_alter_table('api_tokens', schema=None) as batch_op: - batch_op.add_column(sa.Column('tenant_id', models.types.StringUUID(), nullable=True)) - batch_op.create_index('api_token_tenant_idx', ['tenant_id', 'type'], unique=False) - batch_op.drop_column('dataset_id') + with op.batch_alter_table('api_tokens', schema=None) as batch_op: + batch_op.add_column(sa.Column('tenant_id', models.types.StringUUID(), nullable=True)) + batch_op.create_index('api_token_tenant_idx', ['tenant_id', 'type'], unique=False) + batch_op.drop_column('dataset_id') # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('api_tokens', schema=None) as batch_op: - batch_op.add_column(sa.Column('dataset_id', postgresql.UUID(), autoincrement=False, nullable=True)) - batch_op.drop_index('api_token_tenant_idx') - batch_op.drop_column('tenant_id') - else: - with op.batch_alter_table('api_tokens', schema=None) as batch_op: - batch_op.add_column(sa.Column('dataset_id', models.types.StringUUID(), autoincrement=False, nullable=True)) - batch_op.drop_index('api_token_tenant_idx') - batch_op.drop_column('tenant_id') + with op.batch_alter_table('api_tokens', schema=None) as batch_op: + batch_op.add_column(sa.Column('dataset_id', models.types.StringUUID(), autoincrement=False, nullable=True)) + batch_op.drop_index('api_token_tenant_idx') + batch_op.drop_column('tenant_id') # ### end Alembic commands ### diff --git a/api/migrations/versions/42e85ed5564d_conversation_columns_set_nullable.py b/api/migrations/versions/42e85ed5564d_conversation_columns_set_nullable.py index 76056a9460..3491c85e2f 100644 --- a/api/migrations/versions/42e85ed5564d_conversation_columns_set_nullable.py +++ b/api/migrations/versions/42e85ed5564d_conversation_columns_set_nullable.py @@ -7,14 +7,10 @@ Create Date: 2024-03-07 08:30:29.133614 """ import sqlalchemy as sa from alembic import op -from sqlalchemy.dialects import postgresql import models.types -def _is_pg(conn): - return conn.dialect.name == "postgresql" - # revision identifiers, used by Alembic. revision = '42e85ed5564d' down_revision = 'f9107f83abab' @@ -24,59 +20,31 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('conversations', schema=None) as batch_op: - batch_op.alter_column('app_model_config_id', - existing_type=postgresql.UUID(), - nullable=True) - batch_op.alter_column('model_provider', - existing_type=sa.VARCHAR(length=255), - nullable=True) - batch_op.alter_column('model_id', - existing_type=sa.VARCHAR(length=255), - nullable=True) - else: - with op.batch_alter_table('conversations', schema=None) as batch_op: - batch_op.alter_column('app_model_config_id', - existing_type=models.types.StringUUID(), - nullable=True) - batch_op.alter_column('model_provider', - existing_type=sa.VARCHAR(length=255), - nullable=True) - batch_op.alter_column('model_id', - existing_type=sa.VARCHAR(length=255), - nullable=True) + with op.batch_alter_table('conversations', schema=None) as batch_op: + batch_op.alter_column('app_model_config_id', + existing_type=models.types.StringUUID(), + nullable=True) + batch_op.alter_column('model_provider', + existing_type=sa.VARCHAR(length=255), + nullable=True) + batch_op.alter_column('model_id', + existing_type=sa.VARCHAR(length=255), + nullable=True) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('conversations', schema=None) as batch_op: - batch_op.alter_column('model_id', - existing_type=sa.VARCHAR(length=255), - nullable=False) - batch_op.alter_column('model_provider', - existing_type=sa.VARCHAR(length=255), - nullable=False) - batch_op.alter_column('app_model_config_id', - existing_type=postgresql.UUID(), - nullable=False) - else: - with op.batch_alter_table('conversations', schema=None) as batch_op: - batch_op.alter_column('model_id', - existing_type=sa.VARCHAR(length=255), - nullable=False) - batch_op.alter_column('model_provider', - existing_type=sa.VARCHAR(length=255), - nullable=False) - batch_op.alter_column('app_model_config_id', - existing_type=models.types.StringUUID(), - nullable=False) + with op.batch_alter_table('conversations', schema=None) as batch_op: + batch_op.alter_column('model_id', + existing_type=sa.VARCHAR(length=255), + nullable=False) + batch_op.alter_column('model_provider', + existing_type=sa.VARCHAR(length=255), + nullable=False) + batch_op.alter_column('app_model_config_id', + existing_type=models.types.StringUUID(), + nullable=False) # ### end Alembic commands ### diff --git a/api/migrations/versions/4829e54d2fee_change_message_chain_id_to_nullable.py b/api/migrations/versions/4829e54d2fee_change_message_chain_id_to_nullable.py index ef066587b7..8537a87233 100644 --- a/api/migrations/versions/4829e54d2fee_change_message_chain_id_to_nullable.py +++ b/api/migrations/versions/4829e54d2fee_change_message_chain_id_to_nullable.py @@ -6,14 +6,10 @@ Create Date: 2024-01-12 03:42:27.362415 """ from alembic import op -from sqlalchemy.dialects import postgresql import models.types -def _is_pg(conn): - return conn.dialect.name == "postgresql" - # revision identifiers, used by Alembic. revision = '4829e54d2fee' down_revision = '114eed84c228' @@ -23,39 +19,21 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - # PostgreSQL: Keep original syntax - with op.batch_alter_table('message_agent_thoughts', schema=None) as batch_op: - batch_op.alter_column('message_chain_id', - existing_type=postgresql.UUID(), - nullable=True) - else: - # MySQL: Use compatible syntax - with op.batch_alter_table('message_agent_thoughts', schema=None) as batch_op: - batch_op.alter_column('message_chain_id', - existing_type=models.types.StringUUID(), - nullable=True) + + with op.batch_alter_table('message_agent_thoughts', schema=None) as batch_op: + batch_op.alter_column('message_chain_id', + existing_type=models.types.StringUUID(), + nullable=True) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - # PostgreSQL: Keep original syntax - with op.batch_alter_table('message_agent_thoughts', schema=None) as batch_op: - batch_op.alter_column('message_chain_id', - existing_type=postgresql.UUID(), - nullable=False) - else: - # MySQL: Use compatible syntax - with op.batch_alter_table('message_agent_thoughts', schema=None) as batch_op: - batch_op.alter_column('message_chain_id', - existing_type=models.types.StringUUID(), - nullable=False) + + with op.batch_alter_table('message_agent_thoughts', schema=None) as batch_op: + batch_op.alter_column('message_chain_id', + existing_type=models.types.StringUUID(), + nullable=False) # ### end Alembic commands ### diff --git a/api/migrations/versions/563cf8bf777b_enable_tool_file_without_conversation_id.py b/api/migrations/versions/563cf8bf777b_enable_tool_file_without_conversation_id.py index b080e7680b..22405e3cc8 100644 --- a/api/migrations/versions/563cf8bf777b_enable_tool_file_without_conversation_id.py +++ b/api/migrations/versions/563cf8bf777b_enable_tool_file_without_conversation_id.py @@ -6,14 +6,10 @@ Create Date: 2024-03-14 04:54:56.679506 """ from alembic import op -from sqlalchemy.dialects import postgresql import models.types -def _is_pg(conn): - return conn.dialect.name == "postgresql" - # revision identifiers, used by Alembic. revision = '563cf8bf777b' down_revision = 'b5429b71023c' @@ -23,35 +19,19 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('tool_files', schema=None) as batch_op: - batch_op.alter_column('conversation_id', - existing_type=postgresql.UUID(), - nullable=True) - else: - with op.batch_alter_table('tool_files', schema=None) as batch_op: - batch_op.alter_column('conversation_id', - existing_type=models.types.StringUUID(), - nullable=True) + with op.batch_alter_table('tool_files', schema=None) as batch_op: + batch_op.alter_column('conversation_id', + existing_type=models.types.StringUUID(), + nullable=True) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('tool_files', schema=None) as batch_op: - batch_op.alter_column('conversation_id', - existing_type=postgresql.UUID(), - nullable=False) - else: - with op.batch_alter_table('tool_files', schema=None) as batch_op: - batch_op.alter_column('conversation_id', - existing_type=models.types.StringUUID(), - nullable=False) + with op.batch_alter_table('tool_files', schema=None) as batch_op: + batch_op.alter_column('conversation_id', + existing_type=models.types.StringUUID(), + nullable=False) # ### end Alembic commands ### diff --git a/api/migrations/versions/6e2cfb077b04_add_dataset_collection_binding.py b/api/migrations/versions/6e2cfb077b04_add_dataset_collection_binding.py index 1ace8ea5a0..01d7d5ba21 100644 --- a/api/migrations/versions/6e2cfb077b04_add_dataset_collection_binding.py +++ b/api/migrations/versions/6e2cfb077b04_add_dataset_collection_binding.py @@ -48,12 +48,9 @@ def upgrade(): with op.batch_alter_table('dataset_collection_bindings', schema=None) as batch_op: batch_op.create_index('provider_model_name_idx', ['provider_name', 'model_name'], unique=False) - if _is_pg(conn): - with op.batch_alter_table('datasets', schema=None) as batch_op: - batch_op.add_column(sa.Column('collection_binding_id', postgresql.UUID(), nullable=True)) - else: - with op.batch_alter_table('datasets', schema=None) as batch_op: - batch_op.add_column(sa.Column('collection_binding_id', models.types.StringUUID(), nullable=True)) + + with op.batch_alter_table('datasets', schema=None) as batch_op: + batch_op.add_column(sa.Column('collection_binding_id', models.types.StringUUID(), nullable=True)) # ### end Alembic commands ### diff --git a/api/migrations/versions/714aafe25d39_add_anntation_history_match_response.py b/api/migrations/versions/714aafe25d39_add_anntation_history_match_response.py index 457338ef42..0faa48f535 100644 --- a/api/migrations/versions/714aafe25d39_add_anntation_history_match_response.py +++ b/api/migrations/versions/714aafe25d39_add_anntation_history_match_response.py @@ -11,9 +11,6 @@ from alembic import op import models.types -def _is_pg(conn): - return conn.dialect.name == "postgresql" - # revision identifiers, used by Alembic. revision = '714aafe25d39' down_revision = 'f2a6fc85e260' @@ -23,16 +20,9 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('app_annotation_hit_histories', schema=None) as batch_op: - batch_op.add_column(sa.Column('annotation_question', sa.Text(), nullable=False)) - batch_op.add_column(sa.Column('annotation_content', sa.Text(), nullable=False)) - else: - with op.batch_alter_table('app_annotation_hit_histories', schema=None) as batch_op: - batch_op.add_column(sa.Column('annotation_question', models.types.LongText(), nullable=False)) - batch_op.add_column(sa.Column('annotation_content', models.types.LongText(), nullable=False)) + with op.batch_alter_table('app_annotation_hit_histories', schema=None) as batch_op: + batch_op.add_column(sa.Column('annotation_question', models.types.LongText(), nullable=False)) + batch_op.add_column(sa.Column('annotation_content', models.types.LongText(), nullable=False)) # ### end Alembic commands ### diff --git a/api/migrations/versions/77e83833755c_add_app_config_retriever_resource.py b/api/migrations/versions/77e83833755c_add_app_config_retriever_resource.py index 7bcd1a1be3..aa7b4a21e2 100644 --- a/api/migrations/versions/77e83833755c_add_app_config_retriever_resource.py +++ b/api/migrations/versions/77e83833755c_add_app_config_retriever_resource.py @@ -11,9 +11,6 @@ from alembic import op import models.types -def _is_pg(conn): - return conn.dialect.name == "postgresql" - # revision identifiers, used by Alembic. revision = '77e83833755c' down_revision = '6dcb43972bdc' @@ -23,14 +20,8 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('app_model_configs', schema=None) as batch_op: - batch_op.add_column(sa.Column('retriever_resource', sa.Text(), nullable=True)) - else: - with op.batch_alter_table('app_model_configs', schema=None) as batch_op: - batch_op.add_column(sa.Column('retriever_resource', models.types.LongText(), nullable=True)) + with op.batch_alter_table('app_model_configs', schema=None) as batch_op: + batch_op.add_column(sa.Column('retriever_resource', models.types.LongText(), nullable=True)) # ### end Alembic commands ### diff --git a/api/migrations/versions/7ce5a52e4eee_add_tool_providers.py b/api/migrations/versions/7ce5a52e4eee_add_tool_providers.py index 3c0aa082d5..34a17697d3 100644 --- a/api/migrations/versions/7ce5a52e4eee_add_tool_providers.py +++ b/api/migrations/versions/7ce5a52e4eee_add_tool_providers.py @@ -27,7 +27,6 @@ def upgrade(): conn = op.get_bind() if _is_pg(conn): - # PostgreSQL: Keep original syntax op.create_table('tool_providers', sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), sa.Column('tenant_id', postgresql.UUID(), nullable=False), @@ -40,7 +39,6 @@ def upgrade(): sa.UniqueConstraint('tenant_id', 'tool_name', name='unique_tool_provider_tool_name') ) else: - # MySQL: Use compatible syntax op.create_table('tool_providers', sa.Column('id', models.types.StringUUID(), nullable=False), sa.Column('tenant_id', models.types.StringUUID(), nullable=False), @@ -52,12 +50,9 @@ def upgrade(): sa.PrimaryKeyConstraint('id', name='tool_provider_pkey'), sa.UniqueConstraint('tenant_id', 'tool_name', name='unique_tool_provider_tool_name') ) - if _is_pg(conn): - with op.batch_alter_table('app_model_configs', schema=None) as batch_op: - batch_op.add_column(sa.Column('sensitive_word_avoidance', sa.Text(), nullable=True)) - else: - with op.batch_alter_table('app_model_configs', schema=None) as batch_op: - batch_op.add_column(sa.Column('sensitive_word_avoidance', models.types.LongText(), nullable=True)) + + with op.batch_alter_table('app_model_configs', schema=None) as batch_op: + batch_op.add_column(sa.Column('sensitive_word_avoidance', models.types.LongText(), nullable=True)) # ### end Alembic commands ### diff --git a/api/migrations/versions/88072f0caa04_add_custom_config_in_tenant.py b/api/migrations/versions/88072f0caa04_add_custom_config_in_tenant.py index beea90b384..884839c010 100644 --- a/api/migrations/versions/88072f0caa04_add_custom_config_in_tenant.py +++ b/api/migrations/versions/88072f0caa04_add_custom_config_in_tenant.py @@ -11,9 +11,6 @@ from alembic import op import models.types -def _is_pg(conn): - return conn.dialect.name == "postgresql" - # revision identifiers, used by Alembic. revision = '88072f0caa04' down_revision = '246ba09cbbdb' @@ -23,14 +20,8 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('tenants', schema=None) as batch_op: - batch_op.add_column(sa.Column('custom_config', sa.Text(), nullable=True)) - else: - with op.batch_alter_table('tenants', schema=None) as batch_op: - batch_op.add_column(sa.Column('custom_config', models.types.LongText(), nullable=True)) + with op.batch_alter_table('tenants', schema=None) as batch_op: + batch_op.add_column(sa.Column('custom_config', models.types.LongText(), nullable=True)) # ### end Alembic commands ### diff --git a/api/migrations/versions/89c7899ca936_.py b/api/migrations/versions/89c7899ca936_.py index 2420710e74..d26f1e82d6 100644 --- a/api/migrations/versions/89c7899ca936_.py +++ b/api/migrations/versions/89c7899ca936_.py @@ -11,9 +11,6 @@ from alembic import op import models.types -def _is_pg(conn): - return conn.dialect.name == "postgresql" - # revision identifiers, used by Alembic. revision = '89c7899ca936' down_revision = '187385f442fc' @@ -23,39 +20,21 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('sites', schema=None) as batch_op: - batch_op.alter_column('description', - existing_type=sa.VARCHAR(length=255), - type_=sa.Text(), - existing_nullable=True) - else: - with op.batch_alter_table('sites', schema=None) as batch_op: - batch_op.alter_column('description', - existing_type=sa.VARCHAR(length=255), - type_=models.types.LongText(), - existing_nullable=True) + with op.batch_alter_table('sites', schema=None) as batch_op: + batch_op.alter_column('description', + existing_type=sa.VARCHAR(length=255), + type_=models.types.LongText(), + existing_nullable=True) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('sites', schema=None) as batch_op: - batch_op.alter_column('description', - existing_type=sa.Text(), - type_=sa.VARCHAR(length=255), - existing_nullable=True) - else: - with op.batch_alter_table('sites', schema=None) as batch_op: - batch_op.alter_column('description', - existing_type=models.types.LongText(), - type_=sa.VARCHAR(length=255), - existing_nullable=True) + with op.batch_alter_table('sites', schema=None) as batch_op: + batch_op.alter_column('description', + existing_type=models.types.LongText(), + type_=sa.VARCHAR(length=255), + existing_nullable=True) # ### end Alembic commands ### diff --git a/api/migrations/versions/8ec536f3c800_rename_api_provider_credentails.py b/api/migrations/versions/8ec536f3c800_rename_api_provider_credentails.py index 111e81240b..6022ea2c20 100644 --- a/api/migrations/versions/8ec536f3c800_rename_api_provider_credentails.py +++ b/api/migrations/versions/8ec536f3c800_rename_api_provider_credentails.py @@ -11,9 +11,6 @@ from alembic import op import models.types -def _is_pg(conn): - return conn.dialect.name == "postgresql" - # revision identifiers, used by Alembic. revision = '8ec536f3c800' down_revision = 'ad472b61a054' @@ -23,14 +20,8 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('tool_api_providers', schema=None) as batch_op: - batch_op.add_column(sa.Column('credentials_str', sa.Text(), nullable=False)) - else: - with op.batch_alter_table('tool_api_providers', schema=None) as batch_op: - batch_op.add_column(sa.Column('credentials_str', models.types.LongText(), nullable=False)) + with op.batch_alter_table('tool_api_providers', schema=None) as batch_op: + batch_op.add_column(sa.Column('credentials_str', models.types.LongText(), nullable=False)) # ### end Alembic commands ### diff --git a/api/migrations/versions/8fe468ba0ca5_add_gpt4v_supports.py b/api/migrations/versions/8fe468ba0ca5_add_gpt4v_supports.py index 1c1c6cacbb..9d6d40114d 100644 --- a/api/migrations/versions/8fe468ba0ca5_add_gpt4v_supports.py +++ b/api/migrations/versions/8fe468ba0ca5_add_gpt4v_supports.py @@ -57,12 +57,9 @@ def upgrade(): batch_op.create_index('message_file_created_by_idx', ['created_by'], unique=False) batch_op.create_index('message_file_message_idx', ['message_id'], unique=False) - if _is_pg(conn): - with op.batch_alter_table('app_model_configs', schema=None) as batch_op: - batch_op.add_column(sa.Column('file_upload', sa.Text(), nullable=True)) - else: - with op.batch_alter_table('app_model_configs', schema=None) as batch_op: - batch_op.add_column(sa.Column('file_upload', models.types.LongText(), nullable=True)) + + with op.batch_alter_table('app_model_configs', schema=None) as batch_op: + batch_op.add_column(sa.Column('file_upload', models.types.LongText(), nullable=True)) if _is_pg(conn): with op.batch_alter_table('upload_files', schema=None) as batch_op: diff --git a/api/migrations/versions/9f4e3427ea84_add_created_by_role.py b/api/migrations/versions/9f4e3427ea84_add_created_by_role.py index 5d29d354f3..0b3f92a12e 100644 --- a/api/migrations/versions/9f4e3427ea84_add_created_by_role.py +++ b/api/migrations/versions/9f4e3427ea84_add_created_by_role.py @@ -24,7 +24,6 @@ def upgrade(): conn = op.get_bind() if _is_pg(conn): - # PostgreSQL: Keep original syntax with op.batch_alter_table('pinned_conversations', schema=None) as batch_op: batch_op.add_column(sa.Column('created_by_role', sa.String(length=255), server_default=sa.text("'end_user'::character varying"), nullable=False)) batch_op.drop_index('pinned_conversation_conversation_idx') @@ -35,7 +34,6 @@ def upgrade(): batch_op.drop_index('saved_message_message_idx') batch_op.create_index('saved_message_message_idx', ['app_id', 'message_id', 'created_by_role', 'created_by'], unique=False) else: - # MySQL: Use compatible syntax with op.batch_alter_table('pinned_conversations', schema=None) as batch_op: batch_op.add_column(sa.Column('created_by_role', sa.String(length=255), server_default=sa.text("'end_user'"), nullable=False)) batch_op.drop_index('pinned_conversation_conversation_idx') diff --git a/api/migrations/versions/a5b56fb053ef_app_config_add_speech_to_text.py b/api/migrations/versions/a5b56fb053ef_app_config_add_speech_to_text.py index 616cb2f163..c8747a51f7 100644 --- a/api/migrations/versions/a5b56fb053ef_app_config_add_speech_to_text.py +++ b/api/migrations/versions/a5b56fb053ef_app_config_add_speech_to_text.py @@ -11,9 +11,6 @@ from alembic import op import models.types -def _is_pg(conn): - return conn.dialect.name == "postgresql" - # revision identifiers, used by Alembic. revision = 'a5b56fb053ef' down_revision = 'd3d503a3471c' @@ -23,14 +20,8 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('app_model_configs', schema=None) as batch_op: - batch_op.add_column(sa.Column('speech_to_text', sa.Text(), nullable=True)) - else: - with op.batch_alter_table('app_model_configs', schema=None) as batch_op: - batch_op.add_column(sa.Column('speech_to_text', models.types.LongText(), nullable=True)) + with op.batch_alter_table('app_model_configs', schema=None) as batch_op: + batch_op.add_column(sa.Column('speech_to_text', models.types.LongText(), nullable=True)) # ### end Alembic commands ### diff --git a/api/migrations/versions/a9836e3baeee_add_external_data_tools_in_app_model_.py b/api/migrations/versions/a9836e3baeee_add_external_data_tools_in_app_model_.py index 900ff78036..f56aeb7e66 100644 --- a/api/migrations/versions/a9836e3baeee_add_external_data_tools_in_app_model_.py +++ b/api/migrations/versions/a9836e3baeee_add_external_data_tools_in_app_model_.py @@ -11,9 +11,6 @@ from alembic import op import models.types -def _is_pg(conn): - return conn.dialect.name == "postgresql" - # revision identifiers, used by Alembic. revision = 'a9836e3baeee' down_revision = '968fff4c0ab9' @@ -23,14 +20,8 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('app_model_configs', schema=None) as batch_op: - batch_op.add_column(sa.Column('external_data_tools', sa.Text(), nullable=True)) - else: - with op.batch_alter_table('app_model_configs', schema=None) as batch_op: - batch_op.add_column(sa.Column('external_data_tools', models.types.LongText(), nullable=True)) + with op.batch_alter_table('app_model_configs', schema=None) as batch_op: + batch_op.add_column(sa.Column('external_data_tools', models.types.LongText(), nullable=True)) # ### end Alembic commands ### diff --git a/api/migrations/versions/b24be59fbb04_.py b/api/migrations/versions/b24be59fbb04_.py index b0a6d10d8c..ae91eaf1bc 100644 --- a/api/migrations/versions/b24be59fbb04_.py +++ b/api/migrations/versions/b24be59fbb04_.py @@ -11,9 +11,6 @@ from alembic import op import models.types -def _is_pg(conn): - return conn.dialect.name == "postgresql" - # revision identifiers, used by Alembic. revision = 'b24be59fbb04' down_revision = 'de95f5c77138' @@ -23,14 +20,8 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('app_model_configs', schema=None) as batch_op: - batch_op.add_column(sa.Column('text_to_speech', sa.Text(), nullable=True)) - else: - with op.batch_alter_table('app_model_configs', schema=None) as batch_op: - batch_op.add_column(sa.Column('text_to_speech', models.types.LongText(), nullable=True)) + with op.batch_alter_table('app_model_configs', schema=None) as batch_op: + batch_op.add_column(sa.Column('text_to_speech', models.types.LongText(), nullable=True)) # ### end Alembic commands ### diff --git a/api/migrations/versions/b3a09c049e8e_add_advanced_prompt_templates.py b/api/migrations/versions/b3a09c049e8e_add_advanced_prompt_templates.py index 772395c25b..c02c24c23f 100644 --- a/api/migrations/versions/b3a09c049e8e_add_advanced_prompt_templates.py +++ b/api/migrations/versions/b3a09c049e8e_add_advanced_prompt_templates.py @@ -11,9 +11,6 @@ from alembic import op import models.types -def _is_pg(conn): - return conn.dialect.name == "postgresql" - # revision identifiers, used by Alembic. revision = 'b3a09c049e8e' down_revision = '2e9819ca5b28' @@ -23,20 +20,11 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('app_model_configs', schema=None) as batch_op: - batch_op.add_column(sa.Column('prompt_type', sa.String(length=255), nullable=False, server_default='simple')) - batch_op.add_column(sa.Column('chat_prompt_config', sa.Text(), nullable=True)) - batch_op.add_column(sa.Column('completion_prompt_config', sa.Text(), nullable=True)) - batch_op.add_column(sa.Column('dataset_configs', sa.Text(), nullable=True)) - else: - with op.batch_alter_table('app_model_configs', schema=None) as batch_op: - batch_op.add_column(sa.Column('prompt_type', sa.String(length=255), nullable=False, server_default='simple')) - batch_op.add_column(sa.Column('chat_prompt_config', models.types.LongText(), nullable=True)) - batch_op.add_column(sa.Column('completion_prompt_config', models.types.LongText(), nullable=True)) - batch_op.add_column(sa.Column('dataset_configs', models.types.LongText(), nullable=True)) + with op.batch_alter_table('app_model_configs', schema=None) as batch_op: + batch_op.add_column(sa.Column('prompt_type', sa.String(length=255), nullable=False, server_default='simple')) + batch_op.add_column(sa.Column('chat_prompt_config', models.types.LongText(), nullable=True)) + batch_op.add_column(sa.Column('completion_prompt_config', models.types.LongText(), nullable=True)) + batch_op.add_column(sa.Column('dataset_configs', models.types.LongText(), nullable=True)) # ### end Alembic commands ### diff --git a/api/migrations/versions/c031d46af369_remove_app_model_config_trace_config_.py b/api/migrations/versions/c031d46af369_remove_app_model_config_trace_config_.py index 76be794ff4..fe51d1c78d 100644 --- a/api/migrations/versions/c031d46af369_remove_app_model_config_trace_config_.py +++ b/api/migrations/versions/c031d46af369_remove_app_model_config_trace_config_.py @@ -7,7 +7,6 @@ Create Date: 2024-06-17 10:01:00.255189 """ import sqlalchemy as sa from alembic import op -from sqlalchemy.dialects import postgresql import models.types diff --git a/api/migrations/versions/e1901f623fd0_add_annotation_reply.py b/api/migrations/versions/e1901f623fd0_add_annotation_reply.py index 9e02ec5d84..36e934f0fc 100644 --- a/api/migrations/versions/e1901f623fd0_add_annotation_reply.py +++ b/api/migrations/versions/e1901f623fd0_add_annotation_reply.py @@ -54,12 +54,9 @@ def upgrade(): batch_op.create_index('app_annotation_hit_histories_annotation_idx', ['annotation_id'], unique=False) batch_op.create_index('app_annotation_hit_histories_app_idx', ['app_id'], unique=False) - if _is_pg(conn): - with op.batch_alter_table('app_model_configs', schema=None) as batch_op: - batch_op.add_column(sa.Column('annotation_reply', sa.Text(), nullable=True)) - else: - with op.batch_alter_table('app_model_configs', schema=None) as batch_op: - batch_op.add_column(sa.Column('annotation_reply', models.types.LongText(), nullable=True)) + + with op.batch_alter_table('app_model_configs', schema=None) as batch_op: + batch_op.add_column(sa.Column('annotation_reply', models.types.LongText(), nullable=True)) if _is_pg(conn): with op.batch_alter_table('dataset_collection_bindings', schema=None) as batch_op: @@ -68,54 +65,31 @@ def upgrade(): with op.batch_alter_table('dataset_collection_bindings', schema=None) as batch_op: batch_op.add_column(sa.Column('type', sa.String(length=40), server_default=sa.text("'dataset'"), nullable=False)) - if _is_pg(conn): - with op.batch_alter_table('message_annotations', schema=None) as batch_op: - batch_op.add_column(sa.Column('question', sa.Text(), nullable=True)) - batch_op.add_column(sa.Column('hit_count', sa.Integer(), server_default=sa.text('0'), nullable=False)) - batch_op.alter_column('conversation_id', - existing_type=postgresql.UUID(), - nullable=True) - batch_op.alter_column('message_id', - existing_type=postgresql.UUID(), - nullable=True) - else: - with op.batch_alter_table('message_annotations', schema=None) as batch_op: - batch_op.add_column(sa.Column('question', models.types.LongText(), nullable=True)) - batch_op.add_column(sa.Column('hit_count', sa.Integer(), server_default=sa.text('0'), nullable=False)) - batch_op.alter_column('conversation_id', - existing_type=models.types.StringUUID(), - nullable=True) - batch_op.alter_column('message_id', - existing_type=models.types.StringUUID(), - nullable=True) + with op.batch_alter_table('message_annotations', schema=None) as batch_op: + batch_op.add_column(sa.Column('question', models.types.LongText(), nullable=True)) + batch_op.add_column(sa.Column('hit_count', sa.Integer(), server_default=sa.text('0'), nullable=False)) + batch_op.alter_column('conversation_id', + existing_type=models.types.StringUUID(), + nullable=True) + batch_op.alter_column('message_id', + existing_type=models.types.StringUUID(), + nullable=True) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - if _is_pg(conn): - with op.batch_alter_table('message_annotations', schema=None) as batch_op: - batch_op.alter_column('message_id', - existing_type=postgresql.UUID(), - nullable=False) - batch_op.alter_column('conversation_id', - existing_type=postgresql.UUID(), - nullable=False) - batch_op.drop_column('hit_count') - batch_op.drop_column('question') - else: - with op.batch_alter_table('message_annotations', schema=None) as batch_op: - batch_op.alter_column('message_id', - existing_type=models.types.StringUUID(), - nullable=False) - batch_op.alter_column('conversation_id', - existing_type=models.types.StringUUID(), - nullable=False) - batch_op.drop_column('hit_count') - batch_op.drop_column('question') + with op.batch_alter_table('message_annotations', schema=None) as batch_op: + batch_op.alter_column('message_id', + existing_type=models.types.StringUUID(), + nullable=False) + batch_op.alter_column('conversation_id', + existing_type=models.types.StringUUID(), + nullable=False) + batch_op.drop_column('hit_count') + batch_op.drop_column('question') with op.batch_alter_table('dataset_collection_bindings', schema=None) as batch_op: batch_op.drop_column('type') diff --git a/api/migrations/versions/f2a6fc85e260_add_anntation_history_message_id.py b/api/migrations/versions/f2a6fc85e260_add_anntation_history_message_id.py index 02098e91c1..ac1c14e50c 100644 --- a/api/migrations/versions/f2a6fc85e260_add_anntation_history_message_id.py +++ b/api/migrations/versions/f2a6fc85e260_add_anntation_history_message_id.py @@ -12,9 +12,6 @@ from sqlalchemy.dialects import postgresql import models.types -def _is_pg(conn): - return conn.dialect.name == "postgresql" - # revision identifiers, used by Alembic. revision = 'f2a6fc85e260' down_revision = '46976cc39132' @@ -24,16 +21,9 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - conn = op.get_bind() - - if _is_pg(conn): - with op.batch_alter_table('app_annotation_hit_histories', schema=None) as batch_op: - batch_op.add_column(sa.Column('message_id', postgresql.UUID(), nullable=False)) - batch_op.create_index('app_annotation_hit_histories_message_idx', ['message_id'], unique=False) - else: - with op.batch_alter_table('app_annotation_hit_histories', schema=None) as batch_op: - batch_op.add_column(sa.Column('message_id', models.types.StringUUID(), nullable=False)) - batch_op.create_index('app_annotation_hit_histories_message_idx', ['message_id'], unique=False) + with op.batch_alter_table('app_annotation_hit_histories', schema=None) as batch_op: + batch_op.add_column(sa.Column('message_id', models.types.StringUUID(), nullable=False)) + batch_op.create_index('app_annotation_hit_histories_message_idx', ['message_id'], unique=False) # ### end Alembic commands ### From 815ae6c7548564510115d4f094a91add0a837004 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Sun, 4 Jan 2026 10:22:36 +0800 Subject: [PATCH 30/37] chore: remove redundant web/app/page.module.css (#30482) --- web/app/page.module.css | 266 ---------------------------------------- 1 file changed, 266 deletions(-) delete mode 100644 web/app/page.module.css diff --git a/web/app/page.module.css b/web/app/page.module.css deleted file mode 100644 index b51afee7b1..0000000000 --- a/web/app/page.module.css +++ /dev/null @@ -1,266 +0,0 @@ -.main { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - padding: 6rem; - min-height: 100vh; -} - -.description { - display: inherit; - justify-content: inherit; - align-items: inherit; - font-size: 0.85rem; - max-width: var(--max-width); - width: 100%; - z-index: 2; - font-family: var(--font-mono); -} - -.description a { - display: flex; - align-items: center; - justify-content: center; - gap: 0.5rem; -} - -.description p { - position: relative; - margin: 0; - padding: 1rem; - background-color: rgba(var(--callout-rgb), 0.5); - border: 1px solid rgba(var(--callout-border-rgb), 0.3); - border-radius: var(--border-radius); -} - -.code { - font-weight: 700; - font-family: var(--font-mono); -} - -.grid { - display: grid; - grid-template-columns: repeat(3, minmax(33%, auto)); - width: var(--max-width); - max-width: 100%; -} - -.card { - padding: 1rem 1.2rem; - border-radius: var(--border-radius); - background: rgba(var(--card-rgb), 0); - border: 1px solid rgba(var(--card-border-rgb), 0); - transition: background 200ms, border 200ms; -} - -.card span { - display: inline-block; - transition: transform 200ms; -} - -.card h2 { - font-weight: 600; - margin-bottom: 0.7rem; -} - -.card p { - margin: 0; - opacity: 0.6; - font-size: 0.9rem; - line-height: 1.5; - max-width: 34ch; -} - -.center { - display: flex; - justify-content: center; - align-items: center; - position: relative; - padding: 4rem 0; -} - -.center::before { - background: var(--secondary-glow); - border-radius: 50%; - width: 480px; - height: 360px; - margin-left: -400px; -} - -.center::after { - background: var(--primary-glow); - width: 240px; - height: 180px; - z-index: -1; -} - -.center::before, -.center::after { - content: ''; - left: 50%; - position: absolute; - filter: blur(45px); - transform: translateZ(0); -} - -.logo, -.thirteen { - position: relative; -} - -.thirteen { - display: flex; - justify-content: center; - align-items: center; - width: 75px; - height: 75px; - padding: 25px 10px; - margin-left: 16px; - transform: translateZ(0); - border-radius: var(--border-radius); - overflow: hidden; - box-shadow: 0px 2px 8px -1px #0000001a; -} - -.thirteen::before, -.thirteen::after { - content: ''; - position: absolute; - z-index: -1; -} - -/* Conic Gradient Animation */ -.thirteen::before { - animation: 6s rotate linear infinite; - width: 200%; - height: 200%; - background: var(--tile-border); -} - -/* Inner Square */ -.thirteen::after { - inset: 0; - padding: 1px; - border-radius: var(--border-radius); - background: linear-gradient(to bottom right, - rgba(var(--tile-start-rgb), 1), - rgba(var(--tile-end-rgb), 1)); - background-clip: content-box; -} - -/* Enable hover only on non-touch devices */ -@media (hover: hover) and (pointer: fine) { - .card:hover { - background: rgba(var(--card-rgb), 0.1); - border: 1px solid rgba(var(--card-border-rgb), 0.15); - } - - .card:hover span { - transform: translateX(4px); - } -} - -@media (prefers-reduced-motion) { - .thirteen::before { - animation: none; - } - - .card:hover span { - transform: none; - } -} - -/* Mobile and Tablet */ -@media (max-width: 1023px) { - .content { - padding: 4rem; - } - - .grid { - grid-template-columns: 1fr; - margin-bottom: 120px; - max-width: 320px; - text-align: center; - } - - .card { - padding: 1rem 2.5rem; - } - - .card h2 { - margin-bottom: 0.5rem; - } - - .center { - padding: 8rem 0 6rem; - } - - .center::before { - transform: none; - height: 300px; - } - - .description { - font-size: 0.8rem; - } - - .description a { - padding: 1rem; - } - - .description p, - .description div { - display: flex; - justify-content: center; - position: fixed; - width: 100%; - } - - .description p { - align-items: center; - inset: 0 0 auto; - padding: 2rem 1rem 1.4rem; - border-radius: 0; - border: none; - border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); - background: linear-gradient(to bottom, - rgba(var(--background-start-rgb), 1), - rgba(var(--callout-rgb), 0.5)); - background-clip: padding-box; - backdrop-filter: blur(24px); - } - - .description div { - align-items: flex-end; - pointer-events: none; - inset: auto 0 0; - padding: 2rem; - height: 200px; - background: linear-gradient(to bottom, - transparent 0%, - rgb(var(--background-end-rgb)) 40%); - z-index: 1; - } -} - -@media (prefers-color-scheme: dark) { - .vercelLogo { - filter: invert(1); - } - - .logo, - .thirteen img { - filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); - } -} - -@keyframes rotate { - from { - transform: rotate(360deg); - } - - to { - transform: rotate(0deg); - } -} From 1629e22d987aeb8be42ee8265a68b47733be9943 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Sun, 4 Jan 2026 11:15:48 +0800 Subject: [PATCH 31/37] update for merge --- .../provider-added-card/quota-panel.tsx | 19 ++++++++++--------- web/i18n/en-US/billing.json | 2 +- web/i18n/en-US/common.json | 6 +++++- web/i18n/ja-JP/billing.json | 2 +- web/i18n/ja-JP/common.json | 6 +++++- web/i18n/zh-Hans/billing.json | 2 +- web/i18n/zh-Hans/common.json | 6 +++++- 7 files changed, 28 insertions(+), 15 deletions(-) diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx index 90cf1f363c..abb129c600 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx @@ -95,20 +95,21 @@ const QuotaPanel: FC = ({ return (
- {t('common.modelProvider.quota')} - + {t('modelProvider.quota', { ns: 'common' })} +
{formatNumber(credits)} - {t('common.modelProvider.credits')} + {t('modelProvider.credits', { ns: 'common' })} {currentWorkspace.next_credit_reset_date ? ( <> · - {t('common.modelProvider.resetDate', { - date: formatTime(currentWorkspace.next_credit_reset_date, t('appLog.dateFormat') as string), + {t('modelProvider.resetDate', { + ns: 'common', + date: formatTime(currentWorkspace.next_credit_reset_date, t('dateFormat', { ns: 'appLog' })), interpolation: { escapeValue: false }, })} @@ -122,15 +123,15 @@ const QuotaPanel: FC = ({ const usingQuota = providerType === PreferredProviderTypeEnum.system const getTooltipKey = () => { if (usingQuota) - return 'common.modelProvider.card.modelSupported' + return 'modelProvider.card.modelSupported' if (providerType === PreferredProviderTypeEnum.custom) - return 'common.modelProvider.card.modelAPI' - return 'common.modelProvider.card.modelNotSupported' + return 'modelProvider.card.modelAPI' + return 'modelProvider.card.modelNotSupported' } return (
Date: Sun, 4 Jan 2026 11:20:06 +0800 Subject: [PATCH 32/37] chore: integrate @tanstack/eslint-plugin-query and fix service layer lint errors (#30444) --- web/app/components/goto-anything/index.tsx | 2 +- web/eslint.config.mjs | 2 ++ web/package.json | 1 + web/pnpm-lock.yaml | 30 +++++++++++++++++----- web/service/access-control.ts | 2 +- web/service/knowledge/use-document.ts | 6 ++--- web/service/knowledge/use-segment.ts | 8 +++--- web/service/use-explore.ts | 2 +- web/service/use-models.ts | 2 +- web/service/use-pipeline.ts | 3 +-- web/service/use-plugins.ts | 11 ++++---- 11 files changed, 44 insertions(+), 25 deletions(-) diff --git a/web/app/components/goto-anything/index.tsx b/web/app/components/goto-anything/index.tsx index 30add3480d..d34176e4c7 100644 --- a/web/app/components/goto-anything/index.tsx +++ b/web/app/components/goto-anything/index.tsx @@ -110,7 +110,7 @@ const GotoAnything: FC = ({ isWorkflowPage, isRagPipelinePage, defaultLocale, - Object.keys(Actions).sort().join(','), + Actions, ], queryFn: async () => { const query = searchQueryDebouncedValue.toLowerCase() diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index 574dbb091e..3cdd3efedb 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -1,5 +1,6 @@ // @ts-check import antfu from '@antfu/eslint-config' +import pluginQuery from '@tanstack/eslint-plugin-query' import sonar from 'eslint-plugin-sonarjs' import storybook from 'eslint-plugin-storybook' import tailwind from 'eslint-plugin-tailwindcss' @@ -79,6 +80,7 @@ export default antfu( }, }, storybook.configs['flat/recommended'], + ...pluginQuery.configs['flat/recommended'], // sonar { rules: { diff --git a/web/package.json b/web/package.json index 7ee2325dbc..b595d433f9 100644 --- a/web/package.json +++ b/web/package.json @@ -165,6 +165,7 @@ "@storybook/addon-themes": "9.1.13", "@storybook/nextjs": "9.1.13", "@storybook/react": "9.1.13", + "@tanstack/eslint-plugin-query": "^5.91.2", "@tanstack/react-devtools": "^0.9.0", "@tanstack/react-form-devtools": "^0.2.9", "@tanstack/react-query-devtools": "^5.90.2", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index cdd194da37..9ffb092c6e 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -406,6 +406,9 @@ importers: '@storybook/react': specifier: 9.1.13 version: 9.1.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@18.15.0)(jiti@1.21.7)(sass@1.95.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(typescript@5.9.3) + '@tanstack/eslint-plugin-query': + specifier: ^5.91.2 + version: 5.91.2(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@tanstack/react-devtools': specifier: ^0.9.0 version: 0.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10) @@ -3387,6 +3390,11 @@ packages: peerDependencies: solid-js: '>=1.9.7' + '@tanstack/eslint-plugin-query@5.91.2': + resolution: {integrity: sha512-UPeWKl/Acu1IuuHJlsN+eITUHqAaa9/04geHHPedY8siVarSaWprY0SVMKrkpKfk5ehRT7+/MZ5QwWuEtkWrFw==} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + '@tanstack/form-core@1.27.1': resolution: {integrity: sha512-hPM+0tUnZ2C2zb2TE1lar1JJ0S0cbnQHlUwFcCnVBpMV3rjtUzkoM766gUpWrlmTGCzNad0GbJ0aTxVsjT6J8g==} @@ -10130,7 +10138,7 @@ snapshots: '@es-joy/jsdoccomment@0.76.0': dependencies: '@types/estree': 1.0.8 - '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/types': 8.50.1 comment-parser: 1.4.1 esquery: 1.6.0 jsdoc-type-pratt-parser: 6.10.0 @@ -10138,7 +10146,7 @@ snapshots: '@es-joy/jsdoccomment@0.78.0': dependencies: '@types/estree': 1.0.8 - '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/types': 8.50.1 comment-parser: 1.4.1 esquery: 1.6.0 jsdoc-type-pratt-parser: 7.0.0 @@ -11957,7 +11965,7 @@ snapshots: '@stylistic/eslint-plugin@5.6.1(eslint@9.39.2(jiti@1.21.7))': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7)) - '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/types': 8.50.1 eslint: 9.39.2(jiti@1.21.7) eslint-visitor-keys: 4.2.1 espree: 10.4.0 @@ -12039,6 +12047,14 @@ snapshots: - csstype - utf-8-validate + '@tanstack/eslint-plugin-query@5.91.2(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.39.2(jiti@1.21.7) + transitivePeerDependencies: + - supports-color + - typescript + '@tanstack/form-core@1.27.1': dependencies: '@tanstack/devtools-event-client': 0.3.5 @@ -12512,8 +12528,8 @@ snapshots: '@typescript-eslint/project-service@8.50.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3) - '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3) + '@typescript-eslint/types': 8.50.1 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: @@ -12742,7 +12758,7 @@ snapshots: '@vitest/eslint-plugin@1.6.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)(vitest@4.0.16(@types/node@18.15.0)(happy-dom@20.0.11)(jiti@1.21.7)(jsdom@27.3.0(canvas@3.2.0))(sass@1.95.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@typescript-eslint/scope-manager': 8.49.0 + '@typescript-eslint/scope-manager': 8.50.1 '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.2(jiti@1.21.7) optionalDependencies: @@ -14240,7 +14256,7 @@ snapshots: eslint-plugin-perfectionist@4.15.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): dependencies: - '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/types': 8.50.1 '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.2(jiti@1.21.7) natural-orderby: 5.0.0 diff --git a/web/service/access-control.ts b/web/service/access-control.ts index ad0c14fd0a..c87e01f482 100644 --- a/web/service/access-control.ts +++ b/web/service/access-control.ts @@ -73,7 +73,7 @@ export const useUpdateAccessMode = () => { export const useGetUserCanAccessApp = ({ appId, isInstalledApp = true, enabled }: { appId?: string, isInstalledApp?: boolean, enabled?: boolean }) => { const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) return useQuery({ - queryKey: [NAME_SPACE, 'user-can-access-app', appId], + queryKey: [NAME_SPACE, 'user-can-access-app', appId, systemFeatures.webapp_auth.enabled, isInstalledApp], queryFn: () => { if (systemFeatures.webapp_auth.enabled) return getUserCanAccess(appId!, isInstalledApp) diff --git a/web/service/knowledge/use-document.ts b/web/service/knowledge/use-document.ts index 63713130d9..1776ed1f4c 100644 --- a/web/service/knowledge/use-document.ts +++ b/web/service/knowledge/use-document.ts @@ -38,7 +38,7 @@ export const useDocumentList = (payload: { if (normalizedStatus && normalizedStatus !== 'all') params.status = normalizedStatus return useQuery({ - queryKey: [...useDocumentListKey, datasetId, keyword, page, limit, sort, normalizedStatus], + queryKey: [...useDocumentListKey, datasetId, params], queryFn: () => get(`/datasets/${datasetId}/documents`, { params, }), @@ -123,7 +123,7 @@ export const useDocumentDetail = (payload: { }) => { const { datasetId, documentId, params } = payload return useQuery({ - queryKey: [...useDocumentDetailKey, 'withoutMetaData', datasetId, documentId], + queryKey: [...useDocumentDetailKey, 'withoutMetaData', datasetId, documentId, params], queryFn: () => get(`/datasets/${datasetId}/documents/${documentId}`, { params }), }) } @@ -135,7 +135,7 @@ export const useDocumentMetadata = (payload: { }) => { const { datasetId, documentId, params } = payload return useQuery({ - queryKey: [...useDocumentDetailKey, 'onlyMetaData', datasetId, documentId], + queryKey: [...useDocumentDetailKey, 'onlyMetaData', datasetId, documentId, params], queryFn: () => get(`/datasets/${datasetId}/documents/${documentId}`, { params }), }) } diff --git a/web/service/knowledge/use-segment.ts b/web/service/knowledge/use-segment.ts index 1d0ce4b774..c42324ce6c 100644 --- a/web/service/knowledge/use-segment.ts +++ b/web/service/knowledge/use-segment.ts @@ -32,9 +32,9 @@ export const useSegmentList = ( disable?: boolean, ) => { const { datasetId, documentId, params } = payload - const { page, limit, keyword, enabled } = params + return useQuery({ - queryKey: [...useSegmentListKey, { datasetId, documentId, page, limit, keyword, enabled }], + queryKey: [...useSegmentListKey, datasetId, documentId, params], queryFn: () => { return get(`/datasets/${datasetId}/documents/${documentId}/segments`, { params }) }, @@ -111,9 +111,9 @@ export const useChildSegmentList = ( disable?: boolean, ) => { const { datasetId, documentId, segmentId, params } = payload - const { page, limit, keyword } = params + return useQuery({ - queryKey: [...useChildSegmentListKey, { datasetId, documentId, segmentId, page, limit, keyword }], + queryKey: [...useChildSegmentListKey, datasetId, documentId, segmentId, params], queryFn: () => { return get(`/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks`, { params }) }, diff --git a/web/service/use-explore.ts b/web/service/use-explore.ts index 68ddf966ab..a15b926306 100644 --- a/web/service/use-explore.ts +++ b/web/service/use-explore.ts @@ -59,7 +59,7 @@ export const useUpdateAppPinStatus = () => { export const useGetInstalledAppAccessModeByAppId = (appId: string | null) => { const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) return useQuery({ - queryKey: [NAME_SPACE, 'appAccessMode', appId], + queryKey: [NAME_SPACE, 'appAccessMode', appId, systemFeatures.webapp_auth.enabled], queryFn: () => { if (systemFeatures.webapp_auth.enabled === false) { return { diff --git a/web/service/use-models.ts b/web/service/use-models.ts index d960bda33f..05582b4105 100644 --- a/web/service/use-models.ts +++ b/web/service/use-models.ts @@ -82,7 +82,7 @@ export const useGetModelCredential = ( ) => { return useQuery({ enabled, - queryKey: [NAME_SPACE, 'model-list', provider, model, modelType, credentialId], + queryKey: [NAME_SPACE, 'model-list', provider, model, modelType, credentialId, configFrom], queryFn: () => get(`/workspaces/current/model-providers/${provider}/models/credentials?model=${model}&model_type=${modelType}&config_from=${configFrom}${credentialId ? `&credential_id=${credentialId}` : ''}`), staleTime: 0, gcTime: 0, diff --git a/web/service/use-pipeline.ts b/web/service/use-pipeline.ts index c1abbb1984..34c7332f4c 100644 --- a/web/service/use-pipeline.ts +++ b/web/service/use-pipeline.ts @@ -40,9 +40,8 @@ const NAME_SPACE = 'pipeline' export const PipelineTemplateListQueryKeyPrefix = [NAME_SPACE, 'template-list'] export const usePipelineTemplateList = (params: PipelineTemplateListParams, enabled = true) => { - const { type, language } = params return useQuery({ - queryKey: [...PipelineTemplateListQueryKeyPrefix, type, language], + queryKey: [...PipelineTemplateListQueryKeyPrefix, params], queryFn: () => { return get('/rag/pipeline/templates', { params }) }, diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index 5c10bac5d2..4e9776df97 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -677,20 +677,21 @@ export const useMutationCheckDependencies = () => { } export const useModelInList = (currentProvider?: ModelProvider, modelId?: string) => { + const provider = currentProvider?.provider return useQuery({ - queryKey: ['modelInList', currentProvider?.provider, modelId], + queryKey: ['modelInList', provider, modelId], queryFn: async () => { - if (!modelId || !currentProvider) + if (!modelId || !provider) return false try { - const modelsData = await fetchModelProviderModelList(`/workspaces/current/model-providers/${currentProvider?.provider}/models`) + const modelsData = await fetchModelProviderModelList(`/workspaces/current/model-providers/${provider}/models`) return !!modelId && !!modelsData.data.find(item => item.model === modelId) } catch { return false } }, - enabled: !!modelId && !!currentProvider, + enabled: !!modelId && !!provider, }) } @@ -742,7 +743,7 @@ export const usePluginReadme = ({ plugin_unique_identifier, language }: { plugin export const usePluginReadmeAsset = ({ file_name, plugin_unique_identifier }: { file_name?: string, plugin_unique_identifier?: string }) => { const normalizedFileName = file_name?.replace(/(^\.\/_assets\/|^_assets\/)/, '') return useQuery({ - queryKey: ['pluginReadmeAsset', plugin_unique_identifier, file_name], + queryKey: ['pluginReadmeAsset', plugin_unique_identifier, normalizedFileName], queryFn: () => get('/workspaces/current/plugin/asset', { params: { plugin_unique_identifier, file_name: normalizedFileName } }, { silent: true }), enabled: !!plugin_unique_identifier && !!file_name && /(^\.\/_assets|^_assets)/.test(file_name), }) From c976c1cb2d952ac83b042a4cb368df49a878855d Mon Sep 17 00:00:00 2001 From: CodingOnStar Date: Sun, 4 Jan 2026 11:43:29 +0800 Subject: [PATCH 33/37] feat: add new OpenAI icons (OpenaiBlue, OpenaiTeal, OpenaiViolet) and update existing icons for consistency --- .../src/public/llm/AnthropicShortLight.json | 68 +- .../src/public/llm/AnthropicShortLight.tsx | 8 +- .../base/icons/src/public/llm/Deepseek.json | 68 +- .../base/icons/src/public/llm/Deepseek.tsx | 8 +- .../base/icons/src/public/llm/Gemini.json | 1610 ++++++++--------- .../base/icons/src/public/llm/Gemini.tsx | 8 +- .../base/icons/src/public/llm/Grok.json | 140 +- .../base/icons/src/public/llm/Grok.tsx | 8 +- .../base/icons/src/public/llm/OpenaiBlue.json | 37 + .../base/icons/src/public/llm/OpenaiBlue.tsx | 20 + .../icons/src/public/llm/OpenaiSmall.json | 252 +-- .../base/icons/src/public/llm/OpenaiSmall.tsx | 8 +- .../base/icons/src/public/llm/OpenaiTeal.json | 37 + .../base/icons/src/public/llm/OpenaiTeal.tsx | 20 + .../icons/src/public/llm/OpenaiViolet.json | 37 + .../icons/src/public/llm/OpenaiViolet.tsx | 20 + .../base/icons/src/public/llm/index.ts | 3 + 17 files changed, 1263 insertions(+), 1089 deletions(-) create mode 100644 web/app/components/base/icons/src/public/llm/OpenaiBlue.json create mode 100644 web/app/components/base/icons/src/public/llm/OpenaiBlue.tsx create mode 100644 web/app/components/base/icons/src/public/llm/OpenaiTeal.json create mode 100644 web/app/components/base/icons/src/public/llm/OpenaiTeal.tsx create mode 100644 web/app/components/base/icons/src/public/llm/OpenaiViolet.json create mode 100644 web/app/components/base/icons/src/public/llm/OpenaiViolet.tsx diff --git a/web/app/components/base/icons/src/public/llm/AnthropicShortLight.json b/web/app/components/base/icons/src/public/llm/AnthropicShortLight.json index 4de3d7bcdc..2a8ff2f28a 100644 --- a/web/app/components/base/icons/src/public/llm/AnthropicShortLight.json +++ b/web/app/components/base/icons/src/public/llm/AnthropicShortLight.json @@ -1,36 +1,36 @@ { - "icon": { - "type": "element", - "isRootNode": true, - "name": "svg", - "attributes": { - "width": "40", - "height": "40", - "viewBox": "0 0 40 40", - "fill": "none", - "xmlns": "http://www.w3.org/2000/svg" - }, - "children": [ - { - "type": "element", - "name": "rect", - "attributes": { - "width": "40", - "height": "40", - "fill": "white" - }, - "children": [] - }, - { - "type": "element", - "name": "path", - "attributes": { - "d": "M25.7926 10.1311H21.5089L29.3208 29.869H33.6045L25.7926 10.1311ZM13.4164 10.1311L5.60449 29.869H9.97273L11.5703 25.724H19.743L21.3405 29.869H25.7087L17.8969 10.1311H13.4164ZM12.9834 22.0583L15.6566 15.1217L18.3299 22.0583H12.9834Z", - "fill": "black" - }, - "children": [] - } - ] - }, - "name": "AnthropicShortLight" + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "40", + "height": "40", + "viewBox": "0 0 40 40", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "rect", + "attributes": { + "width": "40", + "height": "40", + "fill": "white" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M25.7926 10.1311H21.5089L29.3208 29.869H33.6045L25.7926 10.1311ZM13.4164 10.1311L5.60449 29.869H9.97273L11.5703 25.724H19.743L21.3405 29.869H25.7087L17.8969 10.1311H13.4164ZM12.9834 22.0583L15.6566 15.1217L18.3299 22.0583H12.9834Z", + "fill": "black" + }, + "children": [] + } + ] + }, + "name": "AnthropicShortLight" } diff --git a/web/app/components/base/icons/src/public/llm/AnthropicShortLight.tsx b/web/app/components/base/icons/src/public/llm/AnthropicShortLight.tsx index e54fb286a1..2bd21f48da 100644 --- a/web/app/components/base/icons/src/public/llm/AnthropicShortLight.tsx +++ b/web/app/components/base/icons/src/public/llm/AnthropicShortLight.tsx @@ -1,17 +1,17 @@ // GENERATE BY script // DON NOT EDIT IT MANUALLY -import * as React from 'react' -import data from './AnthropicShortLight.json' -import IconBase from '@/app/components/base/icons/IconBase' import type { IconData } from '@/app/components/base/icons/IconBase' +import * as React from 'react' +import IconBase from '@/app/components/base/icons/IconBase' +import data from './AnthropicShortLight.json' const Icon = ( { ref, ...props }: React.SVGProps & { - ref?: React.RefObject>; + ref?: React.RefObject> }, ) => diff --git a/web/app/components/base/icons/src/public/llm/Deepseek.json b/web/app/components/base/icons/src/public/llm/Deepseek.json index 7501e23f88..1483974a02 100644 --- a/web/app/components/base/icons/src/public/llm/Deepseek.json +++ b/web/app/components/base/icons/src/public/llm/Deepseek.json @@ -1,36 +1,36 @@ { - "icon": { - "type": "element", - "isRootNode": true, - "name": "svg", - "attributes": { - "width": "40", - "height": "40", - "viewBox": "0 0 40 40", - "fill": "none", - "xmlns": "http://www.w3.org/2000/svg" - }, - "children": [ - { - "type": "element", - "name": "rect", - "attributes": { - "width": "40", - "height": "40", - "fill": "white" - }, - "children": [] - }, - { - "type": "element", - "name": "path", - "attributes": { - "d": "M36.6676 11.2917C36.3316 11.1277 36.1871 11.4402 35.9906 11.599C35.9242 11.6511 35.8668 11.7188 35.8108 11.7787C35.3199 12.3048 34.747 12.6485 33.9996 12.6068C32.9046 12.5469 31.971 12.8907 31.1455 13.7293C30.9696 12.6954 30.3863 12.0782 29.4996 11.6824C29.0348 11.4766 28.5647 11.2709 28.2406 10.823C28.0127 10.5053 27.9515 10.1511 27.8368 9.80214C27.7652 9.59121 27.6923 9.37506 27.4502 9.33861C27.1871 9.29694 27.0843 9.51829 26.9814 9.70318C26.5674 10.4584 26.4084 11.2917 26.4228 12.1355C26.4592 14.0313 27.26 15.5417 28.8486 16.6173C29.0296 16.7397 29.0764 16.8646 29.0191 17.0443C28.9111 17.4141 28.7822 17.7735 28.6676 18.1433C28.596 18.3803 28.4879 18.4323 28.2354 18.3282C27.363 17.9637 26.609 17.4246 25.9436 16.7709C24.8135 15.6771 23.7914 14.4689 22.5166 13.5235C22.2171 13.3021 21.919 13.0964 21.609 12.9011C20.3082 11.6355 21.7796 10.5964 22.1194 10.474C22.4762 10.3464 22.2431 9.9037 21.092 9.90891C19.9423 9.91413 18.889 10.2995 17.5478 10.8126C17.3512 10.8907 17.1455 10.948 16.9332 10.9922C15.7158 10.7631 14.4515 10.711 13.1298 10.8594C10.6428 11.1381 8.65587 12.3152 7.19493 14.3255C5.44102 16.7397 5.02826 19.4845 5.53347 22.349C6.06473 25.3646 7.60249 27.8646 9.96707 29.8178C12.4176 31.8413 15.2406 32.8334 18.4606 32.6433C20.4163 32.5313 22.5947 32.2683 25.0504 30.1875C25.6702 30.4949 26.3199 30.6173 27.3994 30.711C28.2302 30.7891 29.0296 30.6694 29.6494 30.5417C30.6194 30.3361 30.5518 29.4375 30.2015 29.2709C27.3578 27.9454 27.9814 28.4845 27.4136 28.0495C28.859 26.3361 31.0374 24.5574 31.889 18.797C31.9554 18.3386 31.898 18.0522 31.889 17.6798C31.8838 17.4558 31.9346 17.3673 32.1923 17.3413C32.9046 17.2605 33.596 17.0651 34.2314 16.7137C36.0739 15.7058 36.816 14.0522 36.9918 12.0678C37.0179 11.7657 36.9866 11.4506 36.6676 11.2917ZM20.613 29.1485C17.8564 26.9793 16.5204 26.2657 15.9684 26.297C15.4527 26.3255 15.5452 26.9167 15.6584 27.3022C15.777 27.6823 15.9319 27.9454 16.1494 28.2787C16.2991 28.5001 16.402 28.8307 15.9996 29.0755C15.1116 29.6277 13.5687 28.8907 13.4958 28.8542C11.7001 27.797 10.1988 26.3985 9.14025 24.487C8.11941 22.6459 7.52566 20.6719 7.42801 18.5651C7.40197 18.0547 7.5517 17.875 8.05691 17.7839C8.72227 17.6615 9.40978 17.6355 10.0751 17.7318C12.8876 18.1433 15.2822 19.4037 17.2887 21.3959C18.4346 22.5339 19.3018 23.8907 20.195 25.2162C21.1442 26.6251 22.1663 27.9662 23.4671 29.0651C23.9254 29.4506 24.2926 29.7449 24.6428 29.961C23.5856 30.0782 21.8199 30.1042 20.613 29.1485ZM21.9332 20.6407C21.9332 20.4141 22.1142 20.2345 22.342 20.2345C22.3928 20.2345 22.4398 20.2449 22.4814 20.2605C22.5374 20.2813 22.5895 20.3126 22.6299 20.3594C22.7027 20.4298 22.7444 20.5339 22.7444 20.6407C22.7444 20.8673 22.5635 21.047 22.3368 21.047C22.109 21.047 21.9332 20.8673 21.9332 20.6407ZM26.036 22.7501C25.7731 22.8569 25.51 22.9506 25.2575 22.961C24.8655 22.9793 24.4371 22.8203 24.204 22.6251C23.8434 22.323 23.5856 22.1537 23.4762 21.6225C23.4306 21.3959 23.4567 21.047 23.497 20.8465C23.5908 20.4141 23.4866 20.1381 23.1832 19.8855C22.9346 19.6798 22.6207 19.6251 22.2744 19.6251C22.1455 19.6251 22.027 19.5678 21.9384 19.5209C21.7939 19.4479 21.6754 19.2683 21.7887 19.047C21.8251 18.9766 22.001 18.8022 22.0426 18.7709C22.5114 18.5027 23.053 18.5913 23.5543 18.7918C24.0191 18.9818 24.3694 19.3307 24.8746 19.823C25.3915 20.4194 25.484 20.5861 25.7783 21.0313C26.01 21.3829 26.2223 21.7422 26.3668 22.1537C26.454 22.4089 26.3408 22.6198 26.036 22.7501Z", - "fill": "#4D6BFE" - }, - "children": [] - } - ] - }, - "name": "Deepseek" + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "40", + "height": "40", + "viewBox": "0 0 40 40", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "rect", + "attributes": { + "width": "40", + "height": "40", + "fill": "white" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M36.6676 11.2917C36.3316 11.1277 36.1871 11.4402 35.9906 11.599C35.9242 11.6511 35.8668 11.7188 35.8108 11.7787C35.3199 12.3048 34.747 12.6485 33.9996 12.6068C32.9046 12.5469 31.971 12.8907 31.1455 13.7293C30.9696 12.6954 30.3863 12.0782 29.4996 11.6824C29.0348 11.4766 28.5647 11.2709 28.2406 10.823C28.0127 10.5053 27.9515 10.1511 27.8368 9.80214C27.7652 9.59121 27.6923 9.37506 27.4502 9.33861C27.1871 9.29694 27.0843 9.51829 26.9814 9.70318C26.5674 10.4584 26.4084 11.2917 26.4228 12.1355C26.4592 14.0313 27.26 15.5417 28.8486 16.6173C29.0296 16.7397 29.0764 16.8646 29.0191 17.0443C28.9111 17.4141 28.7822 17.7735 28.6676 18.1433C28.596 18.3803 28.4879 18.4323 28.2354 18.3282C27.363 17.9637 26.609 17.4246 25.9436 16.7709C24.8135 15.6771 23.7914 14.4689 22.5166 13.5235C22.2171 13.3021 21.919 13.0964 21.609 12.9011C20.3082 11.6355 21.7796 10.5964 22.1194 10.474C22.4762 10.3464 22.2431 9.9037 21.092 9.90891C19.9423 9.91413 18.889 10.2995 17.5478 10.8126C17.3512 10.8907 17.1455 10.948 16.9332 10.9922C15.7158 10.7631 14.4515 10.711 13.1298 10.8594C10.6428 11.1381 8.65587 12.3152 7.19493 14.3255C5.44102 16.7397 5.02826 19.4845 5.53347 22.349C6.06473 25.3646 7.60249 27.8646 9.96707 29.8178C12.4176 31.8413 15.2406 32.8334 18.4606 32.6433C20.4163 32.5313 22.5947 32.2683 25.0504 30.1875C25.6702 30.4949 26.3199 30.6173 27.3994 30.711C28.2302 30.7891 29.0296 30.6694 29.6494 30.5417C30.6194 30.3361 30.5518 29.4375 30.2015 29.2709C27.3578 27.9454 27.9814 28.4845 27.4136 28.0495C28.859 26.3361 31.0374 24.5574 31.889 18.797C31.9554 18.3386 31.898 18.0522 31.889 17.6798C31.8838 17.4558 31.9346 17.3673 32.1923 17.3413C32.9046 17.2605 33.596 17.0651 34.2314 16.7137C36.0739 15.7058 36.816 14.0522 36.9918 12.0678C37.0179 11.7657 36.9866 11.4506 36.6676 11.2917ZM20.613 29.1485C17.8564 26.9793 16.5204 26.2657 15.9684 26.297C15.4527 26.3255 15.5452 26.9167 15.6584 27.3022C15.777 27.6823 15.9319 27.9454 16.1494 28.2787C16.2991 28.5001 16.402 28.8307 15.9996 29.0755C15.1116 29.6277 13.5687 28.8907 13.4958 28.8542C11.7001 27.797 10.1988 26.3985 9.14025 24.487C8.11941 22.6459 7.52566 20.6719 7.42801 18.5651C7.40197 18.0547 7.5517 17.875 8.05691 17.7839C8.72227 17.6615 9.40978 17.6355 10.0751 17.7318C12.8876 18.1433 15.2822 19.4037 17.2887 21.3959C18.4346 22.5339 19.3018 23.8907 20.195 25.2162C21.1442 26.6251 22.1663 27.9662 23.4671 29.0651C23.9254 29.4506 24.2926 29.7449 24.6428 29.961C23.5856 30.0782 21.8199 30.1042 20.613 29.1485ZM21.9332 20.6407C21.9332 20.4141 22.1142 20.2345 22.342 20.2345C22.3928 20.2345 22.4398 20.2449 22.4814 20.2605C22.5374 20.2813 22.5895 20.3126 22.6299 20.3594C22.7027 20.4298 22.7444 20.5339 22.7444 20.6407C22.7444 20.8673 22.5635 21.047 22.3368 21.047C22.109 21.047 21.9332 20.8673 21.9332 20.6407ZM26.036 22.7501C25.7731 22.8569 25.51 22.9506 25.2575 22.961C24.8655 22.9793 24.4371 22.8203 24.204 22.6251C23.8434 22.323 23.5856 22.1537 23.4762 21.6225C23.4306 21.3959 23.4567 21.047 23.497 20.8465C23.5908 20.4141 23.4866 20.1381 23.1832 19.8855C22.9346 19.6798 22.6207 19.6251 22.2744 19.6251C22.1455 19.6251 22.027 19.5678 21.9384 19.5209C21.7939 19.4479 21.6754 19.2683 21.7887 19.047C21.8251 18.9766 22.001 18.8022 22.0426 18.7709C22.5114 18.5027 23.053 18.5913 23.5543 18.7918C24.0191 18.9818 24.3694 19.3307 24.8746 19.823C25.3915 20.4194 25.484 20.5861 25.7783 21.0313C26.01 21.3829 26.2223 21.7422 26.3668 22.1537C26.454 22.4089 26.3408 22.6198 26.036 22.7501Z", + "fill": "#4D6BFE" + }, + "children": [] + } + ] + }, + "name": "Deepseek" } diff --git a/web/app/components/base/icons/src/public/llm/Deepseek.tsx b/web/app/components/base/icons/src/public/llm/Deepseek.tsx index 24ef972caf..b19beb8b8f 100644 --- a/web/app/components/base/icons/src/public/llm/Deepseek.tsx +++ b/web/app/components/base/icons/src/public/llm/Deepseek.tsx @@ -1,17 +1,17 @@ // GENERATE BY script // DON NOT EDIT IT MANUALLY -import * as React from 'react' -import data from './Deepseek.json' -import IconBase from '@/app/components/base/icons/IconBase' import type { IconData } from '@/app/components/base/icons/IconBase' +import * as React from 'react' +import IconBase from '@/app/components/base/icons/IconBase' +import data from './Deepseek.json' const Icon = ( { ref, ...props }: React.SVGProps & { - ref?: React.RefObject>; + ref?: React.RefObject> }, ) => diff --git a/web/app/components/base/icons/src/public/llm/Gemini.json b/web/app/components/base/icons/src/public/llm/Gemini.json index aca8e86a05..3121b1ea19 100644 --- a/web/app/components/base/icons/src/public/llm/Gemini.json +++ b/web/app/components/base/icons/src/public/llm/Gemini.json @@ -1,807 +1,807 @@ { - "icon": { - "type": "element", - "isRootNode": true, - "name": "svg", - "attributes": { - "width": "40", - "height": "40", - "viewBox": "0 0 40 40", - "fill": "none", - "xmlns": "http://www.w3.org/2000/svg" - }, - "children": [ - { - "type": "element", - "name": "rect", - "attributes": { - "width": "40", - "height": "40", - "fill": "white" - }, - "children": [] - }, - { - "type": "element", - "name": "mask", - "attributes": { - "id": "mask0_3892_95663", - "style": "mask-type:alpha", - "maskUnits": "userSpaceOnUse", - "x": "6", - "y": "6", - "width": "28", - "height": "29" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M20 6C20.2936 6 20.5488 6.2005 20.6205 6.48556C20.8393 7.3566 21.1277 8.20866 21.4828 9.03356C22.4116 11.191 23.6854 13.0791 25.3032 14.6968C26.9218 16.3146 28.8095 17.5888 30.9664 18.5172C31.7941 18.8735 32.6436 19.16 33.5149 19.3795C33.6533 19.4143 33.7762 19.4942 33.8641 19.6067C33.9519 19.7192 33.9998 19.8578 34 20.0005C34 20.2941 33.7995 20.5492 33.5149 20.621C32.6437 20.8399 31.7915 21.1282 30.9664 21.4833C28.8095 22.4121 26.9209 23.6859 25.3032 25.3036C23.6854 26.9223 22.4116 28.8099 21.4828 30.9669C21.1278 31.7919 20.8394 32.6439 20.6205 33.5149C20.586 33.6534 20.5062 33.7764 20.3937 33.8644C20.2813 33.9524 20.1427 34.0003 20 34.0005C19.8572 34.0003 19.7186 33.9525 19.6062 33.8645C19.4937 33.7765 19.414 33.6535 19.3795 33.5149C19.1605 32.6439 18.872 31.7918 18.5167 30.9669C17.5884 28.8099 16.3151 26.9214 14.6964 25.3036C13.0782 23.6859 11.1906 22.4121 9.03309 21.4833C8.20814 21.1283 7.35608 20.8399 6.48509 20.621C6.34667 20.5864 6.22377 20.5065 6.13589 20.3941C6.04801 20.2817 6.00018 20.1432 6 20.0005C6.00024 19.8578 6.04808 19.7192 6.13594 19.6067C6.2238 19.4942 6.34667 19.4143 6.48509 19.3795C7.35612 19.1607 8.20819 18.8723 9.03309 18.5172C11.1906 17.5888 13.0786 16.3146 14.6964 14.6968C16.3141 13.0791 17.5884 11.191 18.5167 9.03356C18.8719 8.20862 19.1604 7.35656 19.3795 6.48556C19.4508 6.2005 19.7064 6 20 6Z", - "fill": "black" - }, - "children": [] - }, - { - "type": "element", - "name": "path", - "attributes": { - "d": "M20 6C20.2936 6 20.5488 6.2005 20.6205 6.48556C20.8393 7.3566 21.1277 8.20866 21.4828 9.03356C22.4116 11.191 23.6854 13.0791 25.3032 14.6968C26.9218 16.3146 28.8095 17.5888 30.9664 18.5172C31.7941 18.8735 32.6436 19.16 33.5149 19.3795C33.6533 19.4143 33.7762 19.4942 33.8641 19.6067C33.9519 19.7192 33.9998 19.8578 34 20.0005C34 20.2941 33.7995 20.5492 33.5149 20.621C32.6437 20.8399 31.7915 21.1282 30.9664 21.4833C28.8095 22.4121 26.9209 23.6859 25.3032 25.3036C23.6854 26.9223 22.4116 28.8099 21.4828 30.9669C21.1278 31.7919 20.8394 32.6439 20.6205 33.5149C20.586 33.6534 20.5062 33.7764 20.3937 33.8644C20.2813 33.9524 20.1427 34.0003 20 34.0005C19.8572 34.0003 19.7186 33.9525 19.6062 33.8645C19.4937 33.7765 19.414 33.6535 19.3795 33.5149C19.1605 32.6439 18.872 31.7918 18.5167 30.9669C17.5884 28.8099 16.3151 26.9214 14.6964 25.3036C13.0782 23.6859 11.1906 22.4121 9.03309 21.4833C8.20814 21.1283 7.35608 20.8399 6.48509 20.621C6.34667 20.5864 6.22377 20.5065 6.13589 20.3941C6.04801 20.2817 6.00018 20.1432 6 20.0005C6.00024 19.8578 6.04808 19.7192 6.13594 19.6067C6.2238 19.4942 6.34667 19.4143 6.48509 19.3795C7.35612 19.1607 8.20819 18.8723 9.03309 18.5172C11.1906 17.5888 13.0786 16.3146 14.6964 14.6968C16.3141 13.0791 17.5884 11.191 18.5167 9.03356C18.8719 8.20862 19.1604 7.35656 19.3795 6.48556C19.4508 6.2005 19.7064 6 20 6Z", - "fill": "url(#paint0_linear_3892_95663)" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "g", - "attributes": { - "mask": "url(#mask0_3892_95663)" - }, - "children": [ - { - "type": "element", - "name": "g", - "attributes": { - "filter": "url(#filter0_f_3892_95663)" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M3.47232 27.8921C6.70753 29.0411 10.426 26.8868 11.7778 23.0804C13.1296 19.274 11.6028 15.2569 8.36763 14.108C5.13242 12.959 1.41391 15.1133 0.06211 18.9197C-1.28969 22.7261 0.23711 26.7432 3.47232 27.8921Z", - "fill": "#FFE432" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "g", - "attributes": { - "filter": "url(#filter1_f_3892_95663)" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M17.8359 15.341C22.2806 15.341 25.8838 11.6588 25.8838 7.11644C25.8838 2.57412 22.2806 -1.10815 17.8359 -1.10815C13.3912 -1.10815 9.78809 2.57412 9.78809 7.11644C9.78809 11.6588 13.3912 15.341 17.8359 15.341Z", - "fill": "#FC413D" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "g", - "attributes": { - "filter": "url(#filter2_f_3892_95663)" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M14.7081 41.6431C19.3478 41.4163 22.8707 36.3599 22.5768 30.3493C22.283 24.3387 18.2836 19.65 13.644 19.8769C9.00433 20.1037 5.48139 25.1601 5.77525 31.1707C6.06911 37.1813 10.0685 41.87 14.7081 41.6431Z", - "fill": "#00B95C" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "g", - "attributes": { - "filter": "url(#filter3_f_3892_95663)" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M14.7081 41.6431C19.3478 41.4163 22.8707 36.3599 22.5768 30.3493C22.283 24.3387 18.2836 19.65 13.644 19.8769C9.00433 20.1037 5.48139 25.1601 5.77525 31.1707C6.06911 37.1813 10.0685 41.87 14.7081 41.6431Z", - "fill": "#00B95C" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "g", - "attributes": { - "filter": "url(#filter4_f_3892_95663)" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M19.355 38.0071C23.2447 35.6405 24.2857 30.2506 21.6803 25.9684C19.0748 21.6862 13.8095 20.1334 9.91983 22.5C6.03016 24.8666 4.98909 30.2565 7.59454 34.5387C10.2 38.8209 15.4653 40.3738 19.355 38.0071Z", - "fill": "#00B95C" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "g", - "attributes": { - "filter": "url(#filter5_f_3892_95663)" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M35.0759 24.5504C39.4477 24.5504 42.9917 21.1377 42.9917 16.9278C42.9917 12.7179 39.4477 9.30518 35.0759 9.30518C30.7042 9.30518 27.1602 12.7179 27.1602 16.9278C27.1602 21.1377 30.7042 24.5504 35.0759 24.5504Z", - "fill": "#3186FF" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "g", - "attributes": { - "filter": "url(#filter6_f_3892_95663)" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M0.362818 23.6667C4.3882 26.7279 10.2688 25.7676 13.4976 21.5219C16.7264 17.2762 16.0806 11.3528 12.0552 8.29156C8.02982 5.23037 2.14917 6.19062 -1.07959 10.4364C-4.30835 14.6821 -3.66256 20.6055 0.362818 23.6667Z", - "fill": "#FBBC04" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "g", - "attributes": { - "filter": "url(#filter7_f_3892_95663)" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M20.9877 28.1903C25.7924 31.4936 32.1612 30.5732 35.2128 26.1346C38.2644 21.696 36.8432 15.4199 32.0385 12.1166C27.2338 8.81334 20.865 9.73372 17.8134 14.1723C14.7618 18.611 16.183 24.887 20.9877 28.1903Z", - "fill": "#3186FF" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "g", - "attributes": { - "filter": "url(#filter8_f_3892_95663)" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M29.7231 4.99175C30.9455 6.65415 29.3748 9.88535 26.2149 12.2096C23.0549 14.5338 19.5026 15.0707 18.2801 13.4088C17.0576 11.7468 18.6284 8.51514 21.7883 6.19092C24.9482 3.86717 28.5006 3.32982 29.7231 4.99175Z", - "fill": "#749BFF" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "g", - "attributes": { - "filter": "url(#filter9_f_3892_95663)" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M19.6891 12.9486C24.5759 8.41581 26.2531 2.27858 23.4354 -0.759249C20.6176 -3.79708 14.3718 -2.58516 9.485 1.94765C4.59823 6.48046 2.92099 12.6177 5.73879 15.6555C8.55658 18.6933 14.8024 17.4814 19.6891 12.9486Z", - "fill": "#FC413D" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "g", - "attributes": { - "filter": "url(#filter10_f_3892_95663)" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M9.6712 29.23C12.5757 31.3088 15.9102 31.6247 17.1191 29.9356C18.328 28.2465 16.9535 25.1921 14.049 23.1133C11.1446 21.0345 7.81003 20.7186 6.60113 22.4077C5.39223 24.0968 6.76675 27.1512 9.6712 29.23Z", - "fill": "#FFEE48" - }, - "children": [] - } - ] - } - ] - }, - { - "type": "element", - "name": "defs", - "attributes": {}, - "children": [ - { - "type": "element", - "name": "filter", - "attributes": { - "id": "filter0_f_3892_95663", - "x": "-3.44095", - "y": "10.7885", - "width": "18.7217", - "height": "20.4229", - "filterUnits": "userSpaceOnUse", - "color-interpolation-filters": "sRGB" - }, - "children": [ - { - "type": "element", - "name": "feFlood", - "attributes": { - "flood-opacity": "0", - "result": "BackgroundImageFix" - }, - "children": [] - }, - { - "type": "element", - "name": "feBlend", - "attributes": { - "mode": "normal", - "in": "SourceGraphic", - "in2": "BackgroundImageFix", - "result": "shape" - }, - "children": [] - }, - { - "type": "element", - "name": "feGaussianBlur", - "attributes": { - "stdDeviation": "1.50514", - "result": "effect1_foregroundBlur_3892_95663" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "filter", - "attributes": { - "id": "filter1_f_3892_95663", - "x": "-4.76352", - "y": "-15.6598", - "width": "45.1989", - "height": "45.5524", - "filterUnits": "userSpaceOnUse", - "color-interpolation-filters": "sRGB" - }, - "children": [ - { - "type": "element", - "name": "feFlood", - "attributes": { - "flood-opacity": "0", - "result": "BackgroundImageFix" - }, - "children": [] - }, - { - "type": "element", - "name": "feBlend", - "attributes": { - "mode": "normal", - "in": "SourceGraphic", - "in2": "BackgroundImageFix", - "result": "shape" - }, - "children": [] - }, - { - "type": "element", - "name": "feGaussianBlur", - "attributes": { - "stdDeviation": "7.2758", - "result": "effect1_foregroundBlur_3892_95663" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "filter", - "attributes": { - "id": "filter2_f_3892_95663", - "x": "-6.61209", - "y": "7.49899", - "width": "41.5757", - "height": "46.522", - "filterUnits": "userSpaceOnUse", - "color-interpolation-filters": "sRGB" - }, - "children": [ - { - "type": "element", - "name": "feFlood", - "attributes": { - "flood-opacity": "0", - "result": "BackgroundImageFix" - }, - "children": [] - }, - { - "type": "element", - "name": "feBlend", - "attributes": { - "mode": "normal", - "in": "SourceGraphic", - "in2": "BackgroundImageFix", - "result": "shape" - }, - "children": [] - }, - { - "type": "element", - "name": "feGaussianBlur", - "attributes": { - "stdDeviation": "6.18495", - "result": "effect1_foregroundBlur_3892_95663" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "filter", - "attributes": { - "id": "filter3_f_3892_95663", - "x": "-6.61209", - "y": "7.49899", - "width": "41.5757", - "height": "46.522", - "filterUnits": "userSpaceOnUse", - "color-interpolation-filters": "sRGB" - }, - "children": [ - { - "type": "element", - "name": "feFlood", - "attributes": { - "flood-opacity": "0", - "result": "BackgroundImageFix" - }, - "children": [] - }, - { - "type": "element", - "name": "feBlend", - "attributes": { - "mode": "normal", - "in": "SourceGraphic", - "in2": "BackgroundImageFix", - "result": "shape" - }, - "children": [] - }, - { - "type": "element", - "name": "feGaussianBlur", - "attributes": { - "stdDeviation": "6.18495", - "result": "effect1_foregroundBlur_3892_95663" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "filter", - "attributes": { - "id": "filter4_f_3892_95663", - "x": "-6.21073", - "y": "9.02316", - "width": "41.6959", - "height": "42.4608", - "filterUnits": "userSpaceOnUse", - "color-interpolation-filters": "sRGB" - }, - "children": [ - { - "type": "element", - "name": "feFlood", - "attributes": { - "flood-opacity": "0", - "result": "BackgroundImageFix" - }, - "children": [] - }, - { - "type": "element", - "name": "feBlend", - "attributes": { - "mode": "normal", - "in": "SourceGraphic", - "in2": "BackgroundImageFix", - "result": "shape" - }, - "children": [] - }, - { - "type": "element", - "name": "feGaussianBlur", - "attributes": { - "stdDeviation": "6.18495", - "result": "effect1_foregroundBlur_3892_95663" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "filter", - "attributes": { - "id": "filter5_f_3892_95663", - "x": "15.405", - "y": "-2.44994", - "width": "39.3423", - "height": "38.7556", - "filterUnits": "userSpaceOnUse", - "color-interpolation-filters": "sRGB" - }, - "children": [ - { - "type": "element", - "name": "feFlood", - "attributes": { - "flood-opacity": "0", - "result": "BackgroundImageFix" - }, - "children": [] - }, - { - "type": "element", - "name": "feBlend", - "attributes": { - "mode": "normal", - "in": "SourceGraphic", - "in2": "BackgroundImageFix", - "result": "shape" - }, - "children": [] - }, - { - "type": "element", - "name": "feGaussianBlur", - "attributes": { - "stdDeviation": "5.87756", - "result": "effect1_foregroundBlur_3892_95663" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "filter", - "attributes": { - "id": "filter6_f_3892_95663", - "x": "-13.7886", - "y": "-4.15284", - "width": "39.9951", - "height": "40.2639", - "filterUnits": "userSpaceOnUse", - "color-interpolation-filters": "sRGB" - }, - "children": [ - { - "type": "element", - "name": "feFlood", - "attributes": { - "flood-opacity": "0", - "result": "BackgroundImageFix" - }, - "children": [] - }, - { - "type": "element", - "name": "feBlend", - "attributes": { - "mode": "normal", - "in": "SourceGraphic", - "in2": "BackgroundImageFix", - "result": "shape" - }, - "children": [] - }, - { - "type": "element", - "name": "feGaussianBlur", - "attributes": { - "stdDeviation": "5.32691", - "result": "effect1_foregroundBlur_3892_95663" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "filter", - "attributes": { - "id": "filter7_f_3892_95663", - "x": "6.6925", - "y": "0.620963", - "width": "39.6414", - "height": "39.065", - "filterUnits": "userSpaceOnUse", - "color-interpolation-filters": "sRGB" - }, - "children": [ - { - "type": "element", - "name": "feFlood", - "attributes": { - "flood-opacity": "0", - "result": "BackgroundImageFix" - }, - "children": [] - }, - { - "type": "element", - "name": "feBlend", - "attributes": { - "mode": "normal", - "in": "SourceGraphic", - "in2": "BackgroundImageFix", - "result": "shape" - }, - "children": [] - }, - { - "type": "element", - "name": "feGaussianBlur", - "attributes": { - "stdDeviation": "4.75678", - "result": "effect1_foregroundBlur_3892_95663" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "filter", - "attributes": { - "id": "filter8_f_3892_95663", - "x": "9.35225", - "y": "-4.48661", - "width": "29.2984", - "height": "27.3739", - "filterUnits": "userSpaceOnUse", - "color-interpolation-filters": "sRGB" - }, - "children": [ - { - "type": "element", - "name": "feFlood", - "attributes": { - "flood-opacity": "0", - "result": "BackgroundImageFix" - }, - "children": [] - }, - { - "type": "element", - "name": "feBlend", - "attributes": { - "mode": "normal", - "in": "SourceGraphic", - "in2": "BackgroundImageFix", - "result": "shape" - }, - "children": [] - }, - { - "type": "element", - "name": "feGaussianBlur", - "attributes": { - "stdDeviation": "4.25649", - "result": "effect1_foregroundBlur_3892_95663" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "filter", - "attributes": { - "id": "filter9_f_3892_95663", - "x": "-2.81919", - "y": "-9.62339", - "width": "34.8122", - "height": "34.143", - "filterUnits": "userSpaceOnUse", - "color-interpolation-filters": "sRGB" - }, - "children": [ - { - "type": "element", - "name": "feFlood", - "attributes": { - "flood-opacity": "0", - "result": "BackgroundImageFix" - }, - "children": [] - }, - { - "type": "element", - "name": "feBlend", - "attributes": { - "mode": "normal", - "in": "SourceGraphic", - "in2": "BackgroundImageFix", - "result": "shape" - }, - "children": [] - }, - { - "type": "element", - "name": "feGaussianBlur", - "attributes": { - "stdDeviation": "3.59514", - "result": "effect1_foregroundBlur_3892_95663" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "filter", - "attributes": { - "id": "filter10_f_3892_95663", - "x": "-2.73761", - "y": "12.4221", - "width": "29.1949", - "height": "27.4994", - "filterUnits": "userSpaceOnUse", - "color-interpolation-filters": "sRGB" - }, - "children": [ - { - "type": "element", - "name": "feFlood", - "attributes": { - "flood-opacity": "0", - "result": "BackgroundImageFix" - }, - "children": [] - }, - { - "type": "element", - "name": "feBlend", - "attributes": { - "mode": "normal", - "in": "SourceGraphic", - "in2": "BackgroundImageFix", - "result": "shape" - }, - "children": [] - }, - { - "type": "element", - "name": "feGaussianBlur", - "attributes": { - "stdDeviation": "4.44986", - "result": "effect1_foregroundBlur_3892_95663" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "linearGradient", - "attributes": { - "id": "paint0_linear_3892_95663", - "x1": "13.9595", - "y1": "24.7349", - "x2": "28.5025", - "y2": "12.4738", - "gradientUnits": "userSpaceOnUse" - }, - "children": [ - { - "type": "element", - "name": "stop", - "attributes": { - "stop-color": "#4893FC" - }, - "children": [] - }, - { - "type": "element", - "name": "stop", - "attributes": { - "offset": "0.27", - "stop-color": "#4893FC" - }, - "children": [] - }, - { - "type": "element", - "name": "stop", - "attributes": { - "offset": "0.777", - "stop-color": "#969DFF" - }, - "children": [] - }, - { - "type": "element", - "name": "stop", - "attributes": { - "offset": "1", - "stop-color": "#BD99FE" - }, - "children": [] - } - ] - } - ] - } - ] - }, - "name": "Gemini" + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "40", + "height": "40", + "viewBox": "0 0 40 40", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "rect", + "attributes": { + "width": "40", + "height": "40", + "fill": "white" + }, + "children": [] + }, + { + "type": "element", + "name": "mask", + "attributes": { + "id": "mask0_3892_95663", + "style": "mask-type:alpha", + "maskUnits": "userSpaceOnUse", + "x": "6", + "y": "6", + "width": "28", + "height": "29" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M20 6C20.2936 6 20.5488 6.2005 20.6205 6.48556C20.8393 7.3566 21.1277 8.20866 21.4828 9.03356C22.4116 11.191 23.6854 13.0791 25.3032 14.6968C26.9218 16.3146 28.8095 17.5888 30.9664 18.5172C31.7941 18.8735 32.6436 19.16 33.5149 19.3795C33.6533 19.4143 33.7762 19.4942 33.8641 19.6067C33.9519 19.7192 33.9998 19.8578 34 20.0005C34 20.2941 33.7995 20.5492 33.5149 20.621C32.6437 20.8399 31.7915 21.1282 30.9664 21.4833C28.8095 22.4121 26.9209 23.6859 25.3032 25.3036C23.6854 26.9223 22.4116 28.8099 21.4828 30.9669C21.1278 31.7919 20.8394 32.6439 20.6205 33.5149C20.586 33.6534 20.5062 33.7764 20.3937 33.8644C20.2813 33.9524 20.1427 34.0003 20 34.0005C19.8572 34.0003 19.7186 33.9525 19.6062 33.8645C19.4937 33.7765 19.414 33.6535 19.3795 33.5149C19.1605 32.6439 18.872 31.7918 18.5167 30.9669C17.5884 28.8099 16.3151 26.9214 14.6964 25.3036C13.0782 23.6859 11.1906 22.4121 9.03309 21.4833C8.20814 21.1283 7.35608 20.8399 6.48509 20.621C6.34667 20.5864 6.22377 20.5065 6.13589 20.3941C6.04801 20.2817 6.00018 20.1432 6 20.0005C6.00024 19.8578 6.04808 19.7192 6.13594 19.6067C6.2238 19.4942 6.34667 19.4143 6.48509 19.3795C7.35612 19.1607 8.20819 18.8723 9.03309 18.5172C11.1906 17.5888 13.0786 16.3146 14.6964 14.6968C16.3141 13.0791 17.5884 11.191 18.5167 9.03356C18.8719 8.20862 19.1604 7.35656 19.3795 6.48556C19.4508 6.2005 19.7064 6 20 6Z", + "fill": "black" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M20 6C20.2936 6 20.5488 6.2005 20.6205 6.48556C20.8393 7.3566 21.1277 8.20866 21.4828 9.03356C22.4116 11.191 23.6854 13.0791 25.3032 14.6968C26.9218 16.3146 28.8095 17.5888 30.9664 18.5172C31.7941 18.8735 32.6436 19.16 33.5149 19.3795C33.6533 19.4143 33.7762 19.4942 33.8641 19.6067C33.9519 19.7192 33.9998 19.8578 34 20.0005C34 20.2941 33.7995 20.5492 33.5149 20.621C32.6437 20.8399 31.7915 21.1282 30.9664 21.4833C28.8095 22.4121 26.9209 23.6859 25.3032 25.3036C23.6854 26.9223 22.4116 28.8099 21.4828 30.9669C21.1278 31.7919 20.8394 32.6439 20.6205 33.5149C20.586 33.6534 20.5062 33.7764 20.3937 33.8644C20.2813 33.9524 20.1427 34.0003 20 34.0005C19.8572 34.0003 19.7186 33.9525 19.6062 33.8645C19.4937 33.7765 19.414 33.6535 19.3795 33.5149C19.1605 32.6439 18.872 31.7918 18.5167 30.9669C17.5884 28.8099 16.3151 26.9214 14.6964 25.3036C13.0782 23.6859 11.1906 22.4121 9.03309 21.4833C8.20814 21.1283 7.35608 20.8399 6.48509 20.621C6.34667 20.5864 6.22377 20.5065 6.13589 20.3941C6.04801 20.2817 6.00018 20.1432 6 20.0005C6.00024 19.8578 6.04808 19.7192 6.13594 19.6067C6.2238 19.4942 6.34667 19.4143 6.48509 19.3795C7.35612 19.1607 8.20819 18.8723 9.03309 18.5172C11.1906 17.5888 13.0786 16.3146 14.6964 14.6968C16.3141 13.0791 17.5884 11.191 18.5167 9.03356C18.8719 8.20862 19.1604 7.35656 19.3795 6.48556C19.4508 6.2005 19.7064 6 20 6Z", + "fill": "url(#paint0_linear_3892_95663)" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": { + "mask": "url(#mask0_3892_95663)" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "filter": "url(#filter0_f_3892_95663)" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M3.47232 27.8921C6.70753 29.0411 10.426 26.8868 11.7778 23.0804C13.1296 19.274 11.6028 15.2569 8.36763 14.108C5.13242 12.959 1.41391 15.1133 0.06211 18.9197C-1.28969 22.7261 0.23711 26.7432 3.47232 27.8921Z", + "fill": "#FFE432" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": { + "filter": "url(#filter1_f_3892_95663)" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M17.8359 15.341C22.2806 15.341 25.8838 11.6588 25.8838 7.11644C25.8838 2.57412 22.2806 -1.10815 17.8359 -1.10815C13.3912 -1.10815 9.78809 2.57412 9.78809 7.11644C9.78809 11.6588 13.3912 15.341 17.8359 15.341Z", + "fill": "#FC413D" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": { + "filter": "url(#filter2_f_3892_95663)" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M14.7081 41.6431C19.3478 41.4163 22.8707 36.3599 22.5768 30.3493C22.283 24.3387 18.2836 19.65 13.644 19.8769C9.00433 20.1037 5.48139 25.1601 5.77525 31.1707C6.06911 37.1813 10.0685 41.87 14.7081 41.6431Z", + "fill": "#00B95C" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": { + "filter": "url(#filter3_f_3892_95663)" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M14.7081 41.6431C19.3478 41.4163 22.8707 36.3599 22.5768 30.3493C22.283 24.3387 18.2836 19.65 13.644 19.8769C9.00433 20.1037 5.48139 25.1601 5.77525 31.1707C6.06911 37.1813 10.0685 41.87 14.7081 41.6431Z", + "fill": "#00B95C" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": { + "filter": "url(#filter4_f_3892_95663)" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M19.355 38.0071C23.2447 35.6405 24.2857 30.2506 21.6803 25.9684C19.0748 21.6862 13.8095 20.1334 9.91983 22.5C6.03016 24.8666 4.98909 30.2565 7.59454 34.5387C10.2 38.8209 15.4653 40.3738 19.355 38.0071Z", + "fill": "#00B95C" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": { + "filter": "url(#filter5_f_3892_95663)" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M35.0759 24.5504C39.4477 24.5504 42.9917 21.1377 42.9917 16.9278C42.9917 12.7179 39.4477 9.30518 35.0759 9.30518C30.7042 9.30518 27.1602 12.7179 27.1602 16.9278C27.1602 21.1377 30.7042 24.5504 35.0759 24.5504Z", + "fill": "#3186FF" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": { + "filter": "url(#filter6_f_3892_95663)" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M0.362818 23.6667C4.3882 26.7279 10.2688 25.7676 13.4976 21.5219C16.7264 17.2762 16.0806 11.3528 12.0552 8.29156C8.02982 5.23037 2.14917 6.19062 -1.07959 10.4364C-4.30835 14.6821 -3.66256 20.6055 0.362818 23.6667Z", + "fill": "#FBBC04" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": { + "filter": "url(#filter7_f_3892_95663)" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M20.9877 28.1903C25.7924 31.4936 32.1612 30.5732 35.2128 26.1346C38.2644 21.696 36.8432 15.4199 32.0385 12.1166C27.2338 8.81334 20.865 9.73372 17.8134 14.1723C14.7618 18.611 16.183 24.887 20.9877 28.1903Z", + "fill": "#3186FF" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": { + "filter": "url(#filter8_f_3892_95663)" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M29.7231 4.99175C30.9455 6.65415 29.3748 9.88535 26.2149 12.2096C23.0549 14.5338 19.5026 15.0707 18.2801 13.4088C17.0576 11.7468 18.6284 8.51514 21.7883 6.19092C24.9482 3.86717 28.5006 3.32982 29.7231 4.99175Z", + "fill": "#749BFF" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": { + "filter": "url(#filter9_f_3892_95663)" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M19.6891 12.9486C24.5759 8.41581 26.2531 2.27858 23.4354 -0.759249C20.6176 -3.79708 14.3718 -2.58516 9.485 1.94765C4.59823 6.48046 2.92099 12.6177 5.73879 15.6555C8.55658 18.6933 14.8024 17.4814 19.6891 12.9486Z", + "fill": "#FC413D" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": { + "filter": "url(#filter10_f_3892_95663)" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M9.6712 29.23C12.5757 31.3088 15.9102 31.6247 17.1191 29.9356C18.328 28.2465 16.9535 25.1921 14.049 23.1133C11.1446 21.0345 7.81003 20.7186 6.60113 22.4077C5.39223 24.0968 6.76675 27.1512 9.6712 29.23Z", + "fill": "#FFEE48" + }, + "children": [] + } + ] + } + ] + }, + { + "type": "element", + "name": "defs", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "filter", + "attributes": { + "id": "filter0_f_3892_95663", + "x": "-3.44095", + "y": "10.7885", + "width": "18.7217", + "height": "20.4229", + "filterUnits": "userSpaceOnUse", + "color-interpolation-filters": "sRGB" + }, + "children": [ + { + "type": "element", + "name": "feFlood", + "attributes": { + "flood-opacity": "0", + "result": "BackgroundImageFix" + }, + "children": [] + }, + { + "type": "element", + "name": "feBlend", + "attributes": { + "mode": "normal", + "in": "SourceGraphic", + "in2": "BackgroundImageFix", + "result": "shape" + }, + "children": [] + }, + { + "type": "element", + "name": "feGaussianBlur", + "attributes": { + "stdDeviation": "1.50514", + "result": "effect1_foregroundBlur_3892_95663" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "filter", + "attributes": { + "id": "filter1_f_3892_95663", + "x": "-4.76352", + "y": "-15.6598", + "width": "45.1989", + "height": "45.5524", + "filterUnits": "userSpaceOnUse", + "color-interpolation-filters": "sRGB" + }, + "children": [ + { + "type": "element", + "name": "feFlood", + "attributes": { + "flood-opacity": "0", + "result": "BackgroundImageFix" + }, + "children": [] + }, + { + "type": "element", + "name": "feBlend", + "attributes": { + "mode": "normal", + "in": "SourceGraphic", + "in2": "BackgroundImageFix", + "result": "shape" + }, + "children": [] + }, + { + "type": "element", + "name": "feGaussianBlur", + "attributes": { + "stdDeviation": "7.2758", + "result": "effect1_foregroundBlur_3892_95663" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "filter", + "attributes": { + "id": "filter2_f_3892_95663", + "x": "-6.61209", + "y": "7.49899", + "width": "41.5757", + "height": "46.522", + "filterUnits": "userSpaceOnUse", + "color-interpolation-filters": "sRGB" + }, + "children": [ + { + "type": "element", + "name": "feFlood", + "attributes": { + "flood-opacity": "0", + "result": "BackgroundImageFix" + }, + "children": [] + }, + { + "type": "element", + "name": "feBlend", + "attributes": { + "mode": "normal", + "in": "SourceGraphic", + "in2": "BackgroundImageFix", + "result": "shape" + }, + "children": [] + }, + { + "type": "element", + "name": "feGaussianBlur", + "attributes": { + "stdDeviation": "6.18495", + "result": "effect1_foregroundBlur_3892_95663" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "filter", + "attributes": { + "id": "filter3_f_3892_95663", + "x": "-6.61209", + "y": "7.49899", + "width": "41.5757", + "height": "46.522", + "filterUnits": "userSpaceOnUse", + "color-interpolation-filters": "sRGB" + }, + "children": [ + { + "type": "element", + "name": "feFlood", + "attributes": { + "flood-opacity": "0", + "result": "BackgroundImageFix" + }, + "children": [] + }, + { + "type": "element", + "name": "feBlend", + "attributes": { + "mode": "normal", + "in": "SourceGraphic", + "in2": "BackgroundImageFix", + "result": "shape" + }, + "children": [] + }, + { + "type": "element", + "name": "feGaussianBlur", + "attributes": { + "stdDeviation": "6.18495", + "result": "effect1_foregroundBlur_3892_95663" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "filter", + "attributes": { + "id": "filter4_f_3892_95663", + "x": "-6.21073", + "y": "9.02316", + "width": "41.6959", + "height": "42.4608", + "filterUnits": "userSpaceOnUse", + "color-interpolation-filters": "sRGB" + }, + "children": [ + { + "type": "element", + "name": "feFlood", + "attributes": { + "flood-opacity": "0", + "result": "BackgroundImageFix" + }, + "children": [] + }, + { + "type": "element", + "name": "feBlend", + "attributes": { + "mode": "normal", + "in": "SourceGraphic", + "in2": "BackgroundImageFix", + "result": "shape" + }, + "children": [] + }, + { + "type": "element", + "name": "feGaussianBlur", + "attributes": { + "stdDeviation": "6.18495", + "result": "effect1_foregroundBlur_3892_95663" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "filter", + "attributes": { + "id": "filter5_f_3892_95663", + "x": "15.405", + "y": "-2.44994", + "width": "39.3423", + "height": "38.7556", + "filterUnits": "userSpaceOnUse", + "color-interpolation-filters": "sRGB" + }, + "children": [ + { + "type": "element", + "name": "feFlood", + "attributes": { + "flood-opacity": "0", + "result": "BackgroundImageFix" + }, + "children": [] + }, + { + "type": "element", + "name": "feBlend", + "attributes": { + "mode": "normal", + "in": "SourceGraphic", + "in2": "BackgroundImageFix", + "result": "shape" + }, + "children": [] + }, + { + "type": "element", + "name": "feGaussianBlur", + "attributes": { + "stdDeviation": "5.87756", + "result": "effect1_foregroundBlur_3892_95663" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "filter", + "attributes": { + "id": "filter6_f_3892_95663", + "x": "-13.7886", + "y": "-4.15284", + "width": "39.9951", + "height": "40.2639", + "filterUnits": "userSpaceOnUse", + "color-interpolation-filters": "sRGB" + }, + "children": [ + { + "type": "element", + "name": "feFlood", + "attributes": { + "flood-opacity": "0", + "result": "BackgroundImageFix" + }, + "children": [] + }, + { + "type": "element", + "name": "feBlend", + "attributes": { + "mode": "normal", + "in": "SourceGraphic", + "in2": "BackgroundImageFix", + "result": "shape" + }, + "children": [] + }, + { + "type": "element", + "name": "feGaussianBlur", + "attributes": { + "stdDeviation": "5.32691", + "result": "effect1_foregroundBlur_3892_95663" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "filter", + "attributes": { + "id": "filter7_f_3892_95663", + "x": "6.6925", + "y": "0.620963", + "width": "39.6414", + "height": "39.065", + "filterUnits": "userSpaceOnUse", + "color-interpolation-filters": "sRGB" + }, + "children": [ + { + "type": "element", + "name": "feFlood", + "attributes": { + "flood-opacity": "0", + "result": "BackgroundImageFix" + }, + "children": [] + }, + { + "type": "element", + "name": "feBlend", + "attributes": { + "mode": "normal", + "in": "SourceGraphic", + "in2": "BackgroundImageFix", + "result": "shape" + }, + "children": [] + }, + { + "type": "element", + "name": "feGaussianBlur", + "attributes": { + "stdDeviation": "4.75678", + "result": "effect1_foregroundBlur_3892_95663" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "filter", + "attributes": { + "id": "filter8_f_3892_95663", + "x": "9.35225", + "y": "-4.48661", + "width": "29.2984", + "height": "27.3739", + "filterUnits": "userSpaceOnUse", + "color-interpolation-filters": "sRGB" + }, + "children": [ + { + "type": "element", + "name": "feFlood", + "attributes": { + "flood-opacity": "0", + "result": "BackgroundImageFix" + }, + "children": [] + }, + { + "type": "element", + "name": "feBlend", + "attributes": { + "mode": "normal", + "in": "SourceGraphic", + "in2": "BackgroundImageFix", + "result": "shape" + }, + "children": [] + }, + { + "type": "element", + "name": "feGaussianBlur", + "attributes": { + "stdDeviation": "4.25649", + "result": "effect1_foregroundBlur_3892_95663" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "filter", + "attributes": { + "id": "filter9_f_3892_95663", + "x": "-2.81919", + "y": "-9.62339", + "width": "34.8122", + "height": "34.143", + "filterUnits": "userSpaceOnUse", + "color-interpolation-filters": "sRGB" + }, + "children": [ + { + "type": "element", + "name": "feFlood", + "attributes": { + "flood-opacity": "0", + "result": "BackgroundImageFix" + }, + "children": [] + }, + { + "type": "element", + "name": "feBlend", + "attributes": { + "mode": "normal", + "in": "SourceGraphic", + "in2": "BackgroundImageFix", + "result": "shape" + }, + "children": [] + }, + { + "type": "element", + "name": "feGaussianBlur", + "attributes": { + "stdDeviation": "3.59514", + "result": "effect1_foregroundBlur_3892_95663" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "filter", + "attributes": { + "id": "filter10_f_3892_95663", + "x": "-2.73761", + "y": "12.4221", + "width": "29.1949", + "height": "27.4994", + "filterUnits": "userSpaceOnUse", + "color-interpolation-filters": "sRGB" + }, + "children": [ + { + "type": "element", + "name": "feFlood", + "attributes": { + "flood-opacity": "0", + "result": "BackgroundImageFix" + }, + "children": [] + }, + { + "type": "element", + "name": "feBlend", + "attributes": { + "mode": "normal", + "in": "SourceGraphic", + "in2": "BackgroundImageFix", + "result": "shape" + }, + "children": [] + }, + { + "type": "element", + "name": "feGaussianBlur", + "attributes": { + "stdDeviation": "4.44986", + "result": "effect1_foregroundBlur_3892_95663" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "paint0_linear_3892_95663", + "x1": "13.9595", + "y1": "24.7349", + "x2": "28.5025", + "y2": "12.4738", + "gradientUnits": "userSpaceOnUse" + }, + "children": [ + { + "type": "element", + "name": "stop", + "attributes": { + "stop-color": "#4893FC" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "0.27", + "stop-color": "#4893FC" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "0.777", + "stop-color": "#969DFF" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "1", + "stop-color": "#BD99FE" + }, + "children": [] + } + ] + } + ] + } + ] + }, + "name": "Gemini" } diff --git a/web/app/components/base/icons/src/public/llm/Gemini.tsx b/web/app/components/base/icons/src/public/llm/Gemini.tsx index a8fefe5f66..f5430036bb 100644 --- a/web/app/components/base/icons/src/public/llm/Gemini.tsx +++ b/web/app/components/base/icons/src/public/llm/Gemini.tsx @@ -1,17 +1,17 @@ // GENERATE BY script // DON NOT EDIT IT MANUALLY -import * as React from 'react' -import data from './Gemini.json' -import IconBase from '@/app/components/base/icons/IconBase' import type { IconData } from '@/app/components/base/icons/IconBase' +import * as React from 'react' +import IconBase from '@/app/components/base/icons/IconBase' +import data from './Gemini.json' const Icon = ( { ref, ...props }: React.SVGProps & { - ref?: React.RefObject>; + ref?: React.RefObject> }, ) => diff --git a/web/app/components/base/icons/src/public/llm/Grok.json b/web/app/components/base/icons/src/public/llm/Grok.json index b2758bfe92..590f845eeb 100644 --- a/web/app/components/base/icons/src/public/llm/Grok.json +++ b/web/app/components/base/icons/src/public/llm/Grok.json @@ -1,72 +1,72 @@ { - "icon": { - "type": "element", - "isRootNode": true, - "name": "svg", - "attributes": { - "width": "40", - "height": "40", - "viewBox": "0 0 40 40", - "fill": "none", - "xmlns": "http://www.w3.org/2000/svg" - }, - "children": [ - { - "type": "element", - "name": "rect", - "attributes": { - "width": "40", - "height": "40", - "fill": "white" - }, - "children": [] - }, - { - "type": "element", - "name": "g", - "attributes": { - "clip-path": "url(#clip0_3892_95659)" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M15.745 24.54L26.715 16.35C27.254 15.95 28.022 16.106 28.279 16.73C29.628 20.018 29.025 23.971 26.341 26.685C23.658 29.399 19.924 29.995 16.511 28.639L12.783 30.384C18.13 34.081 24.623 33.166 28.681 29.06C31.9 25.805 32.897 21.368 31.965 17.367L31.973 17.376C30.622 11.498 32.305 9.149 35.755 4.345L36 4L31.46 8.59V8.576L15.743 24.544M13.48 26.531C9.643 22.824 10.305 17.085 13.58 13.776C16 11.327 19.968 10.328 23.432 11.797L27.152 10.06C26.482 9.57 25.622 9.043 24.637 8.673C20.182 6.819 14.848 7.742 11.227 11.401C7.744 14.924 6.648 20.341 8.53 24.962C9.935 28.416 7.631 30.86 5.31 33.326C4.49 34.2 3.666 35.074 3 36L13.478 26.534", - "fill": "black" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "defs", - "attributes": {}, - "children": [ - { - "type": "element", - "name": "clipPath", - "attributes": { - "id": "clip0_3892_95659" - }, - "children": [ - { - "type": "element", - "name": "rect", - "attributes": { - "width": "33", - "height": "32", - "fill": "white", - "transform": "translate(3 4)" - }, - "children": [] - } - ] - } - ] - } - ] - }, - "name": "Grok" + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "40", + "height": "40", + "viewBox": "0 0 40 40", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "rect", + "attributes": { + "width": "40", + "height": "40", + "fill": "white" + }, + "children": [] + }, + { + "type": "element", + "name": "g", + "attributes": { + "clip-path": "url(#clip0_3892_95659)" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M15.745 24.54L26.715 16.35C27.254 15.95 28.022 16.106 28.279 16.73C29.628 20.018 29.025 23.971 26.341 26.685C23.658 29.399 19.924 29.995 16.511 28.639L12.783 30.384C18.13 34.081 24.623 33.166 28.681 29.06C31.9 25.805 32.897 21.368 31.965 17.367L31.973 17.376C30.622 11.498 32.305 9.149 35.755 4.345L36 4L31.46 8.59V8.576L15.743 24.544M13.48 26.531C9.643 22.824 10.305 17.085 13.58 13.776C16 11.327 19.968 10.328 23.432 11.797L27.152 10.06C26.482 9.57 25.622 9.043 24.637 8.673C20.182 6.819 14.848 7.742 11.227 11.401C7.744 14.924 6.648 20.341 8.53 24.962C9.935 28.416 7.631 30.86 5.31 33.326C4.49 34.2 3.666 35.074 3 36L13.478 26.534", + "fill": "black" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "defs", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "clipPath", + "attributes": { + "id": "clip0_3892_95659" + }, + "children": [ + { + "type": "element", + "name": "rect", + "attributes": { + "width": "33", + "height": "32", + "fill": "white", + "transform": "translate(3 4)" + }, + "children": [] + } + ] + } + ] + } + ] + }, + "name": "Grok" } diff --git a/web/app/components/base/icons/src/public/llm/Grok.tsx b/web/app/components/base/icons/src/public/llm/Grok.tsx index 6e5dbcf8b5..8b378de490 100644 --- a/web/app/components/base/icons/src/public/llm/Grok.tsx +++ b/web/app/components/base/icons/src/public/llm/Grok.tsx @@ -1,17 +1,17 @@ // GENERATE BY script // DON NOT EDIT IT MANUALLY -import * as React from 'react' -import data from './Grok.json' -import IconBase from '@/app/components/base/icons/IconBase' import type { IconData } from '@/app/components/base/icons/IconBase' +import * as React from 'react' +import IconBase from '@/app/components/base/icons/IconBase' +import data from './Grok.json' const Icon = ( { ref, ...props }: React.SVGProps & { - ref?: React.RefObject>; + ref?: React.RefObject> }, ) => diff --git a/web/app/components/base/icons/src/public/llm/OpenaiBlue.json b/web/app/components/base/icons/src/public/llm/OpenaiBlue.json new file mode 100644 index 0000000000..c5d4f974a2 --- /dev/null +++ b/web/app/components/base/icons/src/public/llm/OpenaiBlue.json @@ -0,0 +1,37 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "24", + "height": "24", + "viewBox": "0 0 24 24", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "rect", + "attributes": { + "width": "24", + "height": "24", + "rx": "6", + "fill": "#03A4EE" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M19.7758 11.5959C19.9546 11.9948 20.0681 12.4213 20.1145 12.8563C20.1592 13.2913 20.1369 13.7315 20.044 14.1596C19.9529 14.5878 19.7947 14.9987 19.5746 15.377C19.4302 15.6298 19.2599 15.867 19.0639 16.0854C18.8696 16.3021 18.653 16.4981 18.4174 16.67C18.1801 16.842 17.9274 16.9864 17.6591 17.105C17.3926 17.222 17.1141 17.3114 16.8286 17.3698C16.6945 17.7859 16.4951 18.1797 16.2371 18.5339C15.9809 18.8881 15.6697 19.1993 15.3155 19.4555C14.9613 19.7134 14.5693 19.9129 14.1532 20.047C13.7371 20.1829 13.302 20.2499 12.8636 20.2499C12.573 20.2516 12.2807 20.2207 11.9953 20.1622C11.7116 20.102 11.433 20.0109 11.1665 19.8923C10.9 19.7736 10.6472 19.6258 10.4116 19.4538C10.1778 19.2819 9.96115 19.0841 9.76857 18.8658C9.33871 18.9586 8.89853 18.981 8.46351 18.9363C8.02849 18.8898 7.60207 18.7763 7.20143 18.5975C6.80252 18.4204 6.43284 18.1797 6.10786 17.8857C5.78289 17.5916 5.50606 17.2478 5.28769 16.8695C5.14153 16.6167 5.02117 16.3502 4.93004 16.0734C4.83891 15.7965 4.77873 15.5111 4.74778 15.2205C4.71683 14.9317 4.71855 14.6393 4.7495 14.3488C4.78045 14.0599 4.84407 13.7745 4.9352 13.4976C4.64289 13.1727 4.40217 12.803 4.22335 12.4041C4.04624 12.0034 3.93104 11.5787 3.88634 11.1437C3.83991 10.7087 3.86398 10.2685 3.95511 9.84036C4.04624 9.41222 4.20443 9.00127 4.42452 8.62299C4.56896 8.37023 4.73918 8.13123 4.93348 7.91458C5.12778 7.69793 5.34615 7.50191 5.58171 7.32997C5.81728 7.15802 6.07176 7.01187 6.33827 6.89495C6.6065 6.7763 6.88506 6.68861 7.17048 6.63015C7.3046 6.21232 7.50406 5.82029 7.76026 5.46608C8.01817 5.11188 8.32939 4.80066 8.6836 4.54274C9.03781 4.28654 9.42984 4.08708 9.84595 3.95125C10.2621 3.81713 10.6971 3.74835 11.1355 3.75007C11.4261 3.74835 11.7184 3.77758 12.0039 3.83776C12.2893 3.89794 12.5678 3.98736 12.8344 4.106C13.1009 4.22636 13.3536 4.37251 13.5892 4.54446C13.8248 4.71812 14.0414 4.91414 14.234 5.13251C14.6621 5.04138 15.1023 5.01903 15.5373 5.06373C15.9723 5.10844 16.3971 5.22364 16.7977 5.40074C17.1966 5.57957 17.5663 5.81857 17.8913 6.1126C18.2162 6.4049 18.4931 6.74707 18.7114 7.12707C18.8576 7.37811 18.9779 7.64463 19.0691 7.92318C19.1602 8.20001 19.2221 8.48544 19.2513 8.77602C19.2823 9.06661 19.2823 9.35892 19.2496 9.64951C19.2187 9.94009 19.155 10.2255 19.0639 10.5024C19.3579 10.8273 19.5969 11.1953 19.7758 11.5959ZM14.0466 18.9363C14.4214 18.7815 14.7619 18.5528 15.049 18.2657C15.3362 17.9785 15.5648 17.6381 15.7196 17.2615C15.8743 16.8867 15.9552 16.4843 15.9552 16.0785V12.2442C15.954 12.2407 15.9529 12.2367 15.9517 12.2321C15.9506 12.2287 15.9488 12.2252 15.9466 12.2218C15.9443 12.2184 15.9414 12.2155 15.938 12.2132C15.9345 12.2098 15.9311 12.2075 15.9276 12.2063L14.54 11.4051V16.0373C14.54 16.0837 14.5332 16.1318 14.5211 16.1765C14.5091 16.223 14.4919 16.2659 14.4678 16.3072C14.4438 16.3485 14.4162 16.3863 14.3819 16.419C14.3484 16.4523 14.3109 16.4812 14.2701 16.505L10.9842 18.4015C10.9567 18.4187 10.9103 18.4428 10.8862 18.4565C11.0221 18.5717 11.1699 18.6732 11.3247 18.7626C11.4811 18.852 11.6428 18.9277 11.8113 18.9896C11.9798 19.0497 12.1535 19.0962 12.3288 19.1271C12.5059 19.1581 12.6848 19.1735 12.8636 19.1735C13.2694 19.1735 13.6717 19.0927 14.0466 18.9363ZM6.22135 16.333C6.42596 16.6855 6.69592 16.9916 7.01745 17.2392C7.34071 17.4868 7.70695 17.6673 8.09899 17.7722C8.49102 17.8771 8.90025 17.9046 9.3026 17.8513C9.70495 17.798 10.0918 17.6673 10.4443 17.4644L13.7663 15.5472L13.7749 15.5386C13.7772 15.5363 13.7789 15.5329 13.78 15.5283C13.7823 15.5249 13.7841 15.5214 13.7852 15.518V13.9017L9.77545 16.2212C9.73418 16.2453 9.6912 16.2625 9.64649 16.2763C9.60007 16.2883 9.55364 16.2935 9.5055 16.2935C9.45907 16.2935 9.41265 16.2883 9.36622 16.2763C9.32152 16.2625 9.27681 16.2453 9.23554 16.2212L5.94967 14.323C5.92044 14.3058 5.87746 14.28 5.85339 14.2645C5.82244 14.4416 5.80696 14.6204 5.80696 14.7993C5.80696 14.9781 5.82415 15.1569 5.85511 15.334C5.88605 15.5094 5.9342 15.6831 5.99438 15.8516C6.05628 16.0201 6.13194 16.1817 6.22135 16.3364V16.333ZM5.35818 9.1629C5.15529 9.51539 5.02461 9.90398 4.97131 10.3063C4.918 10.7087 4.94552 11.1162 5.0504 11.51C5.15529 11.902 5.33583 12.2682 5.58343 12.5915C5.83103 12.913 6.13881 13.183 6.48958 13.3859L9.80984 15.3048C9.81328 15.3059 9.81729 15.3071 9.82188 15.3082H9.83391C9.8385 15.3082 9.84251 15.3071 9.84595 15.3048C9.84939 15.3036 9.85283 15.3019 9.85627 15.2996L11.249 14.4949L7.23926 12.1805C7.19971 12.1565 7.16189 12.1272 7.1275 12.0946C7.09418 12.0611 7.06529 12.0236 7.04153 11.9828C7.01917 11.9415 7.00026 11.8985 6.98822 11.8521C6.97619 11.8074 6.96931 11.761 6.97103 11.7128V7.80797C6.80252 7.86987 6.63917 7.94553 6.48442 8.03494C6.32967 8.12607 6.18352 8.22924 6.04596 8.34444C5.91013 8.45965 5.78289 8.58688 5.66769 8.72444C5.55248 8.86028 5.45103 9.00815 5.36162 9.1629H5.35818ZM16.7633 11.8177C16.8046 11.8418 16.8424 11.8693 16.8768 11.9037C16.9094 11.9364 16.9387 11.9742 16.9628 12.0155C16.9851 12.0567 17.004 12.1014 17.0161 12.1461C17.0264 12.1926 17.0332 12.239 17.0315 12.2871V16.192C17.5835 15.9891 18.0649 15.6332 18.4208 15.1655C18.7785 14.6978 18.9934 14.139 19.0433 13.5544C19.0931 12.9698 18.9762 12.3817 18.7046 11.8607C18.4329 11.3397 18.0185 10.9064 17.5095 10.6141L14.1893 8.69521C14.1858 8.69406 14.1818 8.69292 14.1772 8.69177H14.1652C14.1618 8.69292 14.1578 8.69406 14.1532 8.69521C14.1497 8.69636 14.1463 8.69808 14.1429 8.70037L12.757 9.50163L16.7667 11.8177H16.7633ZM18.1475 9.7372H18.1457V9.73892L18.1475 9.7372ZM18.1457 9.73548C18.2455 9.15774 18.1784 8.56281 17.9514 8.02119C17.7262 7.47956 17.3496 7.01359 16.8682 6.67658C16.3867 6.34128 15.8193 6.1487 15.233 6.12291C14.6449 6.09884 14.0638 6.24155 13.5548 6.53386L10.2345 8.45105C10.2311 8.45334 10.2282 8.45621 10.2259 8.45965L10.2191 8.46996C10.2179 8.4734 10.2168 8.47741 10.2156 8.482C10.2145 8.48544 10.2139 8.48945 10.2139 8.49403V10.0966L14.2237 7.78046C14.2649 7.75639 14.3096 7.7392 14.3543 7.72544C14.4008 7.7134 14.4472 7.70825 14.4936 7.70825C14.5418 7.70825 14.5882 7.7134 14.6346 7.72544C14.6793 7.7392 14.7223 7.75639 14.7636 7.78046L18.0494 9.67874C18.0787 9.69593 18.1217 9.72 18.1457 9.73548ZM9.45735 7.96101C9.45735 7.91458 9.46423 7.86816 9.47627 7.82173C9.4883 7.77702 9.5055 7.73232 9.52957 7.69105C9.55364 7.6515 9.58115 7.61368 9.61554 7.57929C9.64821 7.54662 9.68604 7.51739 9.72731 7.49503L13.0132 5.59848C13.0441 5.57957 13.0871 5.55549 13.1112 5.54346C12.6607 5.1669 12.1105 4.92618 11.5276 4.85224C10.9447 4.77658 10.3532 4.86943 9.82188 5.11875C9.28885 5.36807 8.83835 5.76527 8.52369 6.26047C8.20903 6.75739 8.04224 7.33169 8.04224 7.91974V11.7541C8.04339 11.7587 8.04454 11.7627 8.04568 11.7661C8.04683 11.7696 8.04855 11.773 8.05084 11.7765C8.05313 11.7799 8.056 11.7833 8.05944 11.7868C8.06173 11.7891 8.06517 11.7914 8.06976 11.7937L9.45735 12.5949V7.96101ZM10.2105 13.0282L11.997 14.0599L13.7835 13.0282V10.9666L11.9987 9.93493L10.2122 10.9666L10.2105 13.0282Z", + "fill": "white" + }, + "children": [] + } + ] + }, + "name": "OpenaiBlue" +} diff --git a/web/app/components/base/icons/src/public/llm/OpenaiBlue.tsx b/web/app/components/base/icons/src/public/llm/OpenaiBlue.tsx new file mode 100644 index 0000000000..9934a77591 --- /dev/null +++ b/web/app/components/base/icons/src/public/llm/OpenaiBlue.tsx @@ -0,0 +1,20 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import type { IconData } from '@/app/components/base/icons/IconBase' +import * as React from 'react' +import IconBase from '@/app/components/base/icons/IconBase' +import data from './OpenaiBlue.json' + +const Icon = ( + { + ref, + ...props + }: React.SVGProps & { + ref?: React.RefObject> + }, +) => + +Icon.displayName = 'OpenaiBlue' + +export default Icon diff --git a/web/app/components/base/icons/src/public/llm/OpenaiSmall.json b/web/app/components/base/icons/src/public/llm/OpenaiSmall.json index baccaeb3fc..aa72f614bc 100644 --- a/web/app/components/base/icons/src/public/llm/OpenaiSmall.json +++ b/web/app/components/base/icons/src/public/llm/OpenaiSmall.json @@ -1,128 +1,128 @@ { - "icon": { - "type": "element", - "isRootNode": true, - "name": "svg", - "attributes": { - "width": "26", - "height": "26", - "viewBox": "0 0 26 26", - "fill": "none", - "xmlns": "http://www.w3.org/2000/svg", - "xmlns:xlink": "http://www.w3.org/1999/xlink" - }, - "children": [ - { - "type": "element", - "name": "g", - "attributes": { - "clip-path": "url(#clip0_3892_83671)" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M1 13C1 9.27247 1 7.4087 1.60896 5.93853C2.42092 3.97831 3.97831 2.42092 5.93853 1.60896C7.4087 1 9.27247 1 13 1C16.7275 1 18.5913 1 20.0615 1.60896C22.0217 2.42092 23.5791 3.97831 24.391 5.93853C25 7.4087 25 9.27247 25 13C25 16.7275 25 18.5913 24.391 20.0615C23.5791 22.0217 22.0217 23.5791 20.0615 24.391C18.5913 25 16.7275 25 13 25C9.27247 25 7.4087 25 5.93853 24.391C3.97831 23.5791 2.42092 22.0217 1.60896 20.0615C1 18.5913 1 16.7275 1 13Z", - "fill": "white" - }, - "children": [] - }, - { - "type": "element", - "name": "rect", - "attributes": { - "width": "24", - "height": "24", - "transform": "translate(1 1)", - "fill": "url(#pattern0_3892_83671)" - }, - "children": [] - }, - { - "type": "element", - "name": "rect", - "attributes": { - "width": "24", - "height": "24", - "transform": "translate(1 1)", - "fill": "white", - "fill-opacity": "0.01" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "path", - "attributes": { - "d": "M13 0.75C14.8603 0.75 16.2684 0.750313 17.3945 0.827148C18.5228 0.904144 19.3867 1.05876 20.1572 1.37793C22.1787 2.21525 23.7847 3.82133 24.6221 5.84277C24.9412 6.61333 25.0959 7.47723 25.1729 8.60547C25.2497 9.73161 25.25 11.1397 25.25 13C25.25 14.8603 25.2497 16.2684 25.1729 17.3945C25.0959 18.5228 24.9412 19.3867 24.6221 20.1572C23.7847 22.1787 22.1787 23.7847 20.1572 24.6221C19.3867 24.9412 18.5228 25.0959 17.3945 25.1729C16.2684 25.2497 14.8603 25.25 13 25.25C11.1397 25.25 9.73161 25.2497 8.60547 25.1729C7.47723 25.0959 6.61333 24.9412 5.84277 24.6221C3.82133 23.7847 2.21525 22.1787 1.37793 20.1572C1.05876 19.3867 0.904144 18.5228 0.827148 17.3945C0.750313 16.2684 0.75 14.8603 0.75 13C0.75 11.1397 0.750313 9.73161 0.827148 8.60547C0.904144 7.47723 1.05876 6.61333 1.37793 5.84277C2.21525 3.82133 3.82133 2.21525 5.84277 1.37793C6.61333 1.05876 7.47723 0.904144 8.60547 0.827148C9.73161 0.750313 11.1397 0.75 13 0.75Z", - "stroke": "#101828", - "stroke-opacity": "0.08", - "stroke-width": "0.5" - }, - "children": [] - }, - { - "type": "element", - "name": "defs", - "attributes": {}, - "children": [ - { - "type": "element", - "name": "pattern", - "attributes": { - "id": "pattern0_3892_83671", - "patternContentUnits": "objectBoundingBox", - "width": "1", - "height": "1" - }, - "children": [ - { - "type": "element", - "name": "use", - "attributes": { - "xlink:href": "#image0_3892_83671", - "transform": "scale(0.00625)" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "clipPath", - "attributes": { - "id": "clip0_3892_83671" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M1 13C1 9.27247 1 7.4087 1.60896 5.93853C2.42092 3.97831 3.97831 2.42092 5.93853 1.60896C7.4087 1 9.27247 1 13 1C16.7275 1 18.5913 1 20.0615 1.60896C22.0217 2.42092 23.5791 3.97831 24.391 5.93853C25 7.4087 25 9.27247 25 13C25 16.7275 25 18.5913 24.391 20.0615C23.5791 22.0217 22.0217 23.5791 20.0615 24.391C18.5913 25 16.7275 25 13 25C9.27247 25 7.4087 25 5.93853 24.391C3.97831 23.5791 2.42092 22.0217 1.60896 20.0615C1 18.5913 1 16.7275 1 13Z", - "fill": "white" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "image", - "attributes": { - "id": "image0_3892_83671", - "width": "160", - "height": "160", - "preserveAspectRatio": "none", - "xlink:href": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKAAAACgCAYAAACLz2ctAAAACXBIWXMAACxLAAAsSwGlPZapAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAA0ySURBVHgB7Z3vsdM6E8Z93nm/QwdABUAFgQqACgIVABUAFcCpAKgAOoBUwKECOBUAFfj6yYzumNx4pV2vtKtkfzO+3CHESezHWu0frS7GiSEIjPjfEASGhAADU0KAgSkhwMCUEGBgSggwMCUEGJgSAgxMCQEGpoQAA1NCgIEpIcDAlBBgYEoIMDAlBBiYEgIMTAkBBqaEAANTQoCBKSHAwJQQYGBKCDAwJQQYmPL/4cz5/fv38PXr12G32w0/f/4crq6u9n+P/0/cvHlzuH379v7Pe/fuDZvNZv8n/i5Yx8U5LkyH6D5+/Dh8/vx5Lz4pEOGDBw+G58+fhxiljGfEly9fxkkweODUD5wX5w94nIUAf/z4UU14x4SIzwvKOHkBvn79epzmbk3ENz9evHgx/vr1awxoTnYOCCfi2bNnq+Z4a8G8cDLLMT8kOEkBwpN9+PDh3tngMvd4EzgPBC05H3j37t3eUTkknffw3PjsdMDROWnGE+PDhw8sk4s526tXr/YORM5k4vVPnz6NT58+HSeRskwypgJ4/yRG9vsnEe5NOj771DgpAUJ8JTcUAoXovn37Nq7h/fv3zZyb+XeHgE/F4z4ZAUJMJTdwMoXqzgFGJu6IqHHgM/HQ9cxJCBBhj5wA8PraES8HRtXWIsTRc+jnJHLBcDjmqbNDttvtMImv+oT+4uLiL+elFfD079y5M7x582bojrFzMLkfiNEBo1JtMB+zMMHHDjgsPY2GXYdhMOrhyV9iEt8wCXSo+fnSWCNG41TQcOvWrb9eQ0jm+vp6H07CwQ3/dBV/HDsG3uCwMBI8fvx4rAWcmNzIOyzM1eA5c50gzF3fvn3LGmXhLdee82rQrQBh9gbC4ahlhiAgbmoPD4PW9+EUVPQgwm4FSI1+EIk2kkqamhUy+I0lI2LNh1GDLgWIC9rK9MJcUmJfGnlgMmuD61Dy3eCYeC2M6FKA1PxLc8SRVNLUCHTnKIk/IpXnkS4FiCd6yeRpIAmrWAeDS0ToMX3XnQAp87t27icpXIVQvdzYnAjx4HqjOwFCZEsXWDoCpbAKx9ymggZvcyuYWup7e8sddyNA3GiIb8n8Sp9uSVgFE3+vk3p8L2r6gNc84V6AMG/wbHMi4U6yvYRVMGpri5mKkXqbC7oVIFcgKPQshZvFgPi1Y4v4vvOHStuJoa6dlrOmgTsBSlewlVYLU05Mi3le7sGCedcQYm4U9DKFcCVAbjm9xKyUjn7aIxInoK1VaEoJnWMxauJGgDnvTUuAORHUCKtIl4auTaNREYOaxRoczAWIkQEXY434tASILIYmWnWCUrOMa7t0TjwQHjAVIC7QUlhl6aLVFKDWvKhGJwYIWWI2qe/hoUjBtCQfxZypGxUFGgChwHJK8A81WVtOj8JRlMXfv39ffUE8il+nacq+ABdNlUqhliGUXPvamAkQNyp3ISEIiA7igwg9MzkNe+GhAru0ghm/aZqnsbprQYhPnjzZP7zUOpjE3bt3F1/78+fPYM5oADU5TsextQ3U+zRMsARJQPtYuVZpadXhAQeHglqumntvC5oLEBc65xFut9uj8zFPAsT3k3juubgirg9nXjwMdNiGinuepQBzTznEt4QXAR5mMUoOblyxtOJ5fhzzlikB4t9b01SAOdObKyiwFiA+QzI6SeOKGCkli91THxoQI+CMXJVGboSwEiC+FzdWmdJ4Gkjmh8ksexdgMy8YXiLltcEb9LaOdR5W4YQ+0nvRKUEDXBdcnynfzfKWJ9Huu0ZQ5zVnbEQuAV9CyxFQK4tRo4GQZH645prVpIkAcxUopZPzFgKs1U9au2WGNGwzPzysGW5igi8vLxdfg5nwYnphbpFpKM1ipC6mJSDrgHOXBpBzJLM8CVEUpHfTfXVsAOU5ckMTQ8URkHOksvw1DoImXLPsZYFSdQFS5pdbmetBgEtl+dIVddpmkBO2OYswzOQ9Ll4AbnWHpQBL43laAeQ1cEZlaxFWFyCVruJ6YRYClJblS7pZaYuh1JO3rI6uLkDKLHFpLcC1bTY8zA9LC36tPOLqAtRcx9tKgHhoNG+IRIjaZjk3N4TwLRYqVRfgUtJesjSwtgBrt9mQzA/ned215Kp3LBoYVRfg0o+VLIrxWA8ogSNAbbOc89RbZ0fMKqItusn3SsrrIpC9Noidyye37rRvJkDpvmvnTGrKviabggcfGZQlkAVqucFjdQEuPW0a6ahzBZVFqHLBru8SkLqj0nctR8EYAR0BUcDUljA3y9xSMZAbBVvdn+oCXEp4r9n+9Bi73W7onVRgwKmNnK+S41xPnJ8aBakCEk3MTDDQnGtgOWSXW1UdASMbqlyw0U6pswazDCFywPmXaDUPrC5A6inTHrUgQqlJ8gh+D/a4KzXLXAcC92ZJ4K3McHUBbjabxdfw1HIoqV/jLtz2zrzur8Qsf//+feBAibvFKFhdgHjClkZBPGGcHwkBlhZfQtzd7iB5BIgPIoQYKbPMHbVaWqhjNPGCNV1+3IBSkwSSWZaGLLyB345gshaUhWpiQcYGUG3CcEiS7pK8KtJ/mtU5UgaiAKEE7aWWS9exRUPzJiMgZYYB5mtcJJ4inJOWUf5esEyLNgtE51x+qTC4nmLwXyyzVc0EmEv/cAOpc5KnCCF6W9zeA2cxAgJqFEzhkzXAS06eYgixHMu0aFMBYgREl88lYIZfvnw5rAXmGE0tqc86Rm1vGb8Pn+GNJQE2GRnHxuS2khoG3ZVaksZC2uXwKO8vWbJp5QVrb3/GwaRDKtW1c5iFTDTXKFgsl+Q2sbQS4NK5WuyoZNYlH8sWczekxhoNbr89SXd6ye6bVgKkdlRqsUbEdJuGUjFYLtyeC7FkXcaarloWAsTDtXSu0u3P1mC+UY1lKwnNndElzco9CNB6HxEXW3Vx2t566beXRmXOHnDeBEidp1XzIhcClOyjpr2ZoMQs43tzvzs14rcWIPV7W60RNhdgiUdcMhJpIdkmgfvAeBCgVtPQtZhu1QXWRuHXrhA7BBkUpPS0sik4B4LiODxlZ6gyOCQMWn1XcwFSCe/SSPzaFWLHwPkgRCp9SIHvjvdKO5jWBNeLqkbfbrdDK8wFeH19vfgaCi+lK8Q0KjkgInS656akUqkYKnW8geuS65zftLJoNIbawTzNQ/CnZFusNY19pCGaksD5YDgHzKUlERFoibkAKU/s2LZT3LwuN2wjabULT5hz46wEmHuILbbu6mIEPEQSt8ttk5DSZ5xz4pB0T7UQYC7EhIeolec7x7UAc00icfMl+dbDC91i88E5LQVYOqK3Nr0JcwFSPf9KcpHSvC7MET5b0tl+bYFEKwHi95U8WFp72kkwF6BWF32MlhrbV1EmSutG1RYgJy8taRSqibkAqYsp7aKqLcS0KY0G1BJVDQFyphKYF1v0hZ7jOheMv5cgMcvHjtKwCgfKE9UQYOmhXfArxYUAqdDKGgFIwjZJ+NqT8pK4YisBYvsJL7gQIFUUqTFH4ZhlSViFghNX1CokaPlgrcWFAHOtO7QEQZVCaZd3ScrySz+fKqNv9fu0cCFAQI0SmrVph/PDGvO80vDH4chbChU5OCa81lsvcHAjwNxT7WFz5RySsnyJQHIZG4gfUxfPwku4ESCg5mle9rc9hqQsf818bEnkuEY4pwfvthRXAqSyItqmWAPJPE+6++acpc/z5mCU4EqAIFcOb7m16BzNFXUcqPmfRycjhzsBlnh42qviOEjKtTQX2C/FNT1PUSjcCRCUFJ+23umb22Zjbm610FiM5A2XAsTNLlmZppmjpZCUa9X4bpzi3V5wKUCAC1oyx6qxUD3RKqxSArV81aKSWQu3AgS46KUjj+aNl+SQa6a5ci3teoj3LeFagIAjwjQiYq7GDVzjJksKVHFo548P0aig8coF/jM4Jy0l5C61xHJKtO3FgeWGN27c+Ot1LAnFOa+urvYHF6z3rd0OGAvIqeWdPwp3UHLL2Am1WmZIjhp9C4+RS7lZltJr0Y0AE9wGk5qHdlhliZKQT4tNZFrQnQCBJBisddSOt2HumhvpIb5ewy6HdCnARI31H6UmWLt7KGddcg+VQaV0LcAE5mPwBiV9BtccGIXXxiAhJk5BQ48FBxQnIcA5GJkwf+J2XYWA0TgdI5GkkTnej8/OhWPwOh4YiI4zjfBYTq9BF2GYNSC8gh6E6UggRIPwBY5j3a+m9Jt405wU/pmDz0bIR9IPEedDf8GSDbt74+QFuAZp/FGTFrFGS8z7A3omdUvlbvmlQWpw6a2zqjpjUETL0I/XFWw1CAEy0dgPhBJez4UFEmIOKATzwsvLy/0OmJI8cgJzvEePHu3b4lru22tFCFCBVNCw2+3+9boPPd40j0uFEZvNZi++cxTdnBBgYEp4wYEpIcDAlBBgYEoIMDAlBBiYEgIMTAkBBqaEAANTQoCBKSHAwJQQYGBKCDAwJQQYmBICDEwJAQamhAADU0KAgSkhwMCUEGBgSggwMCUEGJgSAgxMCQEGpvwDojzI2oXtJzYAAAAASUVORK5CYII=" - }, - "children": [] - } - ] - } - ] - }, - "name": "OpenaiSmall" + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "26", + "height": "26", + "viewBox": "0 0 26 26", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg", + "xmlns:xlink": "http://www.w3.org/1999/xlink" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "clip-path": "url(#clip0_3892_83671)" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M1 13C1 9.27247 1 7.4087 1.60896 5.93853C2.42092 3.97831 3.97831 2.42092 5.93853 1.60896C7.4087 1 9.27247 1 13 1C16.7275 1 18.5913 1 20.0615 1.60896C22.0217 2.42092 23.5791 3.97831 24.391 5.93853C25 7.4087 25 9.27247 25 13C25 16.7275 25 18.5913 24.391 20.0615C23.5791 22.0217 22.0217 23.5791 20.0615 24.391C18.5913 25 16.7275 25 13 25C9.27247 25 7.4087 25 5.93853 24.391C3.97831 23.5791 2.42092 22.0217 1.60896 20.0615C1 18.5913 1 16.7275 1 13Z", + "fill": "white" + }, + "children": [] + }, + { + "type": "element", + "name": "rect", + "attributes": { + "width": "24", + "height": "24", + "transform": "translate(1 1)", + "fill": "url(#pattern0_3892_83671)" + }, + "children": [] + }, + { + "type": "element", + "name": "rect", + "attributes": { + "width": "24", + "height": "24", + "transform": "translate(1 1)", + "fill": "white", + "fill-opacity": "0.01" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M13 0.75C14.8603 0.75 16.2684 0.750313 17.3945 0.827148C18.5228 0.904144 19.3867 1.05876 20.1572 1.37793C22.1787 2.21525 23.7847 3.82133 24.6221 5.84277C24.9412 6.61333 25.0959 7.47723 25.1729 8.60547C25.2497 9.73161 25.25 11.1397 25.25 13C25.25 14.8603 25.2497 16.2684 25.1729 17.3945C25.0959 18.5228 24.9412 19.3867 24.6221 20.1572C23.7847 22.1787 22.1787 23.7847 20.1572 24.6221C19.3867 24.9412 18.5228 25.0959 17.3945 25.1729C16.2684 25.2497 14.8603 25.25 13 25.25C11.1397 25.25 9.73161 25.2497 8.60547 25.1729C7.47723 25.0959 6.61333 24.9412 5.84277 24.6221C3.82133 23.7847 2.21525 22.1787 1.37793 20.1572C1.05876 19.3867 0.904144 18.5228 0.827148 17.3945C0.750313 16.2684 0.75 14.8603 0.75 13C0.75 11.1397 0.750313 9.73161 0.827148 8.60547C0.904144 7.47723 1.05876 6.61333 1.37793 5.84277C2.21525 3.82133 3.82133 2.21525 5.84277 1.37793C6.61333 1.05876 7.47723 0.904144 8.60547 0.827148C9.73161 0.750313 11.1397 0.75 13 0.75Z", + "stroke": "#101828", + "stroke-opacity": "0.08", + "stroke-width": "0.5" + }, + "children": [] + }, + { + "type": "element", + "name": "defs", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "pattern", + "attributes": { + "id": "pattern0_3892_83671", + "patternContentUnits": "objectBoundingBox", + "width": "1", + "height": "1" + }, + "children": [ + { + "type": "element", + "name": "use", + "attributes": { + "xlink:href": "#image0_3892_83671", + "transform": "scale(0.00625)" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "clipPath", + "attributes": { + "id": "clip0_3892_83671" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M1 13C1 9.27247 1 7.4087 1.60896 5.93853C2.42092 3.97831 3.97831 2.42092 5.93853 1.60896C7.4087 1 9.27247 1 13 1C16.7275 1 18.5913 1 20.0615 1.60896C22.0217 2.42092 23.5791 3.97831 24.391 5.93853C25 7.4087 25 9.27247 25 13C25 16.7275 25 18.5913 24.391 20.0615C23.5791 22.0217 22.0217 23.5791 20.0615 24.391C18.5913 25 16.7275 25 13 25C9.27247 25 7.4087 25 5.93853 24.391C3.97831 23.5791 2.42092 22.0217 1.60896 20.0615C1 18.5913 1 16.7275 1 13Z", + "fill": "white" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "image", + "attributes": { + "id": "image0_3892_83671", + "width": "160", + "height": "160", + "preserveAspectRatio": "none", + "xlink:href": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKAAAACgCAYAAACLz2ctAAAACXBIWXMAACxLAAAsSwGlPZapAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAA0ySURBVHgB7Z3vsdM6E8Z93nm/QwdABUAFgQqACgIVABUAFcCpAKgAOoBUwKECOBUAFfj6yYzumNx4pV2vtKtkfzO+3CHESezHWu0frS7GiSEIjPjfEASGhAADU0KAgSkhwMCUEGBgSggwMCUEGJgSAgxMCQEGpoQAA1NCgIEpIcDAlBBgYEoIMDAlBBiYEgIMTAkBBqaEAANTQoCBKSHAwJQQYGBKCDAwJQQYmPL/4cz5/fv38PXr12G32w0/f/4crq6u9n+P/0/cvHlzuH379v7Pe/fuDZvNZv8n/i5Yx8U5LkyH6D5+/Dh8/vx5Lz4pEOGDBw+G58+fhxiljGfEly9fxkkweODUD5wX5w94nIUAf/z4UU14x4SIzwvKOHkBvn79epzmbk3ENz9evHgx/vr1awxoTnYOCCfi2bNnq+Z4a8G8cDLLMT8kOEkBwpN9+PDh3tngMvd4EzgPBC05H3j37t3eUTkknffw3PjsdMDROWnGE+PDhw8sk4s526tXr/YORM5k4vVPnz6NT58+HSeRskwypgJ4/yRG9vsnEe5NOj771DgpAUJ8JTcUAoXovn37Nq7h/fv3zZyb+XeHgE/F4z4ZAUJMJTdwMoXqzgFGJu6IqHHgM/HQ9cxJCBBhj5wA8PraES8HRtXWIsTRc+jnJHLBcDjmqbNDttvtMImv+oT+4uLiL+elFfD079y5M7x582bojrFzMLkfiNEBo1JtMB+zMMHHDjgsPY2GXYdhMOrhyV9iEt8wCXSo+fnSWCNG41TQcOvWrb9eQ0jm+vp6H07CwQ3/dBV/HDsG3uCwMBI8fvx4rAWcmNzIOyzM1eA5c50gzF3fvn3LGmXhLdee82rQrQBh9gbC4ahlhiAgbmoPD4PW9+EUVPQgwm4FSI1+EIk2kkqamhUy+I0lI2LNh1GDLgWIC9rK9MJcUmJfGnlgMmuD61Dy3eCYeC2M6FKA1PxLc8SRVNLUCHTnKIk/IpXnkS4FiCd6yeRpIAmrWAeDS0ToMX3XnQAp87t27icpXIVQvdzYnAjx4HqjOwFCZEsXWDoCpbAKx9ymggZvcyuYWup7e8sddyNA3GiIb8n8Sp9uSVgFE3+vk3p8L2r6gNc84V6AMG/wbHMi4U6yvYRVMGpri5mKkXqbC7oVIFcgKPQshZvFgPi1Y4v4vvOHStuJoa6dlrOmgTsBSlewlVYLU05Mi3le7sGCedcQYm4U9DKFcCVAbjm9xKyUjn7aIxInoK1VaEoJnWMxauJGgDnvTUuAORHUCKtIl4auTaNREYOaxRoczAWIkQEXY434tASILIYmWnWCUrOMa7t0TjwQHjAVIC7QUlhl6aLVFKDWvKhGJwYIWWI2qe/hoUjBtCQfxZypGxUFGgChwHJK8A81WVtOj8JRlMXfv39ffUE8il+nacq+ABdNlUqhliGUXPvamAkQNyp3ISEIiA7igwg9MzkNe+GhAru0ghm/aZqnsbprQYhPnjzZP7zUOpjE3bt3F1/78+fPYM5oADU5TsextQ3U+zRMsARJQPtYuVZpadXhAQeHglqumntvC5oLEBc65xFut9uj8zFPAsT3k3juubgirg9nXjwMdNiGinuepQBzTznEt4QXAR5mMUoOblyxtOJ5fhzzlikB4t9b01SAOdObKyiwFiA+QzI6SeOKGCkli91THxoQI+CMXJVGboSwEiC+FzdWmdJ4Gkjmh8ksexdgMy8YXiLltcEb9LaOdR5W4YQ+0nvRKUEDXBdcnynfzfKWJ9Huu0ZQ5zVnbEQuAV9CyxFQK4tRo4GQZH645prVpIkAcxUopZPzFgKs1U9au2WGNGwzPzysGW5igi8vLxdfg5nwYnphbpFpKM1ipC6mJSDrgHOXBpBzJLM8CVEUpHfTfXVsAOU5ckMTQ8URkHOksvw1DoImXLPsZYFSdQFS5pdbmetBgEtl+dIVddpmkBO2OYswzOQ9Ll4AbnWHpQBL43laAeQ1cEZlaxFWFyCVruJ6YRYClJblS7pZaYuh1JO3rI6uLkDKLHFpLcC1bTY8zA9LC36tPOLqAtRcx9tKgHhoNG+IRIjaZjk3N4TwLRYqVRfgUtJesjSwtgBrt9mQzA/ned215Kp3LBoYVRfg0o+VLIrxWA8ogSNAbbOc89RbZ0fMKqItusn3SsrrIpC9Noidyye37rRvJkDpvmvnTGrKviabggcfGZQlkAVqucFjdQEuPW0a6ahzBZVFqHLBru8SkLqj0nctR8EYAR0BUcDUljA3y9xSMZAbBVvdn+oCXEp4r9n+9Bi73W7onVRgwKmNnK+S41xPnJ8aBakCEk3MTDDQnGtgOWSXW1UdASMbqlyw0U6pswazDCFywPmXaDUPrC5A6inTHrUgQqlJ8gh+D/a4KzXLXAcC92ZJ4K3McHUBbjabxdfw1HIoqV/jLtz2zrzur8Qsf//+feBAibvFKFhdgHjClkZBPGGcHwkBlhZfQtzd7iB5BIgPIoQYKbPMHbVaWqhjNPGCNV1+3IBSkwSSWZaGLLyB345gshaUhWpiQcYGUG3CcEiS7pK8KtJ/mtU5UgaiAKEE7aWWS9exRUPzJiMgZYYB5mtcJJ4inJOWUf5esEyLNgtE51x+qTC4nmLwXyyzVc0EmEv/cAOpc5KnCCF6W9zeA2cxAgJqFEzhkzXAS06eYgixHMu0aFMBYgREl88lYIZfvnw5rAXmGE0tqc86Rm1vGb8Pn+GNJQE2GRnHxuS2khoG3ZVaksZC2uXwKO8vWbJp5QVrb3/GwaRDKtW1c5iFTDTXKFgsl+Q2sbQS4NK5WuyoZNYlH8sWczekxhoNbr89SXd6ye6bVgKkdlRqsUbEdJuGUjFYLtyeC7FkXcaarloWAsTDtXSu0u3P1mC+UY1lKwnNndElzco9CNB6HxEXW3Vx2t566beXRmXOHnDeBEidp1XzIhcClOyjpr2ZoMQs43tzvzs14rcWIPV7W60RNhdgiUdcMhJpIdkmgfvAeBCgVtPQtZhu1QXWRuHXrhA7BBkUpPS0sik4B4LiODxlZ6gyOCQMWn1XcwFSCe/SSPzaFWLHwPkgRCp9SIHvjvdKO5jWBNeLqkbfbrdDK8wFeH19vfgaCi+lK8Q0KjkgInS656akUqkYKnW8geuS65zftLJoNIbawTzNQ/CnZFusNY19pCGaksD5YDgHzKUlERFoibkAKU/s2LZT3LwuN2wjabULT5hz46wEmHuILbbu6mIEPEQSt8ttk5DSZ5xz4pB0T7UQYC7EhIeolec7x7UAc00icfMl+dbDC91i88E5LQVYOqK3Nr0JcwFSPf9KcpHSvC7MET5b0tl+bYFEKwHi95U8WFp72kkwF6BWF32MlhrbV1EmSutG1RYgJy8taRSqibkAqYsp7aKqLcS0KY0G1BJVDQFyphKYF1v0hZ7jOheMv5cgMcvHjtKwCgfKE9UQYOmhXfArxYUAqdDKGgFIwjZJ+NqT8pK4YisBYvsJL7gQIFUUqTFH4ZhlSViFghNX1CokaPlgrcWFAHOtO7QEQZVCaZd3ScrySz+fKqNv9fu0cCFAQI0SmrVph/PDGvO80vDH4chbChU5OCa81lsvcHAjwNxT7WFz5RySsnyJQHIZG4gfUxfPwku4ESCg5mle9rc9hqQsf818bEnkuEY4pwfvthRXAqSyItqmWAPJPE+6++acpc/z5mCU4EqAIFcOb7m16BzNFXUcqPmfRycjhzsBlnh42qviOEjKtTQX2C/FNT1PUSjcCRCUFJ+23umb22Zjbm610FiM5A2XAsTNLlmZppmjpZCUa9X4bpzi3V5wKUCAC1oyx6qxUD3RKqxSArV81aKSWQu3AgS46KUjj+aNl+SQa6a5ci3teoj3LeFagIAjwjQiYq7GDVzjJksKVHFo548P0aig8coF/jM4Jy0l5C61xHJKtO3FgeWGN27c+Ot1LAnFOa+urvYHF6z3rd0OGAvIqeWdPwp3UHLL2Am1WmZIjhp9C4+RS7lZltJr0Y0AE9wGk5qHdlhliZKQT4tNZFrQnQCBJBisddSOt2HumhvpIb5ewy6HdCnARI31H6UmWLt7KGddcg+VQaV0LcAE5mPwBiV9BtccGIXXxiAhJk5BQ48FBxQnIcA5GJkwf+J2XYWA0TgdI5GkkTnej8/OhWPwOh4YiI4zjfBYTq9BF2GYNSC8gh6E6UggRIPwBY5j3a+m9Jt405wU/pmDz0bIR9IPEedDf8GSDbt74+QFuAZp/FGTFrFGS8z7A3omdUvlbvmlQWpw6a2zqjpjUETL0I/XFWw1CAEy0dgPhBJez4UFEmIOKATzwsvLy/0OmJI8cgJzvEePHu3b4lru22tFCFCBVNCw2+3+9boPPd40j0uFEZvNZi++cxTdnBBgYEp4wYEpIcDAlBBgYEoIMDAlBBiYEgIMTAkBBqaEAANTQoCBKSHAwJQQYGBKCDAwJQQYmBICDEwJAQamhAADU0KAgSkhwMCUEGBgSggwMCUEGJgSAgxMCQEGpvwDojzI2oXtJzYAAAAASUVORK5CYII=" + }, + "children": [] + } + ] + } + ] + }, + "name": "OpenaiSmall" } diff --git a/web/app/components/base/icons/src/public/llm/OpenaiSmall.tsx b/web/app/components/base/icons/src/public/llm/OpenaiSmall.tsx index 42cd853fab..6307091e0b 100644 --- a/web/app/components/base/icons/src/public/llm/OpenaiSmall.tsx +++ b/web/app/components/base/icons/src/public/llm/OpenaiSmall.tsx @@ -1,17 +1,17 @@ // GENERATE BY script // DON NOT EDIT IT MANUALLY -import * as React from 'react' -import data from './OpenaiSmall.json' -import IconBase from '@/app/components/base/icons/IconBase' import type { IconData } from '@/app/components/base/icons/IconBase' +import * as React from 'react' +import IconBase from '@/app/components/base/icons/IconBase' +import data from './OpenaiSmall.json' const Icon = ( { ref, ...props }: React.SVGProps & { - ref?: React.RefObject>; + ref?: React.RefObject> }, ) => diff --git a/web/app/components/base/icons/src/public/llm/OpenaiTeal.json b/web/app/components/base/icons/src/public/llm/OpenaiTeal.json new file mode 100644 index 0000000000..ffd0981512 --- /dev/null +++ b/web/app/components/base/icons/src/public/llm/OpenaiTeal.json @@ -0,0 +1,37 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "24", + "height": "24", + "viewBox": "0 0 24 24", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "rect", + "attributes": { + "width": "24", + "height": "24", + "rx": "6", + "fill": "#009688" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M19.7758 11.5959C19.9546 11.9948 20.0681 12.4213 20.1145 12.8563C20.1592 13.2913 20.1369 13.7315 20.044 14.1596C19.9529 14.5878 19.7947 14.9987 19.5746 15.377C19.4302 15.6298 19.2599 15.867 19.0639 16.0854C18.8696 16.3021 18.653 16.4981 18.4174 16.67C18.1801 16.842 17.9274 16.9864 17.6591 17.105C17.3926 17.222 17.1141 17.3114 16.8286 17.3698C16.6945 17.7859 16.4951 18.1797 16.2371 18.5339C15.9809 18.8881 15.6697 19.1993 15.3155 19.4555C14.9613 19.7134 14.5693 19.9129 14.1532 20.047C13.7371 20.1829 13.302 20.2499 12.8636 20.2499C12.573 20.2516 12.2807 20.2207 11.9953 20.1622C11.7116 20.102 11.433 20.0109 11.1665 19.8923C10.9 19.7736 10.6472 19.6258 10.4116 19.4538C10.1778 19.2819 9.96115 19.0841 9.76857 18.8658C9.33871 18.9586 8.89853 18.981 8.46351 18.9363C8.02849 18.8898 7.60207 18.7763 7.20143 18.5975C6.80252 18.4204 6.43284 18.1797 6.10786 17.8857C5.78289 17.5916 5.50606 17.2478 5.28769 16.8695C5.14153 16.6167 5.02117 16.3502 4.93004 16.0734C4.83891 15.7965 4.77873 15.5111 4.74778 15.2205C4.71683 14.9317 4.71855 14.6393 4.7495 14.3488C4.78045 14.0599 4.84407 13.7745 4.9352 13.4976C4.64289 13.1727 4.40217 12.803 4.22335 12.4041C4.04624 12.0034 3.93104 11.5787 3.88634 11.1437C3.83991 10.7087 3.86398 10.2685 3.95511 9.84036C4.04624 9.41222 4.20443 9.00127 4.42452 8.62299C4.56896 8.37023 4.73918 8.13123 4.93348 7.91458C5.12778 7.69793 5.34615 7.50191 5.58171 7.32997C5.81728 7.15802 6.07176 7.01187 6.33827 6.89495C6.6065 6.7763 6.88506 6.68861 7.17048 6.63015C7.3046 6.21232 7.50406 5.82029 7.76026 5.46608C8.01817 5.11188 8.32939 4.80066 8.6836 4.54274C9.03781 4.28654 9.42984 4.08708 9.84595 3.95125C10.2621 3.81713 10.6971 3.74835 11.1355 3.75007C11.4261 3.74835 11.7184 3.77758 12.0039 3.83776C12.2893 3.89794 12.5678 3.98736 12.8344 4.106C13.1009 4.22636 13.3536 4.37251 13.5892 4.54446C13.8248 4.71812 14.0414 4.91414 14.234 5.13251C14.6621 5.04138 15.1023 5.01903 15.5373 5.06373C15.9723 5.10844 16.3971 5.22364 16.7977 5.40074C17.1966 5.57957 17.5663 5.81857 17.8913 6.1126C18.2162 6.4049 18.4931 6.74707 18.7114 7.12707C18.8576 7.37811 18.9779 7.64463 19.0691 7.92318C19.1602 8.20001 19.2221 8.48544 19.2513 8.77602C19.2823 9.06661 19.2823 9.35892 19.2496 9.64951C19.2187 9.94009 19.155 10.2255 19.0639 10.5024C19.3579 10.8273 19.5969 11.1953 19.7758 11.5959ZM14.0466 18.9363C14.4214 18.7815 14.7619 18.5528 15.049 18.2657C15.3362 17.9785 15.5648 17.6381 15.7196 17.2615C15.8743 16.8867 15.9552 16.4843 15.9552 16.0785V12.2442C15.954 12.2407 15.9529 12.2367 15.9517 12.2321C15.9506 12.2287 15.9488 12.2252 15.9466 12.2218C15.9443 12.2184 15.9414 12.2155 15.938 12.2132C15.9345 12.2098 15.9311 12.2075 15.9276 12.2063L14.54 11.4051V16.0373C14.54 16.0837 14.5332 16.1318 14.5211 16.1765C14.5091 16.223 14.4919 16.2659 14.4678 16.3072C14.4438 16.3485 14.4162 16.3863 14.3819 16.419C14.3484 16.4523 14.3109 16.4812 14.2701 16.505L10.9842 18.4015C10.9567 18.4187 10.9103 18.4428 10.8862 18.4565C11.0221 18.5717 11.1699 18.6732 11.3247 18.7626C11.4811 18.852 11.6428 18.9277 11.8113 18.9896C11.9798 19.0497 12.1535 19.0962 12.3288 19.1271C12.5059 19.1581 12.6848 19.1735 12.8636 19.1735C13.2694 19.1735 13.6717 19.0927 14.0466 18.9363ZM6.22135 16.333C6.42596 16.6855 6.69592 16.9916 7.01745 17.2392C7.34071 17.4868 7.70695 17.6673 8.09899 17.7722C8.49102 17.8771 8.90025 17.9046 9.3026 17.8513C9.70495 17.798 10.0918 17.6673 10.4443 17.4644L13.7663 15.5472L13.7749 15.5386C13.7772 15.5363 13.7789 15.5329 13.78 15.5283C13.7823 15.5249 13.7841 15.5214 13.7852 15.518V13.9017L9.77545 16.2212C9.73418 16.2453 9.6912 16.2625 9.64649 16.2763C9.60007 16.2883 9.55364 16.2935 9.5055 16.2935C9.45907 16.2935 9.41265 16.2883 9.36622 16.2763C9.32152 16.2625 9.27681 16.2453 9.23554 16.2212L5.94967 14.323C5.92044 14.3058 5.87746 14.28 5.85339 14.2645C5.82244 14.4416 5.80696 14.6204 5.80696 14.7993C5.80696 14.9781 5.82415 15.1569 5.85511 15.334C5.88605 15.5094 5.9342 15.6831 5.99438 15.8516C6.05628 16.0201 6.13194 16.1817 6.22135 16.3364V16.333ZM5.35818 9.1629C5.15529 9.51539 5.02461 9.90398 4.97131 10.3063C4.918 10.7087 4.94552 11.1162 5.0504 11.51C5.15529 11.902 5.33583 12.2682 5.58343 12.5915C5.83103 12.913 6.13881 13.183 6.48958 13.3859L9.80984 15.3048C9.81328 15.3059 9.81729 15.3071 9.82188 15.3082H9.83391C9.8385 15.3082 9.84251 15.3071 9.84595 15.3048C9.84939 15.3036 9.85283 15.3019 9.85627 15.2996L11.249 14.4949L7.23926 12.1805C7.19971 12.1565 7.16189 12.1272 7.1275 12.0946C7.09418 12.0611 7.06529 12.0236 7.04153 11.9828C7.01917 11.9415 7.00026 11.8985 6.98822 11.8521C6.97619 11.8074 6.96931 11.761 6.97103 11.7128V7.80797C6.80252 7.86987 6.63917 7.94553 6.48442 8.03494C6.32967 8.12607 6.18352 8.22924 6.04596 8.34444C5.91013 8.45965 5.78289 8.58688 5.66769 8.72444C5.55248 8.86028 5.45103 9.00815 5.36162 9.1629H5.35818ZM16.7633 11.8177C16.8046 11.8418 16.8424 11.8693 16.8768 11.9037C16.9094 11.9364 16.9387 11.9742 16.9628 12.0155C16.9851 12.0567 17.004 12.1014 17.0161 12.1461C17.0264 12.1926 17.0332 12.239 17.0315 12.2871V16.192C17.5835 15.9891 18.0649 15.6332 18.4208 15.1655C18.7785 14.6978 18.9934 14.139 19.0433 13.5544C19.0931 12.9698 18.9762 12.3817 18.7046 11.8607C18.4329 11.3397 18.0185 10.9064 17.5095 10.6141L14.1893 8.69521C14.1858 8.69406 14.1818 8.69292 14.1772 8.69177H14.1652C14.1618 8.69292 14.1578 8.69406 14.1532 8.69521C14.1497 8.69636 14.1463 8.69808 14.1429 8.70037L12.757 9.50163L16.7667 11.8177H16.7633ZM18.1475 9.7372H18.1457V9.73892L18.1475 9.7372ZM18.1457 9.73548C18.2455 9.15774 18.1784 8.56281 17.9514 8.02119C17.7262 7.47956 17.3496 7.01359 16.8682 6.67658C16.3867 6.34128 15.8193 6.1487 15.233 6.12291C14.6449 6.09884 14.0638 6.24155 13.5548 6.53386L10.2345 8.45105C10.2311 8.45334 10.2282 8.45621 10.2259 8.45965L10.2191 8.46996C10.2179 8.4734 10.2168 8.47741 10.2156 8.482C10.2145 8.48544 10.2139 8.48945 10.2139 8.49403V10.0966L14.2237 7.78046C14.2649 7.75639 14.3096 7.7392 14.3543 7.72544C14.4008 7.7134 14.4472 7.70825 14.4936 7.70825C14.5418 7.70825 14.5882 7.7134 14.6346 7.72544C14.6793 7.7392 14.7223 7.75639 14.7636 7.78046L18.0494 9.67874C18.0787 9.69593 18.1217 9.72 18.1457 9.73548ZM9.45735 7.96101C9.45735 7.91458 9.46423 7.86816 9.47627 7.82173C9.4883 7.77702 9.5055 7.73232 9.52957 7.69105C9.55364 7.6515 9.58115 7.61368 9.61554 7.57929C9.64821 7.54662 9.68604 7.51739 9.72731 7.49503L13.0132 5.59848C13.0441 5.57957 13.0871 5.55549 13.1112 5.54346C12.6607 5.1669 12.1105 4.92618 11.5276 4.85224C10.9447 4.77658 10.3532 4.86943 9.82188 5.11875C9.28885 5.36807 8.83835 5.76527 8.52369 6.26047C8.20903 6.75739 8.04224 7.33169 8.04224 7.91974V11.7541C8.04339 11.7587 8.04454 11.7627 8.04568 11.7661C8.04683 11.7696 8.04855 11.773 8.05084 11.7765C8.05313 11.7799 8.056 11.7833 8.05944 11.7868C8.06173 11.7891 8.06517 11.7914 8.06976 11.7937L9.45735 12.5949V7.96101ZM10.2105 13.0282L11.997 14.0599L13.7835 13.0282V10.9666L11.9987 9.93493L10.2122 10.9666L10.2105 13.0282Z", + "fill": "white" + }, + "children": [] + } + ] + }, + "name": "OpenaiTeal" +} diff --git a/web/app/components/base/icons/src/public/llm/OpenaiTeal.tsx b/web/app/components/base/icons/src/public/llm/OpenaiTeal.tsx new file mode 100644 index 0000000000..ef803ea52f --- /dev/null +++ b/web/app/components/base/icons/src/public/llm/OpenaiTeal.tsx @@ -0,0 +1,20 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import type { IconData } from '@/app/components/base/icons/IconBase' +import * as React from 'react' +import IconBase from '@/app/components/base/icons/IconBase' +import data from './OpenaiTeal.json' + +const Icon = ( + { + ref, + ...props + }: React.SVGProps & { + ref?: React.RefObject> + }, +) => + +Icon.displayName = 'OpenaiTeal' + +export default Icon diff --git a/web/app/components/base/icons/src/public/llm/OpenaiViolet.json b/web/app/components/base/icons/src/public/llm/OpenaiViolet.json new file mode 100644 index 0000000000..e80a85507e --- /dev/null +++ b/web/app/components/base/icons/src/public/llm/OpenaiViolet.json @@ -0,0 +1,37 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "24", + "height": "24", + "viewBox": "0 0 24 24", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "rect", + "attributes": { + "width": "24", + "height": "24", + "rx": "6", + "fill": "#AB68FF" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M19.7758 11.5959C19.9546 11.9948 20.0681 12.4213 20.1145 12.8563C20.1592 13.2913 20.1369 13.7315 20.044 14.1596C19.9529 14.5878 19.7947 14.9987 19.5746 15.377C19.4302 15.6298 19.2599 15.867 19.0639 16.0854C18.8696 16.3021 18.653 16.4981 18.4174 16.67C18.1801 16.842 17.9274 16.9864 17.6591 17.105C17.3926 17.222 17.1141 17.3114 16.8286 17.3698C16.6945 17.7859 16.4951 18.1797 16.2371 18.5339C15.9809 18.8881 15.6697 19.1993 15.3155 19.4555C14.9613 19.7134 14.5693 19.9129 14.1532 20.047C13.7371 20.1829 13.302 20.2499 12.8636 20.2499C12.573 20.2516 12.2807 20.2207 11.9953 20.1622C11.7116 20.102 11.433 20.0109 11.1665 19.8923C10.9 19.7736 10.6472 19.6258 10.4116 19.4538C10.1778 19.2819 9.96115 19.0841 9.76857 18.8658C9.33871 18.9586 8.89853 18.981 8.46351 18.9363C8.02849 18.8898 7.60207 18.7763 7.20143 18.5975C6.80252 18.4204 6.43284 18.1797 6.10786 17.8857C5.78289 17.5916 5.50606 17.2478 5.28769 16.8695C5.14153 16.6167 5.02117 16.3502 4.93004 16.0734C4.83891 15.7965 4.77873 15.5111 4.74778 15.2205C4.71683 14.9317 4.71855 14.6393 4.7495 14.3488C4.78045 14.0599 4.84407 13.7745 4.9352 13.4976C4.64289 13.1727 4.40217 12.803 4.22335 12.4041C4.04624 12.0034 3.93104 11.5787 3.88634 11.1437C3.83991 10.7087 3.86398 10.2685 3.95511 9.84036C4.04624 9.41222 4.20443 9.00127 4.42452 8.62299C4.56896 8.37023 4.73918 8.13123 4.93348 7.91458C5.12778 7.69793 5.34615 7.50191 5.58171 7.32997C5.81728 7.15802 6.07176 7.01187 6.33827 6.89495C6.6065 6.7763 6.88506 6.68861 7.17048 6.63015C7.3046 6.21232 7.50406 5.82029 7.76026 5.46608C8.01817 5.11188 8.32939 4.80066 8.6836 4.54274C9.03781 4.28654 9.42984 4.08708 9.84595 3.95125C10.2621 3.81713 10.6971 3.74835 11.1355 3.75007C11.4261 3.74835 11.7184 3.77758 12.0039 3.83776C12.2893 3.89794 12.5678 3.98736 12.8344 4.106C13.1009 4.22636 13.3536 4.37251 13.5892 4.54446C13.8248 4.71812 14.0414 4.91414 14.234 5.13251C14.6621 5.04138 15.1023 5.01903 15.5373 5.06373C15.9723 5.10844 16.3971 5.22364 16.7977 5.40074C17.1966 5.57957 17.5663 5.81857 17.8913 6.1126C18.2162 6.4049 18.4931 6.74707 18.7114 7.12707C18.8576 7.37811 18.9779 7.64463 19.0691 7.92318C19.1602 8.20001 19.2221 8.48544 19.2513 8.77602C19.2823 9.06661 19.2823 9.35892 19.2496 9.64951C19.2187 9.94009 19.155 10.2255 19.0639 10.5024C19.3579 10.8273 19.5969 11.1953 19.7758 11.5959ZM14.0466 18.9363C14.4214 18.7815 14.7619 18.5528 15.049 18.2657C15.3362 17.9785 15.5648 17.6381 15.7196 17.2615C15.8743 16.8867 15.9552 16.4843 15.9552 16.0785V12.2442C15.954 12.2407 15.9529 12.2367 15.9517 12.2321C15.9506 12.2287 15.9488 12.2252 15.9466 12.2218C15.9443 12.2184 15.9414 12.2155 15.938 12.2132C15.9345 12.2098 15.9311 12.2075 15.9276 12.2063L14.54 11.4051V16.0373C14.54 16.0837 14.5332 16.1318 14.5211 16.1765C14.5091 16.223 14.4919 16.2659 14.4678 16.3072C14.4438 16.3485 14.4162 16.3863 14.3819 16.419C14.3484 16.4523 14.3109 16.4812 14.2701 16.505L10.9842 18.4015C10.9567 18.4187 10.9103 18.4428 10.8862 18.4565C11.0221 18.5717 11.1699 18.6732 11.3247 18.7626C11.4811 18.852 11.6428 18.9277 11.8113 18.9896C11.9798 19.0497 12.1535 19.0962 12.3288 19.1271C12.5059 19.1581 12.6848 19.1735 12.8636 19.1735C13.2694 19.1735 13.6717 19.0927 14.0466 18.9363ZM6.22135 16.333C6.42596 16.6855 6.69592 16.9916 7.01745 17.2392C7.34071 17.4868 7.70695 17.6673 8.09899 17.7722C8.49102 17.8771 8.90025 17.9046 9.3026 17.8513C9.70495 17.798 10.0918 17.6673 10.4443 17.4644L13.7663 15.5472L13.7749 15.5386C13.7772 15.5363 13.7789 15.5329 13.78 15.5283C13.7823 15.5249 13.7841 15.5214 13.7852 15.518V13.9017L9.77545 16.2212C9.73418 16.2453 9.6912 16.2625 9.64649 16.2763C9.60007 16.2883 9.55364 16.2935 9.5055 16.2935C9.45907 16.2935 9.41265 16.2883 9.36622 16.2763C9.32152 16.2625 9.27681 16.2453 9.23554 16.2212L5.94967 14.323C5.92044 14.3058 5.87746 14.28 5.85339 14.2645C5.82244 14.4416 5.80696 14.6204 5.80696 14.7993C5.80696 14.9781 5.82415 15.1569 5.85511 15.334C5.88605 15.5094 5.9342 15.6831 5.99438 15.8516C6.05628 16.0201 6.13194 16.1817 6.22135 16.3364V16.333ZM5.35818 9.1629C5.15529 9.51539 5.02461 9.90398 4.97131 10.3063C4.918 10.7087 4.94552 11.1162 5.0504 11.51C5.15529 11.902 5.33583 12.2682 5.58343 12.5915C5.83103 12.913 6.13881 13.183 6.48958 13.3859L9.80984 15.3048C9.81328 15.3059 9.81729 15.3071 9.82188 15.3082H9.83391C9.8385 15.3082 9.84251 15.3071 9.84595 15.3048C9.84939 15.3036 9.85283 15.3019 9.85627 15.2996L11.249 14.4949L7.23926 12.1805C7.19971 12.1565 7.16189 12.1272 7.1275 12.0946C7.09418 12.0611 7.06529 12.0236 7.04153 11.9828C7.01917 11.9415 7.00026 11.8985 6.98822 11.8521C6.97619 11.8074 6.96931 11.761 6.97103 11.7128V7.80797C6.80252 7.86987 6.63917 7.94553 6.48442 8.03494C6.32967 8.12607 6.18352 8.22924 6.04596 8.34444C5.91013 8.45965 5.78289 8.58688 5.66769 8.72444C5.55248 8.86028 5.45103 9.00815 5.36162 9.1629H5.35818ZM16.7633 11.8177C16.8046 11.8418 16.8424 11.8693 16.8768 11.9037C16.9094 11.9364 16.9387 11.9742 16.9628 12.0155C16.9851 12.0567 17.004 12.1014 17.0161 12.1461C17.0264 12.1926 17.0332 12.239 17.0315 12.2871V16.192C17.5835 15.9891 18.0649 15.6332 18.4208 15.1655C18.7785 14.6978 18.9934 14.139 19.0433 13.5544C19.0931 12.9698 18.9762 12.3817 18.7046 11.8607C18.4329 11.3397 18.0185 10.9064 17.5095 10.6141L14.1893 8.69521C14.1858 8.69406 14.1818 8.69292 14.1772 8.69177H14.1652C14.1618 8.69292 14.1578 8.69406 14.1532 8.69521C14.1497 8.69636 14.1463 8.69808 14.1429 8.70037L12.757 9.50163L16.7667 11.8177H16.7633ZM18.1475 9.7372H18.1457V9.73892L18.1475 9.7372ZM18.1457 9.73548C18.2455 9.15774 18.1784 8.56281 17.9514 8.02119C17.7262 7.47956 17.3496 7.01359 16.8682 6.67658C16.3867 6.34128 15.8193 6.1487 15.233 6.12291C14.6449 6.09884 14.0638 6.24155 13.5548 6.53386L10.2345 8.45105C10.2311 8.45334 10.2282 8.45621 10.2259 8.45965L10.2191 8.46996C10.2179 8.4734 10.2168 8.47741 10.2156 8.482C10.2145 8.48544 10.2139 8.48945 10.2139 8.49403V10.0966L14.2237 7.78046C14.2649 7.75639 14.3096 7.7392 14.3543 7.72544C14.4008 7.7134 14.4472 7.70825 14.4936 7.70825C14.5418 7.70825 14.5882 7.7134 14.6346 7.72544C14.6793 7.7392 14.7223 7.75639 14.7636 7.78046L18.0494 9.67874C18.0787 9.69593 18.1217 9.72 18.1457 9.73548ZM9.45735 7.96101C9.45735 7.91458 9.46423 7.86816 9.47627 7.82173C9.4883 7.77702 9.5055 7.73232 9.52957 7.69105C9.55364 7.6515 9.58115 7.61368 9.61554 7.57929C9.64821 7.54662 9.68604 7.51739 9.72731 7.49503L13.0132 5.59848C13.0441 5.57957 13.0871 5.55549 13.1112 5.54346C12.6607 5.1669 12.1105 4.92618 11.5276 4.85224C10.9447 4.77658 10.3532 4.86943 9.82188 5.11875C9.28885 5.36807 8.83835 5.76527 8.52369 6.26047C8.20903 6.75739 8.04224 7.33169 8.04224 7.91974V11.7541C8.04339 11.7587 8.04454 11.7627 8.04568 11.7661C8.04683 11.7696 8.04855 11.773 8.05084 11.7765C8.05313 11.7799 8.056 11.7833 8.05944 11.7868C8.06173 11.7891 8.06517 11.7914 8.06976 11.7937L9.45735 12.5949V7.96101ZM10.2105 13.0282L11.997 14.0599L13.7835 13.0282V10.9666L11.9987 9.93493L10.2122 10.9666L10.2105 13.0282Z", + "fill": "white" + }, + "children": [] + } + ] + }, + "name": "OpenaiViolet" +} diff --git a/web/app/components/base/icons/src/public/llm/OpenaiViolet.tsx b/web/app/components/base/icons/src/public/llm/OpenaiViolet.tsx new file mode 100644 index 0000000000..9aa08c0f3b --- /dev/null +++ b/web/app/components/base/icons/src/public/llm/OpenaiViolet.tsx @@ -0,0 +1,20 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import type { IconData } from '@/app/components/base/icons/IconBase' +import * as React from 'react' +import IconBase from '@/app/components/base/icons/IconBase' +import data from './OpenaiViolet.json' + +const Icon = ( + { + ref, + ...props + }: React.SVGProps & { + ref?: React.RefObject> + }, +) => + +Icon.displayName = 'OpenaiViolet' + +export default Icon diff --git a/web/app/components/base/icons/src/public/llm/index.ts b/web/app/components/base/icons/src/public/llm/index.ts index f2b3fe689e..0c5cef4a36 100644 --- a/web/app/components/base/icons/src/public/llm/index.ts +++ b/web/app/components/base/icons/src/public/llm/index.ts @@ -30,10 +30,13 @@ export { default as Localai } from './Localai' export { default as LocalaiText } from './LocalaiText' export { default as Microsoft } from './Microsoft' export { default as OpenaiBlack } from './OpenaiBlack' +export { default as OpenaiBlue } from './OpenaiBlue' export { default as OpenaiGreen } from './OpenaiGreen' export { default as OpenaiSmall } from './OpenaiSmall' +export { default as OpenaiTeal } from './OpenaiTeal' export { default as OpenaiText } from './OpenaiText' export { default as OpenaiTransparent } from './OpenaiTransparent' +export { default as OpenaiViolet } from './OpenaiViolet' export { default as OpenaiYellow } from './OpenaiYellow' export { default as Openllm } from './Openllm' export { default as OpenllmText } from './OpenllmText' From 5362f69083f7ee3ae6b69bce92488421ae5b8793 Mon Sep 17 00:00:00 2001 From: "Byron.wang" Date: Sat, 3 Jan 2026 19:46:46 -0800 Subject: [PATCH 34/37] feat(refactoring): Support Structured Logging (JSON) (#30170) --- api/.env.example | 2 + api/app_factory.py | 29 +- api/configs/feature/__init__.py | 5 + api/core/helper/ssrf_proxy.py | 45 ++- api/core/helper/trace_id_helper.py | 57 ++++ api/core/logging/__init__.py | 20 ++ api/core/logging/context.py | 35 +++ api/core/logging/filters.py | 94 ++++++ api/core/logging/structured_formatter.py | 107 +++++++ api/core/plugin/impl/base.py | 28 ++ api/extensions/ext_celery.py | 4 + api/extensions/ext_logging.py | 105 +++++-- api/extensions/otel/instrumentation.py | 53 ++-- api/libs/external_api.py | 8 +- .../unit_tests/core/helper/test_ssrf_proxy.py | 115 ++++++-- api/tests/unit_tests/core/logging/__init__.py | 0 .../unit_tests/core/logging/test_context.py | 79 ++++++ .../unit_tests/core/logging/test_filters.py | 114 ++++++++ .../core/logging/test_structured_formatter.py | 267 ++++++++++++++++++ .../core/logging/test_trace_helpers.py | 102 +++++++ .../unit_tests/libs/test_external_api.py | 33 +-- docker/.env.example | 2 + docker/docker-compose.middleware.yaml | 1 + docker/docker-compose.yaml | 1 + 24 files changed, 1193 insertions(+), 113 deletions(-) create mode 100644 api/core/logging/__init__.py create mode 100644 api/core/logging/context.py create mode 100644 api/core/logging/filters.py create mode 100644 api/core/logging/structured_formatter.py create mode 100644 api/tests/unit_tests/core/logging/__init__.py create mode 100644 api/tests/unit_tests/core/logging/test_context.py create mode 100644 api/tests/unit_tests/core/logging/test_filters.py create mode 100644 api/tests/unit_tests/core/logging/test_structured_formatter.py create mode 100644 api/tests/unit_tests/core/logging/test_trace_helpers.py diff --git a/api/.env.example b/api/.env.example index 5f8d369ec4..88611e016e 100644 --- a/api/.env.example +++ b/api/.env.example @@ -502,6 +502,8 @@ LOG_FILE_BACKUP_COUNT=5 LOG_DATEFORMAT=%Y-%m-%d %H:%M:%S # Log Timezone LOG_TZ=UTC +# Log output format: text or json +LOG_OUTPUT_FORMAT=text # Log format LOG_FORMAT=%(asctime)s,%(msecs)d %(levelname)-2s [%(filename)s:%(lineno)d] %(req_id)s %(message)s diff --git a/api/app_factory.py b/api/app_factory.py index bcad88e9e0..f827842d68 100644 --- a/api/app_factory.py +++ b/api/app_factory.py @@ -2,9 +2,11 @@ import logging import time from opentelemetry.trace import get_current_span +from opentelemetry.trace.span import INVALID_SPAN_ID, INVALID_TRACE_ID from configs import dify_config from contexts.wrapper import RecyclableContextVar +from core.logging.context import init_request_context from dify_app import DifyApp logger = logging.getLogger(__name__) @@ -25,28 +27,35 @@ def create_flask_app_with_configs() -> DifyApp: # add before request hook @dify_app.before_request def before_request(): - # add an unique identifier to each request + # Initialize logging context for this request + init_request_context() RecyclableContextVar.increment_thread_recycles() - # add after request hook for injecting X-Trace-Id header from OpenTelemetry span context + # add after request hook for injecting trace headers from OpenTelemetry span context + # Only adds headers when OTEL is enabled and has valid context @dify_app.after_request - def add_trace_id_header(response): + def add_trace_headers(response): try: span = get_current_span() ctx = span.get_span_context() if span else None - if ctx and ctx.is_valid: - trace_id_hex = format(ctx.trace_id, "032x") - # Avoid duplicates if some middleware added it - if "X-Trace-Id" not in response.headers: - response.headers["X-Trace-Id"] = trace_id_hex + + if not ctx or not ctx.is_valid: + return response + + # Inject trace headers from OTEL context + if ctx.trace_id != INVALID_TRACE_ID and "X-Trace-Id" not in response.headers: + response.headers["X-Trace-Id"] = format(ctx.trace_id, "032x") + if ctx.span_id != INVALID_SPAN_ID and "X-Span-Id" not in response.headers: + response.headers["X-Span-Id"] = format(ctx.span_id, "016x") + except Exception: # Never break the response due to tracing header injection - logger.warning("Failed to add trace ID to response header", exc_info=True) + logger.warning("Failed to add trace headers to response", exc_info=True) return response # Capture the decorator's return value to avoid pyright reportUnusedFunction _ = before_request - _ = add_trace_id_header + _ = add_trace_headers return dify_app diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index 43dddbd011..6a04171d2d 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -587,6 +587,11 @@ class LoggingConfig(BaseSettings): default="INFO", ) + LOG_OUTPUT_FORMAT: Literal["text", "json"] = Field( + description="Log output format: 'text' for human-readable, 'json' for structured JSON logs.", + default="text", + ) + LOG_FILE: str | None = Field( description="File path for log output.", default=None, diff --git a/api/core/helper/ssrf_proxy.py b/api/core/helper/ssrf_proxy.py index 0b36969cf9..1785cbde4c 100644 --- a/api/core/helper/ssrf_proxy.py +++ b/api/core/helper/ssrf_proxy.py @@ -88,7 +88,41 @@ def _get_user_provided_host_header(headers: dict | None) -> str | None: return None +def _inject_trace_headers(headers: dict | None) -> dict: + """ + Inject W3C traceparent header for distributed tracing. + + When OTEL is enabled, HTTPXClientInstrumentor handles trace propagation automatically. + When OTEL is disabled, we manually inject the traceparent header. + """ + if headers is None: + headers = {} + + # Skip if already present (case-insensitive check) + for key in headers: + if key.lower() == "traceparent": + return headers + + # Skip if OTEL is enabled - HTTPXClientInstrumentor handles this automatically + if dify_config.ENABLE_OTEL: + return headers + + # Generate and inject traceparent for non-OTEL scenarios + try: + from core.helper.trace_id_helper import generate_traceparent_header + + traceparent = generate_traceparent_header() + if traceparent: + headers["traceparent"] = traceparent + except Exception: + # Silently ignore errors to avoid breaking requests + logger.debug("Failed to generate traceparent header", exc_info=True) + + return headers + + def make_request(method, url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs): + # Convert requests-style allow_redirects to httpx-style follow_redirects if "allow_redirects" in kwargs: allow_redirects = kwargs.pop("allow_redirects") if "follow_redirects" not in kwargs: @@ -106,18 +140,21 @@ def make_request(method, url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs): verify_option = kwargs.pop("ssl_verify", dify_config.HTTP_REQUEST_NODE_SSL_VERIFY) client = _get_ssrf_client(verify_option) + # Inject traceparent header for distributed tracing (when OTEL is not enabled) + headers = kwargs.get("headers") or {} + headers = _inject_trace_headers(headers) + kwargs["headers"] = headers + # Preserve user-provided Host header # When using a forward proxy, httpx may override the Host header based on the URL. # We extract and preserve any explicitly set Host header to support virtual hosting. - headers = kwargs.get("headers", {}) user_provided_host = _get_user_provided_host_header(headers) retries = 0 while retries <= max_retries: try: - # Build the request manually to preserve the Host header - # httpx may override the Host header when using a proxy, so we use - # the request API to explicitly set headers before sending + # Preserve the user-provided Host header + # httpx may override the Host header when using a proxy headers = {k: v for k, v in headers.items() if k.lower() != "host"} if user_provided_host is not None: headers["host"] = user_provided_host diff --git a/api/core/helper/trace_id_helper.py b/api/core/helper/trace_id_helper.py index 820502e558..e827859109 100644 --- a/api/core/helper/trace_id_helper.py +++ b/api/core/helper/trace_id_helper.py @@ -103,3 +103,60 @@ def parse_traceparent_header(traceparent: str) -> str | None: if len(parts) == 4 and len(parts[1]) == 32: return parts[1] return None + + +def get_span_id_from_otel_context() -> str | None: + """ + Retrieve the current span ID from the active OpenTelemetry trace context. + + Returns: + A 16-character hex string representing the span ID, or None if not available. + """ + try: + from opentelemetry.trace import get_current_span + from opentelemetry.trace.span import INVALID_SPAN_ID + + span = get_current_span() + if not span: + return None + + span_context = span.get_span_context() + if not span_context or span_context.span_id == INVALID_SPAN_ID: + return None + + return f"{span_context.span_id:016x}" + except Exception: + return None + + +def generate_traceparent_header() -> str | None: + """ + Generate a W3C traceparent header from the current context. + + Uses OpenTelemetry context if available, otherwise uses the + ContextVar-based trace_id from the logging context. + + Format: {version}-{trace_id}-{span_id}-{flags} + Example: 00-5b8aa5a2d2c872e8321cf37308d69df2-051581bf3bb55c45-01 + + Returns: + A valid traceparent header string, or None if generation fails. + """ + import uuid + + # Try OTEL context first + trace_id = get_trace_id_from_otel_context() + span_id = get_span_id_from_otel_context() + + if trace_id and span_id: + return f"00-{trace_id}-{span_id}-01" + + # Fallback: use ContextVar-based trace_id or generate new one + from core.logging.context import get_trace_id as get_logging_trace_id + + trace_id = get_logging_trace_id() or uuid.uuid4().hex + + # Generate a new span_id (16 hex chars) + span_id = uuid.uuid4().hex[:16] + + return f"00-{trace_id}-{span_id}-01" diff --git a/api/core/logging/__init__.py b/api/core/logging/__init__.py new file mode 100644 index 0000000000..db046cc9fa --- /dev/null +++ b/api/core/logging/__init__.py @@ -0,0 +1,20 @@ +"""Structured logging components for Dify.""" + +from core.logging.context import ( + clear_request_context, + get_request_id, + get_trace_id, + init_request_context, +) +from core.logging.filters import IdentityContextFilter, TraceContextFilter +from core.logging.structured_formatter import StructuredJSONFormatter + +__all__ = [ + "IdentityContextFilter", + "StructuredJSONFormatter", + "TraceContextFilter", + "clear_request_context", + "get_request_id", + "get_trace_id", + "init_request_context", +] diff --git a/api/core/logging/context.py b/api/core/logging/context.py new file mode 100644 index 0000000000..18633a0b05 --- /dev/null +++ b/api/core/logging/context.py @@ -0,0 +1,35 @@ +"""Request context for logging - framework agnostic. + +This module provides request-scoped context variables for logging, +using Python's contextvars for thread-safe and async-safe storage. +""" + +import uuid +from contextvars import ContextVar + +_request_id: ContextVar[str] = ContextVar("log_request_id", default="") +_trace_id: ContextVar[str] = ContextVar("log_trace_id", default="") + + +def get_request_id() -> str: + """Get current request ID (10 hex chars).""" + return _request_id.get() + + +def get_trace_id() -> str: + """Get fallback trace ID when OTEL is unavailable (32 hex chars).""" + return _trace_id.get() + + +def init_request_context() -> None: + """Initialize request context. Call at start of each request.""" + req_id = uuid.uuid4().hex[:10] + trace_id = uuid.uuid5(uuid.NAMESPACE_DNS, req_id).hex + _request_id.set(req_id) + _trace_id.set(trace_id) + + +def clear_request_context() -> None: + """Clear request context. Call at end of request (optional).""" + _request_id.set("") + _trace_id.set("") diff --git a/api/core/logging/filters.py b/api/core/logging/filters.py new file mode 100644 index 0000000000..1e8aa8d566 --- /dev/null +++ b/api/core/logging/filters.py @@ -0,0 +1,94 @@ +"""Logging filters for structured logging.""" + +import contextlib +import logging + +import flask + +from core.logging.context import get_request_id, get_trace_id + + +class TraceContextFilter(logging.Filter): + """ + Filter that adds trace_id and span_id to log records. + Integrates with OpenTelemetry when available, falls back to ContextVar-based trace_id. + """ + + def filter(self, record: logging.LogRecord) -> bool: + # Get trace context from OpenTelemetry + trace_id, span_id = self._get_otel_context() + + # Set trace_id (fallback to ContextVar if no OTEL context) + if trace_id: + record.trace_id = trace_id + else: + record.trace_id = get_trace_id() + + record.span_id = span_id or "" + + # For backward compatibility, also set req_id + record.req_id = get_request_id() + + return True + + def _get_otel_context(self) -> tuple[str, str]: + """Extract trace_id and span_id from OpenTelemetry context.""" + with contextlib.suppress(Exception): + from opentelemetry.trace import get_current_span + from opentelemetry.trace.span import INVALID_SPAN_ID, INVALID_TRACE_ID + + span = get_current_span() + if span and span.get_span_context(): + ctx = span.get_span_context() + if ctx.is_valid and ctx.trace_id != INVALID_TRACE_ID: + trace_id = f"{ctx.trace_id:032x}" + span_id = f"{ctx.span_id:016x}" if ctx.span_id != INVALID_SPAN_ID else "" + return trace_id, span_id + return "", "" + + +class IdentityContextFilter(logging.Filter): + """ + Filter that adds user identity context to log records. + Extracts tenant_id, user_id, and user_type from Flask-Login current_user. + """ + + def filter(self, record: logging.LogRecord) -> bool: + identity = self._extract_identity() + record.tenant_id = identity.get("tenant_id", "") + record.user_id = identity.get("user_id", "") + record.user_type = identity.get("user_type", "") + return True + + def _extract_identity(self) -> dict[str, str]: + """Extract identity from current_user if in request context.""" + try: + if not flask.has_request_context(): + return {} + from flask_login import current_user + + # Check if user is authenticated using the proxy + if not current_user.is_authenticated: + return {} + + # Access the underlying user object + user = current_user + + from models import Account + from models.model import EndUser + + identity: dict[str, str] = {} + + if isinstance(user, Account): + if user.current_tenant_id: + identity["tenant_id"] = user.current_tenant_id + identity["user_id"] = user.id + identity["user_type"] = "account" + elif isinstance(user, EndUser): + identity["tenant_id"] = user.tenant_id + identity["user_id"] = user.id + identity["user_type"] = user.type or "end_user" + + return identity + except Exception: + return {} diff --git a/api/core/logging/structured_formatter.py b/api/core/logging/structured_formatter.py new file mode 100644 index 0000000000..4295d2dd34 --- /dev/null +++ b/api/core/logging/structured_formatter.py @@ -0,0 +1,107 @@ +"""Structured JSON log formatter for Dify.""" + +import logging +import traceback +from datetime import UTC, datetime +from typing import Any + +import orjson + +from configs import dify_config + + +class StructuredJSONFormatter(logging.Formatter): + """ + JSON log formatter following the specified schema: + { + "ts": "ISO 8601 UTC", + "severity": "INFO|ERROR|WARN|DEBUG", + "service": "service name", + "caller": "file:line", + "trace_id": "hex 32", + "span_id": "hex 16", + "identity": { "tenant_id", "user_id", "user_type" }, + "message": "log message", + "attributes": { ... }, + "stack_trace": "..." + } + """ + + SEVERITY_MAP: dict[int, str] = { + logging.DEBUG: "DEBUG", + logging.INFO: "INFO", + logging.WARNING: "WARN", + logging.ERROR: "ERROR", + logging.CRITICAL: "ERROR", + } + + def __init__(self, service_name: str | None = None): + super().__init__() + self._service_name = service_name or dify_config.APPLICATION_NAME + + def format(self, record: logging.LogRecord) -> str: + log_dict = self._build_log_dict(record) + try: + return orjson.dumps(log_dict).decode("utf-8") + except TypeError: + # Fallback: convert non-serializable objects to string + import json + + return json.dumps(log_dict, default=str, ensure_ascii=False) + + def _build_log_dict(self, record: logging.LogRecord) -> dict[str, Any]: + # Core fields + log_dict: dict[str, Any] = { + "ts": datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z"), + "severity": self.SEVERITY_MAP.get(record.levelno, "INFO"), + "service": self._service_name, + "caller": f"{record.filename}:{record.lineno}", + "message": record.getMessage(), + } + + # Trace context (from TraceContextFilter) + trace_id = getattr(record, "trace_id", "") + span_id = getattr(record, "span_id", "") + + if trace_id: + log_dict["trace_id"] = trace_id + if span_id: + log_dict["span_id"] = span_id + + # Identity context (from IdentityContextFilter) + identity = self._extract_identity(record) + if identity: + log_dict["identity"] = identity + + # Dynamic attributes + attributes = getattr(record, "attributes", None) + if attributes: + log_dict["attributes"] = attributes + + # Stack trace for errors with exceptions + if record.exc_info and record.levelno >= logging.ERROR: + log_dict["stack_trace"] = self._format_exception(record.exc_info) + + return log_dict + + def _extract_identity(self, record: logging.LogRecord) -> dict[str, str] | None: + tenant_id = getattr(record, "tenant_id", None) + user_id = getattr(record, "user_id", None) + user_type = getattr(record, "user_type", None) + + if not any([tenant_id, user_id, user_type]): + return None + + identity: dict[str, str] = {} + if tenant_id: + identity["tenant_id"] = tenant_id + if user_id: + identity["user_id"] = user_id + if user_type: + identity["user_type"] = user_type + return identity + + def _format_exception(self, exc_info: tuple[Any, ...]) -> str: + if exc_info and exc_info[0] is not None: + return "".join(traceback.format_exception(*exc_info)) + return "" diff --git a/api/core/plugin/impl/base.py b/api/core/plugin/impl/base.py index 7bb2749afa..0e49824ad0 100644 --- a/api/core/plugin/impl/base.py +++ b/api/core/plugin/impl/base.py @@ -103,6 +103,9 @@ class BasePluginClient: prepared_headers["X-Api-Key"] = dify_config.PLUGIN_DAEMON_KEY prepared_headers.setdefault("Accept-Encoding", "gzip, deflate, br") + # Inject traceparent header for distributed tracing + self._inject_trace_headers(prepared_headers) + prepared_data: bytes | dict[str, Any] | str | None = ( data if isinstance(data, (bytes, str, dict)) or data is None else None ) @@ -114,6 +117,31 @@ class BasePluginClient: return str(url), prepared_headers, prepared_data, params, files + def _inject_trace_headers(self, headers: dict[str, str]) -> None: + """ + Inject W3C traceparent header for distributed tracing. + + This ensures trace context is propagated to plugin daemon even if + HTTPXClientInstrumentor doesn't cover module-level httpx functions. + """ + if not dify_config.ENABLE_OTEL: + return + + import contextlib + + # Skip if already present (case-insensitive check) + for key in headers: + if key.lower() == "traceparent": + return + + # Inject traceparent - works as fallback when OTEL instrumentation doesn't cover this call + with contextlib.suppress(Exception): + from core.helper.trace_id_helper import generate_traceparent_header + + traceparent = generate_traceparent_header() + if traceparent: + headers["traceparent"] = traceparent + def _stream_request( self, method: str, diff --git a/api/extensions/ext_celery.py b/api/extensions/ext_celery.py index 764df5d178..2fbab001d0 100644 --- a/api/extensions/ext_celery.py +++ b/api/extensions/ext_celery.py @@ -46,7 +46,11 @@ def _get_celery_ssl_options() -> dict[str, Any] | None: def init_app(app: DifyApp) -> Celery: class FlaskTask(Task): def __call__(self, *args: object, **kwargs: object) -> object: + from core.logging.context import init_request_context + with app.app_context(): + # Initialize logging context for this task (similar to before_request in Flask) + init_request_context() return self.run(*args, **kwargs) broker_transport_options = {} diff --git a/api/extensions/ext_logging.py b/api/extensions/ext_logging.py index 000d03ac41..978a40c503 100644 --- a/api/extensions/ext_logging.py +++ b/api/extensions/ext_logging.py @@ -1,18 +1,19 @@ +"""Logging extension for Dify Flask application.""" + import logging import os import sys -import uuid from logging.handlers import RotatingFileHandler -import flask - from configs import dify_config -from core.helper.trace_id_helper import get_trace_id_from_otel_context from dify_app import DifyApp def init_app(app: DifyApp): + """Initialize logging with support for text or JSON format.""" log_handlers: list[logging.Handler] = [] + + # File handler log_file = dify_config.LOG_FILE if log_file: log_dir = os.path.dirname(log_file) @@ -25,27 +26,53 @@ def init_app(app: DifyApp): ) ) - # Always add StreamHandler to log to console + # Console handler sh = logging.StreamHandler(sys.stdout) log_handlers.append(sh) - # Apply RequestIdFilter to all handlers - for handler in log_handlers: - handler.addFilter(RequestIdFilter()) + # Apply filters to all handlers + from core.logging.filters import IdentityContextFilter, TraceContextFilter + for handler in log_handlers: + handler.addFilter(TraceContextFilter()) + handler.addFilter(IdentityContextFilter()) + + # Configure formatter based on format type + formatter = _create_formatter() + for handler in log_handlers: + handler.setFormatter(formatter) + + # Configure root logger logging.basicConfig( level=dify_config.LOG_LEVEL, - format=dify_config.LOG_FORMAT, - datefmt=dify_config.LOG_DATEFORMAT, handlers=log_handlers, force=True, ) - # Apply RequestIdFormatter to all handlers - apply_request_id_formatter() - # Disable propagation for noisy loggers to avoid duplicate logs logging.getLogger("sqlalchemy.engine").propagate = False + + # Apply timezone if specified (only for text format) + if dify_config.LOG_OUTPUT_FORMAT == "text": + _apply_timezone(log_handlers) + + +def _create_formatter() -> logging.Formatter: + """Create appropriate formatter based on configuration.""" + if dify_config.LOG_OUTPUT_FORMAT == "json": + from core.logging.structured_formatter import StructuredJSONFormatter + + return StructuredJSONFormatter() + else: + # Text format - use existing pattern with backward compatible formatter + return _TextFormatter( + fmt=dify_config.LOG_FORMAT, + datefmt=dify_config.LOG_DATEFORMAT, + ) + + +def _apply_timezone(handlers: list[logging.Handler]): + """Apply timezone conversion to text formatters.""" log_tz = dify_config.LOG_TZ if log_tz: from datetime import datetime @@ -57,34 +84,51 @@ def init_app(app: DifyApp): def time_converter(seconds): return datetime.fromtimestamp(seconds, tz=timezone).timetuple() - for handler in logging.root.handlers: + for handler in handlers: if handler.formatter: - handler.formatter.converter = time_converter + handler.formatter.converter = time_converter # type: ignore[attr-defined] -def get_request_id(): - if getattr(flask.g, "request_id", None): - return flask.g.request_id +class _TextFormatter(logging.Formatter): + """Text formatter that ensures trace_id and req_id are always present.""" - new_uuid = uuid.uuid4().hex[:10] - flask.g.request_id = new_uuid - - return new_uuid + def format(self, record: logging.LogRecord) -> str: + if not hasattr(record, "req_id"): + record.req_id = "" + if not hasattr(record, "trace_id"): + record.trace_id = "" + if not hasattr(record, "span_id"): + record.span_id = "" + return super().format(record) +def get_request_id() -> str: + """Get request ID for current request context. + + Deprecated: Use core.logging.context.get_request_id() directly. + """ + from core.logging.context import get_request_id as _get_request_id + + return _get_request_id() + + +# Backward compatibility aliases class RequestIdFilter(logging.Filter): - # This is a logging filter that makes the request ID available for use in - # the logging format. Note that we're checking if we're in a request - # context, as we may want to log things before Flask is fully loaded. - def filter(self, record): - trace_id = get_trace_id_from_otel_context() or "" - record.req_id = get_request_id() if flask.has_request_context() else "" - record.trace_id = trace_id + """Deprecated: Use TraceContextFilter from core.logging.filters instead.""" + + def filter(self, record: logging.LogRecord) -> bool: + from core.logging.context import get_request_id as _get_request_id + from core.logging.context import get_trace_id as _get_trace_id + + record.req_id = _get_request_id() + record.trace_id = _get_trace_id() return True class RequestIdFormatter(logging.Formatter): - def format(self, record): + """Deprecated: Use _TextFormatter instead.""" + + def format(self, record: logging.LogRecord) -> str: if not hasattr(record, "req_id"): record.req_id = "" if not hasattr(record, "trace_id"): @@ -93,6 +137,7 @@ class RequestIdFormatter(logging.Formatter): def apply_request_id_formatter(): + """Deprecated: Formatter is now applied in init_app.""" for handler in logging.root.handlers: if handler.formatter: handler.formatter = RequestIdFormatter(dify_config.LOG_FORMAT, dify_config.LOG_DATEFORMAT) diff --git a/api/extensions/otel/instrumentation.py b/api/extensions/otel/instrumentation.py index 3597110cba..6617f69513 100644 --- a/api/extensions/otel/instrumentation.py +++ b/api/extensions/otel/instrumentation.py @@ -19,26 +19,43 @@ logger = logging.getLogger(__name__) class ExceptionLoggingHandler(logging.Handler): + """ + Handler that records exceptions to the current OpenTelemetry span. + + Unlike creating a new span, this records exceptions on the existing span + to maintain trace context consistency throughout the request lifecycle. + """ + def emit(self, record: logging.LogRecord): with contextlib.suppress(Exception): - if record.exc_info: - tracer = get_tracer_provider().get_tracer("dify.exception.logging") - with tracer.start_as_current_span( - "log.exception", - attributes={ - "log.level": record.levelname, - "log.message": record.getMessage(), - "log.logger": record.name, - "log.file.path": record.pathname, - "log.file.line": record.lineno, - }, - ) as span: - span.set_status(StatusCode.ERROR) - if record.exc_info[1]: - span.record_exception(record.exc_info[1]) - span.set_attribute("exception.message", str(record.exc_info[1])) - if record.exc_info[0]: - span.set_attribute("exception.type", record.exc_info[0].__name__) + if not record.exc_info: + return + + from opentelemetry.trace import get_current_span + + span = get_current_span() + if not span or not span.is_recording(): + return + + # Record exception on the current span instead of creating a new one + span.set_status(StatusCode.ERROR, record.getMessage()) + + # Add log context as span events/attributes + span.add_event( + "log.exception", + attributes={ + "log.level": record.levelname, + "log.message": record.getMessage(), + "log.logger": record.name, + "log.file.path": record.pathname, + "log.file.line": record.lineno, + }, + ) + + if record.exc_info[1]: + span.record_exception(record.exc_info[1]) + if record.exc_info[0]: + span.set_attribute("exception.type", record.exc_info[0].__name__) def instrument_exception_logging() -> None: diff --git a/api/libs/external_api.py b/api/libs/external_api.py index 61a90ee4a9..e8592407c3 100644 --- a/api/libs/external_api.py +++ b/api/libs/external_api.py @@ -1,5 +1,4 @@ import re -import sys from collections.abc import Mapping from typing import Any @@ -109,11 +108,8 @@ def register_external_error_handlers(api: Api): data.setdefault("code", "unknown") data.setdefault("status", status_code) - # Log stack - exc_info: Any = sys.exc_info() - if exc_info[1] is None: - exc_info = (None, None, None) - current_app.log_exception(exc_info) + # Note: Exception logging is handled by Flask/Flask-RESTX framework automatically + # Explicit log_exception call removed to avoid duplicate log entries return data, status_code diff --git a/api/tests/unit_tests/core/helper/test_ssrf_proxy.py b/api/tests/unit_tests/core/helper/test_ssrf_proxy.py index beae1d0358..d6d75fb72f 100644 --- a/api/tests/unit_tests/core/helper/test_ssrf_proxy.py +++ b/api/tests/unit_tests/core/helper/test_ssrf_proxy.py @@ -14,12 +14,12 @@ def test_successful_request(mock_get_client): mock_client = MagicMock() mock_response = MagicMock() mock_response.status_code = 200 - mock_client.send.return_value = mock_response mock_client.request.return_value = mock_response mock_get_client.return_value = mock_client response = make_request("GET", "http://example.com") assert response.status_code == 200 + mock_client.request.assert_called_once() @patch("core.helper.ssrf_proxy._get_ssrf_client") @@ -27,7 +27,6 @@ def test_retry_exceed_max_retries(mock_get_client): mock_client = MagicMock() mock_response = MagicMock() mock_response.status_code = 500 - mock_client.send.return_value = mock_response mock_client.request.return_value = mock_response mock_get_client.return_value = mock_client @@ -72,34 +71,12 @@ class TestGetUserProvidedHostHeader: assert result in ("first.com", "second.com") -@patch("core.helper.ssrf_proxy._get_ssrf_client") -def test_host_header_preservation_without_user_header(mock_get_client): - """Test that when no Host header is provided, the default behavior is maintained.""" - mock_client = MagicMock() - mock_request = MagicMock() - mock_request.headers = {} - mock_response = MagicMock() - mock_response.status_code = 200 - mock_client.send.return_value = mock_response - mock_client.request.return_value = mock_response - mock_get_client.return_value = mock_client - - response = make_request("GET", "http://example.com") - - assert response.status_code == 200 - # Host should not be set if not provided by user - assert "Host" not in mock_request.headers or mock_request.headers.get("Host") is None - - @patch("core.helper.ssrf_proxy._get_ssrf_client") def test_host_header_preservation_with_user_header(mock_get_client): """Test that user-provided Host header is preserved in the request.""" mock_client = MagicMock() - mock_request = MagicMock() - mock_request.headers = {} mock_response = MagicMock() mock_response.status_code = 200 - mock_client.send.return_value = mock_response mock_client.request.return_value = mock_response mock_get_client.return_value = mock_client @@ -107,3 +84,93 @@ def test_host_header_preservation_with_user_header(mock_get_client): response = make_request("GET", "http://example.com", headers={"Host": custom_host}) assert response.status_code == 200 + # Verify client.request was called with the host header preserved (lowercase) + call_kwargs = mock_client.request.call_args.kwargs + assert call_kwargs["headers"]["host"] == custom_host + + +@patch("core.helper.ssrf_proxy._get_ssrf_client") +@pytest.mark.parametrize("host_key", ["host", "HOST", "Host"]) +def test_host_header_preservation_case_insensitive(mock_get_client, host_key): + """Test that Host header is preserved regardless of case.""" + mock_client = MagicMock() + mock_response = MagicMock() + mock_response.status_code = 200 + mock_client.request.return_value = mock_response + mock_get_client.return_value = mock_client + + response = make_request("GET", "http://example.com", headers={host_key: "api.example.com"}) + + assert response.status_code == 200 + # Host header should be normalized to lowercase "host" + call_kwargs = mock_client.request.call_args.kwargs + assert call_kwargs["headers"]["host"] == "api.example.com" + + +class TestFollowRedirectsParameter: + """Tests for follow_redirects parameter handling. + + These tests verify that follow_redirects is correctly passed to client.request(). + """ + + @patch("core.helper.ssrf_proxy._get_ssrf_client") + def test_follow_redirects_passed_to_request(self, mock_get_client): + """Verify follow_redirects IS passed to client.request().""" + mock_client = MagicMock() + mock_response = MagicMock() + mock_response.status_code = 200 + mock_client.request.return_value = mock_response + mock_get_client.return_value = mock_client + + make_request("GET", "http://example.com", follow_redirects=True) + + # Verify follow_redirects was passed to request + call_kwargs = mock_client.request.call_args.kwargs + assert call_kwargs.get("follow_redirects") is True + + @patch("core.helper.ssrf_proxy._get_ssrf_client") + def test_allow_redirects_converted_to_follow_redirects(self, mock_get_client): + """Verify allow_redirects (requests-style) is converted to follow_redirects (httpx-style).""" + mock_client = MagicMock() + mock_response = MagicMock() + mock_response.status_code = 200 + mock_client.request.return_value = mock_response + mock_get_client.return_value = mock_client + + # Use allow_redirects (requests-style parameter) + make_request("GET", "http://example.com", allow_redirects=True) + + # Verify it was converted to follow_redirects + call_kwargs = mock_client.request.call_args.kwargs + assert call_kwargs.get("follow_redirects") is True + assert "allow_redirects" not in call_kwargs + + @patch("core.helper.ssrf_proxy._get_ssrf_client") + def test_follow_redirects_not_set_when_not_specified(self, mock_get_client): + """Verify follow_redirects is not in kwargs when not specified (httpx default behavior).""" + mock_client = MagicMock() + mock_response = MagicMock() + mock_response.status_code = 200 + mock_client.request.return_value = mock_response + mock_get_client.return_value = mock_client + + make_request("GET", "http://example.com") + + # follow_redirects should not be in kwargs, letting httpx use its default + call_kwargs = mock_client.request.call_args.kwargs + assert "follow_redirects" not in call_kwargs + + @patch("core.helper.ssrf_proxy._get_ssrf_client") + def test_follow_redirects_takes_precedence_over_allow_redirects(self, mock_get_client): + """Verify follow_redirects takes precedence when both are specified.""" + mock_client = MagicMock() + mock_response = MagicMock() + mock_response.status_code = 200 + mock_client.request.return_value = mock_response + mock_get_client.return_value = mock_client + + # Both specified - follow_redirects should take precedence + make_request("GET", "http://example.com", allow_redirects=False, follow_redirects=True) + + call_kwargs = mock_client.request.call_args.kwargs + assert call_kwargs.get("follow_redirects") is True diff --git a/api/tests/unit_tests/core/logging/__init__.py b/api/tests/unit_tests/core/logging/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/unit_tests/core/logging/test_context.py b/api/tests/unit_tests/core/logging/test_context.py new file mode 100644 index 0000000000..f388a3a0b9 --- /dev/null +++ b/api/tests/unit_tests/core/logging/test_context.py @@ -0,0 +1,79 @@ +"""Tests for logging context module.""" + +import uuid + +from core.logging.context import ( + clear_request_context, + get_request_id, + get_trace_id, + init_request_context, +) + + +class TestLoggingContext: + """Tests for the logging context functions.""" + + def test_init_creates_request_id(self): + """init_request_context should create a 10-char request ID.""" + init_request_context() + request_id = get_request_id() + assert len(request_id) == 10 + assert all(c in "0123456789abcdef" for c in request_id) + + def test_init_creates_trace_id(self): + """init_request_context should create a 32-char trace ID.""" + init_request_context() + trace_id = get_trace_id() + assert len(trace_id) == 32 + assert all(c in "0123456789abcdef" for c in trace_id) + + def test_trace_id_derived_from_request_id(self): + """trace_id should be deterministically derived from request_id.""" + init_request_context() + request_id = get_request_id() + trace_id = get_trace_id() + + # Verify trace_id is derived using uuid5 + expected_trace = uuid.uuid5(uuid.NAMESPACE_DNS, request_id).hex + assert trace_id == expected_trace + + def test_clear_resets_context(self): + """clear_request_context should reset both IDs to empty strings.""" + init_request_context() + assert get_request_id() != "" + assert get_trace_id() != "" + + clear_request_context() + assert get_request_id() == "" + assert get_trace_id() == "" + + def test_default_values_are_empty(self): + """Default values should be empty strings before init.""" + clear_request_context() + assert get_request_id() == "" + assert get_trace_id() == "" + + def test_multiple_inits_create_different_ids(self): + """Each init should create new unique IDs.""" + init_request_context() + first_request_id = get_request_id() + first_trace_id = get_trace_id() + + init_request_context() + second_request_id = get_request_id() + second_trace_id = get_trace_id() + + assert first_request_id != second_request_id + assert first_trace_id != second_trace_id + + def test_context_isolation(self): + """Context should be isolated per-call (no thread leakage in same thread).""" + init_request_context() + id1 = get_request_id() + + # Simulate another request + init_request_context() + id2 = get_request_id() + + # IDs should be different + assert id1 != id2 diff --git a/api/tests/unit_tests/core/logging/test_filters.py b/api/tests/unit_tests/core/logging/test_filters.py new file mode 100644 index 0000000000..b66ad111d5 --- /dev/null +++ b/api/tests/unit_tests/core/logging/test_filters.py @@ -0,0 +1,114 @@ +"""Tests for logging filters.""" + +import logging +from unittest import mock + +import pytest + + +@pytest.fixture +def log_record(): + return logging.LogRecord( + name="test", + level=logging.INFO, + pathname="", + lineno=0, + msg="test", + args=(), + exc_info=None, + ) + + +class TestTraceContextFilter: + def test_sets_empty_trace_id_without_context(self, log_record): + from core.logging.context import clear_request_context + from core.logging.filters import TraceContextFilter + + # Ensure no context is set + clear_request_context() + + filter = TraceContextFilter() + result = filter.filter(log_record) + + assert result is True + assert hasattr(log_record, "trace_id") + assert hasattr(log_record, "span_id") + assert hasattr(log_record, "req_id") + # Without context, IDs should be empty + assert log_record.trace_id == "" + assert log_record.req_id == "" + + def test_sets_trace_id_from_context(self, log_record): + """Test that trace_id and req_id are set from ContextVar when initialized.""" + from core.logging.context import init_request_context + from core.logging.filters import TraceContextFilter + + # Initialize context (no Flask needed!) + init_request_context() + + filter = TraceContextFilter() + filter.filter(log_record) + + # With context initialized, IDs should be set + assert log_record.trace_id != "" + assert len(log_record.trace_id) == 32 + assert log_record.req_id != "" + assert len(log_record.req_id) == 10 + + def test_filter_always_returns_true(self, log_record): + from core.logging.filters import TraceContextFilter + + filter = TraceContextFilter() + result = filter.filter(log_record) + assert result is True + + def test_sets_trace_id_from_otel_when_available(self, log_record): + from core.logging.filters import TraceContextFilter + + mock_span = mock.MagicMock() + mock_context = mock.MagicMock() + mock_context.trace_id = 0x5B8AA5A2D2C872E8321CF37308D69DF2 + mock_context.span_id = 0x051581BF3BB55C45 + mock_span.get_span_context.return_value = mock_context + + with ( + mock.patch("opentelemetry.trace.get_current_span", return_value=mock_span), + mock.patch("opentelemetry.trace.span.INVALID_TRACE_ID", 0), + mock.patch("opentelemetry.trace.span.INVALID_SPAN_ID", 0), + ): + filter = TraceContextFilter() + filter.filter(log_record) + + assert log_record.trace_id == "5b8aa5a2d2c872e8321cf37308d69df2" + assert log_record.span_id == "051581bf3bb55c45" + + +class TestIdentityContextFilter: + def test_sets_empty_identity_without_request_context(self, log_record): + from core.logging.filters import IdentityContextFilter + + filter = IdentityContextFilter() + result = filter.filter(log_record) + + assert result is True + assert log_record.tenant_id == "" + assert log_record.user_id == "" + assert log_record.user_type == "" + + def test_filter_always_returns_true(self, log_record): + from core.logging.filters import IdentityContextFilter + + filter = IdentityContextFilter() + result = filter.filter(log_record) + assert result is True + + def test_handles_exception_gracefully(self, log_record): + from core.logging.filters import IdentityContextFilter + + filter = IdentityContextFilter() + + # Should not raise even if something goes wrong + with mock.patch("core.logging.filters.flask.has_request_context", side_effect=Exception("Test error")): + result = filter.filter(log_record) + assert result is True + assert log_record.tenant_id == "" diff --git a/api/tests/unit_tests/core/logging/test_structured_formatter.py b/api/tests/unit_tests/core/logging/test_structured_formatter.py new file mode 100644 index 0000000000..94b91d205e --- /dev/null +++ b/api/tests/unit_tests/core/logging/test_structured_formatter.py @@ -0,0 +1,267 @@ +"""Tests for structured JSON formatter.""" + +import logging +import sys + +import orjson + + +class TestStructuredJSONFormatter: + def test_basic_log_format(self): + from core.logging.structured_formatter import StructuredJSONFormatter + + formatter = StructuredJSONFormatter(service_name="test-service") + record = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="test.py", + lineno=42, + msg="Test message", + args=(), + exc_info=None, + ) + + output = formatter.format(record) + log_dict = orjson.loads(output) + + assert log_dict["severity"] == "INFO" + assert log_dict["service"] == "test-service" + assert log_dict["caller"] == "test.py:42" + assert log_dict["message"] == "Test message" + assert "ts" in log_dict + assert log_dict["ts"].endswith("Z") + + def test_severity_mapping(self): + from core.logging.structured_formatter import StructuredJSONFormatter + + formatter = StructuredJSONFormatter() + + test_cases = [ + (logging.DEBUG, "DEBUG"), + (logging.INFO, "INFO"), + (logging.WARNING, "WARN"), + (logging.ERROR, "ERROR"), + (logging.CRITICAL, "ERROR"), + ] + + for level, expected_severity in test_cases: + record = logging.LogRecord( + name="test", + level=level, + pathname="test.py", + lineno=1, + msg="Test", + args=(), + exc_info=None, + ) + output = formatter.format(record) + log_dict = orjson.loads(output) + assert log_dict["severity"] == expected_severity, f"Level {level} should map to {expected_severity}" + + def test_error_with_stack_trace(self): + from core.logging.structured_formatter import StructuredJSONFormatter + + formatter = StructuredJSONFormatter() + + try: + raise ValueError("Test error") + except ValueError: + exc_info = sys.exc_info() + + record = logging.LogRecord( + name="test", + level=logging.ERROR, + pathname="test.py", + lineno=10, + msg="Error occurred", + args=(), + exc_info=exc_info, + ) + + output = formatter.format(record) + log_dict = orjson.loads(output) + + assert log_dict["severity"] == "ERROR" + assert "stack_trace" in log_dict + assert "ValueError: Test error" in log_dict["stack_trace"] + + def test_no_stack_trace_for_info(self): + from core.logging.structured_formatter import StructuredJSONFormatter + + formatter = StructuredJSONFormatter() + + try: + raise ValueError("Test error") + except ValueError: + exc_info = sys.exc_info() + + record = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="test.py", + lineno=10, + msg="Info message", + args=(), + exc_info=exc_info, + ) + + output = formatter.format(record) + log_dict = orjson.loads(output) + + assert "stack_trace" not in log_dict + + def test_trace_context_included(self): + from core.logging.structured_formatter import StructuredJSONFormatter + + formatter = StructuredJSONFormatter() + record = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="test.py", + lineno=1, + msg="Test", + args=(), + exc_info=None, + ) + record.trace_id = "5b8aa5a2d2c872e8321cf37308d69df2" + record.span_id = "051581bf3bb55c45" + + output = formatter.format(record) + log_dict = orjson.loads(output) + + assert log_dict["trace_id"] == "5b8aa5a2d2c872e8321cf37308d69df2" + assert log_dict["span_id"] == "051581bf3bb55c45" + + def test_identity_context_included(self): + from core.logging.structured_formatter import StructuredJSONFormatter + + formatter = StructuredJSONFormatter() + record = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="test.py", + lineno=1, + msg="Test", + args=(), + exc_info=None, + ) + record.tenant_id = "t-global-corp" + record.user_id = "u-admin-007" + record.user_type = "admin" + + output = formatter.format(record) + log_dict = orjson.loads(output) + + assert "identity" in log_dict + assert log_dict["identity"]["tenant_id"] == "t-global-corp" + assert log_dict["identity"]["user_id"] == "u-admin-007" + assert log_dict["identity"]["user_type"] == "admin" + + def test_no_identity_when_empty(self): + from core.logging.structured_formatter import StructuredJSONFormatter + + formatter = StructuredJSONFormatter() + record = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="test.py", + lineno=1, + msg="Test", + args=(), + exc_info=None, + ) + + output = formatter.format(record) + log_dict = orjson.loads(output) + + assert "identity" not in log_dict + + def test_attributes_included(self): + from core.logging.structured_formatter import StructuredJSONFormatter + + formatter = StructuredJSONFormatter() + record = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="test.py", + lineno=1, + msg="Test", + args=(), + exc_info=None, + ) + record.attributes = {"order_id": "ord-123", "amount": 99.99} + + output = formatter.format(record) + log_dict = orjson.loads(output) + + assert log_dict["attributes"]["order_id"] == "ord-123" + assert log_dict["attributes"]["amount"] == 99.99 + + def test_message_with_args(self): + from core.logging.structured_formatter import StructuredJSONFormatter + + formatter = StructuredJSONFormatter() + record = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="test.py", + lineno=1, + msg="User %s logged in from %s", + args=("john", "192.168.1.1"), + exc_info=None, + ) + + output = formatter.format(record) + log_dict = orjson.loads(output) + + assert log_dict["message"] == "User john logged in from 192.168.1.1" + + def test_timestamp_format(self): + from core.logging.structured_formatter import StructuredJSONFormatter + + formatter = StructuredJSONFormatter() + record = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="test.py", + lineno=1, + msg="Test", + args=(), + exc_info=None, + ) + + output = formatter.format(record) + log_dict = orjson.loads(output) + + # Verify ISO 8601 format with Z suffix + ts = log_dict["ts"] + assert ts.endswith("Z") + assert "T" in ts + # Should have milliseconds + assert "." in ts + + def test_fallback_for_non_serializable_attributes(self): + from core.logging.structured_formatter import StructuredJSONFormatter + + formatter = StructuredJSONFormatter() + record = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="test.py", + lineno=1, + msg="Test with non-serializable", + args=(), + exc_info=None, + ) + # Set is not serializable by orjson + record.attributes = {"items": {1, 2, 3}, "custom": object()} + + # Should not raise, fallback to json.dumps with default=str + output = formatter.format(record) + + # Verify it's valid JSON (parsed by stdlib json since orjson may fail) + import json + + log_dict = json.loads(output) + assert log_dict["message"] == "Test with non-serializable" + assert "attributes" in log_dict diff --git a/api/tests/unit_tests/core/logging/test_trace_helpers.py b/api/tests/unit_tests/core/logging/test_trace_helpers.py new file mode 100644 index 0000000000..aab1753b9b --- /dev/null +++ b/api/tests/unit_tests/core/logging/test_trace_helpers.py @@ -0,0 +1,102 @@ +"""Tests for trace helper functions.""" + +import re +from unittest import mock + + +class TestGetSpanIdFromOtelContext: + def test_returns_none_without_span(self): + from core.helper.trace_id_helper import get_span_id_from_otel_context + + with mock.patch("opentelemetry.trace.get_current_span", return_value=None): + result = get_span_id_from_otel_context() + assert result is None + + def test_returns_span_id_when_available(self): + from core.helper.trace_id_helper import get_span_id_from_otel_context + + mock_span = mock.MagicMock() + mock_context = mock.MagicMock() + mock_context.span_id = 0x051581BF3BB55C45 + mock_span.get_span_context.return_value = mock_context + + with mock.patch("opentelemetry.trace.get_current_span", return_value=mock_span): + with mock.patch("opentelemetry.trace.span.INVALID_SPAN_ID", 0): + result = get_span_id_from_otel_context() + assert result == "051581bf3bb55c45" + + def test_returns_none_on_exception(self): + from core.helper.trace_id_helper import get_span_id_from_otel_context + + with mock.patch("opentelemetry.trace.get_current_span", side_effect=Exception("Test error")): + result = get_span_id_from_otel_context() + assert result is None + + +class TestGenerateTraceparentHeader: + def test_generates_valid_format(self): + from core.helper.trace_id_helper import generate_traceparent_header + + with mock.patch("opentelemetry.trace.get_current_span", return_value=None): + result = generate_traceparent_header() + + assert result is not None + # Format: 00-{trace_id}-{span_id}-01 + parts = result.split("-") + assert len(parts) == 4 + assert parts[0] == "00" # version + assert len(parts[1]) == 32 # trace_id (32 hex chars) + assert len(parts[2]) == 16 # span_id (16 hex chars) + assert parts[3] == "01" # flags + + def test_uses_otel_context_when_available(self): + from core.helper.trace_id_helper import generate_traceparent_header + + mock_span = mock.MagicMock() + mock_context = mock.MagicMock() + mock_context.trace_id = 0x5B8AA5A2D2C872E8321CF37308D69DF2 + mock_context.span_id = 0x051581BF3BB55C45 + mock_span.get_span_context.return_value = mock_context + + with mock.patch("opentelemetry.trace.get_current_span", return_value=mock_span): + with ( + mock.patch("opentelemetry.trace.span.INVALID_TRACE_ID", 0), + mock.patch("opentelemetry.trace.span.INVALID_SPAN_ID", 0), + ): + result = generate_traceparent_header() + + assert result == "00-5b8aa5a2d2c872e8321cf37308d69df2-051581bf3bb55c45-01" + + def test_generates_hex_only_values(self): + from core.helper.trace_id_helper import generate_traceparent_header + + with mock.patch("opentelemetry.trace.get_current_span", return_value=None): + result = generate_traceparent_header() + + parts = result.split("-") + # All parts should be valid hex + assert re.match(r"^[0-9a-f]+$", parts[1]) + assert re.match(r"^[0-9a-f]+$", parts[2]) + + +class TestParseTraceparentHeader: + def test_parses_valid_traceparent(self): + from core.helper.trace_id_helper import parse_traceparent_header + + traceparent = "00-5b8aa5a2d2c872e8321cf37308d69df2-051581bf3bb55c45-01" + result = parse_traceparent_header(traceparent) + + assert result == "5b8aa5a2d2c872e8321cf37308d69df2" + + def test_returns_none_for_invalid_format(self): + from core.helper.trace_id_helper import parse_traceparent_header + + # Wrong number of parts + assert parse_traceparent_header("00-abc-def") is None + # Wrong trace_id length + assert parse_traceparent_header("00-abc-def-01") is None + + def test_returns_none_for_empty_string(self): + from core.helper.trace_id_helper import parse_traceparent_header + + assert parse_traceparent_header("") is None diff --git a/api/tests/unit_tests/libs/test_external_api.py b/api/tests/unit_tests/libs/test_external_api.py index 9aa157a651..5135970bcc 100644 --- a/api/tests/unit_tests/libs/test_external_api.py +++ b/api/tests/unit_tests/libs/test_external_api.py @@ -99,29 +99,20 @@ def test_external_api_json_message_and_bad_request_rewrite(): assert res.get_json()["message"] == "Invalid JSON payload received or JSON payload is empty." -def test_external_api_param_mapping_and_quota_and_exc_info_none(): - # Force exc_info() to return (None,None,None) only during request - import libs.external_api as ext +def test_external_api_param_mapping_and_quota(): + app = _create_api_app() + client = app.test_client() - orig_exc_info = ext.sys.exc_info - try: - ext.sys.exc_info = lambda: (None, None, None) + # Param errors mapping payload path + res = client.get("/api/param-errors") + assert res.status_code == 400 + data = res.get_json() + assert data["code"] == "invalid_param" + assert data["params"] == "field" - app = _create_api_app() - client = app.test_client() - - # Param errors mapping payload path - res = client.get("/api/param-errors") - assert res.status_code == 400 - data = res.get_json() - assert data["code"] == "invalid_param" - assert data["params"] == "field" - - # Quota path — depending on Flask-RESTX internals it may be handled - res = client.get("/api/quota") - assert res.status_code in (400, 429) - finally: - ext.sys.exc_info = orig_exc_info # type: ignore[assignment] + # Quota path — depending on Flask-RESTX internals it may be handled + res = client.get("/api/quota") + assert res.status_code in (400, 429) def test_unauthorized_and_force_logout_clears_cookies(): diff --git a/docker/.env.example b/docker/.env.example index 5c1089408c..c3feccb102 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -69,6 +69,8 @@ PYTHONIOENCODING=utf-8 # The log level for the application. # Supported values are `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` LOG_LEVEL=INFO +# Log output format: text or json +LOG_OUTPUT_FORMAT=text # Log file path LOG_FILE=/app/logs/server.log # Log file max size, the unit is MB diff --git a/docker/docker-compose.middleware.yaml b/docker/docker-compose.middleware.yaml index dba61d1816..81c34fc6a2 100644 --- a/docker/docker-compose.middleware.yaml +++ b/docker/docker-compose.middleware.yaml @@ -129,6 +129,7 @@ services: - ./middleware.env environment: # Use the shared environment variables. + LOG_OUTPUT_FORMAT: ${LOG_OUTPUT_FORMAT:-text} DB_DATABASE: ${DB_PLUGIN_DATABASE:-dify_plugin} REDIS_HOST: ${REDIS_HOST:-redis} REDIS_PORT: ${REDIS_PORT:-6379} diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 9910c95a84..a67141ce05 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -17,6 +17,7 @@ x-shared-env: &shared-api-worker-env LC_ALL: ${LC_ALL:-en_US.UTF-8} PYTHONIOENCODING: ${PYTHONIOENCODING:-utf-8} LOG_LEVEL: ${LOG_LEVEL:-INFO} + LOG_OUTPUT_FORMAT: ${LOG_OUTPUT_FORMAT:-text} LOG_FILE: ${LOG_FILE:-/app/logs/server.log} LOG_FILE_MAX_SIZE: ${LOG_FILE_MAX_SIZE:-20} LOG_FILE_BACKUP_COUNT: ${LOG_FILE_BACKUP_COUNT:-5} From e34513cbc405635825cf18c6aebef7b82ed13bf4 Mon Sep 17 00:00:00 2001 From: CodingOnStar Date: Sun, 4 Jan 2026 12:03:02 +0800 Subject: [PATCH 35/37] fix: correct translation errors in Japanese and Chinese JSON files, and improve type safety in quota panel component --- .../provider-added-card/quota-panel.tsx | 7 ++++--- web/i18n/ja-JP/common.json | 2 +- web/i18n/zh-Hans/billing.json | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx index abb129c600..e296bc4555 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx @@ -1,5 +1,6 @@ import type { FC } from 'react' import type { ModelProvider } from '../declarations' +import type { Plugin } from '@/app/components/plugins/types' import { useBoolean } from 'ahooks' import * as React from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -46,7 +47,7 @@ const QuotaPanel: FC = ({ }) => { const { t } = useTranslation() const { currentWorkspace } = useAppContext() - const credits = Math.max(currentWorkspace.trial_credits - currentWorkspace.trial_credits_used || 0, 0) + const credits = Math.max((currentWorkspace.trial_credits - currentWorkspace.trial_credits_used) || 0, 0) const providerMap = useMemo(() => new Map( providers.map(p => [p.provider, p.preferred_provider_type]), ), [providers]) @@ -54,7 +55,7 @@ const QuotaPanel: FC = ({ const { plugins: allPlugins, } = useMarketplaceAllPlugins(providers, '') - const [selectedPlugin, setSelectedPlugin] = useState(null) + const [selectedPlugin, setSelectedPlugin] = useState(null) const [isShowInstallModal, { setTrue: showInstallFromMarketplace, setFalse: hideInstallFromMarketplace, @@ -147,7 +148,7 @@ const QuotaPanel: FC = ({ })}
- {isShowInstallModal && ( + {isShowInstallModal && selectedPlugin && ( Date: Sun, 4 Jan 2026 15:12:14 +0800 Subject: [PATCH 36/37] feat: conditionally render components based on cloud edition status in model provider page and credential panel --- .../header/account-setting/model-provider-page/index.tsx | 3 ++- .../provider-added-card/credential-panel.tsx | 5 +++-- .../model-provider-page/provider-added-card/index.tsx | 6 +++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/web/app/components/header/account-setting/model-provider-page/index.tsx b/web/app/components/header/account-setting/model-provider-page/index.tsx index 81d4b03136..57b464e0e7 100644 --- a/web/app/components/header/account-setting/model-provider-page/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/index.tsx @@ -8,6 +8,7 @@ import { import { useDebounce } from 'ahooks' import { useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' +import { IS_CLOUD_EDITION } from '@/config' import { useAppContext } from '@/context/app-context' import { useGlobalPublicStore } from '@/context/global-public-context' import { useProviderContext } from '@/context/provider-context' @@ -117,7 +118,7 @@ const ModelProviderPage = ({ searchText }: Props) => { />
- + {IS_CLOUD_EDITION && } {!filteredConfiguredProviders?.length && (
diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx index 59d7b2c0c8..cbaef21a70 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx @@ -7,6 +7,7 @@ import { useToastContext } from '@/app/components/base/toast' import { ConfigProvider } from '@/app/components/header/account-setting/model-provider-page/model-auth' import { useCredentialStatus } from '@/app/components/header/account-setting/model-provider-page/model-auth/hooks' import Indicator from '@/app/components/header/indicator' +import { IS_CLOUD_EDITION } from '@/config' import { useEventEmitterContextContext } from '@/context/event-emitter' import { changeModelProviderPriority } from '@/service/common' import { cn } from '@/utils/classnames' @@ -114,7 +115,7 @@ const CredentialPanel = ({ provider={provider} /> { - systemConfig.enabled && isCustomConfigured && ( + systemConfig.enabled && isCustomConfigured && IS_CLOUD_EDITION && ( = ({ const systemConfig = provider.system_configuration const hasModelList = fetched && !!modelList.length const { isCurrentWorkspaceManager } = useAppContext() - const showQuota = systemConfig.enabled && [...MODEL_PROVIDER_QUOTA_GET_PAID].includes(provider.provider as ModelProviderQuotaGetPaid) && !IS_CE_EDITION + const showModelProvider = systemConfig.enabled && [...MODEL_PROVIDER_QUOTA_GET_PAID].includes(provider.provider as ModelProviderQuotaGetPaid) && !IS_CE_EDITION const showCredential = configurationMethods.includes(ConfigurationMethodEnum.predefinedModel) && isCurrentWorkspaceManager const getModelList = async (providerName: string) => { @@ -115,7 +115,7 @@ const ProviderAddedCard: FC = ({ { collapsed && (
- {(showQuota || !notConfigured) && ( + {(showModelProvider || !notConfigured) && ( <>
{ @@ -143,7 +143,7 @@ const ProviderAddedCard: FC = ({
)} - {!showQuota && notConfigured && ( + {!showModelProvider && notConfigured && (
{t('modelProvider.configureTip', { ns: 'common' })} From 88c3286bd45935c2b164238d278a179d2e0dab3b Mon Sep 17 00:00:00 2001 From: CodingOnStar Date: Tue, 6 Jan 2026 11:05:03 +0800 Subject: [PATCH 37/37] fix: correct capitalization of 'TONGYI' in model name mapping --- .../header/account-setting/model-provider-page/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/components/header/account-setting/model-provider-page/utils.ts b/web/app/components/header/account-setting/model-provider-page/utils.ts index d958f3eef3..7cfa7fc654 100644 --- a/web/app/components/header/account-setting/model-provider-page/utils.ts +++ b/web/app/components/header/account-setting/model-provider-page/utils.ts @@ -34,7 +34,7 @@ export const modelNameMap = { [ModelProviderQuotaGetPaid.GEMINI]: 'Gemini', [ModelProviderQuotaGetPaid.X]: 'xAI', [ModelProviderQuotaGetPaid.DEEPSEEK]: 'DeepSeek', - [ModelProviderQuotaGetPaid.TONGYI]: 'TONGYI', + [ModelProviderQuotaGetPaid.TONGYI]: 'Tongyi', } export const isNullOrUndefined = (value: any) => {