Merge remote-tracking branch 'origin/main' into deploy/dev

This commit is contained in:
CodingOnStar 2025-10-10 10:00:50 +08:00
commit 2c408445ff
17 changed files with 105 additions and 49 deletions

View File

@ -362,11 +362,11 @@ class HttpConfig(BaseSettings):
)
HTTP_REQUEST_MAX_READ_TIMEOUT: int = Field(
ge=1, description="Maximum read timeout in seconds for HTTP requests", default=60
ge=1, description="Maximum read timeout in seconds for HTTP requests", default=600
)
HTTP_REQUEST_MAX_WRITE_TIMEOUT: int = Field(
ge=1, description="Maximum write timeout in seconds for HTTP requests", default=20
ge=1, description="Maximum write timeout in seconds for HTTP requests", default=600
)
HTTP_REQUEST_NODE_MAX_BINARY_SIZE: PositiveInt = Field(
@ -771,7 +771,7 @@ class MailConfig(BaseSettings):
MAIL_TEMPLATING_TIMEOUT: int = Field(
description="""
Timeout for email templating in seconds. Used to prevent infinite loops in malicious templates.
Timeout for email templating in seconds. Used to prevent infinite loops in malicious templates.
Only available in sandbox mode.""",
default=3,
)

View File

@ -1,6 +1,6 @@
from collections.abc import Generator
from dataclasses import dataclass, field
from typing import TypeVar, Union, cast
from typing import TypeVar, Union
from core.agent.entities import AgentInvokeMessage
from core.tools.entities.tool_entities import ToolInvokeMessage
@ -87,7 +87,8 @@ def merge_blob_chunks(
),
meta=resp.meta,
)
yield cast(MessageType, merged_message)
assert isinstance(merged_message, (ToolInvokeMessage, AgentInvokeMessage))
yield merged_message # type: ignore
# Clean up the buffer
del files[chunk_id]
else:

View File

@ -2,7 +2,7 @@ import datetime
import logging
import time
from collections.abc import Mapping
from typing import Any, cast
from typing import Any
from sqlalchemy import func, select
@ -62,7 +62,7 @@ class KnowledgeIndexNode(Node):
return self._node_data
def _run(self) -> NodeRunResult: # type: ignore
node_data = cast(KnowledgeIndexNodeData, self._node_data)
node_data = self._node_data
variable_pool = self.graph_runtime_state.variable_pool
dataset_id = variable_pool.get(["sys", SystemVariableKey.DATASET_ID])
if not dataset_id:

View File

@ -25,7 +25,6 @@
"reportMissingParameterType": "hint",
"reportMissingTypeArgument": "hint",
"reportUnnecessaryComparison": "hint",
"reportUnnecessaryCast": "hint",
"reportUnnecessaryIsInstance": "hint",
"reportUntypedFunctionDecorator": "hint",

View File

@ -1,7 +1,7 @@
import hashlib
import json
from datetime import datetime
from typing import Any, cast
from typing import Any
from sqlalchemy import or_
from sqlalchemy.exc import IntegrityError
@ -55,7 +55,7 @@ class MCPToolManageService:
cache=NoOpProviderCredentialCache(),
)
return cast(dict[str, str], encrypter_instance.encrypt(headers))
return encrypter_instance.encrypt(headers)
@staticmethod
def get_mcp_provider_by_provider_id(provider_id: str, tenant_id: str) -> MCPToolProvider:

View File

@ -15,13 +15,13 @@ def test_dify_config(monkeypatch: pytest.MonkeyPatch):
# Set environment variables using monkeypatch
monkeypatch.setenv("CONSOLE_API_URL", "https://example.com")
monkeypatch.setenv("CONSOLE_WEB_URL", "https://example.com")
monkeypatch.setenv("HTTP_REQUEST_MAX_WRITE_TIMEOUT", "30")
monkeypatch.setenv("HTTP_REQUEST_MAX_WRITE_TIMEOUT", "30") # Custom value for testing
monkeypatch.setenv("DB_USERNAME", "postgres")
monkeypatch.setenv("DB_PASSWORD", "postgres")
monkeypatch.setenv("DB_HOST", "localhost")
monkeypatch.setenv("DB_PORT", "5432")
monkeypatch.setenv("DB_DATABASE", "dify")
monkeypatch.setenv("HTTP_REQUEST_MAX_READ_TIMEOUT", "600")
monkeypatch.setenv("HTTP_REQUEST_MAX_READ_TIMEOUT", "300") # Custom value for testing
# load dotenv file with pydantic-settings
config = DifyConfig()
@ -35,16 +35,36 @@ def test_dify_config(monkeypatch: pytest.MonkeyPatch):
assert config.SENTRY_TRACES_SAMPLE_RATE == 1.0
assert config.TEMPLATE_TRANSFORM_MAX_LENGTH == 400_000
# annotated field with default value
assert config.HTTP_REQUEST_MAX_READ_TIMEOUT == 600
# annotated field with custom configured value
assert config.HTTP_REQUEST_MAX_READ_TIMEOUT == 300
# annotated field with configured value
# annotated field with custom configured value
assert config.HTTP_REQUEST_MAX_WRITE_TIMEOUT == 30
# values from pyproject.toml
assert Version(config.project.version) >= Version("1.0.0")
def test_http_timeout_defaults(monkeypatch: pytest.MonkeyPatch):
"""Test that HTTP timeout defaults are correctly set"""
# clear system environment variables
os.environ.clear()
# Set minimal required env vars
monkeypatch.setenv("DB_USERNAME", "postgres")
monkeypatch.setenv("DB_PASSWORD", "postgres")
monkeypatch.setenv("DB_HOST", "localhost")
monkeypatch.setenv("DB_PORT", "5432")
monkeypatch.setenv("DB_DATABASE", "dify")
config = DifyConfig()
# Verify default timeout values
assert config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT == 10
assert config.HTTP_REQUEST_MAX_READ_TIMEOUT == 600
assert config.HTTP_REQUEST_MAX_WRITE_TIMEOUT == 600
# NOTE: If there is a `.env` file in your Workspace, this test might not succeed as expected.
# This is due to `pymilvus` loading all the variables from the `.env` file into `os.environ`.
def test_flask_configs(monkeypatch: pytest.MonkeyPatch):
@ -55,7 +75,6 @@ def test_flask_configs(monkeypatch: pytest.MonkeyPatch):
# Set environment variables using monkeypatch
monkeypatch.setenv("CONSOLE_API_URL", "https://example.com")
monkeypatch.setenv("CONSOLE_WEB_URL", "https://example.com")
monkeypatch.setenv("HTTP_REQUEST_MAX_WRITE_TIMEOUT", "30")
monkeypatch.setenv("DB_USERNAME", "postgres")
monkeypatch.setenv("DB_PASSWORD", "postgres")
monkeypatch.setenv("DB_HOST", "localhost")
@ -105,7 +124,6 @@ def test_inner_api_config_exist(monkeypatch: pytest.MonkeyPatch):
# Set environment variables using monkeypatch
monkeypatch.setenv("CONSOLE_API_URL", "https://example.com")
monkeypatch.setenv("CONSOLE_WEB_URL", "https://example.com")
monkeypatch.setenv("HTTP_REQUEST_MAX_WRITE_TIMEOUT", "30")
monkeypatch.setenv("DB_USERNAME", "postgres")
monkeypatch.setenv("DB_PASSWORD", "postgres")
monkeypatch.setenv("DB_HOST", "localhost")

View File

@ -930,6 +930,16 @@ WORKFLOW_LOG_CLEANUP_BATCH_SIZE=100
HTTP_REQUEST_NODE_MAX_BINARY_SIZE=10485760
HTTP_REQUEST_NODE_MAX_TEXT_SIZE=1048576
HTTP_REQUEST_NODE_SSL_VERIFY=True
# HTTP request node timeout configuration
# Maximum timeout values (in seconds) that users can set in HTTP request nodes
# - Connect timeout: Time to wait for establishing connection (default: 10s)
# - Read timeout: Time to wait for receiving response data (default: 600s, 10 minutes)
# - Write timeout: Time to wait for sending request data (default: 600s, 10 minutes)
HTTP_REQUEST_MAX_CONNECT_TIMEOUT=10
HTTP_REQUEST_MAX_READ_TIMEOUT=600
HTTP_REQUEST_MAX_WRITE_TIMEOUT=600
# Base64 encoded CA certificate data for custom certificate verification (PEM format, optional)
# HTTP_REQUEST_NODE_SSL_CERT_DATA=LS0tLS1CRUdJTi...
# Base64 encoded client certificate data for mutual TLS authentication (PEM format, optional)

View File

@ -418,6 +418,9 @@ x-shared-env: &shared-api-worker-env
HTTP_REQUEST_NODE_MAX_BINARY_SIZE: ${HTTP_REQUEST_NODE_MAX_BINARY_SIZE:-10485760}
HTTP_REQUEST_NODE_MAX_TEXT_SIZE: ${HTTP_REQUEST_NODE_MAX_TEXT_SIZE:-1048576}
HTTP_REQUEST_NODE_SSL_VERIFY: ${HTTP_REQUEST_NODE_SSL_VERIFY:-True}
HTTP_REQUEST_MAX_CONNECT_TIMEOUT: ${HTTP_REQUEST_MAX_CONNECT_TIMEOUT:-10}
HTTP_REQUEST_MAX_READ_TIMEOUT: ${HTTP_REQUEST_MAX_READ_TIMEOUT:-600}
HTTP_REQUEST_MAX_WRITE_TIMEOUT: ${HTTP_REQUEST_MAX_WRITE_TIMEOUT:-600}
RESPECT_XFORWARD_HEADERS_ENABLED: ${RESPECT_XFORWARD_HEADERS_ENABLED:-false}
SSRF_PROXY_HTTP_URL: ${SSRF_PROXY_HTTP_URL:-http://ssrf_proxy:3128}
SSRF_PROXY_HTTPS_URL: ${SSRF_PROXY_HTTPS_URL:-http://ssrf_proxy:3128}

View File

@ -276,7 +276,7 @@ function Form<
<div key={variable} className={cn(itemClassName, 'py-3')}>
<div className='system-sm-semibold flex items-center justify-between py-2 text-text-secondary'>
<div className='flex items-center space-x-2'>
<span className={cn(fieldLabelClassName, 'system-sm-regular flex items-center py-2 text-text-secondary')}>{label[language] || label.en_US}</span>
<span className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>{label[language] || label.en_US}</span>
{required && (
<span className='ml-1 text-red-500'>*</span>
)}

View File

@ -78,15 +78,15 @@ const Result: FC<IResultProps> = ({
setRespondingFalse()
}, [controlStopResponding])
const [completionRes, doSetCompletionRes] = useState<any>('')
const completionResRef = useRef<any>()
const setCompletionRes = (res: any) => {
const [completionRes, doSetCompletionRes] = useState<string>('')
const completionResRef = useRef<string>('')
const setCompletionRes = (res: string) => {
completionResRef.current = res
doSetCompletionRes(res)
}
const getCompletionRes = () => completionResRef.current
const [workflowProcessData, doSetWorkflowProcessData] = useState<WorkflowProcess>()
const workflowProcessDataRef = useRef<WorkflowProcess>()
const workflowProcessDataRef = useRef<WorkflowProcess | undefined>(undefined)
const setWorkflowProcessData = (data: WorkflowProcess) => {
workflowProcessDataRef.current = data
doSetWorkflowProcessData(data)

View File

@ -45,6 +45,7 @@ export const toolCredentialToFormSchemas = (parameters: ToolCredential[]) => {
return {
...parameter,
variable: parameter.name,
type: toType(parameter.type),
label: parameter.label,
tooltip: parameter.help,
show_on: [],

View File

@ -41,16 +41,16 @@ export const useWorkflowHistory = () => {
const { store: workflowHistoryStore } = useWorkflowHistoryStore()
const { t } = useTranslation()
const [undoCallbacks, setUndoCallbacks] = useState<any[]>([])
const [redoCallbacks, setRedoCallbacks] = useState<any[]>([])
const [undoCallbacks, setUndoCallbacks] = useState<(() => void)[]>([])
const [redoCallbacks, setRedoCallbacks] = useState<(() => void)[]>([])
const onUndo = useCallback((callback: unknown) => {
setUndoCallbacks((prev: any) => [...prev, callback])
const onUndo = useCallback((callback: () => void) => {
setUndoCallbacks(prev => [...prev, callback])
return () => setUndoCallbacks(prev => prev.filter(cb => cb !== callback))
}, [])
const onRedo = useCallback((callback: unknown) => {
setRedoCallbacks((prev: any) => [...prev, callback])
const onRedo = useCallback((callback: () => void) => {
setRedoCallbacks(prev => [...prev, callback])
return () => setRedoCallbacks(prev => prev.filter(cb => cb !== callback))
}, [])

View File

@ -127,7 +127,7 @@ const VarReferencePicker: FC<Props> = ({
const reactflow = useReactFlow()
const startNode = availableNodes.find((node: any) => {
const startNode = availableNodes.find((node: Node) => {
return node.data.type === BlockEnum.Start
})

View File

@ -5,6 +5,8 @@ import { useTranslation } from 'react-i18next'
import type { Timeout as TimeoutPayloadType } from '../../types'
import Input from '@/app/components/base/input'
import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse'
import { useStore } from '@/app/components/workflow/store'
import { BlockEnum } from '@/app/components/workflow/types'
type Props = {
readonly: boolean
@ -61,6 +63,11 @@ const Timeout: FC<Props> = ({ readonly, payload, onChange }) => {
const { t } = useTranslation()
const { connect, read, write, max_connect_timeout, max_read_timeout, max_write_timeout } = payload ?? {}
// Get default config from store for max timeout values
const nodesDefaultConfigs = useStore(s => s.nodesDefaultConfigs)
const defaultConfig = nodesDefaultConfigs?.[BlockEnum.HttpRequest]
const defaultTimeout = defaultConfig?.timeout || {}
return (
<FieldCollapse title={t(`${i18nPrefix}.timeout.title`)}>
<div className='mt-2 space-y-1'>
@ -73,7 +80,7 @@ const Timeout: FC<Props> = ({ readonly, payload, onChange }) => {
value={connect}
onChange={v => onChange?.({ ...payload, connect: v })}
min={1}
max={max_connect_timeout || 300}
max={max_connect_timeout || defaultTimeout.max_connect_timeout || 10}
/>
<InputField
title={t('workflow.nodes.http.timeout.readLabel')!}
@ -83,7 +90,7 @@ const Timeout: FC<Props> = ({ readonly, payload, onChange }) => {
value={read}
onChange={v => onChange?.({ ...payload, read: v })}
min={1}
max={max_read_timeout || 600}
max={max_read_timeout || defaultTimeout.max_read_timeout || 600}
/>
<InputField
title={t('workflow.nodes.http.timeout.writeLabel')!}
@ -93,7 +100,7 @@ const Timeout: FC<Props> = ({ readonly, payload, onChange }) => {
value={write}
onChange={v => onChange?.({ ...payload, write: v })}
min={1}
max={max_write_timeout || 600}
max={max_write_timeout || defaultTimeout.max_write_timeout || 600}
/>
</div>
</div>

View File

@ -120,7 +120,7 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
setJson(JSON.stringify(schema, null, 2))
}, [currentTab])
const handleSubmit = useCallback((schema: any) => {
const handleSubmit = useCallback((schema: Record<string, unknown>) => {
const jsonSchema = jsonToSchema(schema) as SchemaRoot
if (currentTab === SchemaView.VisualEditor)
setJsonSchema(jsonSchema)

View File

@ -152,14 +152,16 @@ const EditCard: FC<EditCardProps> = ({
}, [isAdvancedEditing, emitPropertyOptionsChange, currentFields])
const handleAdvancedOptionsChange = useCallback((options: AdvancedOptionsType) => {
let enumValue: any = options.enum
if (enumValue === '') {
let enumValue: SchemaEnumType | undefined
if (options.enum === '') {
enumValue = undefined
}
else {
enumValue = options.enum.replace(/\s/g, '').split(',')
const stringArray = options.enum.replace(/\s/g, '').split(',')
if (currentFields.type === Type.number)
enumValue = (enumValue as SchemaEnumType).map(value => Number(value)).filter(num => !Number.isNaN(num))
enumValue = stringArray.map(value => Number(value)).filter(num => !Number.isNaN(num))
else
enumValue = stringArray
}
setCurrentFields(prev => ({ ...prev, enum: enumValue }))
if (isAdvancedEditing) return

View File

@ -180,7 +180,7 @@ const handleStream = (
let isFirstMessage = true
function read() {
let hasError = false
reader?.read().then((result: any) => {
reader?.read().then((result: ReadableStreamReadResult<Uint8Array>) => {
if (result.done) {
onCompleted?.()
return
@ -322,7 +322,21 @@ const handleStream = (
const baseFetch = base
export const upload = async (options: any, isPublicAPI?: boolean, url?: string, searchParams?: string): Promise<any> => {
type UploadOptions = {
xhr: XMLHttpRequest
method: string
url?: string
headers?: Record<string, string>
data: FormData
onprogress?: (this: XMLHttpRequest, ev: ProgressEvent<EventTarget>) => void
}
type UploadResponse = {
id: string
[key: string]: unknown
}
export const upload = async (options: UploadOptions, isPublicAPI?: boolean, url?: string, searchParams?: string): Promise<UploadResponse> => {
const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX
const token = await getAccessToken(isPublicAPI)
const defaultOptions = {
@ -331,18 +345,18 @@ export const upload = async (options: any, isPublicAPI?: boolean, url?: string,
headers: {
Authorization: `Bearer ${token}`,
},
data: {},
}
options = {
const mergedOptions = {
...defaultOptions,
...options,
headers: { ...defaultOptions.headers, ...options.headers },
url: options.url || defaultOptions.url,
headers: { ...defaultOptions.headers, ...options.headers } as Record<string, string>,
}
return new Promise((resolve, reject) => {
const xhr = options.xhr
xhr.open(options.method, options.url)
for (const key in options.headers)
xhr.setRequestHeader(key, options.headers[key])
const xhr = mergedOptions.xhr
xhr.open(mergedOptions.method, mergedOptions.url)
for (const key in mergedOptions.headers)
xhr.setRequestHeader(key, mergedOptions.headers[key])
xhr.withCredentials = true
xhr.responseType = 'json'
@ -354,8 +368,9 @@ export const upload = async (options: any, isPublicAPI?: boolean, url?: string,
reject(xhr)
}
}
xhr.upload.onprogress = options.onprogress
xhr.send(options.data)
if (mergedOptions.onprogress)
xhr.upload.onprogress = mergedOptions.onprogress
xhr.send(mergedOptions.data)
})
}
@ -432,7 +447,7 @@ export const ssePost = async (
if (!/^[23]\d{2}$/.test(String(res.status))) {
if (res.status === 401) {
if (isPublicAPI) {
res.json().then((data: any) => {
res.json().then((data: { code?: string; message?: string }) => {
if (isPublicAPI) {
if (data.code === 'web_app_access_denied')
requiredWebSSOLogin(data.message, 403)