From e32490f54ec03af4e541ac1e167332a0a1a295b4 Mon Sep 17 00:00:00 2001
From: Wu Tianwei <30284043+WTW0313@users.noreply.github.com>
Date: Wed, 11 Feb 2026 14:09:33 +0800
Subject: [PATCH 1/9] feat(workflow): enhance workflow run history management
and UI updates (#32230)
---
.../rag-pipeline/hooks/use-pipeline-run.ts | 9 ++++--
.../workflow-app/hooks/use-workflow-run.ts | 15 ++++++++--
.../workflow/header/view-history.tsx | 30 ++++++-------------
.../workflow/panel/workflow-preview.tsx | 10 ++-----
web/app/components/workflow/utils/common.ts | 6 ++--
web/service/use-workflow.ts | 14 ++++++++-
web/types/workflow.ts | 16 ++++++++--
7 files changed, 63 insertions(+), 37 deletions(-)
diff --git a/web/app/components/rag-pipeline/hooks/use-pipeline-run.ts b/web/app/components/rag-pipeline/hooks/use-pipeline-run.ts
index dc2a234d1e..b35441365b 100644
--- a/web/app/components/rag-pipeline/hooks/use-pipeline-run.ts
+++ b/web/app/components/rag-pipeline/hooks/use-pipeline-run.ts
@@ -12,7 +12,7 @@ import { useWorkflowRunEvent } from '@/app/components/workflow/hooks/use-workflo
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
import { ssePost } from '@/service/base'
-import { useInvalidAllLastRun } from '@/service/use-workflow'
+import { useInvalidAllLastRun, useInvalidateWorkflowRunHistory } from '@/service/use-workflow'
import { stopWorkflowRun } from '@/service/workflow'
import { FlowType } from '@/types/common'
import { useNodesSyncDraft } from './use-nodes-sync-draft'
@@ -93,6 +93,7 @@ export const usePipelineRun = () => {
const pipelineId = useStore(s => s.pipelineId)
const invalidAllLastRun = useInvalidAllLastRun(FlowType.ragPipeline, pipelineId)
+ const invalidateRunHistory = useInvalidateWorkflowRunHistory()
const { fetchInspectVars } = useSetWorkflowVarsWithValue({
flowType: FlowType.ragPipeline,
flowId: pipelineId!,
@@ -132,6 +133,7 @@ export const usePipelineRun = () => {
...restCallback
} = callback || {}
const { pipelineId } = workflowStore.getState()
+ const runHistoryUrl = `/rag/pipelines/${pipelineId}/workflow-runs`
workflowStore.setState({ historyWorkflowData: undefined })
const workflowContainer = document.getElementById('workflow-container')
@@ -170,12 +172,14 @@ export const usePipelineRun = () => {
},
onWorkflowStarted: (params) => {
handleWorkflowStarted(params)
+ invalidateRunHistory(runHistoryUrl)
if (onWorkflowStarted)
onWorkflowStarted(params)
},
onWorkflowFinished: (params) => {
handleWorkflowFinished(params)
+ invalidateRunHistory(runHistoryUrl)
fetchInspectVars({})
invalidAllLastRun()
@@ -184,6 +188,7 @@ export const usePipelineRun = () => {
},
onError: (params) => {
handleWorkflowFailed()
+ invalidateRunHistory(runHistoryUrl)
if (onError)
onError(params)
@@ -275,7 +280,7 @@ export const usePipelineRun = () => {
...restCallback,
},
)
- }, [store, doSyncWorkflowDraft, workflowStore, handleWorkflowStarted, handleWorkflowFinished, fetchInspectVars, invalidAllLastRun, handleWorkflowFailed, handleWorkflowNodeStarted, handleWorkflowNodeFinished, handleWorkflowNodeIterationStarted, handleWorkflowNodeIterationNext, handleWorkflowNodeIterationFinished, handleWorkflowNodeLoopStarted, handleWorkflowNodeLoopNext, handleWorkflowNodeLoopFinished, handleWorkflowNodeRetry, handleWorkflowAgentLog, handleWorkflowTextChunk, handleWorkflowTextReplace])
+ }, [store, doSyncWorkflowDraft, workflowStore, handleWorkflowStarted, handleWorkflowFinished, fetchInspectVars, invalidAllLastRun, invalidateRunHistory, handleWorkflowFailed, handleWorkflowNodeStarted, handleWorkflowNodeFinished, handleWorkflowNodeIterationStarted, handleWorkflowNodeIterationNext, handleWorkflowNodeIterationFinished, handleWorkflowNodeLoopStarted, handleWorkflowNodeLoopNext, handleWorkflowNodeLoopFinished, handleWorkflowNodeRetry, handleWorkflowAgentLog, handleWorkflowTextChunk, handleWorkflowTextReplace])
const handleStopRun = useCallback((taskId: string) => {
const { pipelineId } = workflowStore.getState()
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 ef6d7731a4..ae4a21f5a0 100644
--- a/web/app/components/workflow-app/hooks/use-workflow-run.ts
+++ b/web/app/components/workflow-app/hooks/use-workflow-run.ts
@@ -23,7 +23,7 @@ import { useWorkflowStore } from '@/app/components/workflow/store'
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
import { handleStream, post, sseGet, ssePost } from '@/service/base'
import { ContentType } from '@/service/fetch'
-import { useInvalidAllLastRun } from '@/service/use-workflow'
+import { useInvalidAllLastRun, useInvalidateWorkflowRunHistory } from '@/service/use-workflow'
import { stopWorkflowRun } from '@/service/workflow'
import { AppModeEnum } from '@/types/app'
import { useSetWorkflowVarsWithValue } from '../../workflow/hooks/use-fetch-workflow-inspect-vars'
@@ -66,6 +66,7 @@ export const useWorkflowRun = () => {
const configsMap = useConfigsMap()
const { flowId, flowType } = configsMap
const invalidAllLastRun = useInvalidAllLastRun(flowType, flowId)
+ const invalidateRunHistory = useInvalidateWorkflowRunHistory()
const { fetchInspectVars } = useSetWorkflowVarsWithValue({
...configsMap,
@@ -189,6 +190,9 @@ export const useWorkflowRun = () => {
} = callback || {}
workflowStore.setState({ historyWorkflowData: undefined })
const appDetail = useAppStore.getState().appDetail
+ const runHistoryUrl = appDetail?.mode === AppModeEnum.ADVANCED_CHAT
+ ? `/apps/${appDetail.id}/advanced-chat/workflow-runs`
+ : `/apps/${appDetail?.id}/workflow-runs`
const workflowContainer = document.getElementById('workflow-container')
const {
@@ -363,6 +367,7 @@ export const useWorkflowRun = () => {
const wrappedOnError = (params: any) => {
clearAbortController()
handleWorkflowFailed()
+ invalidateRunHistory(runHistoryUrl)
clearListeningState()
if (onError)
@@ -381,6 +386,7 @@ export const useWorkflowRun = () => {
...restCallback,
onWorkflowStarted: (params) => {
handleWorkflowStarted(params)
+ invalidateRunHistory(runHistoryUrl)
if (onWorkflowStarted)
onWorkflowStarted(params)
@@ -388,6 +394,7 @@ export const useWorkflowRun = () => {
onWorkflowFinished: (params) => {
clearListeningState()
handleWorkflowFinished(params)
+ invalidateRunHistory(runHistoryUrl)
if (onWorkflowFinished)
onWorkflowFinished(params)
@@ -496,6 +503,7 @@ export const useWorkflowRun = () => {
},
onWorkflowPaused: (params) => {
handleWorkflowPaused()
+ invalidateRunHistory(runHistoryUrl)
if (onWorkflowPaused)
onWorkflowPaused(params)
const url = `/workflow/${params.workflow_run_id}/events`
@@ -694,6 +702,7 @@ export const useWorkflowRun = () => {
},
onWorkflowFinished: (params) => {
handleWorkflowFinished(params)
+ invalidateRunHistory(runHistoryUrl)
if (onWorkflowFinished)
onWorkflowFinished(params)
@@ -704,6 +713,7 @@ export const useWorkflowRun = () => {
},
onError: (params) => {
handleWorkflowFailed()
+ invalidateRunHistory(runHistoryUrl)
if (onError)
onError(params)
@@ -803,6 +813,7 @@ export const useWorkflowRun = () => {
},
onWorkflowPaused: (params) => {
handleWorkflowPaused()
+ invalidateRunHistory(runHistoryUrl)
if (onWorkflowPaused)
onWorkflowPaused(params)
const url = `/workflow/${params.workflow_run_id}/events`
@@ -837,7 +848,7 @@ export const useWorkflowRun = () => {
},
finalCallbacks,
)
- }, [store, doSyncWorkflowDraft, workflowStore, pathname, handleWorkflowFailed, flowId, handleWorkflowStarted, handleWorkflowFinished, fetchInspectVars, invalidAllLastRun, handleWorkflowNodeStarted, handleWorkflowNodeFinished, handleWorkflowNodeIterationStarted, handleWorkflowNodeIterationNext, handleWorkflowNodeIterationFinished, handleWorkflowNodeLoopStarted, handleWorkflowNodeLoopNext, handleWorkflowNodeLoopFinished, handleWorkflowNodeRetry, handleWorkflowAgentLog, handleWorkflowTextChunk, handleWorkflowTextReplace, handleWorkflowPaused, handleWorkflowNodeHumanInputRequired, handleWorkflowNodeHumanInputFormFilled, handleWorkflowNodeHumanInputFormTimeout])
+ }, [store, doSyncWorkflowDraft, workflowStore, pathname, handleWorkflowFailed, flowId, handleWorkflowStarted, handleWorkflowFinished, fetchInspectVars, invalidAllLastRun, invalidateRunHistory, handleWorkflowNodeStarted, handleWorkflowNodeFinished, handleWorkflowNodeIterationStarted, handleWorkflowNodeIterationNext, handleWorkflowNodeIterationFinished, handleWorkflowNodeLoopStarted, handleWorkflowNodeLoopNext, handleWorkflowNodeLoopFinished, handleWorkflowNodeRetry, handleWorkflowAgentLog, handleWorkflowTextChunk, handleWorkflowTextReplace, handleWorkflowPaused, handleWorkflowNodeHumanInputRequired, handleWorkflowNodeHumanInputFormFilled, handleWorkflowNodeHumanInputFormTimeout])
const handleStopRun = useCallback((taskId: string) => {
const setStoppedState = () => {
diff --git a/web/app/components/workflow/header/view-history.tsx b/web/app/components/workflow/header/view-history.tsx
index f9b446e930..94963e29fc 100644
--- a/web/app/components/workflow/header/view-history.tsx
+++ b/web/app/components/workflow/header/view-history.tsx
@@ -1,18 +1,8 @@
-import {
- RiCheckboxCircleLine,
- RiCloseLine,
- RiErrorWarningLine,
-} from '@remixicon/react'
import {
memo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
-import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
-import {
- ClockPlay,
- ClockPlaySlim,
-} from '@/app/components/base/icons/src/vender/line/time'
import Loading from '@/app/components/base/loading'
import {
PortalToFollowElem,
@@ -89,9 +79,7 @@ const ViewHistory = ({
open && 'bg-components-button-secondary-bg-hover',
)}
>
-
+
{t('common.showRunHistory', { ns: 'workflow' })}
)
@@ -107,7 +95,7 @@ const ViewHistory = ({
onClearLogAndMessageModal?.()
}}
>
-
+
)
@@ -129,7 +117,7 @@ const ViewHistory = ({
setOpen(false)
}}
>
-
+
{
@@ -145,7 +133,7 @@ const ViewHistory = ({
{
!data?.data.length && (
-
+
{t('common.notRunning', { ns: 'workflow' })}
@@ -175,18 +163,18 @@ const ViewHistory = ({
}}
>
{
- !isChatMode && item.status === WorkflowRunningStatus.Stopped && (
-
+ !isChatMode && [WorkflowRunningStatus.Stopped, WorkflowRunningStatus.Paused].includes(item.status) && (
+
)
}
{
!isChatMode && item.status === WorkflowRunningStatus.Failed && (
-
+
)
}
{
!isChatMode && item.status === WorkflowRunningStatus.Succeeded && (
-
+
)
}
@@ -196,7 +184,7 @@ const ViewHistory = ({
item.id === historyWorkflowData?.id && 'text-text-accent',
)}
>
- {`Test ${isChatMode ? 'Chat' : 'Run'}${formatWorkflowRunIdentifier(item.finished_at)}`}
+ {`Test ${isChatMode ? 'Chat' : 'Run'}${formatWorkflowRunIdentifier(item.finished_at, item.status)}`}
{item.created_by_account?.name}
diff --git a/web/app/components/workflow/panel/workflow-preview.tsx b/web/app/components/workflow/panel/workflow-preview.tsx
index 7f23f5bc74..4fdc0b8376 100644
--- a/web/app/components/workflow/panel/workflow-preview.tsx
+++ b/web/app/components/workflow/panel/workflow-preview.tsx
@@ -1,7 +1,3 @@
-import {
- RiClipboardLine,
- RiCloseLine,
-} from '@remixicon/react'
import copy from 'copy-to-clipboard'
import {
memo,
@@ -115,9 +111,9 @@ const WorkflowPreview = () => {
onMouseDown={startResizing}
/>
- {`Test Run${formatWorkflowRunIdentifier(workflowRunningData?.result.finished_at)}`}
+ {`Test Run${formatWorkflowRunIdentifier(workflowRunningData?.result.finished_at, workflowRunningData?.result.status)}`}
handleCancelDebugAndPreviewPanel()}>
-
+
@@ -217,7 +213,7 @@ const WorkflowPreview = () => {
Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) })
}}
>
-
+
{t('operation.copy', { ns: 'common' })}
)}
diff --git a/web/app/components/workflow/utils/common.ts b/web/app/components/workflow/utils/common.ts
index 8452161950..81bb22359c 100644
--- a/web/app/components/workflow/utils/common.ts
+++ b/web/app/components/workflow/utils/common.ts
@@ -41,8 +41,10 @@ export const isEventTargetInputArea = (target: HTMLElement) => {
* @returns Formatted string like " (14:30:25)" or " (Running)"
*/
export const formatWorkflowRunIdentifier = (finishedAt?: number, fallbackText = 'Running'): string => {
- if (!finishedAt)
- return ` (${fallbackText})`
+ if (!finishedAt) {
+ const capitalized = fallbackText.charAt(0).toUpperCase() + fallbackText.slice(1)
+ return ` (${capitalized})`
+ }
const date = new Date(finishedAt * 1000)
const timeStr = date.toLocaleTimeString([], {
diff --git a/web/service/use-workflow.ts b/web/service/use-workflow.ts
index 2f9f5d2fb7..fe20b906fc 100644
--- a/web/service/use-workflow.ts
+++ b/web/service/use-workflow.ts
@@ -26,14 +26,26 @@ export const useAppWorkflow = (appID: string) => {
})
}
+const WorkflowRunHistoryKey = [NAME_SPACE, 'runHistory']
+
export const useWorkflowRunHistory = (url?: string, enabled = true) => {
return useQuery
({
- queryKey: [NAME_SPACE, 'runHistory', url],
+ queryKey: [...WorkflowRunHistoryKey, url],
queryFn: () => get(url as string),
enabled: !!url && enabled,
+ staleTime: 0,
})
}
+export const useInvalidateWorkflowRunHistory = () => {
+ const queryClient = useQueryClient()
+ return (url: string) => {
+ queryClient.invalidateQueries({
+ queryKey: [...WorkflowRunHistoryKey, url],
+ })
+ }
+}
+
export const useInvalidateAppWorkflow = () => {
const queryClient = useQueryClient()
return (appID: string) => {
diff --git a/web/types/workflow.ts b/web/types/workflow.ts
index 156a704b48..f8a53c8d7e 100644
--- a/web/types/workflow.ts
+++ b/web/types/workflow.ts
@@ -4,7 +4,19 @@ import type { BeforeRunFormProps } from '@/app/components/workflow/nodes/_base/c
import type { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
import type { FormInputItem, UserAction } from '@/app/components/workflow/nodes/human-input/types'
import type { SpecialResultPanelProps } from '@/app/components/workflow/run/special-result-panel'
-import type { BlockEnum, CommonNodeType, ConversationVariable, Edge, EnvironmentVariable, InputVar, Node, ValueSelector, Variable, VarType } from '@/app/components/workflow/types'
+import type {
+ BlockEnum,
+ CommonNodeType,
+ ConversationVariable,
+ Edge,
+ EnvironmentVariable,
+ InputVar,
+ Node,
+ ValueSelector,
+ Variable,
+ VarType,
+ WorkflowRunningStatus,
+} from '@/app/components/workflow/types'
import type { RAGPipelineVariables } from '@/models/pipeline'
import type { TransferMethod } from '@/types/app'
@@ -372,7 +384,7 @@ export type WorkflowRunHistory = {
viewport?: Viewport
}
inputs: Record
- status: string
+ status: WorkflowRunningStatus
outputs: Record
error?: string
elapsed_time: number
From e9feeedc011df37946535d87ccd16e5e193664bb Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 11 Feb 2026 15:12:21 +0900
Subject: [PATCH 2/9] chore(deps): bump cryptography from 46.0.3 to 46.0.5 in
/api (#32218)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
api/uv.lock | 74 ++++++++++++++++++++++++++---------------------------
1 file changed, 36 insertions(+), 38 deletions(-)
diff --git a/api/uv.lock b/api/uv.lock
index 4eb5c42659..2ca8384f75 100644
--- a/api/uv.lock
+++ b/api/uv.lock
@@ -1237,49 +1237,47 @@ wheels = [
[[package]]
name = "cryptography"
-version = "46.0.3"
+version = "46.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" },
- { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" },
- { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" },
- { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" },
- { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" },
- { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" },
- { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" },
- { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" },
- { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" },
- { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" },
- { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" },
- { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" },
- { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" },
- { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" },
- { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" },
- { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" },
- { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" },
- { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" },
- { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" },
- { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" },
- { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" },
- { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" },
- { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" },
- { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" },
- { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" },
- { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" },
- { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" },
- { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" },
- { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" },
- { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" },
- { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" },
- { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" },
- { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" },
- { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" },
- { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" },
- { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" },
+ { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" },
+ { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" },
+ { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" },
+ { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" },
+ { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" },
+ { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" },
+ { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" },
+ { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" },
+ { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" },
+ { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" },
+ { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" },
+ { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" },
+ { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" },
]
[[package]]
From ce0192620d011a003f6106df6d38ef20b4bd296c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 11 Feb 2026 15:15:21 +0900
Subject: [PATCH 3/9] chore(deps): bump google-api-python-client from 2.90.0 to
2.189.0 in /api (#32102)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
api/pyproject.toml | 2 +-
api/uv.lock | 8 ++++----
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/api/pyproject.toml b/api/pyproject.toml
index a4efc3c313..42456aa267 100644
--- a/api/pyproject.toml
+++ b/api/pyproject.toml
@@ -23,7 +23,7 @@ dependencies = [
"gevent~=25.9.1",
"gmpy2~=2.2.1",
"google-api-core==2.18.0",
- "google-api-python-client==2.90.0",
+ "google-api-python-client==2.189.0",
"google-auth==2.29.0",
"google-auth-httplib2==0.2.0",
"google-cloud-aiplatform==1.49.0",
diff --git a/api/uv.lock b/api/uv.lock
index 2ca8384f75..a867a3226b 100644
--- a/api/uv.lock
+++ b/api/uv.lock
@@ -1592,7 +1592,7 @@ requires-dist = [
{ name = "gevent", specifier = "~=25.9.1" },
{ name = "gmpy2", specifier = "~=2.2.1" },
{ name = "google-api-core", specifier = "==2.18.0" },
- { name = "google-api-python-client", specifier = "==2.90.0" },
+ { name = "google-api-python-client", specifier = "==2.189.0" },
{ name = "google-auth", specifier = "==2.29.0" },
{ name = "google-auth-httplib2", specifier = "==0.2.0" },
{ name = "google-cloud-aiplatform", specifier = "==1.49.0" },
@@ -2304,7 +2304,7 @@ grpc = [
[[package]]
name = "google-api-python-client"
-version = "2.90.0"
+version = "2.189.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-api-core" },
@@ -2313,9 +2313,9 @@ dependencies = [
{ name = "httplib2" },
{ name = "uritemplate" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/35/8b/d990f947c261304a5c1599d45717d02c27d46af5f23e1fee5dc19c8fa79d/google-api-python-client-2.90.0.tar.gz", hash = "sha256:cbcb3ba8be37c6806676a49df16ac412077e5e5dc7fa967941eff977b31fba03", size = 10891311, upload-time = "2023-06-20T16:29:25.008Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/f8/0783aeca3410ee053d4dd1fccafd85197847b8f84dd038e036634605d083/google_api_python_client-2.189.0.tar.gz", hash = "sha256:45f2d8559b5c895dde6ad3fb33de025f5cb2c197fa5862f18df7f5295a172741", size = 13979470, upload-time = "2026-02-03T19:24:55.432Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/39/03/209b5c36a621ae644dc7d4743746cd3b38b18e133f8779ecaf6b95cc01ce/google_api_python_client-2.90.0-py2.py3-none-any.whl", hash = "sha256:4a41ffb7797d4f28e44635fb1e7076240b741c6493e7c3233c0e4421cec7c913", size = 11379891, upload-time = "2023-06-20T16:29:19.532Z" },
+ { url = "https://files.pythonhosted.org/packages/04/44/3677ff27998214f2fa7957359da48da378a0ffff1bd0bdaba42e752bc13e/google_api_python_client-2.189.0-py3-none-any.whl", hash = "sha256:a258c09660a49c6159173f8bbece171278e917e104a11f0640b34751b79c8a1a", size = 14547633, upload-time = "2026-02-03T19:24:52.845Z" },
]
[[package]]
From 378a1d7d08bd0ac5c75eaadc075a0f35211fcb8e Mon Sep 17 00:00:00 2001
From: veganmosfet <152803318+veganmosfet@users.noreply.github.com>
Date: Wed, 11 Feb 2026 07:22:30 +0100
Subject: [PATCH 4/9] Merge commit from fork
Removed the dangerous `new function` call during echarts parsing and replaced with an error message.
Co-authored-by: Byron Wang
---
.../base/markdown-blocks/code-block.tsx | 37 ++++---------------
1 file changed, 7 insertions(+), 30 deletions(-)
diff --git a/web/app/components/base/markdown-blocks/code-block.tsx b/web/app/components/base/markdown-blocks/code-block.tsx
index b9b3074351..744a578ff6 100644
--- a/web/app/components/base/markdown-blocks/code-block.tsx
+++ b/web/app/components/base/markdown-blocks/code-block.tsx
@@ -204,23 +204,10 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
}
}
catch {
- try {
- // eslint-disable-next-line no-new-func
- const result = new Function(`return ${trimmedContent}`)()
- if (typeof result === 'object' && result !== null) {
- setFinalChartOption(result)
- setChartState('success')
- processedRef.current = true
- return
- }
- }
- catch {
- // If we have a complete JSON structure but it doesn't parse,
- // it's likely an error rather than incomplete data
- setChartState('error')
- processedRef.current = true
- return
- }
+ // Avoid executing arbitrary code; require valid JSON for chart options.
+ setChartState('error')
+ processedRef.current = true
+ return
}
}
@@ -249,19 +236,9 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
}
}
catch {
- try {
- // eslint-disable-next-line no-new-func
- const result = new Function(`return ${trimmedContent}`)()
- if (typeof result === 'object' && result !== null) {
- setFinalChartOption(result)
- isValidOption = true
- }
- }
- catch {
- // Both parsing methods failed, but content looks complete
- setChartState('error')
- processedRef.current = true
- }
+ // Only accept JSON to avoid executing arbitrary code from the message.
+ setChartState('error')
+ processedRef.current = true
}
if (isValidOption) {
From 5b4c7b2a4002b4a330a2be4918e00fda89d9d899 Mon Sep 17 00:00:00 2001
From: Wu Tianwei <30284043+WTW0313@users.noreply.github.com>
Date: Wed, 11 Feb 2026 14:51:43 +0800
Subject: [PATCH 5/9] feat(tests): add mock for useInvalidateWorkflowRunHistory
in pipeline run tests (#32234)
---
.../components/rag-pipeline/hooks/use-pipeline-run.spec.ts | 5 +++++
web/eslint-suppressions.json | 2 +-
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/web/app/components/rag-pipeline/hooks/use-pipeline-run.spec.ts b/web/app/components/rag-pipeline/hooks/use-pipeline-run.spec.ts
index 2b21001839..c8a4a0ebb7 100644
--- a/web/app/components/rag-pipeline/hooks/use-pipeline-run.spec.ts
+++ b/web/app/components/rag-pipeline/hooks/use-pipeline-run.spec.ts
@@ -92,8 +92,10 @@ vi.mock('@/service/workflow', () => ({
}))
const mockInvalidAllLastRun = vi.fn()
+const mockInvalidateRunHistory = vi.fn()
vi.mock('@/service/use-workflow', () => ({
useInvalidAllLastRun: () => mockInvalidAllLastRun,
+ useInvalidateWorkflowRunHistory: () => mockInvalidateRunHistory,
}))
// Mock FlowType
@@ -472,6 +474,7 @@ describe('usePipelineRun', () => {
})
expect(onWorkflowStarted).toHaveBeenCalledWith({ task_id: 'task-1' })
+ expect(mockInvalidateRunHistory).toHaveBeenCalled()
})
it('should call onWorkflowFinished callback when provided', async () => {
@@ -493,6 +496,7 @@ describe('usePipelineRun', () => {
})
expect(onWorkflowFinished).toHaveBeenCalledWith({ status: 'succeeded' })
+ expect(mockInvalidateRunHistory).toHaveBeenCalled()
})
it('should call onError callback when provided', async () => {
@@ -514,6 +518,7 @@ describe('usePipelineRun', () => {
})
expect(onError).toHaveBeenCalledWith({ message: 'error' })
+ expect(mockInvalidateRunHistory).toHaveBeenCalled()
})
it('should call onNodeStarted callback when provided', async () => {
diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json
index 9293446c0c..a939fd7d2f 100644
--- a/web/eslint-suppressions.json
+++ b/web/eslint-suppressions.json
@@ -2299,7 +2299,7 @@
},
"app/components/base/markdown-blocks/code-block.tsx": {
"react-hooks-extra/no-direct-set-state-in-use-effect": {
- "count": 10
+ "count": 7
},
"tailwindcss/enforce-consistent-class-order": {
"count": 1
From 2f87ecc0ce1d05ab3ef9537f29b84a5fa5823017 Mon Sep 17 00:00:00 2001
From: wangxiaolei
Date: Wed, 11 Feb 2026 15:53:51 +0800
Subject: [PATCH 6/9] fix: fix use fastopenapi lead user is anonymouse (#32236)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
---
api/controllers/console/remote_files.py | 129 +++----
.../console/test_fastopenapi_remote_files.py | 336 ++++++++++++++----
.../unit_tests/core/schemas/test_resolver.py | 3 +
3 files changed, 335 insertions(+), 133 deletions(-)
diff --git a/api/controllers/console/remote_files.py b/api/controllers/console/remote_files.py
index 88a9ce3a79..b7a2f230e1 100644
--- a/api/controllers/console/remote_files.py
+++ b/api/controllers/console/remote_files.py
@@ -1,6 +1,7 @@
import urllib.parse
import httpx
+from flask_restx import Resource
from pydantic import BaseModel, Field
import services
@@ -10,12 +11,12 @@ from controllers.common.errors import (
RemoteFileUploadError,
UnsupportedFileTypeError,
)
-from controllers.fastopenapi import console_router
+from controllers.console import console_ns
from core.file import helpers as file_helpers
from core.helper import ssrf_proxy
from extensions.ext_database import db
from fields.file_fields import FileWithSignedUrl, RemoteFileInfo
-from libs.login import current_account_with_tenant
+from libs.login import current_account_with_tenant, login_required
from services.file_service import FileService
@@ -23,69 +24,73 @@ class RemoteFileUploadPayload(BaseModel):
url: str = Field(..., description="URL to fetch")
-@console_router.get(
- "/remote-files/",
- response_model=RemoteFileInfo,
- tags=["console"],
-)
-def get_remote_file_info(url: str) -> RemoteFileInfo:
- decoded_url = urllib.parse.unquote(url)
- resp = ssrf_proxy.head(decoded_url)
- if resp.status_code != httpx.codes.OK:
- resp = ssrf_proxy.get(decoded_url, timeout=3)
- resp.raise_for_status()
- return RemoteFileInfo(
- file_type=resp.headers.get("Content-Type", "application/octet-stream"),
- file_length=int(resp.headers.get("Content-Length", 0)),
- )
-
-
-@console_router.post(
- "/remote-files/upload",
- response_model=FileWithSignedUrl,
- tags=["console"],
- status_code=201,
-)
-def upload_remote_file(payload: RemoteFileUploadPayload) -> FileWithSignedUrl:
- url = payload.url
-
- try:
- resp = ssrf_proxy.head(url=url)
+@console_ns.route("/remote-files/")
+class GetRemoteFileInfo(Resource):
+ @login_required
+ def get(self, url: str):
+ decoded_url = urllib.parse.unquote(url)
+ resp = ssrf_proxy.head(decoded_url)
if resp.status_code != httpx.codes.OK:
- resp = ssrf_proxy.get(url=url, timeout=3, follow_redirects=True)
- if resp.status_code != httpx.codes.OK:
- raise RemoteFileUploadError(f"Failed to fetch file from {url}: {resp.text}")
- except httpx.RequestError as e:
- raise RemoteFileUploadError(f"Failed to fetch file from {url}: {str(e)}")
+ resp = ssrf_proxy.get(decoded_url, timeout=3)
+ resp.raise_for_status()
+ return RemoteFileInfo(
+ file_type=resp.headers.get("Content-Type", "application/octet-stream"),
+ file_length=int(resp.headers.get("Content-Length", 0)),
+ ).model_dump(mode="json")
- file_info = helpers.guess_file_info_from_response(resp)
- if not FileService.is_file_size_within_limit(extension=file_info.extension, file_size=file_info.size):
- raise FileTooLargeError
+@console_ns.route("/remote-files/upload")
+class RemoteFileUpload(Resource):
+ @login_required
+ def post(self):
+ payload = RemoteFileUploadPayload.model_validate(console_ns.payload)
+ url = payload.url
- content = resp.content if resp.request.method == "GET" else ssrf_proxy.get(url).content
+ # Try to fetch remote file metadata/content first
+ try:
+ resp = ssrf_proxy.head(url=url)
+ if resp.status_code != httpx.codes.OK:
+ resp = ssrf_proxy.get(url=url, timeout=3, follow_redirects=True)
+ if resp.status_code != httpx.codes.OK:
+ # Normalize into a user-friendly error message expected by tests
+ raise RemoteFileUploadError(f"Failed to fetch file from {url}: {resp.text}")
+ except httpx.RequestError as e:
+ raise RemoteFileUploadError(f"Failed to fetch file from {url}: {str(e)}")
- try:
- user, _ = current_account_with_tenant()
- upload_file = FileService(db.engine).upload_file(
- filename=file_info.filename,
- content=content,
- mimetype=file_info.mimetype,
- user=user,
- source_url=url,
+ file_info = helpers.guess_file_info_from_response(resp)
+
+ # Enforce file size limit with 400 (Bad Request) per tests' expectation
+ if not FileService.is_file_size_within_limit(extension=file_info.extension, file_size=file_info.size):
+ raise FileTooLargeError()
+
+ # Load content if needed
+ content = resp.content if resp.request.method == "GET" else ssrf_proxy.get(url).content
+
+ try:
+ user, _ = current_account_with_tenant()
+ upload_file = FileService(db.engine).upload_file(
+ filename=file_info.filename,
+ content=content,
+ mimetype=file_info.mimetype,
+ user=user,
+ source_url=url,
+ )
+ except services.errors.file.FileTooLargeError as file_too_large_error:
+ raise FileTooLargeError(file_too_large_error.description)
+ except services.errors.file.UnsupportedFileTypeError:
+ raise UnsupportedFileTypeError()
+
+ # Success: return created resource with 201 status
+ return (
+ FileWithSignedUrl(
+ id=upload_file.id,
+ name=upload_file.name,
+ size=upload_file.size,
+ extension=upload_file.extension,
+ url=file_helpers.get_signed_file_url(upload_file_id=upload_file.id),
+ mime_type=upload_file.mime_type,
+ created_by=upload_file.created_by,
+ created_at=int(upload_file.created_at.timestamp()),
+ ).model_dump(mode="json"),
+ 201,
)
- except services.errors.file.FileTooLargeError as file_too_large_error:
- raise FileTooLargeError(file_too_large_error.description)
- except services.errors.file.UnsupportedFileTypeError:
- raise UnsupportedFileTypeError()
-
- return FileWithSignedUrl(
- id=upload_file.id,
- name=upload_file.name,
- size=upload_file.size,
- extension=upload_file.extension,
- url=file_helpers.get_signed_file_url(upload_file_id=upload_file.id),
- mime_type=upload_file.mime_type,
- created_by=upload_file.created_by,
- created_at=int(upload_file.created_at.timestamp()),
- )
diff --git a/api/tests/unit_tests/controllers/console/test_fastopenapi_remote_files.py b/api/tests/unit_tests/controllers/console/test_fastopenapi_remote_files.py
index cb2604cf1c..c0a984e216 100644
--- a/api/tests/unit_tests/controllers/console/test_fastopenapi_remote_files.py
+++ b/api/tests/unit_tests/controllers/console/test_fastopenapi_remote_files.py
@@ -1,92 +1,286 @@
-import builtins
+"""Tests for remote file upload API endpoints using Flask-RESTX."""
+
+import contextlib
from datetime import datetime
from types import SimpleNamespace
-from unittest.mock import patch
+from unittest.mock import Mock, patch
import httpx
import pytest
-from flask import Flask
-from flask.views import MethodView
-
-from extensions import ext_fastopenapi
-
-if not hasattr(builtins, "MethodView"):
- builtins.MethodView = MethodView # type: ignore[attr-defined]
+from flask import Flask, g
@pytest.fixture
def app() -> Flask:
+ """Create Flask app for testing."""
app = Flask(__name__)
app.config["TESTING"] = True
+ app.config["SECRET_KEY"] = "test-secret-key"
return app
-def test_console_remote_files_fastopenapi_get_info(app: Flask):
- ext_fastopenapi.init_app(app)
+@pytest.fixture
+def client(app):
+ """Create test client with console blueprint registered."""
+ from controllers.console import bp
- response = httpx.Response(
- 200,
- request=httpx.Request("HEAD", "http://example.com/file.txt"),
- headers={"Content-Type": "text/plain", "Content-Length": "10"},
- )
-
- with patch("controllers.console.remote_files.ssrf_proxy.head", return_value=response):
- client = app.test_client()
- encoded_url = "http%3A%2F%2Fexample.com%2Ffile.txt"
- resp = client.get(f"/console/api/remote-files/{encoded_url}")
-
- assert resp.status_code == 200
- assert resp.get_json() == {"file_type": "text/plain", "file_length": 10}
+ app.register_blueprint(bp)
+ return app.test_client()
-def test_console_remote_files_fastopenapi_upload(app: Flask):
- ext_fastopenapi.init_app(app)
+@pytest.fixture
+def mock_account():
+ """Create a mock account for testing."""
+ from models import Account
- head_response = httpx.Response(
- 200,
- request=httpx.Request("GET", "http://example.com/file.txt"),
- content=b"hello",
- )
- file_info = SimpleNamespace(
- extension="txt",
- size=5,
- filename="file.txt",
- mimetype="text/plain",
- )
- uploaded = SimpleNamespace(
- id="file-id",
- name="file.txt",
- size=5,
- extension="txt",
- mime_type="text/plain",
- created_by="user-id",
- created_at=datetime(2024, 1, 1),
- )
+ account = Mock(spec=Account)
+ account.id = "test-account-id"
+ account.current_tenant_id = "test-tenant-id"
+ return account
- with (
- patch("controllers.console.remote_files.db", new=SimpleNamespace(engine=object())),
- patch("controllers.console.remote_files.ssrf_proxy.head", return_value=head_response),
- patch("controllers.console.remote_files.helpers.guess_file_info_from_response", return_value=file_info),
- patch("controllers.console.remote_files.FileService.is_file_size_within_limit", return_value=True),
- patch("controllers.console.remote_files.FileService.__init__", return_value=None),
- patch("controllers.console.remote_files.current_account_with_tenant", return_value=(object(), "tenant-id")),
- patch("controllers.console.remote_files.FileService.upload_file", return_value=uploaded),
- patch("controllers.console.remote_files.file_helpers.get_signed_file_url", return_value="signed-url"),
- ):
- client = app.test_client()
- resp = client.post(
- "/console/api/remote-files/upload",
- json={"url": "http://example.com/file.txt"},
+
+@pytest.fixture
+def auth_ctx(app, mock_account):
+ """Context manager to set auth/tenant context in flask.g for a request."""
+
+ @contextlib.contextmanager
+ def _ctx():
+ with app.test_request_context():
+ g._login_user = mock_account
+ g._current_tenant = mock_account.current_tenant_id
+ yield
+
+ return _ctx
+
+
+class TestGetRemoteFileInfo:
+ """Test GET /console/api/remote-files/ endpoint."""
+
+ def test_get_remote_file_info_success(self, app, client, mock_account):
+ """Test successful retrieval of remote file info."""
+ response = httpx.Response(
+ 200,
+ request=httpx.Request("HEAD", "http://example.com/file.txt"),
+ headers={"Content-Type": "text/plain", "Content-Length": "1024"},
)
- assert resp.status_code == 201
- assert resp.get_json() == {
- "id": "file-id",
- "name": "file.txt",
- "size": 5,
- "extension": "txt",
- "url": "signed-url",
- "mime_type": "text/plain",
- "created_by": "user-id",
- "created_at": int(uploaded.created_at.timestamp()),
- }
+ with (
+ patch(
+ "controllers.console.remote_files.current_account_with_tenant",
+ return_value=(mock_account, "test-tenant-id"),
+ ),
+ patch("controllers.console.remote_files.ssrf_proxy.head", return_value=response),
+ patch("libs.login.check_csrf_token", return_value=None),
+ ):
+ with app.test_request_context():
+ g._login_user = mock_account
+ g._current_tenant = mock_account.current_tenant_id
+ encoded_url = "http%3A%2F%2Fexample.com%2Ffile.txt"
+ resp = client.get(f"/console/api/remote-files/{encoded_url}")
+
+ assert resp.status_code == 200
+ data = resp.get_json()
+ assert data["file_type"] == "text/plain"
+ assert data["file_length"] == 1024
+
+ def test_get_remote_file_info_fallback_to_get_on_head_failure(self, app, client, mock_account):
+ """Test fallback to GET when HEAD returns non-200 status."""
+ head_response = httpx.Response(
+ 404,
+ request=httpx.Request("HEAD", "http://example.com/file.pdf"),
+ )
+ get_response = httpx.Response(
+ 200,
+ request=httpx.Request("GET", "http://example.com/file.pdf"),
+ headers={"Content-Type": "application/pdf", "Content-Length": "2048"},
+ )
+
+ with (
+ patch(
+ "controllers.console.remote_files.current_account_with_tenant",
+ return_value=(mock_account, "test-tenant-id"),
+ ),
+ patch("controllers.console.remote_files.ssrf_proxy.head", return_value=head_response),
+ patch("controllers.console.remote_files.ssrf_proxy.get", return_value=get_response),
+ patch("libs.login.check_csrf_token", return_value=None),
+ ):
+ with app.test_request_context():
+ g._login_user = mock_account
+ g._current_tenant = mock_account.current_tenant_id
+ encoded_url = "http%3A%2F%2Fexample.com%2Ffile.pdf"
+ resp = client.get(f"/console/api/remote-files/{encoded_url}")
+
+ assert resp.status_code == 200
+ data = resp.get_json()
+ assert data["file_type"] == "application/pdf"
+ assert data["file_length"] == 2048
+
+
+class TestRemoteFileUpload:
+ """Test POST /console/api/remote-files/upload endpoint."""
+
+ @pytest.mark.parametrize(
+ ("head_status", "use_get"),
+ [
+ (200, False), # HEAD succeeds
+ (405, True), # HEAD fails -> fallback GET
+ ],
+ )
+ def test_upload_remote_file_success_paths(self, client, mock_account, auth_ctx, head_status, use_get):
+ url = "http://example.com/file.pdf"
+ head_resp = httpx.Response(
+ head_status,
+ request=httpx.Request("HEAD", url),
+ headers={"Content-Type": "application/pdf", "Content-Length": "1024"},
+ )
+ get_resp = httpx.Response(
+ 200,
+ request=httpx.Request("GET", url),
+ headers={"Content-Type": "application/pdf", "Content-Length": "1024"},
+ content=b"file content",
+ )
+
+ file_info = SimpleNamespace(
+ extension="pdf",
+ size=1024,
+ filename="file.pdf",
+ mimetype="application/pdf",
+ )
+ uploaded_file = SimpleNamespace(
+ id="uploaded-file-id",
+ name="file.pdf",
+ size=1024,
+ extension="pdf",
+ mime_type="application/pdf",
+ created_by="test-account-id",
+ created_at=datetime(2024, 1, 1, 12, 0, 0),
+ )
+
+ with (
+ patch(
+ "controllers.console.remote_files.current_account_with_tenant",
+ return_value=(mock_account, "test-tenant-id"),
+ ),
+ patch("controllers.console.remote_files.ssrf_proxy.head", return_value=head_resp) as p_head,
+ patch("controllers.console.remote_files.ssrf_proxy.get", return_value=get_resp) as p_get,
+ patch(
+ "controllers.console.remote_files.helpers.guess_file_info_from_response",
+ return_value=file_info,
+ ),
+ patch(
+ "controllers.console.remote_files.FileService.is_file_size_within_limit",
+ return_value=True,
+ ),
+ patch("controllers.console.remote_files.db", spec=["engine"]),
+ patch("controllers.console.remote_files.FileService") as mock_file_service,
+ patch(
+ "controllers.console.remote_files.file_helpers.get_signed_file_url",
+ return_value="http://example.com/signed-url",
+ ),
+ patch("libs.login.check_csrf_token", return_value=None),
+ ):
+ mock_file_service.return_value.upload_file.return_value = uploaded_file
+
+ with auth_ctx():
+ resp = client.post(
+ "/console/api/remote-files/upload",
+ json={"url": url},
+ )
+
+ assert resp.status_code == 201
+ p_head.assert_called_once()
+ # GET is used either for fallback (HEAD fails) or to fetch content after HEAD succeeds
+ p_get.assert_called_once()
+ mock_file_service.return_value.upload_file.assert_called_once()
+
+ data = resp.get_json()
+ assert data["id"] == "uploaded-file-id"
+ assert data["name"] == "file.pdf"
+ assert data["size"] == 1024
+ assert data["extension"] == "pdf"
+ assert data["url"] == "http://example.com/signed-url"
+ assert data["mime_type"] == "application/pdf"
+ assert data["created_by"] == "test-account-id"
+
+ @pytest.mark.parametrize(
+ ("size_ok", "raises", "expected_status", "expected_msg"),
+ [
+ # When size check fails in controller, API returns 413 with message "File size exceeded..."
+ (False, None, 413, "file size exceeded"),
+ # When service raises unsupported type, controller maps to 415 with message "File type not allowed."
+ (True, "unsupported", 415, "file type not allowed"),
+ ],
+ )
+ def test_upload_remote_file_errors(
+ self, client, mock_account, auth_ctx, size_ok, raises, expected_status, expected_msg
+ ):
+ url = "http://example.com/x.pdf"
+ head_resp = httpx.Response(
+ 200,
+ request=httpx.Request("HEAD", url),
+ headers={"Content-Type": "application/pdf", "Content-Length": "9"},
+ )
+ file_info = SimpleNamespace(extension="pdf", size=9, filename="x.pdf", mimetype="application/pdf")
+
+ with (
+ patch(
+ "controllers.console.remote_files.current_account_with_tenant",
+ return_value=(mock_account, "test-tenant-id"),
+ ),
+ patch("controllers.console.remote_files.ssrf_proxy.head", return_value=head_resp),
+ patch(
+ "controllers.console.remote_files.helpers.guess_file_info_from_response",
+ return_value=file_info,
+ ),
+ patch(
+ "controllers.console.remote_files.FileService.is_file_size_within_limit",
+ return_value=size_ok,
+ ),
+ patch("controllers.console.remote_files.db", spec=["engine"]),
+ patch("libs.login.check_csrf_token", return_value=None),
+ ):
+ if raises == "unsupported":
+ from services.errors.file import UnsupportedFileTypeError
+
+ with patch("controllers.console.remote_files.FileService") as mock_file_service:
+ mock_file_service.return_value.upload_file.side_effect = UnsupportedFileTypeError("bad")
+ with auth_ctx():
+ resp = client.post(
+ "/console/api/remote-files/upload",
+ json={"url": url},
+ )
+ else:
+ with auth_ctx():
+ resp = client.post(
+ "/console/api/remote-files/upload",
+ json={"url": url},
+ )
+
+ assert resp.status_code == expected_status
+ data = resp.get_json()
+ msg = (data.get("error") or {}).get("message") or data.get("message", "")
+ assert expected_msg in msg.lower()
+
+ def test_upload_remote_file_fetch_failure(self, client, mock_account, auth_ctx):
+ """Test upload when fetching of remote file fails."""
+ with (
+ patch(
+ "controllers.console.remote_files.current_account_with_tenant",
+ return_value=(mock_account, "test-tenant-id"),
+ ),
+ patch(
+ "controllers.console.remote_files.ssrf_proxy.head",
+ side_effect=httpx.RequestError("Connection failed"),
+ ),
+ patch("libs.login.check_csrf_token", return_value=None),
+ ):
+ with auth_ctx():
+ resp = client.post(
+ "/console/api/remote-files/upload",
+ json={"url": "http://unreachable.com/file.pdf"},
+ )
+
+ assert resp.status_code == 400
+ data = resp.get_json()
+ msg = (data.get("error") or {}).get("message") or data.get("message", "")
+ assert "failed to fetch" in msg.lower()
diff --git a/api/tests/unit_tests/core/schemas/test_resolver.py b/api/tests/unit_tests/core/schemas/test_resolver.py
index eda8bf4343..239ee85346 100644
--- a/api/tests/unit_tests/core/schemas/test_resolver.py
+++ b/api/tests/unit_tests/core/schemas/test_resolver.py
@@ -496,6 +496,9 @@ class TestSchemaResolverClass:
avg_time_no_cache = sum(results1) / len(results1)
# Second run (with cache) - run multiple times
+ # Warm up cache first
+ resolve_dify_schema_refs(schema)
+
results2 = []
for _ in range(3):
start = time.perf_counter()
From 7e0bccbbf0722fb82f35ab5d2766d96155f01174 Mon Sep 17 00:00:00 2001
From: hj24
Date: Wed, 11 Feb 2026 16:07:52 +0800
Subject: [PATCH 7/9] fix: update index to optimize message clean performance
(#32238)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
---
api/.env.example | 1 +
api/configs/feature/__init__.py | 4 ++
...ix_index_to_optimize_message_clean_job_.py | 39 ++++++++++++
api/models/model.py | 1 -
api/models/web.py | 1 +
.../conversation/messages_clean_service.py | 60 +++++++++++++++++--
...ear_free_plan_expired_workflow_run_logs.py | 50 ++++++++++++++++
docker/.env.example | 1 +
docker/docker-compose.yaml | 1 +
9 files changed, 151 insertions(+), 7 deletions(-)
create mode 100644 api/migrations/versions/2026_02_11_1549-fce013ca180e_fix_index_to_optimize_message_clean_job_.py
diff --git a/api/.env.example b/api/.env.example
index fcadfa1c3b..554b1624ec 100644
--- a/api/.env.example
+++ b/api/.env.example
@@ -715,6 +715,7 @@ ANNOTATION_IMPORT_MAX_CONCURRENT=5
# Sandbox expired records clean configuration
SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD=21
SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_SIZE=1000
+SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_MAX_INTERVAL=200
SANDBOX_EXPIRED_RECORDS_RETENTION_DAYS=30
SANDBOX_EXPIRED_RECORDS_CLEAN_TASK_LOCK_TTL=90000
diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py
index ded5cf03ab..46dad6fc05 100644
--- a/api/configs/feature/__init__.py
+++ b/api/configs/feature/__init__.py
@@ -1344,6 +1344,10 @@ class SandboxExpiredRecordsCleanConfig(BaseSettings):
description="Maximum number of records to process in each batch",
default=1000,
)
+ SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_MAX_INTERVAL: PositiveInt = Field(
+ description="Maximum interval in milliseconds between batches",
+ default=200,
+ )
SANDBOX_EXPIRED_RECORDS_RETENTION_DAYS: PositiveInt = Field(
description="Retention days for sandbox expired workflow_run records and message records",
default=30,
diff --git a/api/migrations/versions/2026_02_11_1549-fce013ca180e_fix_index_to_optimize_message_clean_job_.py b/api/migrations/versions/2026_02_11_1549-fce013ca180e_fix_index_to_optimize_message_clean_job_.py
new file mode 100644
index 0000000000..ed482fbd6d
--- /dev/null
+++ b/api/migrations/versions/2026_02_11_1549-fce013ca180e_fix_index_to_optimize_message_clean_job_.py
@@ -0,0 +1,39 @@
+"""fix index to optimize message clean job performance
+
+Revision ID: fce013ca180e
+Revises: f55813ffe2c8
+Create Date: 2026-02-11 15:49:17.603638
+
+"""
+from alembic import op
+import models as models
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'fce013ca180e'
+down_revision = 'f55813ffe2c8'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('messages', schema=None) as batch_op:
+ batch_op.drop_index(batch_op.f('message_created_at_idx'))
+
+ with op.batch_alter_table('saved_messages', schema=None) as batch_op:
+ batch_op.create_index('saved_message_message_id_idx', ['message_id'], unique=False)
+
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('saved_messages', schema=None) as batch_op:
+ batch_op.drop_index('saved_message_message_id_idx')
+
+ with op.batch_alter_table('messages', schema=None) as batch_op:
+ batch_op.create_index(batch_op.f('message_created_at_idx'), ['created_at'], unique=False)
+
+ # ### end Alembic commands ###
diff --git a/api/models/model.py b/api/models/model.py
index be4e5b819a..e2a9bb70cf 100644
--- a/api/models/model.py
+++ b/api/models/model.py
@@ -1040,7 +1040,6 @@ class Message(Base):
Index("message_end_user_idx", "app_id", "from_source", "from_end_user_id"),
Index("message_account_idx", "app_id", "from_source", "from_account_id"),
Index("message_workflow_run_id_idx", "conversation_id", "workflow_run_id"),
- Index("message_created_at_idx", "created_at"),
Index("message_app_mode_idx", "app_mode"),
Index("message_created_at_id_idx", "created_at", "id"),
)
diff --git a/api/models/web.py b/api/models/web.py
index b2832aa163..5f6a7b40bf 100644
--- a/api/models/web.py
+++ b/api/models/web.py
@@ -16,6 +16,7 @@ class SavedMessage(TypeBase):
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="saved_message_pkey"),
sa.Index("saved_message_message_idx", "app_id", "message_id", "created_by_role", "created_by"),
+ sa.Index("saved_message_message_id_idx", "message_id"),
)
id: Mapped[str] = mapped_column(
diff --git a/api/services/retention/conversation/messages_clean_service.py b/api/services/retention/conversation/messages_clean_service.py
index 3ca5d82860..f7836a2b14 100644
--- a/api/services/retention/conversation/messages_clean_service.py
+++ b/api/services/retention/conversation/messages_clean_service.py
@@ -1,10 +1,13 @@
import datetime
import logging
+import os
import random
+import time
from collections.abc import Sequence
from typing import cast
-from sqlalchemy import delete, select
+import sqlalchemy as sa
+from sqlalchemy import delete, select, tuple_
from sqlalchemy.engine import CursorResult
from sqlalchemy.orm import Session
@@ -193,11 +196,15 @@ class MessagesCleanService:
self._end_before,
)
+ max_batch_interval_ms = int(os.environ.get("SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_MAX_INTERVAL", 200))
+
while True:
stats["batches"] += 1
+ batch_start = time.monotonic()
# Step 1: Fetch a batch of messages using cursor
with Session(db.engine, expire_on_commit=False) as session:
+ fetch_messages_start = time.monotonic()
msg_stmt = (
select(Message.id, Message.app_id, Message.created_at)
.where(Message.created_at < self._end_before)
@@ -209,13 +216,13 @@ class MessagesCleanService:
msg_stmt = msg_stmt.where(Message.created_at >= self._start_from)
# Apply cursor condition: (created_at, id) > (last_created_at, last_message_id)
- # This translates to:
- # created_at > last_created_at OR (created_at = last_created_at AND id > last_message_id)
if _cursor:
- # Continuing from previous batch
msg_stmt = msg_stmt.where(
- (Message.created_at > _cursor[0])
- | ((Message.created_at == _cursor[0]) & (Message.id > _cursor[1]))
+ tuple_(Message.created_at, Message.id)
+ > tuple_(
+ sa.literal(_cursor[0], type_=sa.DateTime()),
+ sa.literal(_cursor[1], type_=Message.id.type),
+ )
)
raw_messages = list(session.execute(msg_stmt).all())
@@ -223,6 +230,12 @@ class MessagesCleanService:
SimpleMessage(id=msg_id, app_id=app_id, created_at=msg_created_at)
for msg_id, app_id, msg_created_at in raw_messages
]
+ logger.info(
+ "clean_messages (batch %s): fetched %s messages in %sms",
+ stats["batches"],
+ len(messages),
+ int((time.monotonic() - fetch_messages_start) * 1000),
+ )
# Track total messages fetched across all batches
stats["total_messages"] += len(messages)
@@ -241,8 +254,16 @@ class MessagesCleanService:
logger.info("clean_messages (batch %s): no app_ids found, skip", stats["batches"])
continue
+ fetch_apps_start = time.monotonic()
app_stmt = select(App.id, App.tenant_id).where(App.id.in_(app_ids))
apps = list(session.execute(app_stmt).all())
+ logger.info(
+ "clean_messages (batch %s): fetched %s apps for %s app_ids in %sms",
+ stats["batches"],
+ len(apps),
+ len(app_ids),
+ int((time.monotonic() - fetch_apps_start) * 1000),
+ )
if not apps:
logger.info("clean_messages (batch %s): no apps found, skip", stats["batches"])
@@ -252,7 +273,15 @@ class MessagesCleanService:
app_to_tenant: dict[str, str] = {app.id: app.tenant_id for app in apps}
# Step 3: Delegate to policy to determine which messages to delete
+ policy_start = time.monotonic()
message_ids_to_delete = self._policy.filter_message_ids(messages, app_to_tenant)
+ logger.info(
+ "clean_messages (batch %s): policy selected %s/%s messages in %sms",
+ stats["batches"],
+ len(message_ids_to_delete),
+ len(messages),
+ int((time.monotonic() - policy_start) * 1000),
+ )
if not message_ids_to_delete:
logger.info("clean_messages (batch %s): no messages to delete, skip", stats["batches"])
@@ -263,14 +292,20 @@ class MessagesCleanService:
# Step 4: Batch delete messages and their relations
if not self._dry_run:
with Session(db.engine, expire_on_commit=False) as session:
+ delete_relations_start = time.monotonic()
# Delete related records first
self._batch_delete_message_relations(session, message_ids_to_delete)
+ delete_relations_ms = int((time.monotonic() - delete_relations_start) * 1000)
# Delete messages
+ delete_messages_start = time.monotonic()
delete_stmt = delete(Message).where(Message.id.in_(message_ids_to_delete))
delete_result = cast(CursorResult, session.execute(delete_stmt))
messages_deleted = delete_result.rowcount
+ delete_messages_ms = int((time.monotonic() - delete_messages_start) * 1000)
+ commit_start = time.monotonic()
session.commit()
+ commit_ms = int((time.monotonic() - commit_start) * 1000)
stats["total_deleted"] += messages_deleted
@@ -280,6 +315,19 @@ class MessagesCleanService:
len(messages),
messages_deleted,
)
+ logger.info(
+ "clean_messages (batch %s): relations %sms, messages %sms, commit %sms, batch total %sms",
+ stats["batches"],
+ delete_relations_ms,
+ delete_messages_ms,
+ commit_ms,
+ int((time.monotonic() - batch_start) * 1000),
+ )
+
+ # Random sleep between batches to avoid overwhelming the database
+ sleep_ms = random.uniform(0, max_batch_interval_ms) # noqa: S311
+ logger.info("clean_messages (batch %s): sleeping for %.2fms", stats["batches"], sleep_ms)
+ time.sleep(sleep_ms / 1000)
else:
# Log random sample of message IDs that would be deleted (up to 10)
sample_size = min(10, len(message_ids_to_delete))
diff --git a/api/services/retention/workflow_run/clear_free_plan_expired_workflow_run_logs.py b/api/services/retention/workflow_run/clear_free_plan_expired_workflow_run_logs.py
index c3e0dce399..2c94cb5324 100644
--- a/api/services/retention/workflow_run/clear_free_plan_expired_workflow_run_logs.py
+++ b/api/services/retention/workflow_run/clear_free_plan_expired_workflow_run_logs.py
@@ -1,5 +1,8 @@
import datetime
import logging
+import os
+import random
+import time
from collections.abc import Iterable, Sequence
import click
@@ -72,7 +75,12 @@ class WorkflowRunCleanup:
batch_index = 0
last_seen: tuple[datetime.datetime, str] | None = None
+ max_batch_interval_ms = int(os.environ.get("SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_MAX_INTERVAL", 200))
+
while True:
+ batch_start = time.monotonic()
+
+ fetch_start = time.monotonic()
run_rows = self.workflow_run_repo.get_runs_batch_by_time_range(
start_from=self.window_start,
end_before=self.window_end,
@@ -80,12 +88,30 @@ class WorkflowRunCleanup:
batch_size=self.batch_size,
)
if not run_rows:
+ logger.info("workflow_run_cleanup (batch #%s): no more rows to process", batch_index + 1)
break
batch_index += 1
last_seen = (run_rows[-1].created_at, run_rows[-1].id)
+ logger.info(
+ "workflow_run_cleanup (batch #%s): fetched %s rows in %sms",
+ batch_index,
+ len(run_rows),
+ int((time.monotonic() - fetch_start) * 1000),
+ )
+
tenant_ids = {row.tenant_id for row in run_rows}
+
+ filter_start = time.monotonic()
free_tenants = self._filter_free_tenants(tenant_ids)
+ logger.info(
+ "workflow_run_cleanup (batch #%s): filtered %s free tenants from %s tenants in %sms",
+ batch_index,
+ len(free_tenants),
+ len(tenant_ids),
+ int((time.monotonic() - filter_start) * 1000),
+ )
+
free_runs = [row for row in run_rows if row.tenant_id in free_tenants]
paid_or_skipped = len(run_rows) - len(free_runs)
@@ -104,11 +130,17 @@ class WorkflowRunCleanup:
total_runs_targeted += len(free_runs)
if self.dry_run:
+ count_start = time.monotonic()
batch_counts = self.workflow_run_repo.count_runs_with_related(
free_runs,
count_node_executions=self._count_node_executions,
count_trigger_logs=self._count_trigger_logs,
)
+ logger.info(
+ "workflow_run_cleanup (batch #%s, dry_run): counted related records in %sms",
+ batch_index,
+ int((time.monotonic() - count_start) * 1000),
+ )
if related_totals is not None:
for key in related_totals:
related_totals[key] += batch_counts.get(key, 0)
@@ -120,14 +152,21 @@ class WorkflowRunCleanup:
fg="yellow",
)
)
+ logger.info(
+ "workflow_run_cleanup (batch #%s, dry_run): batch total %sms",
+ batch_index,
+ int((time.monotonic() - batch_start) * 1000),
+ )
continue
try:
+ delete_start = time.monotonic()
counts = self.workflow_run_repo.delete_runs_with_related(
free_runs,
delete_node_executions=self._delete_node_executions,
delete_trigger_logs=self._delete_trigger_logs,
)
+ delete_ms = int((time.monotonic() - delete_start) * 1000)
except Exception:
logger.exception("Failed to delete workflow runs batch ending at %s", last_seen[0])
raise
@@ -143,6 +182,17 @@ class WorkflowRunCleanup:
fg="green",
)
)
+ logger.info(
+ "workflow_run_cleanup (batch #%s): delete %sms, batch total %sms",
+ batch_index,
+ delete_ms,
+ int((time.monotonic() - batch_start) * 1000),
+ )
+
+ # Random sleep between batches to avoid overwhelming the database
+ sleep_ms = random.uniform(0, max_batch_interval_ms) # noqa: S311
+ logger.info("workflow_run_cleanup (batch #%s): sleeping for %.2fms", batch_index, sleep_ms)
+ time.sleep(sleep_ms / 1000)
if self.dry_run:
if self.window_start:
diff --git a/docker/.env.example b/docker/.env.example
index c8db23b9ed..4a141e37d4 100644
--- a/docker/.env.example
+++ b/docker/.env.example
@@ -1523,6 +1523,7 @@ AMPLITUDE_API_KEY=
# Sandbox expired records clean configuration
SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD=21
SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_SIZE=1000
+SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_MAX_INTERVAL=200
SANDBOX_EXPIRED_RECORDS_RETENTION_DAYS=30
diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml
index afd64963c4..1f38512661 100644
--- a/docker/docker-compose.yaml
+++ b/docker/docker-compose.yaml
@@ -684,6 +684,7 @@ x-shared-env: &shared-api-worker-env
AMPLITUDE_API_KEY: ${AMPLITUDE_API_KEY:-}
SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD: ${SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD:-21}
SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_SIZE: ${SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_SIZE:-1000}
+ SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_MAX_INTERVAL: ${SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_MAX_INTERVAL:-200}
SANDBOX_EXPIRED_RECORDS_RETENTION_DAYS: ${SANDBOX_EXPIRED_RECORDS_RETENTION_DAYS:-30}
PUBSUB_REDIS_URL: ${PUBSUB_REDIS_URL:-}
PUBSUB_REDIS_CHANNEL_TYPE: ${PUBSUB_REDIS_CHANNEL_TYPE:-pubsub}
From b4fec9b7aa7713f292b656ba3be4b1c6697a26ff Mon Sep 17 00:00:00 2001
From: NFish
Date: Wed, 11 Feb 2026 16:31:12 +0800
Subject: [PATCH 8/9] fix: hide invite button if current user is not workspace
manager (#31744)
---
.../components/header/account-setting/members-page/index.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/app/components/header/account-setting/members-page/index.tsx b/web/app/components/header/account-setting/members-page/index.tsx
index 5a8f3aebdb..b9dea78448 100644
--- a/web/app/components/header/account-setting/members-page/index.tsx
+++ b/web/app/components/header/account-setting/members-page/index.tsx
@@ -104,7 +104,7 @@ const MembersPage = () => {
)}
- setInviteModalVisible(true)} />
+ {isCurrentWorkspaceManager && setInviteModalVisible(true)} />}
From c730fec1e422e907fed668291b2ea18c4c243644 Mon Sep 17 00:00:00 2001
From: QuantumGhost
Date: Wed, 11 Feb 2026 17:08:49 +0800
Subject: [PATCH 9/9] chore: bump version to 1.13.0 (#32147)
---
api/pyproject.toml | 2 +-
api/uv.lock | 2 +-
docker/docker-compose-template.yaml | 8 ++++----
docker/docker-compose.yaml | 8 ++++----
web/package.json | 2 +-
5 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/api/pyproject.toml b/api/pyproject.toml
index 42456aa267..a3ea683bda 100644
--- a/api/pyproject.toml
+++ b/api/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "dify-api"
-version = "1.12.1"
+version = "1.13.0"
requires-python = ">=3.11,<3.13"
dependencies = [
diff --git a/api/uv.lock b/api/uv.lock
index a867a3226b..30ff1f8df1 100644
--- a/api/uv.lock
+++ b/api/uv.lock
@@ -1366,7 +1366,7 @@ wheels = [
[[package]]
name = "dify-api"
-version = "1.12.1"
+version = "1.13.0"
source = { virtual = "." }
dependencies = [
{ name = "aliyun-log-python-sdk" },
diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml
index cb5e2c47f7..18a12114da 100644
--- a/docker/docker-compose-template.yaml
+++ b/docker/docker-compose-template.yaml
@@ -21,7 +21,7 @@ services:
# API service
api:
- image: langgenius/dify-api:1.12.1
+ image: langgenius/dify-api:1.13.0
restart: always
environment:
# Use the shared environment variables.
@@ -63,7 +63,7 @@ services:
# worker service
# The Celery worker for processing all queues (dataset, workflow, mail, etc.)
worker:
- image: langgenius/dify-api:1.12.1
+ image: langgenius/dify-api:1.13.0
restart: always
environment:
# Use the shared environment variables.
@@ -102,7 +102,7 @@ services:
# worker_beat service
# Celery beat for scheduling periodic tasks.
worker_beat:
- image: langgenius/dify-api:1.12.1
+ image: langgenius/dify-api:1.13.0
restart: always
environment:
# Use the shared environment variables.
@@ -132,7 +132,7 @@ services:
# Frontend web application.
web:
- image: langgenius/dify-web:1.12.1
+ image: langgenius/dify-web:1.13.0
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml
index 1f38512661..6340dd290e 100644
--- a/docker/docker-compose.yaml
+++ b/docker/docker-compose.yaml
@@ -715,7 +715,7 @@ services:
# API service
api:
- image: langgenius/dify-api:1.12.1
+ image: langgenius/dify-api:1.13.0
restart: always
environment:
# Use the shared environment variables.
@@ -757,7 +757,7 @@ services:
# worker service
# The Celery worker for processing all queues (dataset, workflow, mail, etc.)
worker:
- image: langgenius/dify-api:1.12.1
+ image: langgenius/dify-api:1.13.0
restart: always
environment:
# Use the shared environment variables.
@@ -796,7 +796,7 @@ services:
# worker_beat service
# Celery beat for scheduling periodic tasks.
worker_beat:
- image: langgenius/dify-api:1.12.1
+ image: langgenius/dify-api:1.13.0
restart: always
environment:
# Use the shared environment variables.
@@ -826,7 +826,7 @@ services:
# Frontend web application.
web:
- image: langgenius/dify-web:1.12.1
+ image: langgenius/dify-web:1.13.0
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
diff --git a/web/package.json b/web/package.json
index 66eb8c7d66..9e75d88cd2 100644
--- a/web/package.json
+++ b/web/package.json
@@ -1,7 +1,7 @@
{
"name": "dify-web",
"type": "module",
- "version": "1.12.1",
+ "version": "1.13.0",
"private": true,
"packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a",
"imports": {