Merge remote-tracking branch 'origin/main' into feat/queue-based-graph-engine

This commit is contained in:
-LAN-
2025-09-04 00:27:08 +08:00
46 changed files with 435 additions and 274 deletions

View File

@ -75,6 +75,7 @@ DB_PASSWORD=difyai123456
DB_HOST=localhost
DB_PORT=5432
DB_DATABASE=dify
SQLALCHEMY_POOL_PRE_PING=true
# Storage configuration
# use for store upload files, private keys...

View File

@ -2,7 +2,7 @@ from functools import wraps
from typing import cast
import flask_login
from flask import request
from flask import jsonify, request
from flask_restx import Resource, reqparse
from werkzeug.exceptions import BadRequest, NotFound
@ -46,23 +46,38 @@ def oauth_server_access_token_required(view):
authorization_header = request.headers.get("Authorization")
if not authorization_header:
raise BadRequest("Authorization header is required")
response = jsonify({"error": "Authorization header is required"})
response.status_code = 401
response.headers["WWW-Authenticate"] = "Bearer"
return response
parts = authorization_header.strip().split(" ")
parts = authorization_header.strip().split(None, 1)
if len(parts) != 2:
raise BadRequest("Invalid Authorization header format")
response = jsonify({"error": "Invalid Authorization header format"})
response.status_code = 401
response.headers["WWW-Authenticate"] = "Bearer"
return response
token_type = parts[0].strip()
if token_type.lower() != "bearer":
raise BadRequest("token_type is invalid")
response = jsonify({"error": "token_type is invalid"})
response.status_code = 401
response.headers["WWW-Authenticate"] = "Bearer"
return response
access_token = parts[1].strip()
if not access_token:
raise BadRequest("access_token is required")
response = jsonify({"error": "access_token is required"})
response.status_code = 401
response.headers["WWW-Authenticate"] = "Bearer"
return response
account = OAuthServerService.validate_oauth_access_token(oauth_provider_app.client_id, access_token)
if not account:
raise BadRequest("access_token or client_id is invalid")
response = jsonify({"error": "access_token or client_id is invalid"})
response.status_code = 401
response.headers["WWW-Authenticate"] = "Bearer"
return response
kwargs["account"] = account

View File

@ -305,7 +305,7 @@ class AdvancedChatAppGenerateTaskPipeline:
err = self._base_task_pipeline._handle_error(event=event, session=session, message_id=self._message_id)
yield self._base_task_pipeline._error_to_stream_response(err)
def _handle_workflow_started_event(self, **kwargs) -> Generator[StreamResponse, None, None]:
def _handle_workflow_started_event(self, *args, **kwargs) -> Generator[StreamResponse, None, None]:
"""Handle workflow started events."""
with self._database_session() as session:
workflow_execution = self._workflow_cycle_manager.handle_workflow_run_start()

View File

@ -410,10 +410,9 @@ class ProviderConfiguration(BaseModel):
:return:
"""
with Session(db.engine) as session:
if credential_name and self._check_provider_credential_name_exists(
credential_name=credential_name, session=session
):
raise ValueError(f"Credential with name '{credential_name}' already exists.")
if credential_name:
if self._check_provider_credential_name_exists(credential_name=credential_name, session=session):
raise ValueError(f"Credential with name '{credential_name}' already exists.")
else:
credential_name = self._generate_provider_credential_name(session)
@ -891,10 +890,11 @@ class ProviderConfiguration(BaseModel):
:return:
"""
with Session(db.engine) as session:
if credential_name and self._check_custom_model_credential_name_exists(
model=model, model_type=model_type, credential_name=credential_name, session=session
):
raise ValueError(f"Model credential with name '{credential_name}' already exists for {model}.")
if credential_name:
if self._check_custom_model_credential_name_exists(
model=model, model_type=model_type, credential_name=credential_name, session=session
):
raise ValueError(f"Model credential with name '{credential_name}' already exists for {model}.")
else:
credential_name = self._generate_custom_model_credential_name(
model=model, model_type=model_type, session=session

View File

@ -1,7 +1,7 @@
import os
from collections import OrderedDict
from collections.abc import Callable
from typing import Any
from typing import TypeVar
from configs import dify_config
from core.tools.utils.yaml_utils import load_yaml_file
@ -72,11 +72,14 @@ def pin_position_map(original_position_map: dict[str, int], pin_list: list[str])
return position_map
T = TypeVar("T")
def is_filtered(
include_set: set[str],
exclude_set: set[str],
data: Any,
name_func: Callable[[Any], str],
data: T,
name_func: Callable[[T], str],
) -> bool:
"""
Check if the object should be filtered out.
@ -103,9 +106,9 @@ def is_filtered(
def sort_by_position_map(
position_map: dict[str, int],
data: list[Any],
name_func: Callable[[Any], str],
) -> list[Any]:
data: list[T],
name_func: Callable[[T], str],
):
"""
Sort the objects by the position map.
If the name of the object is not in the position map, it will be put at the end.
@ -122,9 +125,9 @@ def sort_by_position_map(
def sort_to_dict_by_position_map(
position_map: dict[str, int],
data: list[Any],
name_func: Callable[[Any], str],
) -> OrderedDict[str, Any]:
data: list[T],
name_func: Callable[[T], str],
):
"""
Sort the objects into a ordered dict by the position map.
If the name of the object is not in the position map, it will be put at the end.
@ -134,4 +137,4 @@ def sort_to_dict_by_position_map(
:return: an OrderedDict with the sorted pairs of name and object
"""
sorted_items = sort_by_position_map(position_map, data, name_func)
return OrderedDict([(name_func(item), item) for item in sorted_items])
return OrderedDict((name_func(item), item) for item in sorted_items)

View File

@ -246,6 +246,10 @@ class StreamableHTTPTransport:
logger.debug("Received 202 Accepted")
return
if response.status_code == 204:
logger.debug("Received 204 No Content")
return
if response.status_code == 404:
if isinstance(message.root, JSONRPCRequest):
self._send_session_terminated_error(

View File

@ -258,5 +258,5 @@ def convert_input_form_to_parameters(
parameters[item.variable]["type"] = "string"
parameters[item.variable]["enum"] = item.options
elif item.type == VariableEntityType.NUMBER:
parameters[item.variable]["type"] = "float"
parameters[item.variable]["type"] = "number"
return parameters, required

View File

@ -152,8 +152,8 @@ class MyScaleVector(BaseVector):
)
for r in self._client.query(sql).named_results()
]
except Exception as e:
logger.exception("\033[91m\033[1m%s\033[0m \033[95m%s\033[0m", type(e), str(e)) # noqa:TRY401
except Exception:
logger.exception("Vector search operation failed")
return []
def delete(self) -> None:

View File

@ -1,7 +1,8 @@
"""ClickZetta Volume file lifecycle management
This module provides file lifecycle management features including version control, automatic cleanup, backup and restore
Supports complete lifecycle management for knowledge base files.
This module provides file lifecycle management features including version control,
automatic cleanup, backup and restore. Supports complete lifecycle management for
knowledge base files.
"""
import json

View File

@ -1,6 +1,6 @@
[project]
name = "dify-api"
version = "1.8.0"
version = "1.8.1"
requires-python = ">=3.11,<3.13"
dependencies = [

View File

@ -1,6 +1,7 @@
import json
from unittest.mock import Mock, patch
import jsonschema
import pytest
from core.app.app_config.entities import VariableEntity, VariableEntityType
@ -434,7 +435,7 @@ class TestUtilityFunctions:
assert parameters["category"]["enum"] == ["A", "B", "C"]
assert "count" in parameters
assert parameters["count"]["type"] == "float"
assert parameters["count"]["type"] == "number"
# FILE type should be skipped - it creates empty dict but gets filtered later
# Check that it doesn't have any meaningful content
@ -447,3 +448,65 @@ class TestUtilityFunctions:
assert "category" not in required
# Note: _get_request_id function has been removed as request_id is now passed as parameter
def test_convert_input_form_to_parameters_jsonschema_validation_ok(self):
"""Current schema uses 'number' for numeric fields; it should be a valid JSON Schema."""
user_input_form = [
VariableEntity(
type=VariableEntityType.NUMBER,
variable="count",
description="Count",
label="Count",
required=True,
),
VariableEntity(
type=VariableEntityType.TEXT_INPUT,
variable="name",
description="User name",
label="Name",
required=False,
),
]
parameters_dict = {
"count": "Enter count",
"name": "Enter your name",
}
parameters, required = convert_input_form_to_parameters(user_input_form, parameters_dict)
# Build a complete JSON Schema
schema = {
"type": "object",
"properties": parameters,
"required": required,
}
# 1) The schema itself must be valid
jsonschema.Draft202012Validator.check_schema(schema)
# 2) Both float and integer instances should pass validation
jsonschema.validate(instance={"count": 3.14, "name": "alice"}, schema=schema)
jsonschema.validate(instance={"count": 2, "name": "bob"}, schema=schema)
def test_legacy_float_type_schema_is_invalid(self):
"""Legacy/buggy behavior: using 'float' should produce an invalid JSON Schema."""
# Manually construct a legacy/incorrect schema (simulating old behavior)
bad_schema = {
"type": "object",
"properties": {
"count": {
"type": "float", # Invalid type: JSON Schema does not support 'float'
"description": "Enter count",
}
},
"required": ["count"],
}
# The schema itself should raise a SchemaError
with pytest.raises(jsonschema.exceptions.SchemaError):
jsonschema.Draft202012Validator.check_schema(bad_schema)
# Or validation should also raise SchemaError
with pytest.raises(jsonschema.exceptions.SchemaError):
jsonschema.validate(instance={"count": 1.23}, schema=bad_schema)

2
api/uv.lock generated
View File

@ -1260,7 +1260,7 @@ wheels = [
[[package]]
name = "dify-api"
version = "1.8.0"
version = "1.8.1"
source = { virtual = "." }
dependencies = [
{ name = "arize-phoenix-otel" },