From 1b9165624f2cf7534b1ffebc46753061ace2cdb4 Mon Sep 17 00:00:00 2001 From: znn Date: Wed, 10 Dec 2025 06:49:13 +0530 Subject: [PATCH 1/8] adding llm_usage and error_type (#26546) --- api/core/workflow/nodes/llm/node.py | 3 +++ .../nodes/question_classifier/question_classifier_node.py | 1 + 2 files changed, 4 insertions(+) diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index 10682ae38a..a5973862b2 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -334,6 +334,7 @@ class LLMNode(Node[LLMNodeData]): inputs=node_inputs, process_data=process_data, error_type=type(e).__name__, + llm_usage=usage, ) ) except Exception as e: @@ -344,6 +345,8 @@ class LLMNode(Node[LLMNodeData]): error=str(e), inputs=node_inputs, process_data=process_data, + error_type=type(e).__name__, + llm_usage=usage, ) ) diff --git a/api/core/workflow/nodes/question_classifier/question_classifier_node.py b/api/core/workflow/nodes/question_classifier/question_classifier_node.py index db3d4d4aac..4a3e8e56f8 100644 --- a/api/core/workflow/nodes/question_classifier/question_classifier_node.py +++ b/api/core/workflow/nodes/question_classifier/question_classifier_node.py @@ -221,6 +221,7 @@ class QuestionClassifierNode(Node[QuestionClassifierNodeData]): status=WorkflowNodeExecutionStatus.FAILED, inputs=variables, error=str(e), + error_type=type(e).__name__, metadata={ WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: usage.total_tokens, WorkflowNodeExecutionMetadataKey.TOTAL_PRICE: usage.total_price, From 4a88c8fd1932c3f43f242ba697ad7bdd1b121540 Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Wed, 10 Dec 2025 09:44:47 +0800 Subject: [PATCH 2/8] chore: set is_multimodal db define default = false (#29362) --- api/models/dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/models/dataset.py b/api/models/dataset.py index 5bbf44050c..ba2eaf6749 100644 --- a/api/models/dataset.py +++ b/api/models/dataset.py @@ -78,7 +78,7 @@ class Dataset(Base): pipeline_id = mapped_column(StringUUID, nullable=True) chunk_structure = mapped_column(sa.String(255), nullable=True) enable_api = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("true")) - is_multimodal = mapped_column(sa.Boolean, nullable=False, server_default=db.text("false")) + is_multimodal = mapped_column(sa.Boolean, default=False, nullable=False, server_default=db.text("false")) @property def total_documents(self): From e205182e1f9cf6dfa374bfe3240fdc909a15b42a Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Wed, 10 Dec 2025 10:01:45 +0800 Subject: [PATCH 3/8] =?UTF-8?q?fix:=20Parent=20instance=20=20is=20not=20bound=E2=80=A6=20(#29377)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/core/rag/datasource/retrieval_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/core/rag/datasource/retrieval_service.py b/api/core/rag/datasource/retrieval_service.py index cbd7cbeb64..e644e754ec 100644 --- a/api/core/rag/datasource/retrieval_service.py +++ b/api/core/rag/datasource/retrieval_service.py @@ -483,7 +483,7 @@ class RetrievalService: DocumentSegment.status == "completed", DocumentSegment.id == segment_id, ) - segment = db.session.scalar(document_segment_stmt) + segment = session.scalar(document_segment_stmt) if segment: segment_file_map[segment.id] = [attachment_info] else: @@ -496,7 +496,7 @@ class RetrievalService: DocumentSegment.status == "completed", DocumentSegment.index_node_id == index_node_id, ) - segment = db.session.scalar(document_segment_stmt) + segment = session.scalar(document_segment_stmt) if not segment: continue From 7df360a2921f2e256602ca0cd131f65c96150404 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, 10 Dec 2025 10:15:21 +0800 Subject: [PATCH 4/8] fix: workflow log missing trigger icon (#29379) --- api/models/workflow.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/api/models/workflow.py b/api/models/workflow.py index 42ee8a1f2b..853d5afefc 100644 --- a/api/models/workflow.py +++ b/api/models/workflow.py @@ -907,19 +907,29 @@ class WorkflowNodeExecutionModel(Base): # This model is expected to have `offlo @property def extras(self) -> dict[str, Any]: from core.tools.tool_manager import ToolManager + from core.trigger.trigger_manager import TriggerManager extras: dict[str, Any] = {} - if self.execution_metadata_dict: - if self.node_type == NodeType.TOOL and "tool_info" in self.execution_metadata_dict: - tool_info: dict[str, Any] = self.execution_metadata_dict["tool_info"] + execution_metadata = self.execution_metadata_dict + if execution_metadata: + if self.node_type == NodeType.TOOL and "tool_info" in execution_metadata: + tool_info: dict[str, Any] = execution_metadata["tool_info"] extras["icon"] = ToolManager.get_tool_icon( tenant_id=self.tenant_id, provider_type=tool_info["provider_type"], provider_id=tool_info["provider_id"], ) - elif self.node_type == NodeType.DATASOURCE and "datasource_info" in self.execution_metadata_dict: - datasource_info = self.execution_metadata_dict["datasource_info"] + elif self.node_type == NodeType.DATASOURCE and "datasource_info" in execution_metadata: + datasource_info = execution_metadata["datasource_info"] extras["icon"] = datasource_info.get("icon") + elif self.node_type == NodeType.TRIGGER_PLUGIN and "trigger_info" in execution_metadata: + trigger_info = execution_metadata["trigger_info"] or {} + provider_id = trigger_info.get("provider_id") + if provider_id: + extras["icon"] = TriggerManager.get_trigger_plugin_icon( + tenant_id=self.tenant_id, + provider_id=provider_id, + ) return extras def _get_offload_by_type(self, type_: ExecutionOffLoadType) -> Optional["WorkflowNodeExecutionOffload"]: From 51330c0ee631de49f6cc5f7b7436c00cab62d38f Mon Sep 17 00:00:00 2001 From: Nie Ronghua <40586915+NieRonghua@users.noreply.github.com> Date: Wed, 10 Dec 2025 10:47:45 +0800 Subject: [PATCH 5/8] fix(App.deleted_tools): incorrect compare between UUID and map with string-typed key. (#29340) Co-authored-by: 01393547 Co-authored-by: Yeuoly <45712896+Yeuoly@users.noreply.github.com> --- api/models/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/models/model.py b/api/models/model.py index 1731ff5699..6b0bf4b4a2 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -259,7 +259,7 @@ class App(Base): provider_id = tool.get("provider_id", "") if provider_type == ToolProviderType.API: - if uuid.UUID(provider_id) not in existing_api_providers: + if provider_id not in existing_api_providers: deleted_tools.append( { "type": ToolProviderType.API, From e95b7b57c99867c1fee5db0ccd57b81f910fa474 Mon Sep 17 00:00:00 2001 From: yyh Date: Wed, 10 Dec 2025 12:10:47 +0800 Subject: [PATCH 6/8] fix: prevent popup blocker from blocking async window.open Add useAsyncWindowOpen hook to handle async URL fetching with placeholder window pattern. This prevents browser popup blockers (especially Safari) from blocking windows opened after async operations. - Create reusable useAsyncWindowOpen hook with placeholder window pattern - Fix billing subscription management popup (cloud-plan-item) - Fix app card explore popup - Fix app publisher explore popup Fixes #29389 Ref: #29390 --- .../components/app/app-publisher/index.tsx | 29 +++++---- web/app/components/apps/app-card.tsx | 27 ++++---- .../pricing/plans/cloud-plan-item/index.tsx | 12 +++- web/hooks/use-async-window-open.ts | 62 +++++++++++++++++++ 4 files changed, 105 insertions(+), 25 deletions(-) create mode 100644 web/hooks/use-async-window-open.ts diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index bba5ebfa21..39eb53da8b 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -21,7 +21,6 @@ import { import { useKeyPress } from 'ahooks' import Divider from '../../base/divider' import Loading from '../../base/loading' -import Toast from '../../base/toast' import Tooltip from '../../base/tooltip' import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '../../workflow/utils' import AccessControl from '../app-access-control' @@ -50,6 +49,7 @@ import { AppModeEnum } from '@/types/app' import type { PublishWorkflowParams } from '@/types/workflow' import { basePath } from '@/utils/var' import UpgradeBtn from '@/app/components/billing/upgrade-btn' +import { useAsyncWindowOpen } from '@/hooks/use-async-window-open' const ACCESS_MODE_MAP: Record = { [AccessMode.ORGANIZATION]: { @@ -216,18 +216,23 @@ const AppPublisher = ({ setPublished(false) }, [disabled, onToggle, open]) - const handleOpenInExplore = useCallback(async () => { - try { - const { installed_apps }: any = await fetchInstalledAppList(appDetail?.id) || {} - if (installed_apps?.length > 0) - window.open(`${basePath}/explore/installed/${installed_apps[0].id}`, '_blank') - else + const { openAsync } = useAsyncWindowOpen() + + const handleOpenInExplore = useCallback(() => { + if (!appDetail?.id) return + + openAsync( + async () => { + const { installed_apps }: any = await fetchInstalledAppList(appDetail.id) || {} + if (installed_apps?.length > 0) + return `${basePath}/explore/installed/${installed_apps[0].id}` throw new Error('No app found in Explore') - } - catch (e: any) { - Toast.notify({ type: 'error', message: `${e.message || e}` }) - } - }, [appDetail?.id]) + }, + { + errorMessage: 'Failed to open app in Explore', + }, + ) + }, [appDetail?.id, openAsync]) const handleAccessControlUpdate = useCallback(async () => { if (!appDetail) diff --git a/web/app/components/apps/app-card.tsx b/web/app/components/apps/app-card.tsx index 8356cfd31c..b9a4c656d3 100644 --- a/web/app/components/apps/app-card.tsx +++ b/web/app/components/apps/app-card.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next' import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill, RiVerifiedBadgeLine } from '@remixicon/react' import cn from '@/utils/classnames' import { type App, AppModeEnum } from '@/types/app' -import Toast, { ToastContext } from '@/app/components/base/toast' +import { ToastContext } from '@/app/components/base/toast' import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps' import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal' import AppIcon from '@/app/components/base/app-icon' @@ -31,6 +31,7 @@ import { AccessMode } from '@/models/access-control' import { useGlobalPublicStore } from '@/context/global-public-context' import { formatTime } from '@/utils/time' import { useGetUserCanAccessApp } from '@/service/access-control' +import { useAsyncWindowOpen } from '@/hooks/use-async-window-open' import dynamic from 'next/dynamic' const EditAppModal = dynamic(() => import('@/app/components/explore/create-app-modal'), { @@ -242,20 +243,24 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { e.preventDefault() setShowAccessControl(true) } - const onClickInstalledApp = async (e: React.MouseEvent) => { + const { openAsync } = useAsyncWindowOpen() + + const onClickInstalledApp = (e: React.MouseEvent) => { e.stopPropagation() props.onClick?.() e.preventDefault() - try { - const { installed_apps }: any = await fetchInstalledAppList(app.id) || {} - if (installed_apps?.length > 0) - window.open(`${basePath}/explore/installed/${installed_apps[0].id}`, '_blank') - else + + openAsync( + async () => { + const { installed_apps }: any = await fetchInstalledAppList(app.id) || {} + if (installed_apps?.length > 0) + return `${basePath}/explore/installed/${installed_apps[0].id}` throw new Error('No app found in Explore') - } - catch (e: any) { - Toast.notify({ type: 'error', message: `${e.message || e}` }) - } + }, + { + errorMessage: 'Failed to open app in Explore', + }, + ) } return (
diff --git a/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx b/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx index 396dd4a1b0..0fdf51f72e 100644 --- a/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx +++ b/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx @@ -9,6 +9,7 @@ import Toast from '../../../../base/toast' import { PlanRange } from '../../plan-switcher/plan-range-switcher' import { useAppContext } from '@/context/app-context' import { fetchBillingUrl, fetchSubscriptionUrls } from '@/service/billing' +import { useAsyncWindowOpen } from '@/hooks/use-async-window-open' import List from './list' import Button from './button' import { Professional, Sandbox, Team } from '../../assets' @@ -54,6 +55,8 @@ const CloudPlanItem: FC = ({ })[plan] }, [isCurrent, plan, t]) + const { openAsync } = useAsyncWindowOpen() + const handleGetPayUrl = async () => { if (loading) return @@ -72,8 +75,13 @@ const CloudPlanItem: FC = ({ setLoading(true) try { if (isCurrentPaidPlan) { - const res = await fetchBillingUrl() - window.open(res.url, '_blank') + openAsync( + () => fetchBillingUrl().then(res => res.url), + { + errorMessage: 'Failed to open billing page', + windowFeatures: 'noopener,noreferrer', + }, + ) return } diff --git a/web/hooks/use-async-window-open.ts b/web/hooks/use-async-window-open.ts new file mode 100644 index 0000000000..dcc44e7846 --- /dev/null +++ b/web/hooks/use-async-window-open.ts @@ -0,0 +1,62 @@ +import { useCallback } from 'react' +import Toast from '@/app/components/base/toast' + +export type AsyncWindowOpenOptions = { + successMessage?: string + errorMessage?: string + windowFeatures?: string + onError?: (error: any) => void + onSuccess?: (url: string) => void +} + +export const useAsyncWindowOpen = () => { + const openAsync = useCallback(async ( + fetchUrl: () => Promise, + options: AsyncWindowOpenOptions = {}, + ) => { + const { + successMessage, + errorMessage = 'Failed to open page', + windowFeatures = 'noopener,noreferrer', + onError, + onSuccess, + } = options + + const newWindow = window.open('', '_blank', windowFeatures) + + try { + const url = await fetchUrl() + + if (url && newWindow) { + newWindow.location.href = url + onSuccess?.(url) + + if (successMessage) { + Toast.notify({ + type: 'success', + message: successMessage, + }) + } + } + else { + newWindow?.close() + const error = new Error('Invalid URL or window was closed') + onError?.(error) + Toast.notify({ + type: 'error', + message: errorMessage, + }) + } + } + catch (error) { + newWindow?.close() + onError?.(error) + Toast.notify({ + type: 'error', + message: errorMessage, + }) + } + }, []) + + return { openAsync } +} From bac0513b8bead54a878204fcb90a207688349f62 Mon Sep 17 00:00:00 2001 From: yyh Date: Wed, 10 Dec 2025 12:19:25 +0800 Subject: [PATCH 7/8] improve: better popup blocker detection and type safety - Add immediate popup blocker detection with user-friendly error message - Improve type safety by removing any types - Simplify logic flow in useAsyncWindowOpen hook Addresses code review suggestions --- web/app/components/app/app-publisher/index.tsx | 4 ++-- web/app/components/apps/app-card.tsx | 4 ++-- web/hooks/use-async-window-open.ts | 18 ++++++++++++++---- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index 39eb53da8b..801345798b 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -223,8 +223,8 @@ const AppPublisher = ({ openAsync( async () => { - const { installed_apps }: any = await fetchInstalledAppList(appDetail.id) || {} - if (installed_apps?.length > 0) + const { installed_apps }: { installed_apps?: { id: string }[] } = await fetchInstalledAppList(appDetail.id) || {} + if (installed_apps && installed_apps.length > 0) return `${basePath}/explore/installed/${installed_apps[0].id}` throw new Error('No app found in Explore') }, diff --git a/web/app/components/apps/app-card.tsx b/web/app/components/apps/app-card.tsx index b9a4c656d3..407df23913 100644 --- a/web/app/components/apps/app-card.tsx +++ b/web/app/components/apps/app-card.tsx @@ -252,8 +252,8 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { openAsync( async () => { - const { installed_apps }: any = await fetchInstalledAppList(app.id) || {} - if (installed_apps?.length > 0) + const { installed_apps }: { installed_apps?: { id: string }[] } = await fetchInstalledAppList(app.id) || {} + if (installed_apps && installed_apps.length > 0) return `${basePath}/explore/installed/${installed_apps[0].id}` throw new Error('No app found in Explore') }, diff --git a/web/hooks/use-async-window-open.ts b/web/hooks/use-async-window-open.ts index dcc44e7846..582ab28be4 100644 --- a/web/hooks/use-async-window-open.ts +++ b/web/hooks/use-async-window-open.ts @@ -24,10 +24,20 @@ export const useAsyncWindowOpen = () => { const newWindow = window.open('', '_blank', windowFeatures) + if (!newWindow) { + const error = new Error('Popup blocked by browser') + onError?.(error) + Toast.notify({ + type: 'error', + message: 'Popup blocked. Please allow popups for this site.', + }) + return + } + try { const url = await fetchUrl() - if (url && newWindow) { + if (url) { newWindow.location.href = url onSuccess?.(url) @@ -39,8 +49,8 @@ export const useAsyncWindowOpen = () => { } } else { - newWindow?.close() - const error = new Error('Invalid URL or window was closed') + newWindow.close() + const error = new Error('Invalid URL received') onError?.(error) Toast.notify({ type: 'error', @@ -49,7 +59,7 @@ export const useAsyncWindowOpen = () => { } } catch (error) { - newWindow?.close() + newWindow.close() onError?.(error) Toast.notify({ type: 'error', From 8bf9eee91ec2e34e4a9848e1d0e187b3477db372 Mon Sep 17 00:00:00 2001 From: yyh Date: Wed, 10 Dec 2025 12:23:56 +0800 Subject: [PATCH 8/8] fix: maintain loading guard during async billing URL fetch Add await to openAsync call to prevent multiple concurrent requests when users rapidly click the billing button. This maintains the loading state until the billing URL is successfully fetched and opened. Addresses review feedback about regression in loading behavior --- .../components/billing/pricing/plans/cloud-plan-item/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx b/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx index 0fdf51f72e..164ad9061a 100644 --- a/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx +++ b/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx @@ -75,7 +75,7 @@ const CloudPlanItem: FC = ({ setLoading(true) try { if (isCurrentPaidPlan) { - openAsync( + await openAsync( () => fetchBillingUrl().then(res => res.url), { errorMessage: 'Failed to open billing page',