-
+
>
)}
From c7382150b59e523b3fac5095f9cb89b124fecf29 Mon Sep 17 00:00:00 2001
From: Jason Young <44939412+farion1231@users.noreply.github.com>
Date: Mon, 21 Jul 2025 15:58:36 +0800
Subject: [PATCH 15/18] test: add comprehensive unit tests for Firecrawl and
Watercrawl auth providers (#22705)
---
.../services/auth/test_firecrawl_auth.py | 191 ++++++++++++++++
.../services/auth/test_watercrawl_auth.py | 205 ++++++++++++++++++
2 files changed, 396 insertions(+)
create mode 100644 api/tests/unit_tests/services/auth/test_firecrawl_auth.py
create mode 100644 api/tests/unit_tests/services/auth/test_watercrawl_auth.py
diff --git a/api/tests/unit_tests/services/auth/test_firecrawl_auth.py b/api/tests/unit_tests/services/auth/test_firecrawl_auth.py
new file mode 100644
index 0000000000..ffdf5897ed
--- /dev/null
+++ b/api/tests/unit_tests/services/auth/test_firecrawl_auth.py
@@ -0,0 +1,191 @@
+from unittest.mock import MagicMock, patch
+
+import pytest
+import requests
+
+from services.auth.firecrawl.firecrawl import FirecrawlAuth
+
+
+class TestFirecrawlAuth:
+ @pytest.fixture
+ def valid_credentials(self):
+ """Fixture for valid bearer credentials"""
+ return {"auth_type": "bearer", "config": {"api_key": "test_api_key_123"}}
+
+ @pytest.fixture
+ def auth_instance(self, valid_credentials):
+ """Fixture for FirecrawlAuth instance with valid credentials"""
+ return FirecrawlAuth(valid_credentials)
+
+ def test_should_initialize_with_valid_bearer_credentials(self, valid_credentials):
+ """Test successful initialization with valid bearer credentials"""
+ auth = FirecrawlAuth(valid_credentials)
+ assert auth.api_key == "test_api_key_123"
+ assert auth.base_url == "https://api.firecrawl.dev"
+ assert auth.credentials == valid_credentials
+
+ def test_should_initialize_with_custom_base_url(self):
+ """Test initialization with custom base URL"""
+ credentials = {
+ "auth_type": "bearer",
+ "config": {"api_key": "test_api_key_123", "base_url": "https://custom.firecrawl.dev"},
+ }
+ auth = FirecrawlAuth(credentials)
+ assert auth.api_key == "test_api_key_123"
+ assert auth.base_url == "https://custom.firecrawl.dev"
+
+ @pytest.mark.parametrize(
+ ("auth_type", "expected_error"),
+ [
+ ("basic", "Invalid auth type, Firecrawl auth type must be Bearer"),
+ ("x-api-key", "Invalid auth type, Firecrawl auth type must be Bearer"),
+ ("", "Invalid auth type, Firecrawl auth type must be Bearer"),
+ ],
+ )
+ def test_should_raise_error_for_invalid_auth_type(self, auth_type, expected_error):
+ """Test that non-bearer auth types raise ValueError"""
+ credentials = {"auth_type": auth_type, "config": {"api_key": "test_api_key_123"}}
+ with pytest.raises(ValueError) as exc_info:
+ FirecrawlAuth(credentials)
+ assert str(exc_info.value) == expected_error
+
+ @pytest.mark.parametrize(
+ ("credentials", "expected_error"),
+ [
+ ({"auth_type": "bearer", "config": {}}, "No API key provided"),
+ ({"auth_type": "bearer"}, "No API key provided"),
+ ({"auth_type": "bearer", "config": {"api_key": ""}}, "No API key provided"),
+ ({"auth_type": "bearer", "config": {"api_key": None}}, "No API key provided"),
+ ],
+ )
+ def test_should_raise_error_for_missing_api_key(self, credentials, expected_error):
+ """Test that missing or empty API key raises ValueError"""
+ with pytest.raises(ValueError) as exc_info:
+ FirecrawlAuth(credentials)
+ assert str(exc_info.value) == expected_error
+
+ @patch("services.auth.firecrawl.firecrawl.requests.post")
+ def test_should_validate_valid_credentials_successfully(self, mock_post, auth_instance):
+ """Test successful credential validation"""
+ mock_response = MagicMock()
+ mock_response.status_code = 200
+ mock_post.return_value = mock_response
+
+ result = auth_instance.validate_credentials()
+
+ assert result is True
+ expected_data = {
+ "url": "https://example.com",
+ "includePaths": [],
+ "excludePaths": [],
+ "limit": 1,
+ "scrapeOptions": {"onlyMainContent": True},
+ }
+ mock_post.assert_called_once_with(
+ "https://api.firecrawl.dev/v1/crawl",
+ headers={"Content-Type": "application/json", "Authorization": "Bearer test_api_key_123"},
+ json=expected_data,
+ )
+
+ @pytest.mark.parametrize(
+ ("status_code", "error_message"),
+ [
+ (402, "Payment required"),
+ (409, "Conflict error"),
+ (500, "Internal server error"),
+ ],
+ )
+ @patch("services.auth.firecrawl.firecrawl.requests.post")
+ def test_should_handle_http_errors(self, mock_post, status_code, error_message, auth_instance):
+ """Test handling of various HTTP error codes"""
+ mock_response = MagicMock()
+ mock_response.status_code = status_code
+ mock_response.json.return_value = {"error": error_message}
+ mock_post.return_value = mock_response
+
+ with pytest.raises(Exception) as exc_info:
+ auth_instance.validate_credentials()
+ assert str(exc_info.value) == f"Failed to authorize. Status code: {status_code}. Error: {error_message}"
+
+ @pytest.mark.parametrize(
+ ("status_code", "response_text", "has_json_error", "expected_error_contains"),
+ [
+ (403, '{"error": "Forbidden"}', True, "Failed to authorize. Status code: 403. Error: Forbidden"),
+ (404, "", True, "Unexpected error occurred while trying to authorize. Status code: 404"),
+ (401, "Not JSON", True, "Expecting value"), # JSON decode error
+ ],
+ )
+ @patch("services.auth.firecrawl.firecrawl.requests.post")
+ def test_should_handle_unexpected_errors(
+ self, mock_post, status_code, response_text, has_json_error, expected_error_contains, auth_instance
+ ):
+ """Test handling of unexpected errors with various response formats"""
+ mock_response = MagicMock()
+ mock_response.status_code = status_code
+ mock_response.text = response_text
+ if has_json_error:
+ mock_response.json.side_effect = Exception("Not JSON")
+ mock_post.return_value = mock_response
+
+ with pytest.raises(Exception) as exc_info:
+ auth_instance.validate_credentials()
+ assert expected_error_contains in str(exc_info.value)
+
+ @pytest.mark.parametrize(
+ ("exception_type", "exception_message"),
+ [
+ (requests.ConnectionError, "Network error"),
+ (requests.Timeout, "Request timeout"),
+ (requests.ReadTimeout, "Read timeout"),
+ (requests.ConnectTimeout, "Connection timeout"),
+ ],
+ )
+ @patch("services.auth.firecrawl.firecrawl.requests.post")
+ def test_should_handle_network_errors(self, mock_post, exception_type, exception_message, auth_instance):
+ """Test handling of various network-related errors including timeouts"""
+ mock_post.side_effect = exception_type(exception_message)
+
+ with pytest.raises(exception_type) as exc_info:
+ auth_instance.validate_credentials()
+ assert exception_message in str(exc_info.value)
+
+ def test_should_not_expose_api_key_in_error_messages(self):
+ """Test that API key is not exposed in error messages"""
+ credentials = {"auth_type": "bearer", "config": {"api_key": "super_secret_key_12345"}}
+ auth = FirecrawlAuth(credentials)
+
+ # Verify API key is stored but not in any error message
+ assert auth.api_key == "super_secret_key_12345"
+
+ # Test various error scenarios don't expose the key
+ with pytest.raises(ValueError) as exc_info:
+ FirecrawlAuth({"auth_type": "basic", "config": {"api_key": "super_secret_key_12345"}})
+ assert "super_secret_key_12345" not in str(exc_info.value)
+
+ @patch("services.auth.firecrawl.firecrawl.requests.post")
+ def test_should_use_custom_base_url_in_validation(self, mock_post):
+ """Test that custom base URL is used in validation"""
+ mock_response = MagicMock()
+ mock_response.status_code = 200
+ mock_post.return_value = mock_response
+
+ credentials = {
+ "auth_type": "bearer",
+ "config": {"api_key": "test_api_key_123", "base_url": "https://custom.firecrawl.dev"},
+ }
+ auth = FirecrawlAuth(credentials)
+ result = auth.validate_credentials()
+
+ assert result is True
+ assert mock_post.call_args[0][0] == "https://custom.firecrawl.dev/v1/crawl"
+
+ @patch("services.auth.firecrawl.firecrawl.requests.post")
+ def test_should_handle_timeout_with_retry_suggestion(self, mock_post, auth_instance):
+ """Test that timeout errors are handled gracefully with appropriate error message"""
+ mock_post.side_effect = requests.Timeout("The request timed out after 30 seconds")
+
+ with pytest.raises(requests.Timeout) as exc_info:
+ auth_instance.validate_credentials()
+
+ # Verify the timeout exception is raised with original message
+ assert "timed out" in str(exc_info.value)
diff --git a/api/tests/unit_tests/services/auth/test_watercrawl_auth.py b/api/tests/unit_tests/services/auth/test_watercrawl_auth.py
new file mode 100644
index 0000000000..bacf0b24ea
--- /dev/null
+++ b/api/tests/unit_tests/services/auth/test_watercrawl_auth.py
@@ -0,0 +1,205 @@
+from unittest.mock import MagicMock, patch
+
+import pytest
+import requests
+
+from services.auth.watercrawl.watercrawl import WatercrawlAuth
+
+
+class TestWatercrawlAuth:
+ @pytest.fixture
+ def valid_credentials(self):
+ """Fixture for valid x-api-key credentials"""
+ return {"auth_type": "x-api-key", "config": {"api_key": "test_api_key_123"}}
+
+ @pytest.fixture
+ def auth_instance(self, valid_credentials):
+ """Fixture for WatercrawlAuth instance with valid credentials"""
+ return WatercrawlAuth(valid_credentials)
+
+ def test_should_initialize_with_valid_x_api_key_credentials(self, valid_credentials):
+ """Test successful initialization with valid x-api-key credentials"""
+ auth = WatercrawlAuth(valid_credentials)
+ assert auth.api_key == "test_api_key_123"
+ assert auth.base_url == "https://app.watercrawl.dev"
+ assert auth.credentials == valid_credentials
+
+ def test_should_initialize_with_custom_base_url(self):
+ """Test initialization with custom base URL"""
+ credentials = {
+ "auth_type": "x-api-key",
+ "config": {"api_key": "test_api_key_123", "base_url": "https://custom.watercrawl.dev"},
+ }
+ auth = WatercrawlAuth(credentials)
+ assert auth.api_key == "test_api_key_123"
+ assert auth.base_url == "https://custom.watercrawl.dev"
+
+ @pytest.mark.parametrize(
+ ("auth_type", "expected_error"),
+ [
+ ("bearer", "Invalid auth type, WaterCrawl auth type must be x-api-key"),
+ ("basic", "Invalid auth type, WaterCrawl auth type must be x-api-key"),
+ ("", "Invalid auth type, WaterCrawl auth type must be x-api-key"),
+ ],
+ )
+ def test_should_raise_error_for_invalid_auth_type(self, auth_type, expected_error):
+ """Test that non-x-api-key auth types raise ValueError"""
+ credentials = {"auth_type": auth_type, "config": {"api_key": "test_api_key_123"}}
+ with pytest.raises(ValueError) as exc_info:
+ WatercrawlAuth(credentials)
+ assert str(exc_info.value) == expected_error
+
+ @pytest.mark.parametrize(
+ ("credentials", "expected_error"),
+ [
+ ({"auth_type": "x-api-key", "config": {}}, "No API key provided"),
+ ({"auth_type": "x-api-key"}, "No API key provided"),
+ ({"auth_type": "x-api-key", "config": {"api_key": ""}}, "No API key provided"),
+ ({"auth_type": "x-api-key", "config": {"api_key": None}}, "No API key provided"),
+ ],
+ )
+ def test_should_raise_error_for_missing_api_key(self, credentials, expected_error):
+ """Test that missing or empty API key raises ValueError"""
+ with pytest.raises(ValueError) as exc_info:
+ WatercrawlAuth(credentials)
+ assert str(exc_info.value) == expected_error
+
+ @patch("services.auth.watercrawl.watercrawl.requests.get")
+ def test_should_validate_valid_credentials_successfully(self, mock_get, auth_instance):
+ """Test successful credential validation"""
+ mock_response = MagicMock()
+ mock_response.status_code = 200
+ mock_get.return_value = mock_response
+
+ result = auth_instance.validate_credentials()
+
+ assert result is True
+ mock_get.assert_called_once_with(
+ "https://app.watercrawl.dev/api/v1/core/crawl-requests/",
+ headers={"Content-Type": "application/json", "X-API-KEY": "test_api_key_123"},
+ )
+
+ @pytest.mark.parametrize(
+ ("status_code", "error_message"),
+ [
+ (402, "Payment required"),
+ (409, "Conflict error"),
+ (500, "Internal server error"),
+ ],
+ )
+ @patch("services.auth.watercrawl.watercrawl.requests.get")
+ def test_should_handle_http_errors(self, mock_get, status_code, error_message, auth_instance):
+ """Test handling of various HTTP error codes"""
+ mock_response = MagicMock()
+ mock_response.status_code = status_code
+ mock_response.json.return_value = {"error": error_message}
+ mock_get.return_value = mock_response
+
+ with pytest.raises(Exception) as exc_info:
+ auth_instance.validate_credentials()
+ assert str(exc_info.value) == f"Failed to authorize. Status code: {status_code}. Error: {error_message}"
+
+ @pytest.mark.parametrize(
+ ("status_code", "response_text", "has_json_error", "expected_error_contains"),
+ [
+ (403, '{"error": "Forbidden"}', True, "Failed to authorize. Status code: 403. Error: Forbidden"),
+ (404, "", True, "Unexpected error occurred while trying to authorize. Status code: 404"),
+ (401, "Not JSON", True, "Expecting value"), # JSON decode error
+ ],
+ )
+ @patch("services.auth.watercrawl.watercrawl.requests.get")
+ def test_should_handle_unexpected_errors(
+ self, mock_get, status_code, response_text, has_json_error, expected_error_contains, auth_instance
+ ):
+ """Test handling of unexpected errors with various response formats"""
+ mock_response = MagicMock()
+ mock_response.status_code = status_code
+ mock_response.text = response_text
+ if has_json_error:
+ mock_response.json.side_effect = Exception("Not JSON")
+ mock_get.return_value = mock_response
+
+ with pytest.raises(Exception) as exc_info:
+ auth_instance.validate_credentials()
+ assert expected_error_contains in str(exc_info.value)
+
+ @pytest.mark.parametrize(
+ ("exception_type", "exception_message"),
+ [
+ (requests.ConnectionError, "Network error"),
+ (requests.Timeout, "Request timeout"),
+ (requests.ReadTimeout, "Read timeout"),
+ (requests.ConnectTimeout, "Connection timeout"),
+ ],
+ )
+ @patch("services.auth.watercrawl.watercrawl.requests.get")
+ def test_should_handle_network_errors(self, mock_get, exception_type, exception_message, auth_instance):
+ """Test handling of various network-related errors including timeouts"""
+ mock_get.side_effect = exception_type(exception_message)
+
+ with pytest.raises(exception_type) as exc_info:
+ auth_instance.validate_credentials()
+ assert exception_message in str(exc_info.value)
+
+ def test_should_not_expose_api_key_in_error_messages(self):
+ """Test that API key is not exposed in error messages"""
+ credentials = {"auth_type": "x-api-key", "config": {"api_key": "super_secret_key_12345"}}
+ auth = WatercrawlAuth(credentials)
+
+ # Verify API key is stored but not in any error message
+ assert auth.api_key == "super_secret_key_12345"
+
+ # Test various error scenarios don't expose the key
+ with pytest.raises(ValueError) as exc_info:
+ WatercrawlAuth({"auth_type": "bearer", "config": {"api_key": "super_secret_key_12345"}})
+ assert "super_secret_key_12345" not in str(exc_info.value)
+
+ @patch("services.auth.watercrawl.watercrawl.requests.get")
+ def test_should_use_custom_base_url_in_validation(self, mock_get):
+ """Test that custom base URL is used in validation"""
+ mock_response = MagicMock()
+ mock_response.status_code = 200
+ mock_get.return_value = mock_response
+
+ credentials = {
+ "auth_type": "x-api-key",
+ "config": {"api_key": "test_api_key_123", "base_url": "https://custom.watercrawl.dev"},
+ }
+ auth = WatercrawlAuth(credentials)
+ result = auth.validate_credentials()
+
+ assert result is True
+ assert mock_get.call_args[0][0] == "https://custom.watercrawl.dev/api/v1/core/crawl-requests/"
+
+ @pytest.mark.parametrize(
+ ("base_url", "expected_url"),
+ [
+ ("https://app.watercrawl.dev", "https://app.watercrawl.dev/api/v1/core/crawl-requests/"),
+ ("https://app.watercrawl.dev/", "https://app.watercrawl.dev/api/v1/core/crawl-requests/"),
+ ("https://app.watercrawl.dev//", "https://app.watercrawl.dev/api/v1/core/crawl-requests/"),
+ ],
+ )
+ @patch("services.auth.watercrawl.watercrawl.requests.get")
+ def test_should_use_urljoin_for_url_construction(self, mock_get, base_url, expected_url):
+ """Test that urljoin is used correctly for URL construction with various base URLs"""
+ mock_response = MagicMock()
+ mock_response.status_code = 200
+ mock_get.return_value = mock_response
+
+ credentials = {"auth_type": "x-api-key", "config": {"api_key": "test_api_key_123", "base_url": base_url}}
+ auth = WatercrawlAuth(credentials)
+ auth.validate_credentials()
+
+ # Verify the correct URL was called
+ assert mock_get.call_args[0][0] == expected_url
+
+ @patch("services.auth.watercrawl.watercrawl.requests.get")
+ def test_should_handle_timeout_with_retry_suggestion(self, mock_get, auth_instance):
+ """Test that timeout errors are handled gracefully with appropriate error message"""
+ mock_get.side_effect = requests.Timeout("The request timed out after 30 seconds")
+
+ with pytest.raises(requests.Timeout) as exc_info:
+ auth_instance.validate_credentials()
+
+ # Verify the timeout exception is raised with original message
+ assert "timed out" in str(exc_info.value)
From ab012fe1a2faef3a703323599c11a4bc960f7f46 Mon Sep 17 00:00:00 2001
From: uply23333
Date: Mon, 21 Jul 2025 15:59:37 +0800
Subject: [PATCH 16/18] fix: improve document filtering in full text
search(elasticsearch) (#22683)
---
.../vdb/elasticsearch/elasticsearch_vector.py | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/api/core/rag/datasource/vdb/elasticsearch/elasticsearch_vector.py b/api/core/rag/datasource/vdb/elasticsearch/elasticsearch_vector.py
index 44cc5d3e98..ad39717183 100644
--- a/api/core/rag/datasource/vdb/elasticsearch/elasticsearch_vector.py
+++ b/api/core/rag/datasource/vdb/elasticsearch/elasticsearch_vector.py
@@ -147,10 +147,17 @@ class ElasticSearchVector(BaseVector):
return docs
def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]:
- query_str = {"match": {Field.CONTENT_KEY.value: query}}
+ query_str: dict[str, Any] = {"match": {Field.CONTENT_KEY.value: query}}
document_ids_filter = kwargs.get("document_ids_filter")
+
if document_ids_filter:
- query_str["filter"] = {"terms": {"metadata.document_id": document_ids_filter}} # type: ignore
+ query_str = {
+ "bool": {
+ "must": {"match": {Field.CONTENT_KEY.value: query}},
+ "filter": {"terms": {"metadata.document_id": document_ids_filter}},
+ }
+ }
+
results = self._client.search(index=self._collection_name, query=query_str, size=kwargs.get("top_k", 4))
docs = []
for hit in results["hits"]["hits"]:
From a83e4ed9a41ed373361d0668ea2d9b4e0ac494d6 Mon Sep 17 00:00:00 2001
From: KVOJJJin
Date: Mon, 21 Jul 2025 16:35:52 +0800
Subject: [PATCH 17/18] Perf: remove user profile loading (#22710)
---
web/app/(commonLayout)/layout.tsx | 6 +-
web/app/account/account-page/index.tsx | 9 ++-
web/app/account/layout.tsx | 2 +-
web/app/components/app-sidebar/app-info.tsx | 16 ++---
.../components/app/create-app-modal/index.tsx | 8 +--
.../app/overview/embedded/index.tsx | 4 +-
web/app/components/apps/app-card.tsx | 21 ++-----
web/app/components/base/avatar/index.tsx | 2 +-
.../billing/apps-full-in-dialog/index.tsx | 4 +-
...ser-initor.tsx => browser-initializer.tsx} | 6 +-
.../components/header/account-about/index.tsx | 14 ++---
.../header/account-dropdown/index.tsx | 8 +--
.../header/account-dropdown/support.tsx | 4 +-
web/app/components/header/env-nav/index.tsx | 10 ++--
.../steps/install.tsx | 9 ++-
.../steps/install.tsx | 9 ++-
.../components/plugins/plugin-item/index.tsx | 8 +--
...ntry-initor.tsx => sentry-initializer.tsx} | 6 +-
.../{swr-initor.tsx => swr-initializer.tsx} | 8 +--
web/app/layout.tsx | 18 +++---
web/context/app-context.tsx | 58 +++++++------------
web/context/query-client.tsx | 2 +-
web/service/common.ts | 2 +-
23 files changed, 100 insertions(+), 134 deletions(-)
rename web/app/components/{browser-initor.tsx => browser-initializer.tsx} (88%)
rename web/app/components/{sentry-initor.tsx => sentry-initializer.tsx} (85%)
rename web/app/components/{swr-initor.tsx => swr-initializer.tsx} (95%)
diff --git a/web/app/(commonLayout)/layout.tsx b/web/app/(commonLayout)/layout.tsx
index d07e2a99d9..64186a1b10 100644
--- a/web/app/(commonLayout)/layout.tsx
+++ b/web/app/(commonLayout)/layout.tsx
@@ -1,6 +1,6 @@
import React from 'react'
import type { ReactNode } from 'react'
-import SwrInitor from '@/app/components/swr-initor'
+import SwrInitializer from '@/app/components/swr-initializer'
import { AppContextProvider } from '@/context/app-context'
import GA, { GaType } from '@/app/components/base/ga'
import HeaderWrapper from '@/app/components/header/header-wrapper'
@@ -13,7 +13,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
return (
<>
-
+
@@ -26,7 +26,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
-
+
>
)
}
diff --git a/web/app/account/account-page/index.tsx b/web/app/account/account-page/index.tsx
index 55fa2983dd..47b8f045d2 100644
--- a/web/app/account/account-page/index.tsx
+++ b/web/app/account/account-page/index.tsx
@@ -1,5 +1,6 @@
'use client'
import { useState } from 'react'
+import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import {
RiGraduationCapFill,
@@ -22,6 +23,8 @@ import PremiumBadge from '@/app/components/base/premium-badge'
import { useGlobalPublicStore } from '@/context/global-public-context'
import EmailChangeModal from './email-change-modal'
import { validPassword } from '@/config'
+import { fetchAppList } from '@/service/apps'
+import type { App } from '@/types/app'
const titleClassName = `
system-sm-semibold text-text-secondary
@@ -33,7 +36,9 @@ const descriptionClassName = `
export default function AccountPage() {
const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
- const { mutateUserProfile, userProfile, apps } = useAppContext()
+ const { data: appList } = useSWR({ url: '/apps', params: { page: 1, limit: 100, name: '' } }, fetchAppList)
+ const apps = appList?.data || []
+ const { mutateUserProfile, userProfile } = useAppContext()
const { isEducationAccount } = useProviderContext()
const { notify } = useContext(ToastContext)
const [editNameModalVisible, setEditNameModalVisible] = useState(false)
@@ -202,7 +207,7 @@ export default function AccountPage() {
{!!apps.length && (
({ ...app, key: app.id, name: app.name }))}
+ items={apps.map((app: App) => ({ ...app, key: app.id, name: app.name }))}
renderItem={renderAppItem}
wrapperClassName='mt-2'
/>
diff --git a/web/app/account/layout.tsx b/web/app/account/layout.tsx
index e74716fb3b..b3225b5341 100644
--- a/web/app/account/layout.tsx
+++ b/web/app/account/layout.tsx
@@ -1,7 +1,7 @@
import React from 'react'
import type { ReactNode } from 'react'
import Header from './header'
-import SwrInitor from '@/app/components/swr-initor'
+import SwrInitor from '@/app/components/swr-initializer'
import { AppContextProvider } from '@/context/app-context'
import GA, { GaType } from '@/app/components/base/ga'
import HeaderWrapper from '@/app/components/header/header-wrapper'
diff --git a/web/app/components/app-sidebar/app-info.tsx b/web/app/components/app-sidebar/app-info.tsx
index e85eaa2f53..c35047bbc5 100644
--- a/web/app/components/app-sidebar/app-info.tsx
+++ b/web/app/components/app-sidebar/app-info.tsx
@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next'
import { useRouter } from 'next/navigation'
-import { useContext, useContextSelector } from 'use-context-selector'
+import { useContext } from 'use-context-selector'
import React, { useCallback, useState } from 'react'
import {
RiDeleteBinLine,
@@ -15,7 +15,7 @@ import AppIcon from '../base/app-icon'
import cn from '@/utils/classnames'
import { useStore as useAppStore } from '@/app/components/app/store'
import { ToastContext } from '@/app/components/base/toast'
-import AppsContext, { useAppContext } from '@/context/app-context'
+import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
@@ -73,11 +73,6 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
const [showImportDSLModal, setShowImportDSLModal] = useState(false)
const [secretEnvList, setSecretEnvList] = useState([])
- const mutateApps = useContextSelector(
- AppsContext,
- state => state.mutateApps,
- )
-
const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({
name,
icon_type,
@@ -106,12 +101,11 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
message: t('app.editDone'),
})
setAppDetail(app)
- mutateApps()
}
catch {
notify({ type: 'error', message: t('app.editFailed') })
}
- }, [appDetail, mutateApps, notify, setAppDetail, t])
+ }, [appDetail, notify, setAppDetail, t])
const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon_type, icon, icon_background }) => {
if (!appDetail)
@@ -131,7 +125,6 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
message: t('app.newApp.appCreated'),
})
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
- mutateApps()
onPlanInfoChanged()
getRedirection(true, newApp, replace)
}
@@ -186,7 +179,6 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
try {
await deleteApp(appDetail.id)
notify({ type: 'success', message: t('app.appDeleted') })
- mutateApps()
onPlanInfoChanged()
setAppDetail()
replace('/apps')
@@ -198,7 +190,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
})
}
setShowConfirmDelete(false)
- }, [appDetail, mutateApps, notify, onPlanInfoChanged, replace, setAppDetail, t])
+ }, [appDetail, notify, onPlanInfoChanged, replace, setAppDetail, t])
const { isCurrentWorkspaceEditor } = useAppContext()
diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx
index f0a0da41a5..bdc839e848 100644
--- a/web/app/components/app/create-app-modal/index.tsx
+++ b/web/app/components/app/create-app-modal/index.tsx
@@ -4,7 +4,7 @@ import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useRouter } from 'next/navigation'
-import { useContext, useContextSelector } from 'use-context-selector'
+import { useContext } from 'use-context-selector'
import { RiArrowRightLine, RiArrowRightSLine, RiCommandLine, RiCornerDownLeftLine, RiExchange2Fill } from '@remixicon/react'
import Link from 'next/link'
import { useDebounceFn, useKeyPress } from 'ahooks'
@@ -15,7 +15,7 @@ import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import cn from '@/utils/classnames'
import { basePath } from '@/utils/var'
-import AppsContext, { useAppContext } from '@/context/app-context'
+import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { ToastContext } from '@/app/components/base/toast'
import type { AppMode } from '@/types/app'
@@ -41,7 +41,6 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate }: CreateAppProps)
const { t } = useTranslation()
const { push } = useRouter()
const { notify } = useContext(ToastContext)
- const mutateApps = useContextSelector(AppsContext, state => state.mutateApps)
const [appMode, setAppMode] = useState('advanced-chat')
const [appIcon, setAppIcon] = useState({ type: 'emoji', icon: '🤖', background: '#FFEAD5' })
@@ -80,7 +79,6 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate }: CreateAppProps)
notify({ type: 'success', message: t('app.newApp.appCreated') })
onSuccess()
onClose()
- mutateApps()
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
getRedirection(isCurrentWorkspaceEditor, app, push)
}
@@ -88,7 +86,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate }: CreateAppProps)
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
isCreatingRef.current = false
- }, [name, notify, t, appMode, appIcon, description, onSuccess, onClose, mutateApps, push, isCurrentWorkspaceEditor])
+ }, [name, notify, t, appMode, appIcon, description, onSuccess, onClose, push, isCurrentWorkspaceEditor])
const { run: handleCreateApp } = useDebounceFn(onCreate, { wait: 300 })
useKeyPress(['meta.enter', 'ctrl.enter'], () => {
diff --git a/web/app/components/app/overview/embedded/index.tsx b/web/app/components/app/overview/embedded/index.tsx
index b48eac5458..9d97eae38d 100644
--- a/web/app/components/app/overview/embedded/index.tsx
+++ b/web/app/components/app/overview/embedded/index.tsx
@@ -90,10 +90,10 @@ const Embedded = ({ siteInfo, isShow, onClose, appBaseUrl, accessToken, classNam
const [option, setOption] = useState