fix: enforce tool provider type contracts (#39380)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh 2026-07-22 09:30:55 +08:00 committed by GitHub
parent 5216ee1d20
commit 9b432d0d29
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 342 additions and 124 deletions

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
from typing import Any, Literal, Protocol, cast
from typing import Any, Final, Literal, Protocol
from dify_agent.layers.dify_core_tools import DifyCoreToolConfig, DifyCoreToolProviderType, DifyCoreToolsLayerConfig
from dify_agent.layers.dify_plugin import (
@ -29,6 +29,13 @@ from models.provider_ids import ToolProviderID
from models.tools import WorkflowToolProvider
from services.tools.mcp_tools_manage_service import MCPToolManageService
_CORE_TOOL_PROVIDER_TYPES: Final[dict[ToolProviderType, DifyCoreToolProviderType]] = {
ToolProviderType.BUILT_IN: "builtin",
ToolProviderType.API: "api",
ToolProviderType.WORKFLOW: "workflow",
ToolProviderType.MCP: "mcp",
}
class WorkflowAgentDifyToolsBuildError(ValueError):
"""Raised when Agent Soul tools cannot be prepared for Agent backend."""
@ -235,7 +242,7 @@ class WorkflowAgentDifyToolsBuilder:
if tool_config.tool_name is not None:
expanded.append(tool_config)
continue
provider_type = ToolProviderType.value_of(tool_config.provider_type)
provider_type = tool_config.provider_type
provider_id = self._provider_id(tool_config)
try:
tool_names = self._provider_declared_tool_names(
@ -279,7 +286,7 @@ class WorkflowAgentDifyToolsBuilder:
tenant_id: str,
tool_config: AgentSoulDifyToolConfig,
) -> AgentSoulDifyToolConfig:
if tool_config.provider_type != ToolProviderType.MCP.value:
if tool_config.provider_type is not ToolProviderType.MCP:
return tool_config
provider_id = self._mcp_provider_id_resolver(tenant_id=tenant_id, provider_id=self._provider_id(tool_config))
return tool_config.model_copy(update={"provider_id": provider_id, "plugin_id": None, "provider": None})
@ -326,7 +333,7 @@ class WorkflowAgentDifyToolsBuilder:
def _to_agent_tool_entity(tool_config: AgentSoulDifyToolConfig) -> AgentToolEntity:
assert tool_config.tool_name is not None
return AgentToolEntity(
provider_type=ToolProviderType.value_of(tool_config.provider_type),
provider_type=tool_config.provider_type,
provider_id=WorkflowAgentDifyToolsBuilder._provider_id(tool_config),
tool_name=tool_config.tool_name,
tool_parameters=dict(tool_config.runtime_parameters),
@ -343,24 +350,16 @@ class WorkflowAgentDifyToolsBuilder:
@staticmethod
def _provider_key(tool_config: AgentSoulDifyToolConfig) -> tuple[ToolProviderType, str]:
return (
ToolProviderType.value_of(tool_config.provider_type),
WorkflowAgentDifyToolsBuilder._provider_id(tool_config),
)
return (tool_config.provider_type, WorkflowAgentDifyToolsBuilder._provider_id(tool_config))
@staticmethod
def _tool_layer_destination(tool_config: AgentSoulDifyToolConfig) -> Literal["plugin", "core"]:
provider_type = ToolProviderType.value_of(tool_config.provider_type)
provider_type = tool_config.provider_type
if provider_type is ToolProviderType.PLUGIN or (
provider_type is ToolProviderType.BUILT_IN and _is_plugin_provider_id(tool_config.provider_id)
):
return "plugin"
if provider_type in {
ToolProviderType.BUILT_IN,
ToolProviderType.API,
ToolProviderType.WORKFLOW,
ToolProviderType.MCP,
}:
if provider_type in _CORE_TOOL_PROVIDER_TYPES:
return "core"
if provider_type is ToolProviderType.DATASET_RETRIEVAL:
raise WorkflowAgentDifyToolsBuildError(
@ -417,7 +416,7 @@ class WorkflowAgentDifyToolsBuilder:
) -> DifyCoreToolConfig:
parameters = self._prepared_parameters(tool_runtime)
return DifyCoreToolConfig(
provider_type=cast(DifyCoreToolProviderType, tool_config.provider_type),
provider_type=_CORE_TOOL_PROVIDER_TYPES[tool_config.provider_type],
provider_id=self._provider_id(tool_config),
tool_name=tool_config.tool_name or exposed_name,
credential_id=tool_config.credential_ref.id if tool_config.credential_ref else None,

View File

@ -7,6 +7,7 @@ from typing import Annotated, Any, Final, Literal, Self
from pydantic import BaseModel, ConfigDict, Field, WithJsonSchema, field_validator, model_validator
from core.rag.entities.metadata_entities import ConditionValue, SupportedComparisonOperator
from core.tools.entities.tool_entities import ToolProviderType
from core.workflow.file_reference import is_canonical_file_reference
from graphon.file import FileTransferMethod, FileType
@ -653,11 +654,7 @@ class AgentSoulDifyToolConfig(BaseModel):
model_config = ConfigDict(extra="ignore")
enabled: bool = True
# ``plugin`` remains the default for legacy Agent Soul payloads. The runtime
# now also accepts ``builtin`` / ``api`` / ``workflow`` / ``mcp`` here and
# routes them through ``dify.core.tools``; keeping the default narrow still
# makes a missing field resolve against the plugin provider table.
provider_type: str = "plugin"
provider_type: ToolProviderType
provider_id: str | None = Field(default=None, max_length=255)
plugin_id: str | None = Field(default=None, max_length=255)
provider: str | None = Field(default=None, max_length=255)

View File

@ -14737,7 +14737,7 @@ should send ``plugin_id`` + ``provider`` when available.
| plugin_id | string | | No |
| provider | string | | No |
| provider_id | string | | No |
| provider_type | string, <br>**Default:** plugin | | No |
| provider_type | [ToolProviderType](#toolprovidertype) | | Yes |
| runtime_parameters | object | | No |
| tool_name | string | | No |

View File

@ -235,6 +235,7 @@ def test_builds_dify_plugin_tools_layer_from_existing_tool_runtime():
"dify_tools": [
{
"provider_id": "langgenius/search/search",
"provider_type": "plugin",
"tool_name": "search",
"credential_type": "api-key",
"credential_id": "credential-1",
@ -258,9 +259,6 @@ def test_builds_dify_plugin_tools_layer_from_existing_tool_runtime():
assert "region" not in prepared.parameters_json_schema["properties"]
assert runtime_provider.last_agent_tool is not None
assert runtime_provider.last_agent_tool.credential_id == "credential-1"
# Default ``provider_type`` is now ``"plugin"`` — the agent tool entity
# must surface that so ToolManager hits the plugin provider table, not the
# built-in legacy table.
assert runtime_provider.last_agent_tool.provider_type.value == "plugin"
@ -599,6 +597,7 @@ def test_rejects_duplicate_exposed_tool_names():
"dify_tools": [
{
"provider_id": "langgenius/search/search",
"provider_type": "plugin",
"tool_name": "search",
"credential_type": "api-key",
"credential_id": "credential-1",
@ -606,6 +605,7 @@ def test_rejects_duplicate_exposed_tool_names():
},
{
"provider_id": "langgenius/search/search",
"provider_type": "plugin",
"tool_name": "search",
"credential_type": "api-key",
"credential_id": "credential-1",
@ -629,6 +629,7 @@ def test_rejects_missing_required_runtime_parameter():
"dify_tools": [
{
"provider_id": "langgenius/search/search",
"provider_type": "plugin",
"tool_name": "search",
"credential_type": "api-key",
"credential_id": "credential-1",
@ -663,6 +664,7 @@ def test_invoke_from_is_forwarded_to_tool_runtime_provider():
"dify_tools": [
{
"provider_id": "langgenius/search/search",
"provider_type": "plugin",
"tool_name": "search",
"credential_type": "api-key",
"credential_id": "credential-1",
@ -690,6 +692,7 @@ def test_disabled_tools_are_skipped():
"dify_tools": [
{
"provider_id": "langgenius/search/search",
"provider_type": "plugin",
"tool_name": "search",
"credential_type": "api-key",
"credential_id": "credential-1",
@ -717,6 +720,7 @@ def test_plugin_id_plus_provider_fallback_when_provider_id_missing():
{
"plugin_id": "langgenius/search",
"provider": "search",
"provider_type": "plugin",
"tool_name": "search",
"credential_type": "api-key",
"credential_id": "credential-1",
@ -752,6 +756,7 @@ def test_unauthorized_tool_without_credentials():
"dify_tools": [
{
"provider_id": "langgenius/time/time",
"provider_type": "plugin",
"tool_name": "current_time",
"credential_type": "unauthorized",
"runtime_parameters": {"region": "us"},
@ -777,6 +782,7 @@ def _standard_tools_payload() -> AgentSoulToolsConfig:
"dify_tools": [
{
"provider_id": "langgenius/search/search",
"provider_type": "plugin",
"tool_name": "search",
"credential_type": "api-key",
"credential_id": "credential-1",
@ -859,6 +865,7 @@ def test_legacy_provider_name_and_tool_parameters_normalized():
"dify_tools": [
{
"provider_name": "langgenius/search/search",
"provider_type": "plugin",
"tool_name": "search",
"credential_type": "api-key",
"credential_id": "credential-1",
@ -875,6 +882,21 @@ def test_legacy_provider_name_and_tool_parameters_normalized():
assert tool.credential_ref.id == "credential-1"
def test_rejects_unknown_provider_type_at_config_boundary():
with pytest.raises(ValueError, match="provider_type"):
AgentSoulToolsConfig.model_validate(
{
"dify_tools": [
{
"provider_id": "future-provider",
"provider_type": "future-provider",
"credential_type": "unauthorized",
}
]
}
)
# ── provider-level entries (tool_name omitted = all tools of the provider) ───
@ -888,7 +910,15 @@ def test_provider_level_entry_expands_to_all_tools():
builder = WorkflowAgentDifyToolsBuilder(tool_runtime_provider=runtime_provider, provider_tools_lister=lister)
tools = AgentSoulToolsConfig.model_validate(
{"dify_tools": [{"provider_id": "langgenius/search/search", "credential_type": "unauthorized"}]}
{
"dify_tools": [
{
"provider_id": "langgenius/search/search",
"provider_type": "plugin",
"credential_type": "unauthorized",
}
]
}
)
result = _build(builder, tools)
@ -906,9 +936,14 @@ def test_explicit_tool_entry_wins_over_provider_expansion():
tools = AgentSoulToolsConfig.model_validate(
{
"dify_tools": [
{"provider_id": "langgenius/search/search", "credential_type": "unauthorized"},
{
"provider_id": "langgenius/search/search",
"provider_type": "plugin",
"credential_type": "unauthorized",
},
{
"provider_id": "langgenius/search/search",
"provider_type": "plugin",
"tool_name": "search",
"credential_type": "unauthorized",
"runtime_parameters": {"region": "eu"},
@ -930,7 +965,15 @@ def test_provider_level_entry_with_no_tools_maps_to_declaration_not_found():
provider_tools_lister=lambda *, tenant_id, provider_type, provider_id: [],
)
tools = AgentSoulToolsConfig.model_validate(
{"dify_tools": [{"provider_id": "langgenius/search/search", "credential_type": "unauthorized"}]}
{
"dify_tools": [
{
"provider_id": "langgenius/search/search",
"provider_type": "plugin",
"credential_type": "unauthorized",
}
]
}
)
with pytest.raises(WorkflowAgentDifyToolsBuildError) as exc_info:
@ -948,7 +991,15 @@ def test_provider_level_entry_unknown_provider_maps_to_declaration_not_found():
tool_runtime_provider=FakeRuntimeProvider(_tool()), provider_tools_lister=lister
)
tools = AgentSoulToolsConfig.model_validate(
{"dify_tools": [{"provider_id": "langgenius/search/search", "credential_type": "unauthorized"}]}
{
"dify_tools": [
{
"provider_id": "langgenius/search/search",
"provider_type": "plugin",
"credential_type": "unauthorized",
}
]
}
)
with pytest.raises(WorkflowAgentDifyToolsBuildError) as exc_info:

View File

@ -667,6 +667,7 @@ def test_builds_workflow_run_request_with_dify_plugin_tools_layer(monkeypatch: p
"dify_tools": [
{
"provider_id": "langgenius/time/time",
"provider_type": "plugin",
"tool_name": "current_time",
"credential_type": "unauthorized",
}

View File

@ -295,8 +295,16 @@ def test_publish_validation_dedupes_provider_level_tool_entries():
),
tools={
"dify_tools": [
{"provider_id": "langgenius/duckduckgo/duckduckgo", "credential_type": "unauthorized"},
{"provider_id": "langgenius/duckduckgo/duckduckgo", "credential_type": "unauthorized"},
{
"provider_id": "langgenius/duckduckgo/duckduckgo",
"provider_type": "plugin",
"credential_type": "unauthorized",
},
{
"provider_id": "langgenius/duckduckgo/duckduckgo",
"provider_type": "plugin",
"credential_type": "unauthorized",
},
]
},
)
@ -321,9 +329,14 @@ def test_publish_validation_accepts_provider_level_plus_explicit_tool_entry():
),
tools={
"dify_tools": [
{"provider_id": "langgenius/duckduckgo/duckduckgo", "credential_type": "unauthorized"},
{
"provider_id": "langgenius/duckduckgo/duckduckgo",
"provider_type": "plugin",
"credential_type": "unauthorized",
},
{
"provider_id": "langgenius/duckduckgo/duckduckgo",
"provider_type": "plugin",
"tool_name": "ddg_search",
"credential_type": "unauthorized",
},

View File

@ -78,6 +78,7 @@ def test_make_portable_agent_package_strips_workspace_credentials_and_assets() -
"dify_tools": [
{
"provider_id": "langgenius/google/google",
"provider_type": "plugin",
"tool_name": "search",
"credential_type": "api-key",
"credential_ref": {"type": "tool", "id": "tool-secret"},
@ -196,6 +197,7 @@ def test_import_warnings_cover_runtime_setup_removed_from_package(monkeypatch) -
"dify_tools": [
{
"provider_id": "langgenius/google/google",
"provider_type": "plugin",
"tool_name": "search",
"credential_type": "unauthorized",
}
@ -513,10 +515,15 @@ def test_extract_package_dependencies_covers_model_tools_and_knowledge(monkeypat
"model": {"plugin_id": "model-plugin", "model_provider": "provider/model", "model": "model"},
"tools": {
"dify_tools": [
{"provider_id": "provider/tool", "credential_type": "unauthorized"},
{
"provider_id": "provider/tool",
"provider_type": "plugin",
"credential_type": "unauthorized",
},
{
"plugin_id": "plugin-id",
"provider": "fallback-provider",
"provider_type": "plugin",
"credential_type": "unauthorized",
},
]

View File

@ -199,6 +199,7 @@ def test_provider_all_tools_mention_resolves_against_provider_level_entry():
{
"plugin_id": "langgenius/duckduckgo",
"provider": "duckduckgo",
"provider_type": "plugin",
"credential_type": "unauthorized",
}
]

View File

@ -140,6 +140,7 @@ def soul() -> AgentSoulConfig:
{
"plugin_id": "langgenius/tavily",
"provider": "tavily",
"provider_type": "plugin",
"tool_name": "tavily_search",
"credential_type": "unauthorized",
},

View File

@ -1,6 +1,7 @@
import type {
AgentKnowledgeDatasetConfig,
AgentSoulConfig,
AgentSoulDifyToolConfig,
} from '@dify/contracts/api/console/agent/types.gen'
import type {
ConsoleSegmentListResponse,
@ -713,14 +714,15 @@ const getStableModelResource = (context: SeedContext): StableModel | undefined =
const getToolResource = (context: SeedContext, displayName: string) =>
context.resources.get(`tool:${displayName}`) as ToolResource | undefined
const toolConfig = (tool: ToolResource) => ({
credential_type: 'unauthorized' as const,
enabled: true,
provider_id: tool.providerName,
provider_type: 'builtin',
runtime_parameters: {},
tool_name: tool.toolName,
})
const toolConfig = (tool: ToolResource) =>
({
credential_type: 'unauthorized',
enabled: true,
provider_id: tool.providerName,
provider_type: 'builtin',
runtime_parameters: {},
tool_name: tool.toolName,
}) satisfies AgentSoulDifyToolConfig
const saveSeededAgentComposer = async ({
agentId,

View File

@ -3875,7 +3875,7 @@
},
"web/app/components/tools/types.ts": {
"erasable-syntax-only/enums": {
"count": 4
"count": 3
},
"typescript/no-explicit-any": {
"count": 4

View File

@ -1579,7 +1579,7 @@ export type AgentSoulDifyToolConfig = {
plugin_id?: string | null
provider?: string | null
provider_id?: string | null
provider_type?: string
provider_type: ToolProviderType
runtime_parameters?: {
[key: string]:
| string
@ -1744,6 +1744,15 @@ export type AgentSoulDifyToolCredentialRef = {
type?: 'provider' | 'tool'
}
export type ToolProviderType =
| 'api'
| 'app'
| 'builtin'
| 'dataset-retrieval'
| 'mcp'
| 'plugin'
| 'workflow'
export type OutputErrorStrategy = 'default_value' | 'fail_branch' | 'stop'
export type DeclaredOutputRetryConfig = {

View File

@ -2065,6 +2065,21 @@ export const zAgentSoulDifyToolCredentialRef = z.object({
type: z.enum(['provider', 'tool']).optional().default('tool'),
})
/**
* ToolProviderType
*
* Enum class for tool provider
*/
export const zToolProviderType = z.enum([
'api',
'app',
'builtin',
'dataset-retrieval',
'mcp',
'plugin',
'workflow',
])
/**
* AgentSoulDifyToolConfig
*
@ -2087,7 +2102,7 @@ export const zAgentSoulDifyToolConfig = z.object({
plugin_id: z.string().max(255).nullish(),
provider: z.string().max(255).nullish(),
provider_id: z.string().max(255).nullish(),
provider_type: z.string().optional().default('plugin'),
provider_type: zToolProviderType,
runtime_parameters: z
.record(
z.string(),

View File

@ -2813,7 +2813,7 @@ export type AgentSoulDifyToolConfig = {
plugin_id?: string | null
provider?: string | null
provider_id?: string | null
provider_type?: string
provider_type: ToolProviderType
runtime_parameters?: {
[key: string]:
| string
@ -2964,6 +2964,15 @@ export type AgentSoulDifyToolCredentialRef = {
type?: 'provider' | 'tool'
}
export type ToolProviderType =
| 'api'
| 'app'
| 'builtin'
| 'dataset-retrieval'
| 'mcp'
| 'plugin'
| 'workflow'
export type StringSource = {
selector?: Array<string>
type: ValueSourceType

View File

@ -3682,6 +3682,21 @@ export const zAgentSoulDifyToolCredentialRef = z.object({
type: z.enum(['provider', 'tool']).optional().default('tool'),
})
/**
* ToolProviderType
*
* Enum class for tool provider
*/
export const zToolProviderType = z.enum([
'api',
'app',
'builtin',
'dataset-retrieval',
'mcp',
'plugin',
'workflow',
])
/**
* AgentSoulDifyToolConfig
*
@ -3704,7 +3719,7 @@ export const zAgentSoulDifyToolConfig = z.object({
plugin_id: z.string().max(255).nullish(),
provider: z.string().max(255).nullish(),
provider_id: z.string().max(255).nullish(),
provider_type: z.string().optional().default('plugin'),
provider_type: zToolProviderType,
runtime_parameters: z
.record(
z.string(),

View File

@ -943,7 +943,7 @@ export type AgentSoulDifyToolConfig = {
plugin_id?: string | null
provider?: string | null
provider_id?: string | null
provider_type?: string
provider_type: ToolProviderType
runtime_parameters?: {
[key: string]:
| string
@ -1063,6 +1063,15 @@ export type AgentSoulDifyToolCredentialRef = {
type?: 'provider' | 'tool'
}
export type ToolProviderType =
| 'api'
| 'app'
| 'builtin'
| 'dataset-retrieval'
| 'mcp'
| 'plugin'
| 'workflow'
export type AgentModerationIoConfig = {
enabled?: boolean
preset_response?: string | null

View File

@ -1261,6 +1261,21 @@ export const zAgentSoulDifyToolCredentialRef = z.object({
type: z.enum(['provider', 'tool']).optional().default('tool'),
})
/**
* ToolProviderType
*
* Enum class for tool provider
*/
export const zToolProviderType = z.enum([
'api',
'app',
'builtin',
'dataset-retrieval',
'mcp',
'plugin',
'workflow',
])
/**
* AgentSoulDifyToolConfig
*
@ -1283,7 +1298,7 @@ export const zAgentSoulDifyToolConfig = z.object({
plugin_id: z.string().max(255).nullish(),
provider: z.string().max(255).nullish(),
provider_id: z.string().max(255).nullish(),
provider_type: z.string().optional().default('plugin'),
provider_type: zToolProviderType,
runtime_parameters: z
.record(
z.string(),

View File

@ -22,6 +22,7 @@ import AppIcon from '@/app/components/base/app-icon'
import { DefaultToolIcon } from '@/app/components/base/icons/src/public/other'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import { Infotip } from '@/app/components/base/infotip'
import { parseToolProviderType } from '@/app/components/tools/provider-type'
import { CollectionType } from '@/app/components/tools/types'
import {
addDefaultValue,
@ -115,16 +116,17 @@ const AgentTools: FC = () => {
? toolParametersToFormSchemas(currToolWithConfigs.parameters)
: []
const paramsWithDefaultValue = addDefaultValue(tool.params, formSchemas)
const providerType = parseToolProviderType(tool.provider_type)
return {
provider_id: tool.provider_id,
provider_type: tool.provider_type as CollectionType,
provider_type: providerType,
provider_name: tool.provider_name,
tool_name: tool.tool_name,
tool_label: tool.tool_label,
tool_parameters: paramsWithDefaultValue,
notAuthor: !tool.is_team_authorization,
enabled: true,
type: tool.provider_type as CollectionType,
type: providerType,
}
}
const handleSelectTool = (tool: ToolDefaultValue) => {

View File

@ -1,3 +1,4 @@
import type { PluginPayload } from '../../types'
import { renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthCategory, CredentialTypeEnum } from '../../types'
@ -57,7 +58,7 @@ const toolPayload = {
category: AuthCategory.tool,
provider: 'test-provider',
providerType: 'builtin',
}
} satisfies PluginPayload
describe('use-credential hooks', () => {
beforeEach(() => {

View File

@ -1,4 +1,4 @@
import type { CollectionType } from '../../tools/types'
import type { CollectionProviderType } from '../../tools/types'
import type { PluginDetail } from '../types'
export type { AddApiKeyButtonProps } from './authorize/add-api-key-button'
@ -14,7 +14,7 @@ export enum AuthCategory {
export type PluginPayload = {
category: AuthCategory
provider: string
providerType?: CollectionType | string
providerType?: CollectionProviderType
detail?: PluginDetail
}

View File

@ -346,7 +346,7 @@ const DetailHeader = ({
pluginPayload={{
provider: provider?.name || '',
category: AuthCategory.tool,
providerType: provider?.type || '',
providerType: provider?.type,
detail,
}}
/>

View File

@ -1,3 +1,4 @@
import type { PluginPayload } from '@/app/components/plugins/plugin-auth/types'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import { render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
@ -12,7 +13,7 @@ vi.mock('@/app/components/plugins/plugin-auth', () => ({
pluginPayload,
credentialId,
}: {
pluginPayload: { provider: string; providerType: string }
pluginPayload: PluginPayload
credentialId?: string
}) => (
<div data-testid="plugin-auth-in-agent">

View File

@ -0,0 +1,5 @@
import type { ToolProviderType } from '@dify/contracts/api/console/workspaces/types.gen'
import { zToolProviderType } from '@dify/contracts/api/console/workspaces/zod.gen'
export const parseToolProviderType = (providerType: unknown): ToolProviderType =>
zToolProviderType.parse(providerType)

View File

@ -1,3 +1,7 @@
import type {
DatasourceProviderType,
ToolProviderType,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { VarType } from '../workflow/types'
type LocalizedText<T = string> = {
@ -27,16 +31,20 @@ export type Credential = {
api_key_query_param?: string
}
export enum CollectionType {
all = 'all',
builtIn = 'builtin',
custom = 'api',
model = 'model',
workflow = 'workflow',
mcp = 'mcp',
datasource = 'datasource',
trigger = 'trigger',
}
export const CollectionType = {
all: 'all',
builtIn: 'builtin',
custom: 'api',
model: 'model',
workflow: 'workflow',
mcp: 'mcp',
datasource: 'datasource',
trigger: 'trigger',
} as const
export type CollectionType = (typeof CollectionType)[keyof typeof CollectionType]
export type CollectionProviderType = CollectionType | DatasourceProviderType | ToolProviderType
export type Emoji = {
background: string
@ -51,7 +59,7 @@ export type Collection = {
icon: string | Emoji
icon_dark?: string | Emoji
label: LocalizedText
type: CollectionType | string
type: CollectionProviderType
team_credentials: Record<string, any>
is_team_authorization: boolean
allow_delete: boolean

View File

@ -4,7 +4,6 @@ import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useMarketplacePlugins } from '@/app/components/plugins/marketplace/query'
import { PluginCategoryEnum } from '@/app/components/plugins/types'
import { CollectionType } from '@/app/components/tools/types'
import { useGetLanguage } from '@/context/i18n'
import useTheme from '@/hooks/use-theme'
import { renderWithConsoleQuery } from '@/test/console/query-data'
@ -43,7 +42,7 @@ const createToolProvider = (overrides: Partial<ToolWithProvider> = {}): ToolWith
description: { en_US: 'desc', zh_Hans: '描述' },
icon: 'icon',
label: { en_US: 'File Source', zh_Hans: '文件源' },
type: CollectionType.datasource,
type: 'local_file',
team_credentials: {},
is_team_authorization: false,
allow_delete: false,

View File

@ -77,7 +77,7 @@ describe('ToolBrowser', () => {
customTools={[
createToolProvider({
id: 'provider-custom',
type: 'custom',
type: CollectionType.custom,
label: { en_US: 'Custom Provider', zh_Hans: 'Custom Provider' },
}),
]}
@ -89,7 +89,7 @@ describe('ToolBrowser', () => {
expect(screen.getByText('Built In Provider')).toBeInTheDocument()
expect(screen.getByText('Custom Provider')).toBeInTheDocument()
await user.click(screen.getByText('workflow.tabs.customTool'))
await user.click(screen.getByRole('button', { name: 'workflow.tabs.customTool' }))
expect(screen.getByText('Custom Provider')).toBeInTheDocument()
expect(screen.queryByText('Built In Provider')).not.toBeInTheDocument()

View File

@ -71,7 +71,7 @@ describe('createToolListData', () => {
const result = createToolListData(
[
createToolProvider({ id: 'workflow', type: CollectionType.workflow }),
createToolProvider({ id: 'data-source', type: CollectionType.datasource }),
createToolProvider({ id: 'data-source', type: 'local_file' }),
],
() => 'A',
)
@ -90,19 +90,19 @@ describe('createToolListData', () => {
])
})
it('merges providers across letters and keeps MCP and unknown provider identities explicit', () => {
it('merges providers across letters and keeps MCP and plugin provider identities explicit', () => {
const lettersByToolId: Record<string, string> = {
'author-a': 'A',
'author-b': 'B',
mcp: 'C',
unknown: 'D',
plugin: 'D',
}
const result = createToolListData(
[
createToolProvider({ id: 'author-a', author: 'Dify' }),
createToolProvider({ id: 'author-b', author: 'Dify' }),
createToolProvider({ id: 'mcp', type: CollectionType.mcp }),
createToolProvider({ id: 'unknown', author: 'Future', type: 'future-provider' }),
createToolProvider({ id: 'plugin', author: 'Plugin Author', type: 'plugin' }),
],
(tool) => lettersByToolId[tool.id] ?? '#',
)
@ -123,8 +123,8 @@ describe('createToolListData', () => {
},
{
kind: 'author',
author: 'Future',
tools: [expect.objectContaining({ id: 'unknown' })],
author: 'Plugin Author',
tools: [expect.objectContaining({ id: 'plugin' })],
},
])
})

View File

@ -12,7 +12,7 @@ const createDataSourceItem = (overrides: Partial<DataSourceItem> = {}): DataSour
provider: 'provider-a',
declaration: {
credentials_schema: [{ name: 'api_key' }],
provider_type: 'hosted',
provider_type: 'local_file',
identity: {
author: 'Dify',
description: createLocalizedText('Datasource provider'),
@ -58,7 +58,7 @@ describe('transformDataSourceToTool', () => {
description: createLocalizedText('Datasource provider'),
icon: 'provider-icon',
label: createLocalizedText('Provider A'),
type: 'hosted',
type: 'local_file',
allow_delete: true,
is_authorized: true,
is_team_authorization: true,

View File

@ -1,6 +1,7 @@
import type { OnSelectBlock, ToolWithProvider } from '../types'
import type { DataSourceDefaultValue, ToolDefaultValue } from './types'
import type { ListRef } from '@/app/components/workflow/block-selector/marketplace-plugin/list'
import { zDatasourceProviderType } from '@dify/contracts/api/console/workspaces/zod.gen'
import { cn } from '@langgenius/dify-ui/cn'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useDebounce } from 'ahooks'
@ -60,7 +61,7 @@ function DataSources({
(_: BlockEnum, toolDefaultValue: ToolDefaultValue) => {
let defaultValue: DataSourceDefaultValue = {
plugin_id: toolDefaultValue?.provider_id,
provider_type: toolDefaultValue?.provider_type,
provider_type: zDatasourceProviderType.parse(toolDefaultValue.provider_type),
provider_name: toolDefaultValue?.provider_name,
datasource_name: toolDefaultValue?.tool_name,
datasource_label: toolDefaultValue?.tool_label,

View File

@ -1,9 +1,17 @@
import type { DatasourceProviderType } from '@dify/contracts/api/console/workspaces/types.gen'
import type { ToolWithProvider } from '../types'
import { pinyin } from 'pinyin-pro'
import { CollectionType } from '../../tools/types'
type ToolCategoryGroup = 'custom' | 'data-source' | 'mcp' | 'workflow'
const datasourceProviderTypes: Record<DatasourceProviderType, true> = {
local_file: true,
online_document: true,
online_drive: true,
website_crawl: true,
}
type AuthorToolGroup = {
kind: 'author'
author: string
@ -114,10 +122,14 @@ function addToolToAuthorGroup(bucket: LetterBucket, tool: ToolWithProvider) {
function getToolCategoryGroup(type: ToolWithProvider['type']): ToolCategoryGroup | undefined {
if (type === CollectionType.custom) return 'custom'
if (type === CollectionType.workflow) return 'workflow'
if (type === CollectionType.datasource) return 'data-source'
if (isDatasourceProviderType(type)) return 'data-source'
if (type === CollectionType.mcp) return 'mcp'
}
function isDatasourceProviderType(type: ToolWithProvider['type']): type is DatasourceProviderType {
return Object.hasOwn(datasourceProviderTypes, type)
}
function mergeGroupsByProvider(buckets: LetterBucket[]) {
const groups: ToolGroup[] = []
const authorGroups = new Map<string, AuthorToolGroup>()

View File

@ -50,7 +50,7 @@ describe('ToolListTreeView', () => {
tools: [
createToolProvider({
id: 'custom-provider',
type: 'custom',
type: CollectionType.custom,
label: { en_US: 'Custom Provider', zh_Hans: 'Custom Provider' },
}),
],
@ -83,7 +83,7 @@ describe('ToolListTreeView', () => {
tools: [
createToolProvider({
id: 'data-source-provider',
type: CollectionType.datasource,
type: 'local_file',
label: { en_US: 'Data Source Provider', zh_Hans: 'Data Source Provider' },
}),
],

View File

@ -62,7 +62,7 @@ const TriggerPluginActionItem: FC<Props> = ({
onSelect(BlockEnum.TriggerPlugin, {
plugin_id: provider.plugin_id,
provider_id: provider.name,
provider_type: provider.type as string,
provider_type: provider.type,
provider_name: provider.name,
event_name: payload.name,
event_label: payload.label[language]!,

View File

@ -1,4 +1,5 @@
import type { AgentInviteOptionResponse } from '@dify/contracts/api/console/agent/types.gen'
import type { DatasourceProviderType } from '@dify/contracts/api/console/workspaces/types.gen'
import type {
ParametersSchema,
PluginMeta,
@ -6,7 +7,7 @@ import type {
SupportedCreationMethods,
TriggerEvent,
} from '../../plugins/types'
import type { Collection, Event } from '../../tools/types'
import type { Collection, CollectionProviderType, Event } from '../../tools/types'
import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations'
export const TabType = {
@ -48,7 +49,7 @@ export type BlockClassification = (typeof BlockClassification)[keyof typeof Bloc
type PluginCommonDefaultValue = {
provider_id: string
provider_type: string
provider_type: CollectionProviderType
provider_name: string
}
@ -87,7 +88,6 @@ export type ToolDefaultValue = PluginCommonDefaultValue & {
export type DataSourceDefaultValue = Omit<PluginCommonDefaultValue, 'provider_id'> & {
plugin_id: string
provider_type: string
provider_name: string
datasource_name: string
datasource_label: string
@ -145,7 +145,7 @@ export type DataSourceItem = {
provider: string
declaration: {
credentials_schema: unknown[]
provider_type: string
provider_type: DatasourceProviderType
identity: {
author: string
description: TypeWithI18N

View File

@ -1,4 +1,5 @@
import type { CommonNodeType } from '../../types'
import type { CollectionProviderType } from '@/app/components/tools/types'
import { act } from '@testing-library/react'
import { CollectionType } from '@/app/components/tools/types'
import { renderWorkflowHook } from '../../__tests__/workflow-test-env'
@ -27,7 +28,8 @@ vi.mock('@/service/use-tools', () => ({
useAllCustomTools: (enabled: boolean) => mockCustomTools(enabled),
useAllWorkflowTools: (enabled: boolean) => mockWorkflowTools(enabled),
useAllMCPTools: (enabled: boolean) => mockMcpTools(enabled),
useInvalidToolsByType: (providerType?: string) => mockInvalidToolsByType(providerType),
useInvalidToolsByType: (providerType?: CollectionProviderType) =>
mockInvalidToolsByType(providerType),
}))
vi.mock('@/service/use-triggers', () => ({
@ -164,11 +166,11 @@ describe('useNodePluginInstallation', () => {
expect(result.current.shouldDim).toBe(false)
})
it('should keep unknown tool collection types installable without collection state', () => {
it('should keep plugin tool collections installable without collection state', () => {
const { result } = renderWorkflowHook(() =>
useNodePluginInstallation(
makeToolNode({
provider_type: 'unknown' as CollectionType,
provider_type: 'plugin',
plugin_unique_identifier: undefined,
plugin_id: undefined,
provider_id: 'legacy-provider',

View File

@ -61,7 +61,7 @@ type Props = Readonly<{
showManageInputField?: boolean
onManageInputField?: () => void
extraParams?: Record<string, unknown>
providerType?: string
providerType?: 'tool' | 'trigger'
disableVariableInsertion?: boolean
}>

View File

@ -73,7 +73,7 @@ describe('workflow-panel helpers', () => {
const dataSourceData = asNodeData({
type: BlockEnum.DataSource,
plugin_id: 'source-1',
provider_type: 'remote',
provider_type: 'online_document',
})
const triggerPlugins = [{ plugin_id: 'trigger-1', id: '1' }]
const dataSources = [{ plugin_id: 'source-1' }]

View File

@ -592,7 +592,7 @@ describe('workflow-panel index', () => {
createData({
type: BlockEnum.DataSource,
plugin_id: 'source-1',
provider_type: 'remote',
provider_type: 'online_document',
}) as never
}
>

View File

@ -637,7 +637,7 @@ const BasePanel: FC<BasePanelProps> = ({ id, data, children }) => {
className="px-4 pb-2"
pluginPayload={{
provider: currToolCollection?.name || '',
providerType: currToolCollection?.type || '',
providerType: currToolCollection?.type,
category: AuthCategory.tool,
detail: currToolCollection as any,
}}
@ -647,7 +647,7 @@ const BasePanel: FC<BasePanelProps> = ({ id, data, children }) => {
<AuthorizedInNode
pluginPayload={{
provider: currToolCollection?.name || '',
providerType: currToolCollection?.type || '',
providerType: currToolCollection?.type,
category: AuthCategory.tool,
detail: currToolCollection as any,
}}

View File

@ -26,7 +26,7 @@ vi.mock('@/app/components/workflow/block-selector', () => ({
onClick={() =>
onSelect(BlockEnum.DataSource, {
plugin_id: 'plugin-id',
provider_type: 'datasource',
provider_type: 'local_file',
provider_name: 'file',
datasource_name: 'local-file',
datasource_label: 'Local File',
@ -89,7 +89,7 @@ describe('DataSourceEmptyNode', () => {
expect(handleReplaceNode).toHaveBeenCalledWith(BlockEnum.DataSource, {
plugin_id: 'plugin-id',
provider_type: 'datasource',
provider_type: 'local_file',
provider_name: 'file',
datasource_name: 'local-file',
datasource_label: 'Local File',

View File

@ -25,7 +25,7 @@ const createNodeData = (overrides: Partial<DataSourceNodeType> = {}): DataSource
desc: '',
type: BlockEnum.DataSource,
plugin_id: 'plugin-id',
provider_type: 'datasource',
provider_type: 'local_file',
provider_name: 'file',
datasource_name: 'local-file',
datasource_label: 'Local File',

View File

@ -106,7 +106,7 @@ const createData = (overrides: Partial<DataSourceNodeType> = {}): DataSourceNode
desc: '',
type: BlockEnum.DataSource,
plugin_id: 'plugin-1',
provider_type: 'remote',
provider_type: 'online_document',
provider_name: 'provider',
datasource_name: 'source-a',
datasource_label: 'Source A',

View File

@ -1,3 +1,4 @@
import type { DatasourceProviderType } from '@dify/contracts/api/console/workspaces/types.gen'
import type { Dispatch, SetStateAction } from 'react'
import type { ResourceVarInputs } from '../_base/types'
import type { CommonNodeType, Node } from '@/app/components/workflow/types'
@ -13,7 +14,7 @@ export type ToolVarInputs = ResourceVarInputs
export type DataSourceNodeType = CommonNodeType & {
fileExtensions?: string[]
plugin_id: string
provider_type: string
provider_type: DatasourceProviderType
provider_name: string
datasource_name: string
datasource_label: string

View File

@ -1,6 +1,5 @@
import type { DataSourceNodeType } from '../../nodes/data-source/types'
import type { ToolWithProvider } from '../../types'
import { CollectionType } from '@/app/components/tools/types'
import { BlockEnum } from '../../types'
import { getDataSourceCheckParams } from '../data-source'
@ -23,7 +22,7 @@ function createDataSourceData(overrides: Partial<DataSourceNodeType> = {}): Data
desc: '',
type: BlockEnum.DataSource,
plugin_id: 'plugin-ds-1',
provider_type: CollectionType.builtIn,
provider_type: 'local_file',
datasource_name: 'mysql_query',
datasource_parameters: {},
datasource_configurations: {},
@ -70,7 +69,7 @@ describe('getDataSourceCheckParams', () => {
])
})
it('should mark notAuthed for builtin datasource without authorization', () => {
it('should mark an unauthorized datasource as not authed', () => {
const result = getDataSourceCheckParams(
createDataSourceData(),
[createDataSourceCollection()],

View File

@ -1,6 +1,5 @@
import type { DataSourceNodeType } from '../nodes/data-source/types'
import type { InputVar, ToolWithProvider } from '../types'
import { CollectionType } from '@/app/components/tools/types'
import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
export const getDataSourceCheckParams = (
@ -8,8 +7,7 @@ export const getDataSourceCheckParams = (
dataSourceList: ToolWithProvider[],
language: string,
) => {
const { plugin_id, provider_type, datasource_name } = toolData
const isBuiltIn = provider_type === CollectionType.builtIn
const { plugin_id, datasource_name } = toolData
const currentDataSource = dataSourceList.find((item) => item.plugin_id === plugin_id)
const currentDataSourceItem = currentDataSource?.tools.find(
(tool) => tool.name === datasource_name,
@ -32,7 +30,7 @@ export const getDataSourceCheckParams = (
})
return formInputs
})(),
notAuthed: isBuiltIn && !!currentDataSource?.allow_delete && !currentDataSource?.is_authorized,
notAuthed: !!currentDataSource?.allow_delete && !currentDataSource?.is_authorized,
language,
}
}

View File

@ -428,6 +428,7 @@ describe('agent composer store conversions', () => {
kind: 'provider',
name: 'duckduckgo',
iconClassName: 'i-custom-public-other-default-tool-icon text-text-tertiary',
providerType: 'builtin',
credentialVariant: 'none',
actions: [
{

View File

@ -287,7 +287,7 @@ const toDifyToolConfigs = (
enabled: true,
provider: tool.name,
provider_id: tool.id,
provider_type: tool.providerType ?? 'builtin',
provider_type: tool.providerType,
tool_name: action.toolName,
runtime_parameters: toToolRuntimeParameters(toolSettings[action.id]),
credential_type: credentialType,

View File

@ -2,6 +2,7 @@ import type {
AgentKnowledgeDatasetConfig,
AgentSoulAppFeaturesConfig,
AgentSoulModelConfig,
ToolProviderType,
} from '@dify/contracts/api/console/agent/types.gen'
import type { FileTreeIconType } from '@langgenius/dify-ui/file-tree'
import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
@ -95,7 +96,7 @@ export type AgentProviderTool = AgentToolBase & {
iconClassName: string
icon?: ToolDefaultValue['provider_icon']
iconDark?: ToolDefaultValue['provider_icon_dark']
providerType?: string
providerType: ToolProviderType
allowDelete?: boolean
credentialId?: string
credentialKey?: I18nKeysWithPrefix<'agentV2', 'agentDetail.configure.tools.'>

View File

@ -151,6 +151,7 @@ describe('agent composer tools store', () => {
kind: 'provider',
name: 'DuckDuckGo',
iconClassName: 'i-simple-icons-duckduckgo',
providerType: 'builtin',
credentialVariant: 'none',
actions: [
{

View File

@ -1,3 +1,4 @@
import type { ToolProviderType } from '@dify/contracts/api/console/agent/types.gen'
import type {
AgentCliTool,
AgentProviderTool,
@ -11,7 +12,8 @@ import { syncCliToolReferenceLabels } from '../reference-labels'
import { agentComposerDraftAtom } from '../store'
import { resolveDraftFieldUpdate } from './utils'
export type AgentProviderToolDefaultValue = ToolDefaultValue & {
export type AgentProviderToolDefaultValue = Omit<ToolDefaultValue, 'provider_type'> & {
provider_type: ToolProviderType
allowDelete?: boolean
credentialType?: AgentProviderTool['credentialType']
credentialRequired?: boolean

View File

@ -198,6 +198,7 @@ const duckDuckGoProviderTool: AgentTool = {
name: 'DuckDuckGo',
kind: 'provider',
iconClassName: 'i-simple-icons-duckduckgo',
providerType: 'builtin',
credentialKey: 'agentDetail.configure.tools.credential.authOne',
credentialVariant: 'authorized',
actions: [duckDuckGoSearchAction],

View File

@ -19,6 +19,7 @@ import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { getMarketplaceCategoryUrl } from '@/app/components/plugins/marketplace/utils'
import { PluginCategoryEnum } from '@/app/components/plugins/types'
import { parseToolProviderType } from '@/app/components/tools/provider-type'
import { CollectionType } from '@/app/components/tools/types'
import BlockIcon from '@/app/components/workflow/block-icon'
import { ToolType } from '@/app/components/workflow/block-selector/types'
@ -493,7 +494,7 @@ function toToolDefaultValue(
return {
provider_id: provider.id,
provider_type: provider.type,
provider_type: parseToolProviderType(provider.type),
provider_name: provider.name,
provider_show_name: providerLabel,
plugin_id: provider.plugin_id,

View File

@ -111,6 +111,7 @@ const agentToolsDraft = {
kind: 'provider',
name: 'DuckDuckGo',
iconClassName: 'i-simple-icons-duckduckgo',
providerType: 'builtin',
credentialKey: 'agentDetail.configure.tools.credential.authOne',
credentialVariant: 'none',
actions: [
@ -144,6 +145,7 @@ const reflectedAgentToolsDraft = {
kind: 'provider',
name: 'google',
iconClassName: 'i-custom-public-other-default-tool-icon',
providerType: 'builtin',
credentialVariant: 'none',
actions: [
{
@ -165,6 +167,7 @@ const reflectedUnauthorizedNoCredentialDraft = {
kind: 'provider',
name: 'duckduckgo',
iconClassName: 'i-custom-public-other-default-tool-icon',
providerType: 'builtin',
credentialType: 'unauthorized',
credentialVariant: 'unauthorized',
actions: [
@ -187,6 +190,7 @@ const reflectedUnauthorizedOAuthCredentialTypeDraft = {
kind: 'provider',
name: 'google',
iconClassName: 'i-custom-public-other-default-tool-icon',
providerType: 'builtin',
credentialType: 'unauthorized',
credentialVariant: 'none',
actions: [

View File

@ -15,6 +15,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/pop
import { useAtomValue, useSetAtom } from 'jotai'
import { memo, useCallback, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { parseToolProviderType } from '@/app/components/tools/provider-type'
import { CollectionType } from '@/app/components/tools/types'
import { ToolPickerContent } from '@/app/components/workflow/block-selector/tool-picker'
import { useGetLanguage } from '@/context/i18n'
@ -211,7 +212,7 @@ function useDisplayTools(tools: AgentTool[], providerById: Map<string, ToolWithP
displayName: tool.displayName ?? getLocalizedText(provider.label, language) ?? tool.name,
icon: tool.icon ?? provider.icon,
iconDark: tool.iconDark ?? provider.icon_dark,
providerType: tool.providerType ?? provider.type,
providerType: tool.providerType,
allowDelete: tool.allowDelete ?? provider.allow_delete,
credentialKey: providerCredentialType
? (tool.credentialKey ?? 'agentDetail.configure.tools.credential.authOne')
@ -314,6 +315,7 @@ function AddToolMenu({
const toAgentToolDefaultValue = useCallback(
(tool: ToolDefaultValue): AgentProviderToolDefaultValue => ({
...tool,
provider_type: parseToolProviderType(tool.provider_type),
allowDelete: (
providerById.get(tool.provider_id) ??
providerById.get(tool.provider_name) ??

View File

@ -7,7 +7,6 @@ import type { AgentProviderTool } from '@/features/agent-v2/agent-composer/form-
import { useAtomValue, useSetAtom } from 'jotai'
import { useCallback, useMemo } from 'react'
import SettingBuiltInTool from '@/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool'
import { CollectionType } from '@/app/components/tools/types'
import {
agentComposerToolsAtom,
agentComposerToolSettingsAtom,
@ -28,7 +27,7 @@ const createFallbackToolCollection = (tool: AgentProviderTool): ToolWithProvider
icon: tool.icon ?? '',
icon_dark: tool.iconDark,
label: localize(tool.displayName ?? tool.name),
type: (tool.providerType as CollectionType | undefined) ?? CollectionType.builtIn,
type: tool.providerType,
team_credentials: {},
is_team_authorization: true,
allow_delete: tool.allowDelete ?? false,

View File

@ -60,7 +60,7 @@ function UnauthorizedCredentialStatus({
() => ({
provider: tool.id,
category: AuthCategory.tool,
providerType: tool.providerType ?? CollectionType.builtIn,
providerType: tool.providerType,
}),
[tool.id, tool.providerType],
)
@ -129,8 +129,7 @@ function CredentialStatus({
credentialType?: AgentProviderTool['credentialType'],
) => void
}) {
const canSwitchCredential =
(tool.providerType ?? CollectionType.builtIn) === CollectionType.builtIn && tool.allowDelete
const canSwitchCredential = tool.providerType === CollectionType.builtIn && tool.allowDelete
const handleAuthorizationItemClick = useCallback(
(id: string) => {
onCredentialChange(
@ -161,7 +160,7 @@ function CredentialStatus({
pluginPayload={{
provider: tool.id,
category: AuthCategory.tool,
providerType: tool.providerType ?? CollectionType.builtIn,
providerType: tool.providerType,
}}
credentialId={tool.credentialId}
onAuthorizationItemClick={handleAuthorizationItemClick}

View File

@ -1195,6 +1195,39 @@ describe('AgentPreviewChat', () => {
)
})
it('should preserve the tool provider type in the preview runtime config', async () => {
renderPreviewChat({
agentSoulConfig: {
tools: {
dify_tools: [
{
provider_id: 'workflow-provider',
provider_type: 'workflow',
tool_name: 'search',
credential_type: 'unauthorized',
},
],
},
},
})
await waitFor(() => expect(useChatMock).toHaveBeenCalled())
const config = useChatMock.mock.calls.at(-1)?.[0]
expect(config.agent_mode).toEqual(
expect.objectContaining({
enabled: true,
tools: [
expect.objectContaining({
provider_id: 'workflow-provider',
provider_type: 'workflow',
tool_name: 'search',
}),
],
}),
)
})
it('should enable build chat file upload when chat features file upload is enabled', async () => {
renderPreviewChat({
agentSoulConfig: {

View File

@ -132,7 +132,7 @@ export const getAgentSoulInputs = (inputsForm: InputForm[]) => {
const toAgentTool = (tool: AgentSoulDifyToolConfig) => ({
provider_id: tool.provider_id ?? tool.provider ?? tool.plugin_id ?? '',
provider_type: tool.provider_type ?? 'builtin',
provider_type: tool.provider_type,
provider_name: tool.provider ?? '',
tool_name: tool.tool_name,
tool_label: tool.name ?? tool.tool_name,

View File

@ -1430,7 +1430,7 @@ export const useFetchDynamicOptions = (
provider: string,
action: string,
parameter: string,
provider_type?: string,
provider_type?: 'tool' | 'trigger',
extra?: Record<string, unknown>,
) => {
return useMutation({

View File

@ -1,6 +1,7 @@
import type { QueryKey, UseQueryOptions } from '@tanstack/react-query'
import type {
Collection,
CollectionProviderType,
MCPServerDetail,
Tool,
WorkflowToolProviderResponse,
@ -79,13 +80,13 @@ export const useInvalidateAllMCPTools = () => {
return useInvalid(useAllMCPToolsKey)
}
const useInvalidToolsKeyMap: Record<string, QueryKey> = {
const useInvalidToolsKeyMap: Partial<Record<CollectionProviderType, QueryKey>> = {
[CollectionType.builtIn]: useAllBuiltInToolsKey,
[CollectionType.custom]: useAllCustomToolsKey,
[CollectionType.workflow]: useAllWorkflowToolsKey,
[CollectionType.mcp]: useAllMCPToolsKey,
}
export const useInvalidToolsByType = (type?: CollectionType | string) => {
export const useInvalidToolsByType = (type?: CollectionProviderType) => {
const queryKey = type ? useInvalidToolsKeyMap[type] : undefined
return useInvalid(queryKey)
}

View File

@ -1,5 +1,5 @@
import type { ToolProviderType } from '@dify/contracts/api/console/agent/types.gen'
import type { TagResponse as Tag } from '@dify/contracts/api/console/tags/types.gen'
import type { CollectionType } from '@/app/components/tools/types'
import type { UploadFileSetting } from '@/app/components/workflow/types'
import type { LanguagesSupported } from '@/i18n-config/language'
import type { AccessMode } from '@/models/access-control'
@ -172,7 +172,7 @@ export type UserInputFormItem =
export type AgentTool = {
provider_id: string
provider_type: CollectionType
provider_type: ToolProviderType
provider_name: string
tool_name: string
tool_label: string