Compare commits

..

2 Commits

Author SHA1 Message Date
585d292511 feat(web): add Forward-user-identity toggle to MCP provider modal (M3)
Exposes the M2 backend flags as a switch on the MCP provider create/edit
modal so workspace admins can opt in to enterprise SSO identity-forwarding
per provider. When the toggle flips on, the modal sends
forward_user_identity=true + identity_mode="idp_token" to the console API
(which the M2 backend persists on tool_mcp_providers).

- The toggle lives between Server Identifier and the Authentication tabs;
  it overrides the static Authorization (from Auth/Headers) at invoke time.
- The form-state hook hydrates from the GET response so editing preserves
  the previous choice across sessions.
- en-US + zh-Hans strings added; other locales fall back to en-US.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 19:51:36 -07:00
d3fa24d7e3 feat(api): add MCP user-identity forwarding (M2)
When an MCP provider has forward_user_identity enabled, MCPTool now asks
dify-enterprise to mint a short-lived per-user SSO id_token (via the M1
/inner/api/mcp/issue-token endpoint) and stamps it as the Authorization
Bearer on every outbound MCP request — so an MCP server can act on behalf
of the verified end user instead of seeing only "Dify is calling."

- Adds forward_user_identity (bool) + identity_mode ("off" | "idp_token")
  to tool_mcp_providers, plumbed through MCPProviderEntity, the controller,
  service-layer CRUD, and the tool/provider runtime.
- EnterpriseService.issue_mcp_token wraps the enterprise endpoint and maps
  428 → MCPNoRefreshTokenError, 401 → MCPIdentityRefreshError so workflows
  halt with a clear "please re-authenticate" instead of silently going
  anonymous.
- Identity_mode is intentionally an enum-shaped string column so future
  modes (e.g. RFC 8693 token exchange) land without UI/DB churn.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 19:51:20 -07:00
93 changed files with 346 additions and 204 deletions

View File

@ -209,6 +209,11 @@ class MCPProviderBasePayload(BaseModel):
configuration: dict[str, Any] | None = Field(default_factory=dict)
headers: dict[str, Any] | None = Field(default_factory=dict)
authentication: dict[str, Any] | None = Field(default_factory=dict)
# M3 — user-identity forwarding (M2 backend already supports these on the
# service layer). Defaults preserve pre-M3 behavior for clients that don't
# send the fields yet.
forward_user_identity: bool = False
identity_mode: Literal["off", "idp_token"] = "off"
class MCPProviderCreatePayload(MCPProviderBasePayload):
@ -985,6 +990,8 @@ class ToolProviderMCPApi(Resource):
headers=payload.headers or {},
configuration=configuration,
authentication=authentication,
forward_user_identity=payload.forward_user_identity,
identity_mode=payload.identity_mode,
)
# 2) Try to fetch tools immediately after creation so they appear without a second save.
@ -1052,6 +1059,8 @@ class ToolProviderMCPApi(Resource):
configuration=configuration,
authentication=authentication,
validation_result=validation_result,
forward_user_identity=payload.forward_user_identity,
identity_mode=payload.identity_mode,
)
return {"result": "success"}

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import json
from datetime import datetime
from enum import StrEnum
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Literal
from urllib.parse import urlparse
from pydantic import BaseModel
@ -76,6 +76,14 @@ class MCPProviderEntity(BaseModel):
created_at: datetime
updated_at: datetime
# M2 — user-identity forwarding. When forward_user_identity is True AND
# identity_mode is "idp_token", the MCP tool runtime asks dify-enterprise
# to mint a fresh SSO id_token for the calling user and stamps it on the
# outbound MCP request as `Authorization: Bearer <token>`. Defaults keep
# pre-M2 providers unchanged (no forwarding).
forward_user_identity: bool = False
identity_mode: Literal["off", "idp_token"] = "off"
@classmethod
def from_db_model(cls, db_provider: MCPToolProvider) -> MCPProviderEntity:
"""Create entity from database model with decryption"""
@ -96,6 +104,8 @@ class MCPProviderEntity(BaseModel):
icon=db_provider.icon or "",
created_at=db_provider.created_at,
updated_at=db_provider.updated_at,
forward_user_identity=db_provider.forward_user_identity,
identity_mode=db_provider.identity_mode, # type: ignore[arg-type]
)
@property
@ -170,6 +180,8 @@ class MCPProviderEntity(BaseModel):
"updated_at": int(self.updated_at.timestamp()),
"label": I18nObject(en_US=self.name, zh_Hans=self.name).to_dict(),
"description": I18nObject(en_US="", zh_Hans="").to_dict(),
"forward_user_identity": self.forward_user_identity,
"identity_mode": self.identity_mode,
}
# Add configuration

View File

@ -54,6 +54,14 @@ class ToolProviderApiEntity(BaseModel):
configuration: MCPConfiguration | None = Field(
default=None, description="The timeout and sse_read_timeout of the MCP tool"
)
# M3 — user-identity forwarding flags. Round-tripped through the console
# API so the create/edit modal can hydrate the toggle state.
forward_user_identity: bool = Field(
default=False, description="Whether Dify forwards the calling user's SSO identity to this MCP server"
)
identity_mode: str = Field(
default="off", description="Identity-forwarding mechanism: 'off' or 'idp_token'"
)
# Workflow
workflow_app_id: str | None = Field(default=None, description="The app id of the workflow tool")
@ -92,6 +100,10 @@ class ToolProviderApiEntity(BaseModel):
optional_fields.update(self.optional_field("is_dynamic_registration", self.is_dynamic_registration))
optional_fields.update(self.optional_field("masked_headers", self.masked_headers))
optional_fields.update(self.optional_field("original_headers", self.original_headers))
# M3 — forwarding flags. Always emit (False/"off" are valid
# values that the UI must hydrate, not skip).
optional_fields["forward_user_identity"] = self.forward_user_identity
optional_fields["identity_mode"] = self.identity_mode
case ToolProviderType.WORKFLOW:
optional_fields.update(self.optional_field("workflow_app_id", self.workflow_app_id))
case _:

View File

@ -1,4 +1,4 @@
from typing import Any, Self
from typing import Any, Literal, Self
from core.entities.mcp_provider import MCPProviderEntity
from core.mcp.types import Tool as RemoteMCPTool
@ -28,6 +28,8 @@ class MCPToolProviderController(ToolProviderController):
headers: dict[str, str] | None = None,
timeout: float | None = None,
sse_read_timeout: float | None = None,
forward_user_identity: bool = False,
identity_mode: Literal["off", "idp_token"] = "off",
):
super().__init__(entity)
self.entity: ToolProviderEntityWithPlugin = entity
@ -37,6 +39,8 @@ class MCPToolProviderController(ToolProviderController):
self.headers = headers or {}
self.timeout = timeout
self.sse_read_timeout = sse_read_timeout
self.forward_user_identity = forward_user_identity
self.identity_mode: Literal["off", "idp_token"] = identity_mode
@property
def provider_type(self) -> ToolProviderType:
@ -105,6 +109,8 @@ class MCPToolProviderController(ToolProviderController):
headers=entity.headers,
timeout=entity.timeout,
sse_read_timeout=entity.sse_read_timeout,
forward_user_identity=entity.forward_user_identity,
identity_mode=entity.identity_mode,
)
def _validate_credentials(self, user_id: str, credentials: dict[str, Any]):
@ -134,6 +140,8 @@ class MCPToolProviderController(ToolProviderController):
headers=self.headers,
timeout=self.timeout,
sse_read_timeout=self.sse_read_timeout,
forward_user_identity=self.forward_user_identity,
identity_mode=self.identity_mode,
)
def get_tools(self) -> list[MCPTool]:
@ -151,6 +159,8 @@ class MCPToolProviderController(ToolProviderController):
headers=self.headers,
timeout=self.timeout,
sse_read_timeout=self.sse_read_timeout,
forward_user_identity=self.forward_user_identity,
identity_mode=self.identity_mode,
)
for tool_entity in self.entity.tools
]

View File

@ -4,7 +4,7 @@ import base64
import json
import logging
from collections.abc import Generator, Mapping
from typing import Any, cast
from typing import Any, Literal, cast
from core.mcp.auth_client import MCPClientWithAuthRetry
from core.mcp.error import MCPConnectionError
@ -38,6 +38,8 @@ class MCPTool(Tool):
headers: dict[str, str] | None = None,
timeout: float | None = None,
sse_read_timeout: float | None = None,
forward_user_identity: bool = False,
identity_mode: Literal["off", "idp_token"] = "off",
):
super().__init__(entity, runtime)
self.tenant_id = tenant_id
@ -47,6 +49,8 @@ class MCPTool(Tool):
self.headers = headers or {}
self.timeout = timeout
self.sse_read_timeout = sse_read_timeout
self.forward_user_identity = forward_user_identity
self.identity_mode: Literal["off", "idp_token"] = identity_mode
self._latest_usage = LLMUsage.empty_usage()
def tool_provider_type(self) -> ToolProviderType:
@ -60,7 +64,7 @@ class MCPTool(Tool):
app_id: str | None = None,
message_id: str | None = None,
) -> Generator[ToolInvokeMessage, None, None]:
result = self.invoke_remote_mcp_tool(tool_parameters)
result = self.invoke_remote_mcp_tool(tool_parameters, user_id=user_id, app_id=app_id)
# Extract usage metadata from MCP protocol's _meta field
self._latest_usage = self._derive_usage_from_result(result)
@ -234,6 +238,8 @@ class MCPTool(Tool):
headers=self.headers,
timeout=self.timeout,
sse_read_timeout=self.sse_read_timeout,
forward_user_identity=self.forward_user_identity,
identity_mode=self.identity_mode,
)
def _handle_none_parameter(self, parameter: dict[str, Any]) -> dict[str, Any]:
@ -246,7 +252,12 @@ class MCPTool(Tool):
if value is not None and not (isinstance(value, str) and value.strip() == "")
}
def invoke_remote_mcp_tool(self, tool_parameters: dict[str, Any]) -> CallToolResult:
def invoke_remote_mcp_tool(
self,
tool_parameters: dict[str, Any],
user_id: str | None = None,
app_id: str | None = None,
) -> CallToolResult:
headers = self.headers.copy() if self.headers else {}
tool_parameters = self._handle_none_parameter(tool_parameters)
@ -271,6 +282,14 @@ class MCPTool(Tool):
if tokens and tokens.access_token:
headers["Authorization"] = f"{tokens.token_type.capitalize()} {tokens.access_token}"
# User-identity forwarding: if enabled on this provider, ask the
# enterprise side to mint a fresh SSO id_token (audience-scoped to
# the MCP server's URL per RFC 8707) and stamp it as Authorization.
# This OVERRIDES any Authorization already on the request — the
# forwarded identity is what the MCP server should trust.
if self.forward_user_identity and self.identity_mode == "idp_token" and user_id:
self._inject_forwarded_identity(headers, user_id=user_id, app_id=app_id, audience=server_url)
# Step 2: Session is now closed, perform network operations without holding database connection
# MCPClientWithAuthRetry will create a new session lazily only if auth retry is needed
try:
@ -286,3 +305,31 @@ class MCPTool(Tool):
raise ToolInvokeError(f"Failed to connect to MCP server: {e}") from e
except Exception as e:
raise ToolInvokeError(f"Failed to invoke tool: {e}") from e
def _inject_forwarded_identity(
self,
headers: dict[str, str],
*,
user_id: str,
app_id: str | None,
audience: str,
) -> None:
"""Call the enterprise IssueMCPToken endpoint and stamp Authorization.
Errors are surfaced as ToolInvokeError so the workflow halts with a
clear message instead of silently dropping identity and hitting the
MCP server unauthenticated.
"""
from services.enterprise.base import MCPTokenError
from services.enterprise.enterprise_service import EnterpriseService
try:
token, _expires_at = EnterpriseService.issue_mcp_token(
user_id=user_id,
tenant_id=self.tenant_id,
app_id=app_id,
audience=audience,
)
except MCPTokenError as e:
raise ToolInvokeError(f"Failed to obtain forwarded identity token: {e}") from e
headers["Authorization"] = f"Bearer {token}"

View File

@ -0,0 +1,56 @@
"""add identity mode to mcp tool provider
Revision ID: 3df4dbcc1e21
Revises: 7885bd53f9a9
Create Date: 2026-05-29 15:00:00.000000
Adds two columns to `tool_mcp_providers` that drive the M2 MCP user-identity
forwarding feature:
* `forward_user_identity` (bool, default false) — master switch per provider.
* `identity_mode` (string, default "off") — which forwarding mechanism to use:
"off" — no header forwarded (default; pre-M2 behaviour).
"idp_token" — call dify-enterprise /inner/api/mcp/issue-token, stamp
the returned id_token on the outbound MCP request as
`Authorization: Bearer <token>`.
The columns are filled with safe defaults for existing rows so older providers
keep their current behaviour (no identity forwarding) until an admin opts in.
"""
import sqlalchemy as sa
from alembic import op
import models as models
# revision identifiers, used by Alembic.
revision = "3df4dbcc1e21"
down_revision = "7885bd53f9a9"
branch_labels = None
depends_on = None
def upgrade():
op.add_column(
"tool_mcp_providers",
sa.Column(
"forward_user_identity",
sa.Boolean(),
nullable=False,
server_default=sa.text("false"),
),
)
op.add_column(
"tool_mcp_providers",
sa.Column(
"identity_mode",
sa.String(length=32),
nullable=False,
server_default=sa.text("'off'"),
),
)
def downgrade():
op.drop_column("tool_mcp_providers", "identity_mode")
op.drop_column("tool_mcp_providers", "forward_user_identity")

View File

@ -343,6 +343,21 @@ class MCPToolProvider(TypeBase):
# encrypted headers for MCP server requests
encrypted_headers: Mapped[str | None] = mapped_column(LongText, nullable=True, default=None)
# M2 (MCP user-identity forwarding) — master switch per provider. When True
# AND identity_mode is "idp_token", workflows that invoke tools on this
# provider will have the caller's SSO id_token stamped on the outbound
# request as `Authorization: Bearer …`. Off by default so existing
# providers retain pre-M2 behaviour.
forward_user_identity: Mapped[bool] = mapped_column(
sa.Boolean, nullable=False, server_default=sa.text("false"), default=False
)
# M2 — which identity-forwarding mechanism to use. Reserved values:
# "off" — no forwarding (default).
# "idp_token" — forward a Bearer id_token minted by dify-enterprise.
identity_mode: Mapped[str] = mapped_column(
sa.String(32), nullable=False, server_default=sa.text("'off'"), default="off"
)
def load_user(self) -> Account | None:
return db.session.scalar(select(Account).where(Account.id == self.user_id))

View File

@ -12,8 +12,33 @@ from services.errors.enterprise import (
EnterpriseAPIForbiddenError,
EnterpriseAPINotFoundError,
EnterpriseAPIUnauthorizedError,
EnterpriseServiceError,
)
# M2 — IssueMCPToken specific errors. Co-located here (rather than in
# services/errors/enterprise.py) because services.enterprise.base is part of
# the leaf-mounted file set the local dev override applies; the errors module
# stays at the EE image's baked-in version.
class MCPTokenError(EnterpriseServiceError):
"""Generic failure of the IssueMCPToken RPC."""
class MCPNoRefreshTokenError(MCPTokenError):
"""The user has no stored SSO refresh_token on the enterprise side.
The workflow should ask them to re-authenticate."""
def __init__(self, description: str = ""):
super().__init__(description, status_code=428)
class MCPIdentityRefreshError(MCPTokenError):
"""The enterprise side tried to refresh the user's SSO refresh_token
against the IdP and failed (revoked/expired/IdP error)."""
def __init__(self, description: str = ""):
super().__init__(description, status_code=401)
logger = logging.getLogger(__name__)

View File

@ -11,7 +11,16 @@ from pydantic import BaseModel, ConfigDict, Field, model_validator
from configs import dify_config
from extensions.ext_redis import redis_client
from services.enterprise.base import EnterpriseRequest
from services.enterprise.base import (
EnterpriseRequest,
MCPIdentityRefreshError,
MCPNoRefreshTokenError,
MCPTokenError,
)
from services.errors.enterprise import (
EnterpriseAPIError,
EnterpriseAPIUnauthorizedError,
)
if TYPE_CHECKING:
from services.feature_service import LicenseStatus
@ -121,6 +130,62 @@ class EnterpriseService:
def get_workspace_info(cls, tenant_id: str):
return EnterpriseRequest.send_request("GET", f"/workspace/{tenant_id}/info")
@classmethod
def issue_mcp_token(
cls,
user_id: str,
tenant_id: str,
app_id: str | None,
audience: str,
) -> tuple[str, int]:
"""Mint a short-lived SSO id_token (or OAuth2 access_token) representing
the calling Dify user, audience-scoped to the given MCP server identifier.
Used by MCPTool.invoke_remote_mcp_tool to stamp `Authorization: Bearer
<token>` on outbound MCP requests when the provider has
forward_user_identity=True and identity_mode="idp_token".
Returns:
(token, expires_at_unix_seconds)
Raises:
MCPNoRefreshTokenError: user has no stored SSO refresh_token on the
enterprise side; surface to the workflow as "please log in via SSO".
MCPIdentityRefreshError: enterprise tried to refresh against the IdP
and the IdP rejected (revoked/expired session).
MCPTokenError: any other failure of the enterprise endpoint.
"""
try:
response = EnterpriseRequest.send_request(
"POST",
"/mcp/issue-token",
json={
"user_id": user_id,
"tenant_id": tenant_id,
"app_id": app_id or "",
"audience": audience,
},
)
except EnterpriseAPIUnauthorizedError as e:
# Enterprise side returns 401 when the IdP rejected the refresh.
raise MCPIdentityRefreshError(str(e) or "identity refresh failed; please re-authenticate") from e
except EnterpriseAPIError as e:
# Map the 428 PreconditionRequired we emit on no-stored-refresh-token.
if getattr(e, "status_code", None) == 428:
raise MCPNoRefreshTokenError(
str(e) or "user has no stored SSO refresh token; please re-authenticate"
) from e
raise MCPTokenError(f"issue_mcp_token failed: {e}") from e
if not isinstance(response, dict):
raise MCPTokenError("invalid response shape from enterprise /mcp/issue-token")
token = response.get("token")
expires_at = response.get("expires_at")
if not token or not isinstance(token, str) or not isinstance(expires_at, int):
raise MCPTokenError(f"missing token/expires_at in enterprise response: {response}")
return token, expires_at
@classmethod
def initiate_device_flow_sso(cls, signed_state: str) -> dict:
return EnterpriseRequest.send_request(

View File

@ -4,7 +4,7 @@ import logging
from collections.abc import Mapping
from datetime import datetime
from enum import StrEnum
from typing import Any
from typing import Any, Literal
from urllib.parse import urlparse
from pydantic import BaseModel, Field
@ -136,6 +136,8 @@ class MCPToolManageService:
configuration: MCPConfiguration,
authentication: MCPAuthentication | None = None,
headers: dict[str, str] | None = None,
forward_user_identity: bool = False,
identity_mode: Literal["off", "idp_token"] = "off",
) -> ToolProviderApiEntity:
"""Create a new MCP provider."""
# Validate URL format
@ -171,6 +173,8 @@ class MCPToolManageService:
sse_read_timeout=configuration.sse_read_timeout,
encrypted_headers=encrypted_headers,
encrypted_credentials=encrypted_credentials,
forward_user_identity=forward_user_identity,
identity_mode=identity_mode,
)
self._session.add(mcp_tool)
@ -194,6 +198,8 @@ class MCPToolManageService:
configuration: MCPConfiguration,
authentication: MCPAuthentication | None = None,
validation_result: ServerUrlValidationResult | None = None,
forward_user_identity: bool | None = None,
identity_mode: Literal["off", "idp_token"] | None = None,
) -> None:
"""
Update an MCP provider.
@ -255,6 +261,14 @@ class MCPToolManageService:
if authentication and authentication.client_id:
mcp_provider.encrypted_credentials = self._process_credentials(authentication, mcp_provider, tenant_id)
# Update user-identity forwarding settings if provided.
# None means "leave unchanged" so this stays backwards-compatible
# with existing callers that don't know about M2.
if forward_user_identity is not None:
mcp_provider.forward_user_identity = forward_user_identity
if identity_mode is not None:
mcp_provider.identity_mode = identity_mode
# Flush changes to database
self._session.flush()

View File

@ -19,10 +19,6 @@ vi.mock('@/context/web-app-context', () => ({
useWebAppStore: vi.fn(),
}))
vi.mock('@/hooks/use-document-title', () => ({
default: vi.fn(),
}))
vi.mock('@/service/access-control', () => ({
useGetUserCanAccessApp: vi.fn(),
}))

View File

@ -1,14 +1,8 @@
import * as React from 'react'
import ExternalKnowledgeBaseConnector from '@/app/components/datasets/external-knowledge-base/connector'
import { DocumentTitleSetter } from '../document-title-setter'
const ExternalKnowledgeBaseCreation = () => {
return (
<>
<DocumentTitleSetter i18nKey="pageTitle.connectExternalKnowledgeBase" namespace="dataset" />
<ExternalKnowledgeBaseConnector />
</>
)
return <ExternalKnowledgeBaseConnector />
}
export default ExternalKnowledgeBaseCreation

View File

@ -1,13 +1,9 @@
import * as React from 'react'
import CreateFromPipeline from '@/app/components/datasets/create-from-pipeline'
import { DocumentTitleSetter } from '../document-title-setter'
const DatasetCreation = async () => {
return (
<>
<DocumentTitleSetter i18nKey="creation.pageTitle" namespace="datasetPipeline" />
<CreateFromPipeline />
</>
<CreateFromPipeline />
)
}

View File

@ -1,13 +1,9 @@
import * as React from 'react'
import DatasetUpdateForm from '@/app/components/datasets/create'
import { DocumentTitleSetter } from '../document-title-setter'
const DatasetCreation = async () => {
return (
<>
<DocumentTitleSetter i18nKey="createDataset" namespace="dataset" />
<DatasetUpdateForm />
</>
<DatasetUpdateForm />
)
}

View File

@ -1,20 +0,0 @@
'use client'
import type { Namespace } from '@/i18n-config/resources'
import { useTranslation } from 'react-i18next'
import useDocumentTitle from '@/hooks/use-document-title'
type DocumentTitleSetterProps = {
i18nKey: string
namespace: Namespace
}
export function DocumentTitleSetter({
i18nKey,
namespace,
}: DocumentTitleSetterProps) {
const { t } = useTranslation()
useDocumentTitle(t(i18nKey, { ns: namespace }))
return null
}

View File

@ -1,18 +1,15 @@
'use client'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { FullScreenLoading } from '@/app/components/full-screen-loading'
import EducationApplyPage from '@/app/education-apply/education-apply-page'
import { useProviderContext } from '@/context/provider-context'
import useDocumentTitle from '@/hooks/use-document-title'
import {
useRouter,
useSearchParams,
} from '@/next/navigation'
export default function EducationApply() {
const { t } = useTranslation()
const router = useRouter()
const {
enableEducationPlan,
@ -21,7 +18,6 @@ export default function EducationApply() {
} = useProviderContext()
const searchParams = useSearchParams()
const token = searchParams.get('token')
useDocumentTitle(t('pageTitle.verification', { ns: 'education' }))
useEffect(() => {
if (!isFetchedPlanInfo)

View File

@ -3,7 +3,6 @@ import type { InstalledApp as InstalledAppType } from '@/models/explore'
import { render, screen, waitFor } from '@testing-library/react'
import { useWebAppStore } from '@/context/web-app-context'
import useDocumentTitle from '@/hooks/use-document-title'
import { AccessMode } from '@/models/access-control'
import { useGetUserCanAccessApp } from '@/service/access-control'
import { useGetInstalledAppAccessModeByAppId, useGetInstalledAppMeta, useGetInstalledAppParams, useGetInstalledApps } from '@/service/use-explore'
@ -13,9 +12,6 @@ import InstalledApp from '../index'
vi.mock('@/context/web-app-context', () => ({
useWebAppStore: vi.fn(),
}))
vi.mock('@/hooks/use-document-title', () => ({
default: vi.fn(),
}))
vi.mock('@/service/access-control', () => ({
useGetUserCanAccessApp: vi.fn(),
}))
@ -365,20 +361,6 @@ describe('InstalledApp', () => {
})
describe('Effects', () => {
it('should set document title to installed app name', () => {
render(<InstalledApp id="installed-app-123" />)
expect(useDocumentTitle).toHaveBeenCalledWith('Test App')
})
it('should not set document title when installedApp is not found', () => {
setupMocks([])
render(<InstalledApp id="nonexistent-app" />)
expect(useDocumentTitle).not.toHaveBeenCalled()
})
it('should update app info when installedApp is available', async () => {
render(<InstalledApp id="installed-app-123" />)

View File

@ -7,22 +7,11 @@ import ChatWithHistory from '@/app/components/base/chat/chat-with-history'
import Loading from '@/app/components/base/loading'
import TextGenerationApp from '@/app/components/share/text-generation'
import { useWebAppStore } from '@/context/web-app-context'
import useDocumentTitle from '@/hooks/use-document-title'
import { useGetUserCanAccessApp } from '@/service/access-control'
import { useGetInstalledAppAccessModeByAppId, useGetInstalledAppMeta, useGetInstalledAppParams, useGetInstalledApps } from '@/service/use-explore'
import { AppModeEnum } from '@/types/app'
import AppUnavailable from '../../base/app-unavailable'
const InstalledAppDocumentTitle = ({
title,
}: {
title: string
}) => {
useDocumentTitle(title)
return null
}
const InstalledApp = ({
id,
}: {
@ -30,10 +19,6 @@ const InstalledApp = ({
}) => {
const { data, isPending: isPendingInstalledApps, isFetching: isFetchingInstalledApps } = useGetInstalledApps()
const installedApp = data?.installed_apps?.find(item => item.id === id)
const installedAppDocumentTitle = installedApp
? <InstalledAppDocumentTitle title={installedApp.app.name} />
: null
const updateAppInfo = useWebAppStore(s => s.updateAppInfo)
const updateWebAppAccessMode = useWebAppStore(s => s.updateWebAppAccessMode)
const updateAppParams = useWebAppStore(s => s.updateAppParams)
@ -79,52 +64,37 @@ const InstalledApp = ({
if (appParamsError) {
return (
<>
{installedAppDocumentTitle}
<div className="flex h-full items-center justify-center">
<AppUnavailable unknownReason={appParamsError.message} />
</div>
</>
<div className="flex h-full items-center justify-center">
<AppUnavailable unknownReason={appParamsError.message} />
</div>
)
}
if (appMetaError) {
return (
<>
{installedAppDocumentTitle}
<div className="flex h-full items-center justify-center">
<AppUnavailable unknownReason={appMetaError.message} />
</div>
</>
<div className="flex h-full items-center justify-center">
<AppUnavailable unknownReason={appMetaError.message} />
</div>
)
}
if (useCanAccessAppError) {
return (
<>
{installedAppDocumentTitle}
<div className="flex h-full items-center justify-center">
<AppUnavailable unknownReason={useCanAccessAppError.message} />
</div>
</>
<div className="flex h-full items-center justify-center">
<AppUnavailable unknownReason={useCanAccessAppError.message} />
</div>
)
}
if (webAppAccessModeError) {
return (
<>
{installedAppDocumentTitle}
<div className="flex h-full items-center justify-center">
<AppUnavailable unknownReason={webAppAccessModeError.message} />
</div>
</>
<div className="flex h-full items-center justify-center">
<AppUnavailable unknownReason={webAppAccessModeError.message} />
</div>
)
}
if (userCanAccessApp && !userCanAccessApp.result) {
return (
<>
{installedAppDocumentTitle}
<div className="flex h-full flex-col items-center justify-center gap-y-2">
<AppUnavailable className="size-auto" code={403} unknownReason="no permission." />
</div>
</>
<div className="flex h-full flex-col items-center justify-center gap-y-2">
<AppUnavailable className="size-auto" code={403} unknownReason="no permission." />
</div>
)
}
if (
@ -133,12 +103,9 @@ const InstalledApp = ({
|| (installedApp && (isPendingAppParams || isPendingAppMeta || isPendingWebAppAccessMode))
) {
return (
<>
{installedAppDocumentTitle}
<div className="flex h-full items-center justify-center">
<Loading />
</div>
</>
<div className="flex h-full items-center justify-center">
<Loading />
</div>
)
}
if (!installedApp) {
@ -149,20 +116,17 @@ const InstalledApp = ({
)
}
return (
<>
{installedAppDocumentTitle}
<div className="h-full bg-background-default py-2 pr-2 pl-0 sm:p-2">
{installedApp?.app.mode !== AppModeEnum.COMPLETION && installedApp?.app.mode !== AppModeEnum.WORKFLOW && (
<ChatWithHistory installedAppInfo={installedApp} className="overflow-hidden rounded-2xl shadow-md" />
)}
{installedApp?.app.mode === AppModeEnum.COMPLETION && (
<TextGenerationApp isInstalledApp installedAppInfo={installedApp} />
)}
{installedApp?.app.mode === AppModeEnum.WORKFLOW && (
<TextGenerationApp isWorkflow isInstalledApp installedAppInfo={installedApp} />
)}
</div>
</>
<div className="h-full bg-background-default py-2 pr-2 pl-0 sm:p-2">
{installedApp?.app.mode !== AppModeEnum.COMPLETION && installedApp?.app.mode !== AppModeEnum.WORKFLOW && (
<ChatWithHistory installedAppInfo={installedApp} className="overflow-hidden rounded-2xl shadow-md" />
)}
{installedApp?.app.mode === AppModeEnum.COMPLETION && (
<TextGenerationApp isInstalledApp installedAppInfo={installedApp} />
)}
{installedApp?.app.mode === AppModeEnum.WORKFLOW && (
<TextGenerationApp isWorkflow isInstalledApp installedAppInfo={installedApp} />
)}
</div>
)
}
export default React.memo(InstalledApp)

View File

@ -54,6 +54,7 @@ type MCPModalFormState = {
isDynamicRegistration: boolean
clientID: string
credentials: string
forwardUserIdentity: boolean
}
type MCPModalFormActions = {
setUrl: (url: string) => void
@ -68,6 +69,7 @@ type MCPModalFormActions = {
setIsDynamicRegistration: (value: boolean) => void
setClientID: (id: string) => void
setCredentials: (credentials: string) => void
setForwardUserIdentity: (value: boolean) => void
handleUrlBlur: (url: string) => Promise<void>
resetIcon: () => void
}
@ -100,6 +102,11 @@ export const useMCPModalForm = (data?: ToolWithProvider) => {
const [isDynamicRegistration, setIsDynamicRegistration] = useState(() => isCreate ? true : (data?.is_dynamic_registration ?? true))
const [clientID, setClientID] = useState(() => data?.authentication?.client_id || '')
const [credentials, setCredentials] = useState(() => data?.authentication?.client_secret || '')
// M3 — user-identity forwarding. Identity mode is implied by the toggle:
// off → "off", on → "idp_token" (only mode currently supported).
const [forwardUserIdentity, setForwardUserIdentity] = useState(
() => Boolean(data?.forward_user_identity),
)
const handleUrlBlur = useCallback(async (urlValue: string) => {
if (data)
return
@ -163,6 +170,7 @@ export const useMCPModalForm = (data?: ToolWithProvider) => {
isDynamicRegistration,
clientID,
credentials,
forwardUserIdentity,
} satisfies MCPModalFormState,
// Actions
actions: {
@ -178,6 +186,7 @@ export const useMCPModalForm = (data?: ToolWithProvider) => {
setIsDynamicRegistration,
setClientID,
setCredentials,
setForwardUserIdentity,
handleUrlBlur,
resetIcon,
} satisfies MCPModalFormActions,

View File

@ -5,6 +5,7 @@ import type { ToolWithProvider } from '@/app/components/workflow/types'
import type { AppIconType } from '@/types/app'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { Switch } from '@langgenius/dify-ui/switch'
import { toast } from '@langgenius/dify-ui/toast'
import { RiCloseLine, RiEditLine } from '@remixicon/react'
import { useHover } from 'ahooks'
@ -39,6 +40,8 @@ type MCPModalConfirmPayload = {
timeout: number
sse_read_timeout: number
}
forward_user_identity?: boolean
identity_mode?: 'off' | 'idp_token'
}
type DuplicateAppModalProps = {
@ -110,6 +113,8 @@ const MCPModalContent: FC<MCPModalContentProps> = ({
timeout: state.timeout || 30,
sse_read_timeout: state.sseReadTimeout || 300,
},
forward_user_identity: state.forwardUserIdentity,
identity_mode: state.forwardUserIdentity ? 'idp_token' : 'off',
})
if (isCreate)
onHide()
@ -207,6 +212,23 @@ const MCPModalContent: FC<MCPModalContentProps> = ({
)}
</div>
{/* Forward user identity (M3 — enterprise SSO identity-forwarding) */}
<div>
<div className="mb-1 flex h-6 items-center">
<Switch
className="mr-2"
checked={state.forwardUserIdentity}
onCheckedChange={actions.setForwardUserIdentity}
/>
<span className="system-sm-medium text-text-secondary">
{t('mcp.modal.forwardUserIdentity', { ns: 'tools' })}
</span>
</div>
<div className="body-xs-regular text-text-tertiary">
{t('mcp.modal.forwardUserIdentityTip', { ns: 'tools' })}
</div>
</div>
{/* Auth Method Tabs */}
<TabSlider
className="w-full"

View File

@ -78,6 +78,9 @@ export type Collection = {
timeout?: number
sse_read_timeout?: number
}
// M3 — user-identity forwarding (MCP)
forward_user_identity?: boolean
identity_mode?: 'off' | 'idp_token'
// Workflow
workflow_app_id?: string
}

View File

@ -27,7 +27,6 @@
"creation.createKnowledge": "إنشاء المعرفة",
"creation.errorTip": "فشل إنشاء قاعدة المعرفة",
"creation.importDSL": "استيراد من ملف DSL",
"creation.pageTitle": "إنشاء مسار معرفة",
"creation.successTip": "تم إنشاء قاعدة المعرفة بنجاح",
"deletePipeline.content": "حذف قالب سير العمل لا رجعة فيه.",
"deletePipeline.title": "هل أنت متأكد من حذف قالب سير العمل هذا؟",

View File

@ -146,7 +146,6 @@
"nTo1RetrievalLegacyLink": "تعرف على المزيد",
"nTo1RetrievalLegacyLinkText": " سيتم إيقاف الاسترجاع من N إلى 1 رسميًا في سبتمبر.",
"noExternalKnowledge": "لا توجد واجهة برمجة تطبيقات معرفة خارجية حتى الآن، انقر هنا لإنشاء",
"pageTitle.connectExternalKnowledgeBase": "ربط قاعدة معرفة خارجية",
"parentMode.fullDoc": "مستند كامل",
"parentMode.paragraph": "فقرة",
"partialEnabled_one": "إجمالي {{count}} مستند، {{num}} متاح",

View File

@ -51,7 +51,6 @@
"notice.stillInEducation.expired": "تحقق مرة أخرى الآن للحصول على كوبون جديد للعام الدراسي القادم. سنضيفه إلى حسابك ويمكنك استخدامه للترقية التالية.",
"notice.stillInEducation.isAboutToExpire": "تحقق مرة أخرى الآن للحصول على كوبون جديد للعام الدراسي القادم. سيتم حفظه في حسابك وجاهز للاستخدام في تجديدك التالي.",
"notice.stillInEducation.title": "هل ما زلت في التعليم؟",
"pageTitle.verification": "التحقق من التعليم",
"planNotSupportEducationDiscount": "غير مؤهل لأسعار التعليم",
"rejectContent": "لسوء الحظ، أنت غير مؤهل للحصول على حالة التحقق التعليمي وبالتالي لا يمكنك الحصول على كوبون حصري 100٪ لخطة Dify Professional إذا كنت تستخدم عنوان البريد الإلكتروني هذا.",
"rejectTitle": "تم رفض التحقق التعليمي الخاص بك في Dify",

View File

@ -27,7 +27,6 @@
"creation.createKnowledge": "Wissen schaffen",
"creation.errorTip": "Fehler beim Erstellen einer Wissensdatenbank",
"creation.importDSL": "Importieren aus einer DSL-Datei",
"creation.pageTitle": "Knowledge Pipeline erstellen",
"creation.successTip": "Erfolgreich eine Wissensdatenbank erstellt",
"deletePipeline.content": "Das Löschen der Pipelinevorlage kann nicht rückgängig gemacht werden.",
"deletePipeline.title": "Sind Sie sicher, dass Sie diese Pipeline-Vorlage löschen möchten?",

View File

@ -146,7 +146,6 @@
"nTo1RetrievalLegacyLink": "Mehr erfahren",
"nTo1RetrievalLegacyLinkText": "N-zu-1-Abruf wird im September offiziell eingestellt.",
"noExternalKnowledge": "Es gibt noch keine External Knowledge API, klicken Sie hier, um zu erstellen",
"pageTitle.connectExternalKnowledgeBase": "Externe Wissensdatenbank verbinden",
"parentMode.fullDoc": "Vollständiges Dokument",
"parentMode.paragraph": "Absatz",
"partialEnabled_one": "Insgesamt {{count}} Dokumente, {{num}} verfügbar",

View File

@ -51,7 +51,6 @@
"notice.stillInEducation.expired": "Überprüfen Sie jetzt erneut, um einen neuen Gutschein für das kommende akademische Jahr zu erhalten. Wir fügen ihn Ihrem Konto hinzu und Sie können ihn für das nächste Upgrade verwenden.",
"notice.stillInEducation.isAboutToExpire": "Überprüfen Sie jetzt erneut, um einen neuen Gutschein für das kommende Studienjahr zu erhalten. Er wird in Ihrem Konto gespeichert und ist bereit zur Nutzung bei Ihrer nächsten Verlängerung.",
"notice.stillInEducation.title": "Immer noch in der Ausbildung?",
"pageTitle.verification": "Bildungsverifizierung",
"planNotSupportEducationDiscount": "Nicht für Bildungspreise berechtigt",
"rejectContent": "Leider sind Sie nicht für den Status \"Education Verified\" berechtigt und können daher den exklusiven 100%-Gutschein für den Dify Professional Plan nicht erhalten, wenn Sie diese E-Mail-Adresse verwenden.",
"rejectTitle": "Ihre Dify-Ausbildungsüberprüfung wurde abgelehnt.",

View File

@ -27,7 +27,6 @@
"creation.createKnowledge": "Create Knowledge",
"creation.errorTip": "Failed to create a Knowledge Base",
"creation.importDSL": "Import from a DSL file",
"creation.pageTitle": "Create Knowledge Pipeline",
"creation.successTip": "Successfully created a Knowledge Base",
"deletePipeline.content": "Deleting the pipeline template is irreversible.",
"deletePipeline.title": "Are you sure to delete this pipeline template?",

View File

@ -146,7 +146,6 @@
"nTo1RetrievalLegacyLink": "Learn more",
"nTo1RetrievalLegacyLinkText": " N-to-1 retrieval will be officially deprecated in September.",
"noExternalKnowledge": "There is no External Knowledge API yet, click here to create",
"pageTitle.connectExternalKnowledgeBase": "Connect External Knowledge Base",
"parentMode.fullDoc": "Full-doc",
"parentMode.paragraph": "Paragraph",
"partialEnabled_one": "Total of {{count}} document, {{num}} available",

View File

@ -51,7 +51,6 @@
"notice.stillInEducation.expired": "Re-verify now to get a new coupon for the upcoming academic year. We'll add it to your account and you can use it for the next upgrade.",
"notice.stillInEducation.isAboutToExpire": "Re-verify now to get a new coupon for the upcoming academic year. It'll be saved to your account and ready to use at your next renewal.",
"notice.stillInEducation.title": "Still in education?",
"pageTitle.verification": "Education Verification",
"planNotSupportEducationDiscount": "Not eligible for education pricing",
"rejectContent": "Unfortunately, you are not eligible for Education Verified status and therefore cannot receive the exclusive 100% coupon for the Dify Professional Plan if you use this email address.",
"rejectTitle": "Your Dify Educational Verification Has Been Rejected",

View File

@ -145,6 +145,8 @@
"mcp.modal.timeout": "Timeout",
"mcp.modal.timeoutPlaceholder": "30",
"mcp.modal.title": "Add MCP Server (HTTP)",
"mcp.modal.forwardUserIdentity": "Forward user identity",
"mcp.modal.forwardUserIdentityTip": "Send the calling user's verified SSO identity to this MCP server as an Authorization Bearer token. Requires Dify Enterprise SSO.",
"mcp.modal.useDynamicClientRegistration": "Use Dynamic Client Registration",
"mcp.noConfigured": "Unconfigured",
"mcp.noTools": "No tools available",

View File

@ -27,7 +27,6 @@
"creation.createKnowledge": "Crear conocimiento",
"creation.errorTip": "No se pudo crear una base de conocimiento",
"creation.importDSL": "Importar desde un archivo DSL",
"creation.pageTitle": "Crear pipeline de conocimiento",
"creation.successTip": "Creó con éxito una base de conocimientos",
"deletePipeline.content": "La eliminación de la plantilla de canalización es irreversible.",
"deletePipeline.title": "¿Está seguro de eliminar esta plantilla de canalización?",

View File

@ -146,7 +146,6 @@
"nTo1RetrievalLegacyLink": "Más información",
"nTo1RetrievalLegacyLinkText": "La recuperación N-a-1 será oficialmente obsoleta en septiembre.",
"noExternalKnowledge": "Todavía no hay una API de conocimiento externo, haga clic aquí para crear",
"pageTitle.connectExternalKnowledgeBase": "Conectar base de conocimientos externa",
"parentMode.fullDoc": "Documento completo",
"parentMode.paragraph": "Párrafo",
"partialEnabled_one": "Total de {{count}} documentos, {{num}} disponibles",

View File

@ -51,7 +51,6 @@
"notice.stillInEducation.expired": "Verifica de nuevo ahora para obtener un nuevo cupón para el próximo año académico. Lo añadiremos a tu cuenta y podrás usarlo para la próxima actualización.",
"notice.stillInEducation.isAboutToExpire": "Verifica de nuevo ahora para obtener un nuevo cupón para el próximo año académico. Se guardará en tu cuenta y estará listo para usar en tu próxima renovación.",
"notice.stillInEducation.title": "¿Aún en educación?",
"pageTitle.verification": "Verificación educativa",
"planNotSupportEducationDiscount": "No elegible para precios educativos",
"rejectContent": "Desafortunadamente, no eres elegible para el estado de Educación Verificada y, por lo tanto, no puedes recibir el exclusivo cupón del 100% para el Plan Profesional de Dify si utilizas esta dirección de correo electrónico.",
"rejectTitle": "Su verificación educativa de Dify ha sido rechazada.",

View File

@ -27,7 +27,6 @@
"creation.createKnowledge": "ایجاد دانش",
"creation.errorTip": "ایجاد پایگاه دانش ناموفق است",
"creation.importDSL": "ایمپورت از یک فایل DSL",
"creation.pageTitle": "ایجاد پایپ‌لاین دانش",
"creation.successTip": "با موفقیت یک پایگاه دانش ایجاد کرد",
"deletePipeline.content": "حذف الگوی خط لوله برگشت ناپذیر است.",
"deletePipeline.title": "آیا مطمئن هستید که این الگوی خط لوله را حذف می کنید؟",

View File

@ -146,7 +146,6 @@
"nTo1RetrievalLegacyLink": "بیشتر بدانید",
"nTo1RetrievalLegacyLinkText": " بازیابی N-to-1 از سپتامبر به طور رسمی منسوخ خواهد شد.",
"noExternalKnowledge": "هنوز هیچ API دانش خارجی وجود ندارد، برای ایجاد اینجا را کلیک کنید",
"pageTitle.connectExternalKnowledgeBase": "اتصال به پایگاه دانش خارجی",
"parentMode.fullDoc": "مستند کامل",
"parentMode.paragraph": "پاراگراف",
"partialEnabled_one": "مجموعاً {{count}} سند، {{num}} موجود",

View File

@ -51,7 +51,6 @@
"notice.stillInEducation.expired": "هم‌اکنون دوباره تأیید کنید تا یک کوپن جدید برای سال تحصیلی آینده دریافت کنید. ما آن را به حساب شما اضافه خواهیم کرد و می‌توانید از آن برای ارتقاء بعدی استفاده کنید.",
"notice.stillInEducation.isAboutToExpire": "در حال حاضر دوباره تأیید کنید تا یک کوپن جدید برای سال تحصیلی آینده دریافت کنید. این کوپن به حساب شما ذخیره خواهد شد و در زمان تمدید بعدی شما آماده استفاده است.",
"notice.stillInEducation.title": "آیا هنوز در حال تحصیل هستید؟",
"pageTitle.verification": "تأیید آموزشی",
"planNotSupportEducationDiscount": "واجد شرایط قیمت‌گذاری آموزشی نیست",
"rejectContent": "متاسفانه، شما واجد شرایط وضعیت تأیید شده آموزشی نیستید و به همین دلیل نمی‌توانید کوپن انحصاری ۱۰۰٪ برای طرح حرفه‌ای Dify را در صورت استفاده از این آدرس ایمیل دریافت کنید.",
"rejectTitle": "تأییدیه آموزشی دیفی شما رد شده است",

View File

@ -27,7 +27,6 @@
"creation.createKnowledge": "Créer des connaissances",
"creation.errorTip": "Échec de la création dune base de connaissances",
"creation.importDSL": "Importation à partir dun fichier DSL",
"creation.pageTitle": "Créer un pipeline de connaissances",
"creation.successTip": "Création réussie dune base de connaissances",
"deletePipeline.content": "La suppression du modèle de pipeline est irréversible.",
"deletePipeline.title": "Êtes-vous sûr de supprimer ce modèle de pipeline ?",

View File

@ -146,7 +146,6 @@
"nTo1RetrievalLegacyLink": "En savoir plus",
"nTo1RetrievalLegacyLinkText": "La récupération N-à-1 sera officiellement obsolète en septembre.",
"noExternalKnowledge": "Il ny a pas encore dAPI de connaissances externes, cliquez ici pour créer",
"pageTitle.connectExternalKnowledgeBase": "Connecter une base de connaissances externe",
"parentMode.fullDoc": "Doc complet",
"parentMode.paragraph": "Paragraphe",
"partialEnabled_one": "Total de {{count}} documents, {{num}} disponibles",

View File

@ -51,7 +51,6 @@
"notice.stillInEducation.expired": "Veuillez vérifier à nouveau maintenant pour obtenir un nouveau coupon pour la prochaine année académique. Nous l'ajouterons à votre compte et vous pourrez l'utiliser pour la prochaine mise à niveau.",
"notice.stillInEducation.isAboutToExpire": "Vérifiez de nouveau maintenant pour obtenir un nouveau coupon pour la prochaine année académique. Il sera enregistré dans votre compte et prêt à être utilisé lors de votre prochain renouvellement.",
"notice.stillInEducation.title": "Encore dans l'éducation ?",
"pageTitle.verification": "Vérification du statut éducatif",
"planNotSupportEducationDiscount": "Non éligible aux tarifs éducatifs",
"rejectContent": "Malheureusement, vous n'êtes pas éligible au statut Éducation Vérifié et ne pouvez donc pas recevoir le coupon exclusif de 100 % pour le Plan Professionnel Dify si vous utilisez cette adresse e-mail.",
"rejectTitle": "Votre vérification éducative Dify a été rejetée.",

View File

@ -27,7 +27,6 @@
"creation.createKnowledge": "ज्ञान उत्पन्न करें",
"creation.errorTip": "ज्ञान आधार बनाने में विफल",
"creation.importDSL": "एक DSL फ़ाइल से आयात करें",
"creation.pageTitle": "ज्ञान पाइपलाइन बनाएँ",
"creation.successTip": "सफलता से एक ज्ञान आधार बनाया गया",
"deletePipeline.content": "पाइपलाइन टेम्पलेट को हटाना वापस नहीं किया जा सकता।",
"deletePipeline.title": "क्या आप इस पाइपलाइन टेम्पलेट को हटाने के लिए निश्चित हैं?",

View File

@ -146,7 +146,6 @@
"nTo1RetrievalLegacyLink": "और जानें",
"nTo1RetrievalLegacyLinkText": "N-से-1 पुनर्प्राप्ति सितंबर में आधिकारिक तौर पर बंद कर दी जाएगी।",
"noExternalKnowledge": "अभी तक कोई बाहरी ज्ञान एपीआई नहीं है, बनाने के लिए यहां क्लिक करें",
"pageTitle.connectExternalKnowledgeBase": "बाहरी ज्ञान आधार कनेक्ट करें",
"parentMode.fullDoc": "पूर्ण-दस्तावेज़",
"parentMode.paragraph": "अनुच्‍छेद",
"partialEnabled_one": "कुल {{count}} दस्तावेज़, {{num}} उपलब्ध",

View File

@ -51,7 +51,6 @@
"notice.stillInEducation.expired": "अब पुनः सत्यापित करें ताकि आप आगामी शैक्षणिक वर्ष के लिए एक नया कूपन प्राप्त कर सकें। हम इसे आपके खाते में जोड़ देंगे और आप इसे अगले अपग्रेड के लिए उपयोग कर सकेंगे।",
"notice.stillInEducation.isAboutToExpire": "अब फिर से सत्यापित करें ताकि आगामी शैक्षणिक वर्ष के लिए एक नया कूपन मिल सके। यह आपके खाते में सहेजा जाएगा और आपकी अगली नवीनीकरण पर उपयोग के लिए तैयार होगा।",
"notice.stillInEducation.title": "क्या आप अभी भी शिक्षा में हैं?",
"pageTitle.verification": "शैक्षिक सत्यापन",
"planNotSupportEducationDiscount": "शिक्षा मूल्य निर्धारण के लिए पात्र नहीं",
"rejectContent": "दुर्भाग्यवश, आप शिक्षा सत्यापित स्थिति के लिए योग्य नहीं हैं और इसलिए यदि आप इस ईमेल पते का उपयोग करते हैं, तो आप डिफाई प्रोफेशनल योजना के लिए विशेष 100% कूपन प्राप्त नहीं कर सकते।",
"rejectTitle": "आपकी डिफाई शैक्षणिक सत्यापन को अस्वीकृत कर दिया गया है",

View File

@ -27,7 +27,6 @@
"creation.createKnowledge": "Ciptakan Pengetahuan",
"creation.errorTip": "Gagal membuat Basis Pengetahuan",
"creation.importDSL": "Mengimpor dari file DSL",
"creation.pageTitle": "Buat Pipeline Pengetahuan",
"creation.successTip": "Berhasil membuat Basis Pengetahuan",
"deletePipeline.content": "Menghapus templat alur tidak dapat diubah.",
"deletePipeline.title": "Apakah Anda yakin akan menghapus templat alur ini?",

View File

@ -146,7 +146,6 @@
"nTo1RetrievalLegacyLink": "Pelajari lebih lanjut",
"nTo1RetrievalLegacyLinkText": "Pengambilan N-to-1 akan secara resmi tidak digunakan lagi pada bulan September.",
"noExternalKnowledge": "Belum ada API Pengetahuan Eksternal, klik di sini untuk membuat",
"pageTitle.connectExternalKnowledgeBase": "Hubungkan Basis Pengetahuan Eksternal",
"parentMode.fullDoc": "Dokumen lengkap",
"parentMode.paragraph": "Paragraf",
"partialEnabled_one": "Total {{count}} dokumen, {{num}} tersedia",

View File

@ -51,7 +51,6 @@
"notice.stillInEducation.expired": "Verifikasi ulang sekarang untuk mendapatkan kupon baru untuk tahun akademik mendatang. Kami akan menambahkannya ke akun Anda dan Anda dapat menggunakannya untuk peningkatan berikutnya.",
"notice.stillInEducation.isAboutToExpire": "Verifikasi ulang sekarang untuk mendapatkan kupon baru untuk tahun akademik mendatang. Ini akan disimpan ke akun Anda dan siap digunakan pada perpanjangan berikutnya.",
"notice.stillInEducation.title": "Masih dalam pendidikan?",
"pageTitle.verification": "Verifikasi Pendidikan",
"planNotSupportEducationDiscount": "Tidak memenuhi syarat untuk harga pendidikan",
"rejectContent": "Sayangnya, Anda tidak memenuhi syarat untuk status Education Verified dan oleh karena itu tidak dapat menerima kupon 100% eksklusif untuk Paket Dify Professional jika Anda menggunakan alamat email ini.",
"rejectTitle": "Verifikasi Pendidikan Dify Anda telah ditolak",

View File

@ -27,7 +27,6 @@
"creation.createKnowledge": "Creare conoscenza",
"creation.errorTip": "Impossibile creare una Knowledge Base",
"creation.importDSL": "Importazione da un file DSL",
"creation.pageTitle": "Crea pipeline di conoscenza",
"creation.successTip": "Creazione di una Knowledge Base",
"deletePipeline.content": "L'eliminazione del modello di pipeline è irreversibile.",
"deletePipeline.title": "Sei sicuro di eliminare questo modello di pipeline?",

View File

@ -146,7 +146,6 @@
"nTo1RetrievalLegacyLink": "Scopri di più",
"nTo1RetrievalLegacyLinkText": "Il recupero N-a-1 sarà ufficialmente deprecato a settembre.",
"noExternalKnowledge": "Non esiste ancora un'API di conoscenza esterna, fai clic qui per creare",
"pageTitle.connectExternalKnowledgeBase": "Connetti base di conoscenza esterna",
"parentMode.fullDoc": "Full-doc",
"parentMode.paragraph": "Paragrafo",
"partialEnabled_one": "Totale di {{count}} documenti, {{num}} disponibili",

View File

@ -51,7 +51,6 @@
"notice.stillInEducation.expired": "Verifica di nuovo ora per ottenere un nuovo coupon per il prossimo anno accademico. Lo aggiungeremo al tuo account e potrai usarlo per il prossimo aggiornamento.",
"notice.stillInEducation.isAboutToExpire": "Verifica di nuovo ora per ottenere un nuovo coupon per il prossimo anno accademico. Sarà salvato nel tuo account e pronto per essere utilizzato al tuo prossimo rinnovo.",
"notice.stillInEducation.title": "Ancora in formazione?",
"pageTitle.verification": "Verifica istruzione",
"planNotSupportEducationDiscount": "Non idoneo per i prezzi educativi",
"rejectContent": "Sfortunatamente, non sei idoneo per lo status di Educazione Verificata e quindi non puoi ricevere il coupon esclusivo del 100% per il Piano Professionale Dify se usi questo indirizzo email.",
"rejectTitle": "La tua verifica educativa Dify è stata rifiutata.",

View File

@ -27,7 +27,6 @@
"creation.createKnowledge": "ナレッジベースを作成する",
"creation.errorTip": "ナレッジベースの作成に失敗しました",
"creation.importDSL": "DSLファイルからインポートする",
"creation.pageTitle": "ナレッジパイプラインを作成",
"creation.successTip": "ナレッジベースが正常に作成されました",
"deletePipeline.content": "パイプラインテンプレートの削除は元に戻せません。",
"deletePipeline.title": "このパイプラインテンプレートを削除してもよろしいですか?",

View File

@ -146,7 +146,6 @@
"nTo1RetrievalLegacyLink": "詳細はこちら",
"nTo1RetrievalLegacyLinkText": " N-to-1 Retrieval は 9 月に正式に廃止されます。",
"noExternalKnowledge": "外部ナレッジベース連携 API がありません。ここをクリックして作成してください",
"pageTitle.connectExternalKnowledgeBase": "外部知識ベースに接続",
"parentMode.fullDoc": "全体",
"parentMode.paragraph": "段落",
"partialEnabled_one": "合計 {{count}} 件のドキュメント、{{num}} 件が利用可能",

View File

@ -51,7 +51,6 @@
"notice.stillInEducation.expired": "今すぐ再認証して、次の学年度向けの教育クーポンを取得してください。クーポンはあなたのアカウントに追加され、次回のアップグレード時にご利用いただけます。",
"notice.stillInEducation.isAboutToExpire": "今すぐ再認証して、次の学年度向けの教育クーポンを取得してください。クーポンは個人のアカウントに保存され、次回の更新時に使用できます。",
"notice.stillInEducation.title": "まだ在学中ですか?",
"pageTitle.verification": "教育認証",
"planNotSupportEducationDiscount": "教育価格の対象外",
"rejectContent": "申し訳ございませんが、このメールアドレスでは 教育認証 の資格を取得できず、Dify プロフェッショナルプランの 100割引クーポン を受け取ることはできません。",
"rejectTitle": "Dify 教育認証が拒否されました",

View File

@ -27,7 +27,6 @@
"creation.createKnowledge": "지식 창출",
"creation.errorTip": "기술 자료를 만들지 못했습니다.",
"creation.importDSL": "DSL 파일에서 가져오기",
"creation.pageTitle": "지식 파이프라인 생성",
"creation.successTip": "기술 자료를 성공적으로 만들었습니다.",
"deletePipeline.content": "파이프라인 템플릿을 삭제하는 것은 되돌릴 수 없습니다.",
"deletePipeline.title": "이 파이프라인 템플릿을 삭제하시겠습니까?",

View File

@ -146,7 +146,6 @@
"nTo1RetrievalLegacyLink": "자세히 알아보기",
"nTo1RetrievalLegacyLinkText": "N-대 -1 검색은 9 월에 공식적으로 더 이상 사용되지 않습니다.",
"noExternalKnowledge": "아직 외부 지식 API 가 없으므로 여기를 클릭하여 생성하십시오.",
"pageTitle.connectExternalKnowledgeBase": "외부 지식 베이스 연결",
"parentMode.fullDoc": "전체 문서",
"parentMode.paragraph": "단락",
"partialEnabled_one": "총 {{count}}개의 문서 중 {{num}}개 사용 가능",

View File

@ -51,7 +51,6 @@
"notice.stillInEducation.expired": "지금 다시 확인하여 다가오는 학년도에 사용할 새 쿠폰을 받아보세요. 우리는 그것을 귀하의 계정에 추가하며, 다음 업그레이드에 사용할 수 있습니다.",
"notice.stillInEducation.isAboutToExpire": "새로운 학년을 위한 쿠폰을 받으시려면 지금 다시 인증하십시오. 쿠폰은 귀하의 계정에 저장되어 다음 갱신 시 사용할 수 있습니다.",
"notice.stillInEducation.title": "아직 학업 중이신가요?",
"pageTitle.verification": "교육 인증",
"planNotSupportEducationDiscount": "교육 가격 대상 아님",
"rejectContent": "안타깝게도, 귀하는 교육 인증 상태에 적합하지 않으므로 이 이메일 주소를 사용할 경우 Dify Professional Plan 의 독점 100% 쿠폰을 받을 수 없습니다.",
"rejectTitle": "귀하의 Dify 교육 인증이 거부되었습니다.",

View File

@ -27,7 +27,6 @@
"creation.createKnowledge": "Create Knowledge",
"creation.errorTip": "Failed to create a Knowledge Base",
"creation.importDSL": "Import from a DSL file",
"creation.pageTitle": "Knowledge Pipeline maken",
"creation.successTip": "Successfully created a Knowledge Base",
"deletePipeline.content": "Deleting the pipeline template is irreversible.",
"deletePipeline.title": "Are you sure to delete this pipeline template?",

View File

@ -146,7 +146,6 @@
"nTo1RetrievalLegacyLink": "Learn more",
"nTo1RetrievalLegacyLinkText": " N-to-1 retrieval will be officially deprecated in September.",
"noExternalKnowledge": "There is no External Knowledge API yet, click here to create",
"pageTitle.connectExternalKnowledgeBase": "Externe kennisbank verbinden",
"parentMode.fullDoc": "Full-doc",
"parentMode.paragraph": "Paragraph",
"partialEnabled_one": "Total of {{count}} document, {{num}} available",

View File

@ -51,7 +51,6 @@
"notice.stillInEducation.expired": "Re-verify now to get a new coupon for the upcoming academic year. We'll add it to your account and you can use it for the next upgrade.",
"notice.stillInEducation.isAboutToExpire": "Re-verify now to get a new coupon for the upcoming academic year. It'll be saved to your account and ready to use at your next renewal.",
"notice.stillInEducation.title": "Still in education?",
"pageTitle.verification": "Onderwijsverificatie",
"planNotSupportEducationDiscount": "Niet in aanmerking voor onderwijsprijzen",
"rejectContent": "Unfortunately, you are not eligible for Education Verified status and therefore cannot receive the exclusive 100% coupon for the Dify Professional Plan if you use this email address.",
"rejectTitle": "Your Dify Educational Verification Has Been Rejected",

View File

@ -27,7 +27,6 @@
"creation.createKnowledge": "Tworzenie wiedzy",
"creation.errorTip": "Nie można utworzyć bazy wiedzy",
"creation.importDSL": "Importowanie z pliku DSL",
"creation.pageTitle": "Utwórz potok wiedzy",
"creation.successTip": "Pomyślnie utworzono bazę wiedzy",
"deletePipeline.content": "Usunięcie szablonu potoku jest nieodwracalne.",
"deletePipeline.title": "Czy na pewno chcesz usunąć ten szablon potoku?",

View File

@ -146,7 +146,6 @@
"nTo1RetrievalLegacyLink": "Dowiedz się więcej",
"nTo1RetrievalLegacyLinkText": "Wyszukiwanie N-do-1 zostanie oficjalnie wycofane we wrześniu.",
"noExternalKnowledge": "Nie ma jeszcze interfejsu API wiedzy zewnętrznej, kliknij tutaj, aby utworzyć",
"pageTitle.connectExternalKnowledgeBase": "Połącz zewnętrzną bazę wiedzy",
"parentMode.fullDoc": "Pełna wersja dokumentu",
"parentMode.paragraph": "Akapit",
"partialEnabled_one": "Łącznie {{count}} dokumentów, {{num}} dostępnych",

View File

@ -51,7 +51,6 @@
"notice.stillInEducation.expired": "Sprawdź ponownie teraz, aby otrzymać nowy kupon na nadchodzący rok akademicki. Dodamy go do twojego konta i będziesz mógł go użyć przy następnej aktualizacji.",
"notice.stillInEducation.isAboutToExpire": "Zweryfikuj ponownie teraz, aby otrzymać nowy kupon na nadchodzący rok akademicki. Zostanie zapisany na Twoim koncie i gotowy do użycia przy następnej odnowie.",
"notice.stillInEducation.title": "Wciąż w edukacji?",
"pageTitle.verification": "Weryfikacja edukacyjna",
"planNotSupportEducationDiscount": "Nie kwalifikuje się do cen edukacyjnych",
"rejectContent": "Niestety, nie kwalifikujesz się do statusu Zweryfikowanej Edukacji i w związku z tym nie możesz otrzymać ekskluzywnego kuponu 100% na plan Dify Professional, jeśli korzystasz z tego adresu e-mail.",
"rejectTitle": "Twoja weryfikacja edukacyjna Dify została odrzucona",

View File

@ -27,7 +27,6 @@
"creation.createKnowledge": "Criar conhecimento",
"creation.errorTip": "Falha ao criar uma base de dados de conhecimento",
"creation.importDSL": "Importar de um arquivo DSL",
"creation.pageTitle": "Criar pipeline de conhecimento",
"creation.successTip": "Criou com sucesso uma Base de Dados de Conhecimento",
"deletePipeline.content": "A exclusão do modelo de pipeline é irreversível.",
"deletePipeline.title": "Tem certeza de que deseja excluir este modelo de pipeline?",

View File

@ -146,7 +146,6 @@
"nTo1RetrievalLegacyLink": "Saiba mais",
"nTo1RetrievalLegacyLinkText": "A recuperação N-para-1 será oficialmente descontinuada em setembro.",
"noExternalKnowledge": "Ainda não existe uma API de conhecimento externo, clique aqui para criar",
"pageTitle.connectExternalKnowledgeBase": "Conectar base de conhecimento externa",
"parentMode.fullDoc": "Documento completo",
"parentMode.paragraph": "Parágrafo",
"partialEnabled_one": "Total de {{count}} documentos, {{num}} disponíveis",

View File

@ -51,7 +51,6 @@
"notice.stillInEducation.expired": "Reveja agora para obter um novo cupom para o próximo ano acadêmico. Nós o adicionaremos à sua conta e você poderá usá-lo na próxima atualização.",
"notice.stillInEducation.isAboutToExpire": "Verifique novamente agora para receber um novo cupom para o próximo ano acadêmico. Ele será salvo na sua conta e estará pronto para ser usado na sua próxima renovação.",
"notice.stillInEducation.title": "Ainda na educação?",
"pageTitle.verification": "Verificação educacional",
"planNotSupportEducationDiscount": "Não elegível para preço educacional",
"rejectContent": "Infelizmente, você não é elegível para o status de Educação Verificada e, portanto, não pode receber o cupom exclusivo de 100% para o Plano Profissional Dify se usar este endereço de e-mail.",
"rejectTitle": "A sua verificação educacional Dify foi rejeitada.",

View File

@ -27,7 +27,6 @@
"creation.createKnowledge": "Creați cunoștințe",
"creation.errorTip": "Nu s-a reușit crearea unei baze de cunoștințe",
"creation.importDSL": "Importul dintr-un fișier DSL",
"creation.pageTitle": "Creează pipeline de cunoștințe",
"creation.successTip": "Crearea cu succes a unei baze de cunoștințe",
"deletePipeline.content": "Ștergerea șablonului de conductă este ireversibilă.",
"deletePipeline.title": "Sunteți sigur că ștergeți acest șablon de conductă?",

View File

@ -146,7 +146,6 @@
"nTo1RetrievalLegacyLink": "Află mai multe",
"nTo1RetrievalLegacyLinkText": "Recuperarea N-la-1 va fi oficial depreciată în septembrie.",
"noExternalKnowledge": "Nu există încă un API de cunoștințe externe, faceți clic aici pentru a crea",
"pageTitle.connectExternalKnowledgeBase": "Conectează baza de cunoștințe externă",
"parentMode.fullDoc": "Documentar complet",
"parentMode.paragraph": "Paragraf",
"partialEnabled_one": "Total de {{count}} documente, {{num}} disponibile",

View File

@ -51,7 +51,6 @@
"notice.stillInEducation.expired": "Re-verificați acum pentru a obține un nou cupon pentru următorul an academic. Vom adăuga acest cupon în contul dvs. și îl puteți folosi pentru următoarea actualizare.",
"notice.stillInEducation.isAboutToExpire": "Re-verifică acum pentru a obține un nou cupon pentru anul universitar următor. Va fi salvat în contul tău și gata de utilizat la următoarea reînnoire.",
"notice.stillInEducation.title": "Încă în educație?",
"pageTitle.verification": "Verificare educațională",
"planNotSupportEducationDiscount": "Nu este eligibil pentru prețuri educaționale",
"rejectContent": "Din păcate, nu ești eligibil pentru statutul de Verificat Educațional și, prin urmare, nu poți primi cuponul exclusiv de 100% pentru Planul Profesional Dify dacă folosești această adresă de email.",
"rejectTitle": "Verificarea educațională Dify a fost respinsă",

View File

@ -27,7 +27,6 @@
"creation.createKnowledge": "Создание знаний",
"creation.errorTip": "Не удалось создать базу знаний",
"creation.importDSL": "Импорт из файла DSL",
"creation.pageTitle": "Создать конвейер знаний",
"creation.successTip": "Успешно создали базу знаний",
"deletePipeline.content": "Удаление шаблона конвейера является необратимым.",
"deletePipeline.title": "Вы уверены, что удалите этот шаблон воронки продаж?",

View File

@ -146,7 +146,6 @@
"nTo1RetrievalLegacyLink": "Узнать больше",
"nTo1RetrievalLegacyLinkText": " Поиск N-к-1 будет официально прекращен в сентябре.",
"noExternalKnowledge": "У нас еще нет External Knowledge API, нажмите здесь, чтобы создать",
"pageTitle.connectExternalKnowledgeBase": "Подключить внешнюю базу знаний",
"parentMode.fullDoc": "Полный документ",
"parentMode.paragraph": "Параграф",
"partialEnabled_one": "Всего {{count}} документов, доступно {{num}}",

View File

@ -51,7 +51,6 @@
"notice.stillInEducation.expired": "Переутвердите сейчас, чтобы получить новый купон на предстоящий учебный год. Мы добавим его на ваш аккаунт, и вы сможете использовать его для следующего обновления.",
"notice.stillInEducation.isAboutToExpire": "Проверьте еще раз, чтобы получить новый купон на предстоящий учебный год. Он будет сохранен в вашем аккаунте и готов к использованию при следующем продлении.",
"notice.stillInEducation.title": "Все еще учишься?",
"pageTitle.verification": "Проверка статуса обучения",
"planNotSupportEducationDiscount": "Не подходит для образовательной цены",
"rejectContent": "К сожалению, вы не имеете права на статус Проверенного образованием и, следовательно, не можете получить эксклюзивный купон на 100% для профессионального плана Dify, если вы используете этот адрес электронной почты.",
"rejectTitle": "Ваша образовательная проверка Dify была отклонена",

View File

@ -27,7 +27,6 @@
"creation.createKnowledge": "Ustvarite znanje",
"creation.errorTip": "Ustvarjanje zbirke znanja ni uspelo",
"creation.importDSL": "Uvoz iz datoteke DSL",
"creation.pageTitle": "Ustvari cevovod znanja",
"creation.successTip": "Uspešno ustvarjena baza znanja",
"deletePipeline.content": "Brisanje predloge cevovoda je nepovratno.",
"deletePipeline.title": "Ali ste prepričani, da boste izbrisali to predlogo cevovoda?",

View File

@ -146,7 +146,6 @@
"nTo1RetrievalLegacyLink": "Izvedite več",
"nTo1RetrievalLegacyLinkText": "N-to-1 pridobivanje bo uradno ukinjeno septembra.",
"noExternalKnowledge": "Zunanjega API-ja za znanje še ni, kliknite tukaj za ustvarjanje",
"pageTitle.connectExternalKnowledgeBase": "Poveži zunanjo bazo znanja",
"parentMode.fullDoc": "Celoten dokument",
"parentMode.paragraph": "Odstavek",
"partialEnabled_one": "Skupno {{count}} dokumentov, na voljo {{num}}",

View File

@ -51,7 +51,6 @@
"notice.stillInEducation.expired": "Ponovno preverite zdaj, da pridobite nov kupon za prihajajoče šolsko leto. Dodali ga bomo vašemu računu in lahko ga uporabite za naslednjo nadgradnjo.",
"notice.stillInEducation.isAboutToExpire": "Ponovno preverite zdaj, da pridobite nov kupon za prihajajoče akademsko leto. Shranjen bo na vašem računu in pripravljen za uporabo ob vaši naslednji obnovitvi.",
"notice.stillInEducation.title": "Še vedno v izobraževanju?",
"pageTitle.verification": "Preverjanje izobraževalnega statusa",
"planNotSupportEducationDiscount": "Ni upravičen do izobraževalnih cen",
"rejectContent": "Na žalost niste upravičeni do statusa Verificirane izobrazbe in zato ne morete prejeti ekskluzivnega 100-odstotnega kupona za Dify profesionalni načrt, če uporabljate ta e-poštni naslov.",
"rejectTitle": "Vaša Dify izobraževalna verifikacija je bila zavrnjena.",

View File

@ -27,7 +27,6 @@
"creation.createKnowledge": "สร้างความรู้",
"creation.errorTip": "สร้างฐานความรู้ไม่สําเร็จ",
"creation.importDSL": "นําเข้าจากไฟล์ DSL",
"creation.pageTitle": "สร้างไปป์ไลน์ความรู้",
"creation.successTip": "สร้างฐานความรู้สําเร็จ",
"deletePipeline.content": "การลบเทมเพลตไปป์ไลน์ไม่สามารถย้อนกลับได้",
"deletePipeline.title": "คุณแน่ใจที่จะลบเทมเพลตไปป์ไลน์นี้หรือไม่",

View File

@ -146,7 +146,6 @@
"nTo1RetrievalLegacyLink": "ศึกษาเพิ่มเติม",
"nTo1RetrievalLegacyLinkText": "การดึงข้อมูล N-to-1 จะเลิกใช้อย่างเป็นทางการในเดือนกันยายน",
"noExternalKnowledge": "ยังไม่มี External Knowledge API คลิกที่นี่เพื่อสร้าง",
"pageTitle.connectExternalKnowledgeBase": "เชื่อมต่อฐานความรู้ภายนอก",
"parentMode.fullDoc": "เอกสารฉบับเต็ม",
"parentMode.paragraph": "วรรค",
"partialEnabled_one": "รวม {{count}} เอกสาร, {{num}} ใช้งานได้",

View File

@ -51,7 +51,6 @@
"notice.stillInEducation.expired": "ตรวจสอบอีกครั้งตอนนี้เพื่อรับคูปองใหม่สำหรับปีการศึกษาใหม่ เราจะเพิ่มมันเข้ากับบัญชีของคุณและคุณสามารถใช้มันสำหรับการอัปเกรดครั้งถัดไปได้",
"notice.stillInEducation.isAboutToExpire": "ตรวจสอบอีกครั้งเดี๋ยวนี้เพื่อรับคูปองใหม่สำหรับปีการศึกษาที่จะมาถึง มันจะถูกบันทึกในบัญชีของคุณและพร้อมใช้งานในการต่ออายุครั้งถัดไปของคุณ.",
"notice.stillInEducation.title": "ยังอยู่ในวัยเรียนใช่ไหม?",
"pageTitle.verification": "การยืนยันสถานะการศึกษา",
"planNotSupportEducationDiscount": "ไม่มีสิทธิ์รับราคาการศึกษา",
"rejectContent": "น่าเสียดายที่คุณไม่มีสิทธิ์ได้รับสถานะการตรวจสอบการศึกษาและดังนั้นคุณจึงไม่สามารถรับคูปองพิเศษ 100% สำหรับแผนมืออาชีพ Dify หากคุณใช้ที่อยู่อีเมลนี้.",
"rejectTitle": "การตรวจสอบการศึกษา Dify ของคุณถูกปฏิเสธ",

View File

@ -27,7 +27,6 @@
"creation.createKnowledge": "Bilgi Oluştur",
"creation.errorTip": "Bilgi Bankası oluşturulamadı",
"creation.importDSL": "DSL dosyasından içe aktarma",
"creation.pageTitle": "Bilgi işlem hattı oluştur",
"creation.successTip": "Başarıyla bir Bilgi Bankası oluşturuldu",
"deletePipeline.content": "İşlem hattı şablonunun silinmesi geri alınamaz.",
"deletePipeline.title": "Bu işlem hattı şablonunu sildiğinizden emin misiniz?",

View File

@ -146,7 +146,6 @@
"nTo1RetrievalLegacyLink": "Daha fazla bilgi edin",
"nTo1RetrievalLegacyLinkText": "N-1 geri alma Eylül ayında resmi olarak kullanımdan kaldırılacaktır.",
"noExternalKnowledge": "Henüz Harici Bilgi API'si yok, oluşturmak için buraya tıklayın",
"pageTitle.connectExternalKnowledgeBase": "Harici bilgi tabanını bağla",
"parentMode.fullDoc": "Tam doküman",
"parentMode.paragraph": "Paragraf",
"partialEnabled_one": "Toplam {{count}} belge, {{num}} mevcut",

View File

@ -51,7 +51,6 @@
"notice.stillInEducation.expired": "Şimdi yeniden doğrulayın, böylece yaklaşan akademik yıl için yeni bir kupon alın. Bu kuponu hesabınıza ekleyeceğiz ve sonraki yükseltme için kullanabilirsiniz.",
"notice.stillInEducation.isAboutToExpire": "Şimdi yeniden doğrulayın ve gelecek akademik yıl için yeni bir kupon alın. Bu, hesabınıza kaydedilecek ve bir sonraki yenilemenizde kullanıma hazır olacak.",
"notice.stillInEducation.title": "Hala eğitimde misin?",
"pageTitle.verification": "Eğitim doğrulaması",
"planNotSupportEducationDiscount": "Eğitim fiyatlandırması için uygun değil",
"rejectContent": "Maalesef, Eğitim Doğrulama statüsüne uygun değilsiniz ve bu nedenle bu e-posta adresini kullanıyorsanız Dify Profesyonel Planı için özel %100 kuponu alamazsınız.",
"rejectTitle": "Dify Eğitim Doğrulamanız Rededildi",

View File

@ -27,7 +27,6 @@
"creation.createKnowledge": "Створюйте знання",
"creation.errorTip": "Не вдалося створити базу знань",
"creation.importDSL": "Імпорт із файлу DSL",
"creation.pageTitle": "Створити конвеєр знань",
"creation.successTip": "Успішно створили Базу знань",
"deletePipeline.content": "Видалення шаблону трубопроводу є незворотнім.",
"deletePipeline.title": "Ви впевнені, що видалили цей шаблон пайплайну?",

View File

@ -146,7 +146,6 @@
"nTo1RetrievalLegacyLink": "Дізнатися більше",
"nTo1RetrievalLegacyLinkText": "N-до-1 пошук буде офіційно застарілим у вересні.",
"noExternalKnowledge": "API зовнішніх знань поки що не існує, натисніть тут, щоб створити",
"pageTitle.connectExternalKnowledgeBase": "Підключити зовнішню базу знань",
"parentMode.fullDoc": "Повний документ",
"parentMode.paragraph": "Абзац",
"partialEnabled_one": "Всього {{count}} документів, доступно {{num}}",

View File

@ -51,7 +51,6 @@
"notice.stillInEducation.expired": "Перевірте ще раз зараз, щоб отримати новий купон на наступний навчальний рік. Ми додамо його до вашого облікового запису, і ви зможете скористатися ним для наступного оновлення.",
"notice.stillInEducation.isAboutToExpire": "Перевірте ще раз зараз, щоб отримати новий купон на наступний навчальний рік. Він буде збережений у вашому обліковому записі та готовий до використання при наступному поновленні.",
"notice.stillInEducation.title": "Все ще навчаєшся?",
"pageTitle.verification": "Перевірка освітнього статусу",
"planNotSupportEducationDiscount": "Не підходить для освітньої ціни",
"rejectContent": "На жаль, ви не відповідаєте вимогам для статусу Education Verified і тому не можете отримати ексклюзивний купон на 100% для професійного плану Dify, якщо використовуєте цю електронну адресу.",
"rejectTitle": "Ваша перевірка освіти Dify була відхилена",

View File

@ -27,7 +27,6 @@
"creation.createKnowledge": "Tạo kiến thức",
"creation.errorTip": "Không thể tạo Cơ sở kiến thức",
"creation.importDSL": "Nhập từ tệp DSL",
"creation.pageTitle": "Tạo quy trình tri thức",
"creation.successTip": "Tạo thành công Cơ sở tri thức",
"deletePipeline.content": "Xóa mẫu quy trình là không thể đảo ngược.",
"deletePipeline.title": "Bạn có chắc chắn xóa mẫu quy trình này không?",

View File

@ -146,7 +146,6 @@
"nTo1RetrievalLegacyLink": "Tìm hiểu thêm",
"nTo1RetrievalLegacyLinkText": "Truy xuất N-đến-1 sẽ chính thức bị loại bỏ vào tháng 9.",
"noExternalKnowledge": "Chưa có API Kiến thức Bên ngoài, hãy nhấp vào đây để tạo",
"pageTitle.connectExternalKnowledgeBase": "Kết nối cơ sở kiến thức bên ngoài",
"parentMode.fullDoc": "Tài liệu đầy đủ",
"parentMode.paragraph": "Đoạn",
"partialEnabled_one": "Tổng cộng {{count}} tài liệu, {{num}} có sẵn",

View File

@ -51,7 +51,6 @@
"notice.stillInEducation.expired": "Xác minh lại ngay bây giờ để nhận một phiếu giảm giá mới cho năm học sắp tới. Chúng tôi sẽ thêm nó vào tài khoản của bạn và bạn có thể sử dụng nó cho lần nâng cấp tiếp theo.",
"notice.stillInEducation.isAboutToExpire": "Xác minh lại ngay bây giờ để nhận một phiếu giảm giá mới cho năm học sắp tới. Nó sẽ được lưu vào tài khoản của bạn và sẵn sàng sử dụng khi bạn gia hạn tiếp theo.",
"notice.stillInEducation.title": "Vẫn đang học tập?",
"pageTitle.verification": "Xác minh giáo dục",
"planNotSupportEducationDiscount": "Không đủ điều kiện cho giá giáo dục",
"rejectContent": "Rất tiếc, bạn không đủ điều kiện để nhận trạng thái Xác minh Giáo dục và do đó không thể nhận được mã giảm giá độc quyền 100% cho Kế hoạch Chuyên nghiệp Dify nếu bạn sử dụng địa chỉ email này.",
"rejectTitle": "Yêu cầu xác minh giáo dục Dify của bạn đã bị từ chối",

View File

@ -27,7 +27,6 @@
"creation.createKnowledge": "创建知识流水线",
"creation.errorTip": "创建知识流水线失败",
"creation.importDSL": "从 DSL 文件导入",
"creation.pageTitle": "创建知识流水线",
"creation.successTip": "成功创建知识流水线",
"deletePipeline.content": "删除知识流水线模板是不可逆的。",
"deletePipeline.title": "要删除此知识流水线模板吗?",

View File

@ -146,7 +146,6 @@
"nTo1RetrievalLegacyLink": "了解更多",
"nTo1RetrievalLegacyLinkText": "9 月 1 日起我们将不再提供此能力。",
"noExternalKnowledge": "还没有外部知识库 API点击此处创建",
"pageTitle.connectExternalKnowledgeBase": "连接外部知识库",
"parentMode.fullDoc": "全文",
"parentMode.paragraph": "段落",
"partialEnabled_one": "共计 {{count}} 个文档, {{num}} 可用",

View File

@ -51,7 +51,6 @@
"notice.stillInEducation.expired": "立即重新认证,获取新学年的教育优惠券。优惠券将发放至您的账户,并可在下次升级时使用。",
"notice.stillInEducation.isAboutToExpire": "立即重新验证,获取新学年的教育优惠券。优惠券将发放至您的账户,并可在下次续订时使用。",
"notice.stillInEducation.title": "仍在就读?",
"pageTitle.verification": "教育认证",
"planNotSupportEducationDiscount": "不适用教育优惠价格",
"rejectContent": "非常遗憾,您无法使用此电子邮件以获得教育版认证资格,也无法领取 Dify Professional 版的 100% 独家优惠券。",
"rejectTitle": "您的 Dify 教育版认证已被拒绝",

View File

@ -145,6 +145,8 @@
"mcp.modal.timeout": "超时时间",
"mcp.modal.timeoutPlaceholder": "30",
"mcp.modal.title": "添加 MCP 服务 (HTTP)",
"mcp.modal.forwardUserIdentity": "转发用户身份",
"mcp.modal.forwardUserIdentityTip": "将调用用户的已验证 SSO 身份作为 Authorization Bearer token 转发到该 MCP 服务器。需要 Dify Enterprise SSO。",
"mcp.modal.useDynamicClientRegistration": "使用动态客户端注册",
"mcp.noConfigured": "未配置",
"mcp.noTools": "没有可用的工具",

View File

@ -27,7 +27,6 @@
"creation.createKnowledge": "創造知識",
"creation.errorTip": "無法建立知識庫",
"creation.importDSL": "從 DSL 檔案匯入",
"creation.pageTitle": "建立知識流水線",
"creation.successTip": "成功建立知識庫",
"deletePipeline.content": "刪除管線範本是不可逆的。",
"deletePipeline.title": "您確定要刪除此管線範本嗎?",

View File

@ -146,7 +146,6 @@
"nTo1RetrievalLegacyLink": "了解更多",
"nTo1RetrievalLegacyLinkText": "N 對 1 檢索將於 9 月正式棄用。",
"noExternalKnowledge": "目前還沒有外部知識 API按兩下此處創建",
"pageTitle.connectExternalKnowledgeBase": "連接外部知識庫",
"parentMode.fullDoc": "完整文件",
"parentMode.paragraph": "段",
"partialEnabled_one": "共 {{count}} 份文件,{{num}} 份可用",

View File

@ -51,7 +51,6 @@
"notice.stillInEducation.expired": "立即重新驗證,以獲得即將到來的學年新優惠券。我們會將其新增到您的帳戶中,您可以用於下一次升級。",
"notice.stillInEducation.isAboutToExpire": "現在重新驗證以獲得即將到來的學年新優惠券。它將保存在您的帳戶中,並在下次續訂時隨時可以使用。",
"notice.stillInEducation.title": "仍在接受教育嗎?",
"pageTitle.verification": "教育認證",
"planNotSupportEducationDiscount": "不適用教育優惠價格",
"rejectContent": "不幸的是,您不符合教育驗證狀態,因此如果您使用此電子郵件地址,將無法獲得 Dify 專業計劃的 100% 獨家優惠券。",
"rejectTitle": "您的 Dify 教育驗證已被拒絕",

View File

@ -106,6 +106,8 @@ export const useCreateMCP = () => {
timeout?: number
sse_read_timeout?: number
headers?: Record<string, string>
forward_user_identity?: boolean
identity_mode?: 'off' | 'idp_token'
}) => {
return post<ToolWithProvider>('workspaces/current/tool-provider/mcp', {
body: {
@ -133,6 +135,8 @@ export const useUpdateMCP = ({
timeout?: number
sse_read_timeout?: number
headers?: Record<string, string>
forward_user_identity?: boolean
identity_mode?: 'off' | 'idp_token'
}) => {
return put('workspaces/current/tool-provider/mcp', {
body: {