diff --git a/api/controllers/inner_api/plugin/wraps.py b/api/controllers/inner_api/plugin/wraps.py
index 709bba3f30..b2849a7962 100644
--- a/api/controllers/inner_api/plugin/wraps.py
+++ b/api/controllers/inner_api/plugin/wraps.py
@@ -2,12 +2,14 @@ from collections.abc import Callable
from functools import wraps
from typing import Optional
-from flask import request
+from flask import current_app, request
+from flask_login import user_logged_in
from flask_restful import reqparse
from pydantic import BaseModel
from sqlalchemy.orm import Session
from extensions.ext_database import db
+from libs.login import _get_user
from models.account import Account, Tenant
from models.model import EndUser
from services.account_service import AccountService
@@ -80,7 +82,12 @@ def get_user_tenant(view: Optional[Callable] = None):
raise ValueError("tenant not found")
kwargs["tenant_model"] = tenant_model
- kwargs["user_model"] = get_user(tenant_id, user_id)
+
+ user = get_user(tenant_id, user_id)
+ kwargs["user_model"] = user
+
+ current_app.login_manager._update_request_context_with_user(user) # type: ignore
+ user_logged_in.send(current_app._get_current_object(), user=_get_user()) # type: ignore
return view_func(*args, **kwargs)
diff --git a/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py b/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py
index a98a42f5df..6c768fd86c 100644
--- a/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py
+++ b/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py
@@ -455,8 +455,6 @@ class EasyUIBasedGenerateTaskPipeline(BasedGenerateTaskPipeline, MessageCycleMan
agent_thought: Optional[MessageAgentThought] = (
db.session.query(MessageAgentThought).filter(MessageAgentThought.id == event.agent_thought_id).first()
)
- db.session.refresh(agent_thought)
- db.session.close()
if agent_thought:
return AgentThoughtStreamResponse(
diff --git a/api/core/workflow/nodes/agent/agent_node.py b/api/core/workflow/nodes/agent/agent_node.py
index 771e0ca7a5..9bed8862fc 100644
--- a/api/core/workflow/nodes/agent/agent_node.py
+++ b/api/core/workflow/nodes/agent/agent_node.py
@@ -356,7 +356,9 @@ class AgentNode(ToolNode):
def _remove_unsupported_model_features_for_old_version(self, model_schema: AIModelEntity) -> AIModelEntity:
if model_schema.features:
- for feature in model_schema.features:
- if feature.value not in AgentOldVersionModelFeatures:
+ for feature in model_schema.features[:]: # Create a copy to safely modify during iteration
+ try:
+ AgentOldVersionModelFeatures(feature.value) # Try to create enum member from value
+ except ValueError:
model_schema.features.remove(feature)
return model_schema
diff --git a/api/core/workflow/nodes/agent/entities.py b/api/core/workflow/nodes/agent/entities.py
index 77e94375bf..075a41fb2f 100644
--- a/api/core/workflow/nodes/agent/entities.py
+++ b/api/core/workflow/nodes/agent/entities.py
@@ -1,4 +1,4 @@
-from enum import Enum
+from enum import Enum, StrEnum
from typing import Any, Literal, Union
from pydantic import BaseModel
@@ -26,7 +26,7 @@ class ParamsAutoGenerated(Enum):
OPEN = 1
-class AgentOldVersionModelFeatures(Enum):
+class AgentOldVersionModelFeatures(StrEnum):
"""
Enum class for old SDK version llm feature.
"""
diff --git a/api/core/workflow/nodes/http_request/executor.py b/api/core/workflow/nodes/http_request/executor.py
index 2c42f5a1be..e28ac6343b 100644
--- a/api/core/workflow/nodes/http_request/executor.py
+++ b/api/core/workflow/nodes/http_request/executor.py
@@ -235,6 +235,10 @@ class Executor:
files[key].append(file_tuple)
# convert files to list for httpx request
+ # If there are no actual files, we still need to force httpx to use `multipart/form-data`.
+ # This is achieved by inserting a harmless placeholder file that will be ignored by the server.
+ if not files:
+ self.files = [("__multipart_placeholder__", ("", b"", "application/octet-stream"))]
if files:
self.files = []
for key, file_tuples in files.items():
@@ -373,7 +377,10 @@ class Executor:
raw += f"{k}: {v}\r\n"
body_string = ""
- if self.files:
+ # Only log actual files if present.
+ # '__multipart_placeholder__' is inserted to force multipart encoding but is not a real file.
+ # This prevents logging meaningless placeholder entries.
+ if self.files and not all(f[0] == "__multipart_placeholder__" for f in self.files):
for key, (filename, content, mime_type) in self.files:
body_string += f"--{boundary}\r\n"
body_string += f'Content-Disposition: form-data; name="{key}"\r\n\r\n'
diff --git a/api/pyproject.toml b/api/pyproject.toml
index 1c6adb6587..50a765c0e1 100644
--- a/api/pyproject.toml
+++ b/api/pyproject.toml
@@ -149,6 +149,7 @@ dev = [
"types-tqdm~=4.67.0",
"types-ujson~=5.10.0",
"boto3-stubs>=1.38.20",
+ "types-jmespath>=1.0.2.20240106",
]
############################################################
diff --git a/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py
index 58b910e17b..d066fc1e33 100644
--- a/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py
+++ b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py
@@ -246,7 +246,9 @@ def test_executor_with_form_data():
assert "multipart/form-data" in executor.headers["Content-Type"]
assert executor.params == []
assert executor.json is None
- assert executor.files is None
+ # '__multipart_placeholder__' is expected when no file inputs exist,
+ # to ensure the request is treated as multipart/form-data by the backend.
+ assert executor.files == [("__multipart_placeholder__", ("", b"", "application/octet-stream"))]
assert executor.content is None
# Check that the form data is correctly loaded in executor.data
diff --git a/api/uv.lock b/api/uv.lock
index 033dc8762b..a1e1d6146a 100644
--- a/api/uv.lock
+++ b/api/uv.lock
@@ -1315,6 +1315,7 @@ dev = [
{ name = "types-gevent" },
{ name = "types-greenlet" },
{ name = "types-html5lib" },
+ { name = "types-jmespath" },
{ name = "types-jsonschema" },
{ name = "types-markdown" },
{ name = "types-oauthlib" },
@@ -1486,6 +1487,7 @@ dev = [
{ name = "types-gevent", specifier = "~=24.11.0" },
{ name = "types-greenlet", specifier = "~=3.1.0" },
{ name = "types-html5lib", specifier = "~=1.1.11" },
+ { name = "types-jmespath", specifier = ">=1.0.2.20240106" },
{ name = "types-jsonschema", specifier = "~=4.23.0" },
{ name = "types-markdown", specifier = "~=3.7.0" },
{ name = "types-oauthlib", specifier = "~=3.2.0" },
@@ -5726,6 +5728,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/7c/f862b1dc31268ef10fe95b43dcdf216ba21a592fafa2d124445cd6b92e93/types_html5lib-1.1.11.20241018-py3-none-any.whl", hash = "sha256:3f1e064d9ed2c289001ae6392c84c93833abb0816165c6ff0abfc304a779f403", size = 17292 },
]
+[[package]]
+name = "types-jmespath"
+version = "1.0.2.20240106"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1b/e4/1f7414dbca03975f66f1ab1b3f7b3deb7c19b104ef14dd3c99036bbc39b2/types-jmespath-1.0.2.20240106.tar.gz", hash = "sha256:b4a65a116bfc1c700a4fd9d24e2e397f4a431122e0320a77b7f1989a6b5d819e", size = 5071 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f5/30/3d6443f782601dd88820ba31e7668abfec7e19d685ac7f6fbcfd6ebba519/types_jmespath-1.0.2.20240106-py3-none-any.whl", hash = "sha256:c3e715fcaae9e5f8d74e14328fdedc4f2b3f0e18df17f3e457ae0a18e245bde0", size = 6087 },
+]
+
[[package]]
name = "types-jsonschema"
version = "4.23.0.20241208"
diff --git a/web/app/account/header.tsx b/web/app/account/header.tsx
index 11b6beec08..d033bfab61 100644
--- a/web/app/account/header.tsx
+++ b/web/app/account/header.tsx
@@ -6,10 +6,12 @@ import Button from '../components/base/button'
import Avatar from './avatar'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import { useCallback } from 'react'
+import { useGlobalPublicStore } from '@/context/global-public-context'
const Header = () => {
const { t } = useTranslation()
const router = useRouter()
+ const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const back = useCallback(() => {
router.back()
@@ -19,7 +21,13 @@ const Header = () => {
-
+ {systemFeatures.branding.enabled && systemFeatures.branding.login_page_logo
+ ?

+ :
}
{t('common.account.account')}
diff --git a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx
index 5fde657f68..fd317ccf91 100644
--- a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx
+++ b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx
@@ -148,10 +148,12 @@ const Sidebar = ({ isPanel }: Props) => {
'flex shrink-0 items-center gap-1.5 px-1',
)}>
{t('share.chat.poweredBy')}
- {systemFeatures.branding.enabled ? (
-

- ) : (
-
)
+ {
+ systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
+ ?

+ : appData?.custom_config?.replace_webapp_logo
+ ?

+ :
}
)}
diff --git a/web/app/components/base/chat/embedded-chatbot/header/index.tsx b/web/app/components/base/chat/embedded-chatbot/header/index.tsx
index c6c02a4d44..95975e29e7 100644
--- a/web/app/components/base/chat/embedded-chatbot/header/index.tsx
+++ b/web/app/components/base/chat/embedded-chatbot/header/index.tsx
@@ -13,6 +13,7 @@ import Divider from '@/app/components/base/divider'
import ViewFormDropdown from '@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import cn from '@/utils/classnames'
+import { useGlobalPublicStore } from '@/context/global-public-context'
export type IHeaderProps = {
isMobile?: boolean
@@ -42,6 +43,7 @@ const Header: FC
= ({
const [parentOrigin, setParentOrigin] = useState('')
const [showToggleExpandButton, setShowToggleExpandButton] = useState(false)
const [expanded, setExpanded] = useState(false)
+ const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const handleMessageReceived = useCallback((event: MessageEvent) => {
let currentParentOrigin = parentOrigin
@@ -85,12 +87,13 @@ const Header: FC = ({
'flex shrink-0 items-center gap-1.5 px-2',
)}>
{t('share.chat.poweredBy')}
- {appData?.custom_config?.replace_webapp_logo && (
-
- )}
- {!appData?.custom_config?.replace_webapp_logo && (
-
- )}
+ {
+ systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
+ ?
+ : appData?.custom_config?.replace_webapp_logo
+ ?
+ :
+ }
)}
diff --git a/web/app/components/base/chat/embedded-chatbot/index.tsx b/web/app/components/base/chat/embedded-chatbot/index.tsx
index ffcb128c8f..002d142542 100644
--- a/web/app/components/base/chat/embedded-chatbot/index.tsx
+++ b/web/app/components/base/chat/embedded-chatbot/index.tsx
@@ -22,6 +22,7 @@ import ChatWrapper from '@/app/components/base/chat/embedded-chatbot/chat-wrappe
import DifyLogo from '@/app/components/base/logo/dify-logo'
import cn from '@/utils/classnames'
import useDocumentTitle from '@/hooks/use-document-title'
+import { useGlobalPublicStore } from '@/context/global-public-context'
const Chatbot = () => {
const {
@@ -37,6 +38,7 @@ const Chatbot = () => {
themeBuilder,
} = useEmbeddedChatbotContext()
const { t } = useTranslation()
+ const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const customConfig = appData?.custom_config
const site = appData?.site
@@ -115,12 +117,13 @@ const Chatbot = () => {
'flex shrink-0 items-center gap-1.5 px-2',
)}>
{t('share.chat.poweredBy')}
- {appData?.custom_config?.replace_webapp_logo && (
-
- )}
- {!appData?.custom_config?.replace_webapp_logo && (
-
- )}
+ {
+ systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
+ ?
+ : appData?.custom_config?.replace_webapp_logo
+ ?
+ :
+ }
)}
diff --git a/web/app/components/base/logo/dify-logo.tsx b/web/app/components/base/logo/dify-logo.tsx
index 9e8f077372..5369144e1c 100644
--- a/web/app/components/base/logo/dify-logo.tsx
+++ b/web/app/components/base/logo/dify-logo.tsx
@@ -3,7 +3,6 @@ import type { FC } from 'react'
import classNames from '@/utils/classnames'
import useTheme from '@/hooks/use-theme'
import { basePath } from '@/utils/var'
-import { useGlobalPublicStore } from '@/context/global-public-context'
export type LogoStyle = 'default' | 'monochromeWhite'
export const logoPathMap: Record = {
@@ -32,18 +31,12 @@ const DifyLogo: FC = ({
}) => {
const { theme } = useTheme()
const themedStyle = (theme === 'dark' && style === 'default') ? 'monochromeWhite' : style
- const { systemFeatures } = useGlobalPublicStore()
- const hasBrandingLogo = Boolean(systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo)
-
- let src = `${basePath}${logoPathMap[themedStyle]}`
- if (hasBrandingLogo)
- src = systemFeatures.branding.workspace_logo
return (
)
}
diff --git a/web/app/components/base/tab-slider/index.tsx b/web/app/components/base/tab-slider/index.tsx
index fd6b876d02..56cde52154 100644
--- a/web/app/components/base/tab-slider/index.tsx
+++ b/web/app/components/base/tab-slider/index.tsx
@@ -40,7 +40,7 @@ const TabSlider: FC = ({
const newIndex = options.findIndex(option => option.value === value)
setActiveIndex(newIndex)
updateSliderStyle(newIndex)
- }, [value, options, pluginList])
+ }, [value, options, pluginList?.total])
return (
@@ -69,13 +69,13 @@ const TabSlider: FC = ({
{option.text}
{/* if no plugin installed, the badge won't show */}
{option.value === 'plugins'
- && (pluginList?.plugins.length ?? 0) > 0
+ && (pluginList?.total ?? 0) > 0
&&
- {pluginList?.plugins.length}
+ {pluginList?.total}
}
diff --git a/web/app/components/custom/custom-web-app-brand/index.tsx b/web/app/components/custom/custom-web-app-brand/index.tsx
index 444df98f24..f6f617be85 100644
--- a/web/app/components/custom/custom-web-app-brand/index.tsx
+++ b/web/app/components/custom/custom-web-app-brand/index.tsx
@@ -24,6 +24,7 @@ import {
} from '@/service/common'
import { useAppContext } from '@/context/app-context'
import cn from '@/utils/classnames'
+import { useGlobalPublicStore } from '@/context/global-public-context'
const ALLOW_FILE_EXTENSIONS = ['svg', 'png']
@@ -39,6 +40,7 @@ const CustomWebAppBrand = () => {
const [fileId, setFileId] = useState('')
const [imgKey, setImgKey] = useState(Date.now())
const [uploadProgress, setUploadProgress] = useState(0)
+ const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const isSandbox = enableBilling && plan.type === Plan.sandbox
const uploading = uploadProgress > 0 && uploadProgress < 100
const webappLogo = currentWorkspace.custom_config?.replace_webapp_logo || ''
@@ -244,9 +246,12 @@ const CustomWebAppBrand = () => {
{!webappBrandRemoved && (
<>
POWERED BY
- {webappLogo
- ?
- :
+ {
+ systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
+ ?
+ : webappLogo
+ ?
+ :
}
>
)}
@@ -303,9 +308,12 @@ const CustomWebAppBrand = () => {
{!webappBrandRemoved && (
<>
POWERED BY
- {webappLogo
- ?
- :
+ {
+ systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
+ ?
+ : webappLogo
+ ?
+ :
}
>
)}
diff --git a/web/app/components/header/account-about/index.tsx b/web/app/components/header/account-about/index.tsx
index 6129b48dce..280e276be9 100644
--- a/web/app/components/header/account-about/index.tsx
+++ b/web/app/components/header/account-about/index.tsx
@@ -9,6 +9,7 @@ import type { LangGeniusVersionResponse } from '@/models/common'
import { IS_CE_EDITION } from '@/config'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import { noop } from 'lodash-es'
+import { useGlobalPublicStore } from '@/context/global-public-context'
type IAccountSettingProps = {
langeniusVersionInfo: LangGeniusVersionResponse
@@ -21,6 +22,7 @@ export default function AccountAbout({
}: IAccountSettingProps) {
const { t } = useTranslation()
const isLatest = langeniusVersionInfo.current_version === langeniusVersionInfo.latest_version
+ const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
return (
-
+ {systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
+ ?

+ :
}
+
Version {langeniusVersionInfo?.current_version}
© {dayjs().year()} LangGenius, Inc., Contributors.
diff --git a/web/app/components/header/index.tsx b/web/app/components/header/index.tsx
index 6e8d1704dd..a9c26e0070 100644
--- a/web/app/components/header/index.tsx
+++ b/web/app/components/header/index.tsx
@@ -21,6 +21,7 @@ import { useModalContext } from '@/context/modal-context'
import PlanBadge from './plan-badge'
import LicenseNav from './license-env'
import { Plan } from '../billing/type'
+import { useGlobalPublicStore } from '@/context/global-public-context'
const navClassName = `
flex items-center relative mr-0 sm:mr-3 px-3 h-8 rounded-xl
@@ -36,6 +37,7 @@ const Header = () => {
const [isShowNavMenu, { toggle, setFalse: hideNavMenu }] = useBoolean(false)
const { enableBilling, plan } = useProviderContext()
const { setShowPricingModal, setShowAccountSettingModal } = useModalContext()
+ const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const isFreePlan = plan.type === Plan.sandbox
const handlePlanClick = useCallback(() => {
if (isFreePlan)
@@ -61,7 +63,13 @@ const Header = () => {
!isMobile
&&
-
+ {systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
+ ?

+ :
}
/
@@ -76,7 +84,13 @@ const Header = () => {
{isMobile && (
-
+ {systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
+ ?

+ :
}
/
{enableBilling ?
:
}
diff --git a/web/app/components/plugins/plugin-page/plugins-panel.tsx b/web/app/components/plugins/plugin-page/plugins-panel.tsx
index 513641f4b9..a5f411c37e 100644
--- a/web/app/components/plugins/plugin-page/plugins-panel.tsx
+++ b/web/app/components/plugins/plugin-page/plugins-panel.tsx
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'
import type { FilterState } from './filter-management'
import FilterManagement from './filter-management'
import List from './list'
-import { useInstalledLatestVersion, useInstalledPluginListWithPagination, useInvalidateInstalledPluginList } from '@/service/use-plugins'
+import { useInstalledLatestVersion, useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
import { usePluginPageContext } from './context'
import { useDebounceFn } from 'ahooks'
@@ -17,7 +17,7 @@ const PluginsPanel = () => {
const { t } = useTranslation()
const filters = usePluginPageContext(v => v.filters) as FilterState
const setFilters = usePluginPageContext(v => v.setFilters)
- const { data: pluginList, isLoading: isPluginListLoading, isFetching, isLastPage, loadNextPage } = useInstalledPluginListWithPagination()
+ const { data: pluginList, isLoading: isPluginListLoading, isFetching, isLastPage, loadNextPage } = useInstalledPluginList()
const { data: installedLatestVersion } = useInstalledLatestVersion(
pluginList?.plugins
.filter(plugin => plugin.source === PluginSource.marketplace)
diff --git a/web/app/components/share/text-generation/index.tsx b/web/app/components/share/text-generation/index.tsx
index 5450fa7ce6..6fd6d17278 100644
--- a/web/app/components/share/text-generation/index.tsx
+++ b/web/app/components/share/text-generation/index.tsx
@@ -641,11 +641,13 @@ const TextGeneration: FC
= ({
!isPC && resultExisted && 'rounded-b-2xl border-b-[0.5px] border-divider-regular',
)}>
{t('share.chat.poweredBy')}
- {systemFeatures.branding.enabled ? (
-
- ) : (
-
- )}
+ {
+ systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
+ ?
+ : customConfig?.replace_webapp_logo
+ ?
+ :
+ }
)}
diff --git a/web/app/reset-password/set-password/page.tsx b/web/app/reset-password/set-password/page.tsx
index dd1c4ef1f4..ee4c114a77 100644
--- a/web/app/reset-password/set-password/page.tsx
+++ b/web/app/reset-password/set-password/page.tsx
@@ -105,7 +105,7 @@ const ChangePasswordForm = () => {
-
+
{/* Password */}