diff --git a/api/controllers/web/passport.py b/api/controllers/web/passport.py
index 776b743e92..a344777783 100644
--- a/api/controllers/web/passport.py
+++ b/api/controllers/web/passport.py
@@ -14,8 +14,6 @@ from extensions.ext_database import db
from libs.passport import PassportService
from libs.token import extract_access_token
from models.model import App, EndUser, Site
-from services.app_service import AppService
-from services.enterprise.enterprise_service import EnterpriseService
from services.feature_service import FeatureService
from services.webapp_auth_service import WebAppAuthService, WebAppAuthType
@@ -38,22 +36,17 @@ class PassportResource(Resource):
app_code = request.headers.get(HEADER_NAME_APP_CODE)
user_id = request.args.get("user_id")
access_token = extract_access_token(request)
-
if app_code is None:
raise Unauthorized("X-App-Code header is missing.")
- app_id = AppService.get_app_id_by_code(app_code)
- # exchange token for enterprise logined web user
- enterprise_user_decoded = decode_enterprise_webapp_user_id(access_token)
- if enterprise_user_decoded:
- # a web user has already logged in, exchange a token for this app without redirecting to the login page
- return exchange_token_for_existing_web_user(
- app_code=app_code, enterprise_user_decoded=enterprise_user_decoded
- )
-
if system_features.webapp_auth.enabled:
- app_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=app_id)
- if not app_settings or not app_settings.access_mode == "public":
- raise WebAppAuthRequiredError()
+ enterprise_user_decoded = decode_enterprise_webapp_user_id(access_token)
+ app_auth_type = WebAppAuthService.get_app_auth_type(app_code=app_code)
+ if app_auth_type != WebAppAuthType.PUBLIC:
+ if not enterprise_user_decoded:
+ raise WebAppAuthRequiredError()
+ return exchange_token_for_existing_web_user(
+ app_code=app_code, enterprise_user_decoded=enterprise_user_decoded, auth_type=app_auth_type
+ )
# get site from db and check if it is normal
site = db.session.scalar(select(Site).where(Site.code == app_code, Site.status == "normal"))
@@ -124,7 +117,7 @@ def decode_enterprise_webapp_user_id(jwt_token: str | None):
return decoded
-def exchange_token_for_existing_web_user(app_code: str, enterprise_user_decoded: dict):
+def exchange_token_for_existing_web_user(app_code: str, enterprise_user_decoded: dict, auth_type: WebAppAuthType):
"""
Exchange a token for an existing web user session.
"""
@@ -145,13 +138,11 @@ def exchange_token_for_existing_web_user(app_code: str, enterprise_user_decoded:
if not app_model or app_model.status != "normal" or not app_model.enable_site:
raise NotFound()
- app_auth_type = WebAppAuthService.get_app_auth_type(app_code=app_code)
-
- if app_auth_type == WebAppAuthType.PUBLIC:
+ if auth_type == WebAppAuthType.PUBLIC:
return _exchange_for_public_app_token(app_model, site, enterprise_user_decoded)
- elif app_auth_type == WebAppAuthType.EXTERNAL and user_auth_type != "external":
+ elif auth_type == WebAppAuthType.EXTERNAL and user_auth_type != "external":
raise WebAppAuthRequiredError("Please login as external user.")
- elif app_auth_type == WebAppAuthType.INTERNAL and user_auth_type != "internal":
+ elif auth_type == WebAppAuthType.INTERNAL and user_auth_type != "internal":
raise WebAppAuthRequiredError("Please login as internal user.")
end_user = None
diff --git a/api/core/plugin/entities/parameters.py b/api/core/plugin/entities/parameters.py
index 68b5c1084a..1e7f8e4c86 100644
--- a/api/core/plugin/entities/parameters.py
+++ b/api/core/plugin/entities/parameters.py
@@ -76,7 +76,7 @@ class PluginParameter(BaseModel):
auto_generate: PluginParameterAutoGenerate | None = None
template: PluginParameterTemplate | None = None
required: bool = False
- default: Union[float, int, str] | None = None
+ default: Union[float, int, str, bool] | None = None
min: Union[float, int] | None = None
max: Union[float, int] | None = None
precision: int | None = None
diff --git a/api/core/plugin/impl/exc.py b/api/core/plugin/impl/exc.py
index 23a69bd92f..e28a324217 100644
--- a/api/core/plugin/impl/exc.py
+++ b/api/core/plugin/impl/exc.py
@@ -40,7 +40,7 @@ class PluginDaemonBadRequestError(PluginDaemonClientSideError):
description: str = "Bad Request"
-class PluginInvokeError(PluginDaemonClientSideError):
+class PluginInvokeError(PluginDaemonClientSideError, ValueError):
description: str = "Invoke Error"
def _get_error_object(self) -> Mapping:
diff --git a/api/core/tools/utils/parser.py b/api/core/tools/utils/parser.py
index c7ac3387e5..6eabde3991 100644
--- a/api/core/tools/utils/parser.py
+++ b/api/core/tools/utils/parser.py
@@ -62,6 +62,11 @@ class ApiBasedToolSchemaParser:
root = root[ref]
interface["operation"]["parameters"][i] = root
for parameter in interface["operation"]["parameters"]:
+ # Handle complex type defaults that are not supported by PluginParameter
+ default_value = None
+ if "schema" in parameter and "default" in parameter["schema"]:
+ default_value = ApiBasedToolSchemaParser._sanitize_default_value(parameter["schema"]["default"])
+
tool_parameter = ToolParameter(
name=parameter["name"],
label=I18nObject(en_US=parameter["name"], zh_Hans=parameter["name"]),
@@ -72,9 +77,7 @@ class ApiBasedToolSchemaParser:
required=parameter.get("required", False),
form=ToolParameter.ToolParameterForm.LLM,
llm_description=parameter.get("description"),
- default=parameter["schema"]["default"]
- if "schema" in parameter and "default" in parameter["schema"]
- else None,
+ default=default_value,
placeholder=I18nObject(
en_US=parameter.get("description", ""), zh_Hans=parameter.get("description", "")
),
@@ -134,6 +137,11 @@ class ApiBasedToolSchemaParser:
required = body_schema.get("required", [])
properties = body_schema.get("properties", {})
for name, property in properties.items():
+ # Handle complex type defaults that are not supported by PluginParameter
+ default_value = ApiBasedToolSchemaParser._sanitize_default_value(
+ property.get("default", None)
+ )
+
tool = ToolParameter(
name=name,
label=I18nObject(en_US=name, zh_Hans=name),
@@ -144,12 +152,11 @@ class ApiBasedToolSchemaParser:
required=name in required,
form=ToolParameter.ToolParameterForm.LLM,
llm_description=property.get("description", ""),
- default=property.get("default", None),
+ default=default_value,
placeholder=I18nObject(
en_US=property.get("description", ""), zh_Hans=property.get("description", "")
),
)
-
# check if there is a type
typ = ApiBasedToolSchemaParser._get_tool_parameter_type(property)
if typ:
@@ -197,6 +204,22 @@ class ApiBasedToolSchemaParser:
return bundles
+ @staticmethod
+ def _sanitize_default_value(value):
+ """
+ Sanitize default values for PluginParameter compatibility.
+ Complex types (list, dict) are converted to None to avoid validation errors.
+
+ Args:
+ value: The default value from OpenAPI schema
+
+ Returns:
+ None for complex types (list, dict), otherwise the original value
+ """
+ if isinstance(value, (list, dict)):
+ return None
+ return value
+
@staticmethod
def _get_tool_parameter_type(parameter: dict) -> ToolParameter.ToolParameterType | None:
parameter = parameter or {}
@@ -217,7 +240,11 @@ class ApiBasedToolSchemaParser:
return ToolParameter.ToolParameterType.STRING
elif typ == "array":
items = parameter.get("items") or parameter.get("schema", {}).get("items")
- return ToolParameter.ToolParameterType.FILES if items and items.get("format") == "binary" else None
+ if items and items.get("format") == "binary":
+ return ToolParameter.ToolParameterType.FILES
+ else:
+ # For regular arrays, return ARRAY type instead of None
+ return ToolParameter.ToolParameterType.ARRAY
else:
return None
diff --git a/api/core/workflow/nodes/agent/entities.py b/api/core/workflow/nodes/agent/entities.py
index ce6eb33ecc..985ee5eef2 100644
--- a/api/core/workflow/nodes/agent/entities.py
+++ b/api/core/workflow/nodes/agent/entities.py
@@ -26,8 +26,8 @@ class AgentNodeData(BaseNodeData):
class ParamsAutoGenerated(IntEnum):
- CLOSE = auto()
- OPEN = auto()
+ CLOSE = 0
+ OPEN = 1
class AgentOldVersionModelFeatures(StrEnum):
diff --git a/api/libs/helper.py b/api/libs/helper.py
index b878141d8e..60484dd40b 100644
--- a/api/libs/helper.py
+++ b/api/libs/helper.py
@@ -81,6 +81,8 @@ class AvatarUrlField(fields.Raw):
from models import Account
if isinstance(obj, Account) and obj.avatar is not None:
+ if obj.avatar.startswith(("http://", "https://")):
+ return obj.avatar
return file_helpers.get_signed_file_url(obj.avatar)
return None
diff --git a/api/services/variable_truncator.py b/api/services/variable_truncator.py
index 9a014b400f..4e13d2d964 100644
--- a/api/services/variable_truncator.py
+++ b/api/services/variable_truncator.py
@@ -57,7 +57,7 @@ class UnknownTypeError(Exception):
pass
-JSONTypes: TypeAlias = int | float | str | list | dict | None | bool
+JSONTypes: TypeAlias = int | float | str | list[object] | dict[str, object] | None | bool
@dataclasses.dataclass(frozen=True)
@@ -252,14 +252,14 @@ class VariableTruncator:
truncated_value = value[:truncated_size] + "..."
return _PartResult(truncated_value, self.calculate_json_size(truncated_value), True)
- def _truncate_array(self, value: list, target_size: int) -> _PartResult[list]:
+ def _truncate_array(self, value: list[object], target_size: int) -> _PartResult[list[object]]:
"""
Truncate array with correct strategy:
1. First limit to 20 items
2. If still too large, truncate individual items
"""
- truncated_value: list[Any] = []
+ truncated_value: list[object] = []
truncated = False
used_size = self.calculate_json_size([])
@@ -282,7 +282,11 @@ class VariableTruncator:
if used_size > target_size:
break
- part_result = self._truncate_json_primitives(item, target_size - used_size)
+ remaining_budget = target_size - used_size
+ if item is None or isinstance(item, (str, list, dict, bool, int, float)):
+ part_result = self._truncate_json_primitives(item, remaining_budget)
+ else:
+ raise UnknownTypeError(f"got unknown type {type(item)} in array truncation")
truncated_value.append(part_result.value)
used_size += part_result.value_size
truncated = part_result.truncated
@@ -373,10 +377,10 @@ class VariableTruncator:
def _truncate_json_primitives(self, val: str, target_size: int) -> _PartResult[str]: ...
@overload
- def _truncate_json_primitives(self, val: list, target_size: int) -> _PartResult[list]: ...
+ def _truncate_json_primitives(self, val: list[object], target_size: int) -> _PartResult[list[object]]: ...
@overload
- def _truncate_json_primitives(self, val: dict, target_size: int) -> _PartResult[dict]: ...
+ def _truncate_json_primitives(self, val: dict[str, object], target_size: int) -> _PartResult[dict[str, object]]: ...
@overload
def _truncate_json_primitives(self, val: bool, target_size: int) -> _PartResult[bool]: ... # type: ignore
@@ -391,7 +395,9 @@ class VariableTruncator:
def _truncate_json_primitives(self, val: None, target_size: int) -> _PartResult[None]: ...
def _truncate_json_primitives(
- self, val: UpdatedVariable | str | list | dict | bool | int | float | None, target_size: int
+ self,
+ val: UpdatedVariable | str | list[object] | dict[str, object] | bool | int | float | None,
+ target_size: int,
) -> _PartResult[Any]:
"""Truncate a value within an object to fit within budget."""
if isinstance(val, UpdatedVariable):
diff --git a/api/tests/unit_tests/core/tools/utils/test_parser.py b/api/tests/unit_tests/core/tools/utils/test_parser.py
index e1eab21ca4..f39158aa59 100644
--- a/api/tests/unit_tests/core/tools/utils/test_parser.py
+++ b/api/tests/unit_tests/core/tools/utils/test_parser.py
@@ -109,3 +109,83 @@ def test_parse_openapi_to_tool_bundle_properties_all_of(app):
assert tool_bundles[0].parameters[0].llm_description == "desc prop1"
# TODO: support enum in OpenAPI
# assert set(tool_bundles[0].parameters[0].options) == {"option1", "option2", "option3"}
+
+
+def test_parse_openapi_to_tool_bundle_default_value_type_casting(app):
+ """
+ Test that default values are properly cast to match parameter types.
+ This addresses the issue where array default values like [] cause validation errors
+ when parameter type is inferred as string/number/boolean.
+ """
+ openapi = {
+ "openapi": "3.0.0",
+ "info": {"title": "Test API", "version": "1.0.0"},
+ "servers": [{"url": "https://example.com"}],
+ "paths": {
+ "/product/create": {
+ "post": {
+ "operationId": "createProduct",
+ "summary": "Create a product",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "categories": {
+ "description": "List of category identifiers",
+ "default": [],
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "name": {
+ "description": "Product name",
+ "default": "Default Product",
+ "type": "string",
+ },
+ "price": {"description": "Product price", "default": 0.0, "type": "number"},
+ "available": {
+ "description": "Product availability",
+ "default": True,
+ "type": "boolean",
+ },
+ },
+ }
+ }
+ }
+ },
+ "responses": {"200": {"description": "Default Response"}},
+ }
+ }
+ },
+ }
+
+ with app.test_request_context():
+ tool_bundles = ApiBasedToolSchemaParser.parse_openapi_to_tool_bundle(openapi)
+
+ assert len(tool_bundles) == 1
+ bundle = tool_bundles[0]
+ assert len(bundle.parameters) == 4
+
+ # Find parameters by name
+ params_by_name = {param.name: param for param in bundle.parameters}
+
+ # Check categories parameter (array type with [] default)
+ categories_param = params_by_name["categories"]
+ assert categories_param.type == "array" # Will be detected by _get_tool_parameter_type
+ assert categories_param.default is None # Array default [] is converted to None
+
+ # Check name parameter (string type with string default)
+ name_param = params_by_name["name"]
+ assert name_param.type == "string"
+ assert name_param.default == "Default Product"
+
+ # Check price parameter (number type with number default)
+ price_param = params_by_name["price"]
+ assert price_param.type == "number"
+ assert price_param.default == 0.0
+
+ # Check available parameter (boolean type with boolean default)
+ available_param = params_by_name["available"]
+ assert available_param.type == "boolean"
+ assert available_param.default is True
diff --git a/dev/basedpyright-check b/dev/basedpyright-check
index ef58ed1f57..1c87b27d6f 100755
--- a/dev/basedpyright-check
+++ b/dev/basedpyright-check
@@ -10,7 +10,7 @@ PATH_TO_CHECK="$1"
# run basedpyright checks
if [ -n "$PATH_TO_CHECK" ]; then
- uv run --directory api --dev basedpyright "$PATH_TO_CHECK"
+ uv run --directory api --dev -- basedpyright --threads $(nproc) "$PATH_TO_CHECK"
else
- uv run --directory api --dev basedpyright
+ uv run --directory api --dev -- basedpyright --threads $(nproc)
fi
diff --git a/web/app/(commonLayout)/layout.tsx b/web/app/(commonLayout)/layout.tsx
index ed1c995e25..be9c4fe49a 100644
--- a/web/app/(commonLayout)/layout.tsx
+++ b/web/app/(commonLayout)/layout.tsx
@@ -9,6 +9,7 @@ import { EventEmitterContextProvider } from '@/context/event-emitter'
import { ProviderContextProvider } from '@/context/provider-context'
import { ModalContextProvider } from '@/context/modal-context'
import GotoAnything from '@/app/components/goto-anything'
+import Zendesk from '@/app/components/base/zendesk'
const Layout = ({ children }: { children: ReactNode }) => {
return (
@@ -28,6 +29,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
+
>
)
diff --git a/web/app/components/app/annotation/index.tsx b/web/app/components/app/annotation/index.tsx
index 264b1ac727..bc63b85f6d 100644
--- a/web/app/components/app/annotation/index.tsx
+++ b/web/app/components/app/annotation/index.tsx
@@ -53,7 +53,6 @@ const Annotation: FC = (props) => {
const [isShowViewModal, setIsShowViewModal] = useState(false)
const [selectedIds, setSelectedIds] = useState([])
const debouncedQueryParams = useDebounce(queryParams, { wait: 500 })
- const [isBatchDeleting, setIsBatchDeleting] = useState(false)
const fetchAnnotationConfig = async () => {
const res = await doFetchAnnotationConfig(appDetail.id)
@@ -108,9 +107,6 @@ const Annotation: FC = (props) => {
}
const handleBatchDelete = async () => {
- if (isBatchDeleting)
- return
- setIsBatchDeleting(true)
try {
await delAnnotations(appDetail.id, selectedIds)
Toast.notify({ message: t('common.api.actionSuccess'), type: 'success' })
@@ -121,9 +117,6 @@ const Annotation: FC = (props) => {
catch (e: any) {
Toast.notify({ type: 'error', message: e.message || t('common.api.actionFailed') })
}
- finally {
- setIsBatchDeleting(false)
- }
}
const handleView = (item: AnnotationItem) => {
@@ -213,7 +206,6 @@ const Annotation: FC = (props) => {
onSelectedIdsChange={setSelectedIds}
onBatchDelete={handleBatchDelete}
onCancel={() => setSelectedIds([])}
- isBatchDeleting={isBatchDeleting}
/>
:
}
diff --git a/web/app/components/app/annotation/list.tsx b/web/app/components/app/annotation/list.tsx
index 6705ac5768..70ecedb869 100644
--- a/web/app/components/app/annotation/list.tsx
+++ b/web/app/components/app/annotation/list.tsx
@@ -19,7 +19,6 @@ type Props = {
onSelectedIdsChange: (selectedIds: string[]) => void
onBatchDelete: () => Promise
onCancel: () => void
- isBatchDeleting?: boolean
}
const List: FC = ({
@@ -30,7 +29,6 @@ const List: FC = ({
onSelectedIdsChange,
onBatchDelete,
onCancel,
- isBatchDeleting,
}) => {
const { t } = useTranslation()
const { formatTime } = useTimestamp()
@@ -142,7 +140,6 @@ const List: FC = ({
selectedIds={selectedIds}
onBatchDelete={onBatchDelete}
onCancel={onCancel}
- isBatchDeleting={isBatchDeleting}
/>
)}
diff --git a/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx b/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx
index 70e0334e98..aa8d0f65ca 100644
--- a/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx
+++ b/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx
@@ -78,7 +78,9 @@ const AdvancedPromptInput: FC = ({
const handleOpenExternalDataToolModal = () => {
setShowExternalDataToolModal({
payload: {},
- onSaveCallback: (newExternalDataTool: ExternalDataTool) => {
+ onSaveCallback: (newExternalDataTool?: ExternalDataTool) => {
+ if (!newExternalDataTool)
+ return
eventEmitter?.emit({
type: ADD_EXTERNAL_DATA_TOOL,
payload: newExternalDataTool,
diff --git a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx
index 169e8a14a2..8634232b2b 100644
--- a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx
+++ b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx
@@ -76,7 +76,9 @@ const Prompt: FC = ({
const handleOpenExternalDataToolModal = () => {
setShowExternalDataToolModal({
payload: {},
- onSaveCallback: (newExternalDataTool: ExternalDataTool) => {
+ onSaveCallback: (newExternalDataTool?: ExternalDataTool) => {
+ if (!newExternalDataTool)
+ return
eventEmitter?.emit({
type: ADD_EXTERNAL_DATA_TOOL,
payload: newExternalDataTool,
diff --git a/web/app/components/app/configuration/config-var/config-modal/index.tsx b/web/app/components/app/configuration/config-var/config-modal/index.tsx
index de7d2c9eac..3f32c9b0c7 100644
--- a/web/app/components/app/configuration/config-var/config-modal/index.tsx
+++ b/web/app/components/app/configuration/config-var/config-modal/index.tsx
@@ -320,7 +320,7 @@ const ConfigModal: FC = ({
{type === InputVarType.paragraph && (