diff --git a/api/core/rag/datasource/retrieval_service.py b/api/core/rag/datasource/retrieval_service.py index ddf49f8392..7c5759e01b 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 diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index 9d968d19c8..04e2802191 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, 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): 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, 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"]: diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index bba5ebfa21..801345798b 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 }: { 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') - } - 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..407df23913 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 }: { 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') - } - 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..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 @@ -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') + await 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..582ab28be4 --- /dev/null +++ b/web/hooks/use-async-window-open.ts @@ -0,0 +1,72 @@ +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) + + 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.location.href = url + onSuccess?.(url) + + if (successMessage) { + Toast.notify({ + type: 'success', + message: successMessage, + }) + } + } + else { + newWindow.close() + const error = new Error('Invalid URL received') + onError?.(error) + Toast.notify({ + type: 'error', + message: errorMessage, + }) + } + } + catch (error) { + newWindow.close() + onError?.(error) + Toast.notify({ + type: 'error', + message: errorMessage, + }) + } + }, []) + + return { openAsync } +}