mirror of
https://github.com/langgenius/dify.git
synced 2026-01-05 22:15:52 +00:00
Compare commits
16 Commits
fix/docume
...
release/e-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f6d48175a | ||
|
|
ac9e9b26d0 | ||
|
|
07ca4071d4 | ||
|
|
34c05bcd1a | ||
|
|
8f7ed7df8c | ||
|
|
548884c71b | ||
|
|
aec72ba2d5 | ||
|
|
941ad3b4a0 | ||
|
|
d97e936590 | ||
|
|
50fec8fc3c | ||
|
|
2f3d09c1ca | ||
|
|
134dfe7c9b | ||
|
|
8a43aedc15 | ||
|
|
f07abfb781 | ||
|
|
ba1a20ee59 | ||
|
|
3c463c1e3a |
@@ -0,0 +1,33 @@
|
|||||||
|
"""Add credential status for provider table
|
||||||
|
|
||||||
|
Revision ID: cf7c38a32b2d
|
||||||
|
Revises: c20211f18133
|
||||||
|
Create Date: 2025-09-11 15:37:17.771298
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import models as models
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'cf7c38a32b2d'
|
||||||
|
down_revision = 'c20211f18133'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('providers', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('credential_status', sa.String(length=20), server_default=sa.text("'active'::character varying"), nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('providers', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('credential_status')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -9,9 +9,9 @@ from services.errors.base import BaseServiceError
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PluginCredentialType(enum.IntEnum):
|
class PluginCredentialType(enum.Enum):
|
||||||
MODEL = enum.auto()
|
MODEL = 0 # must be 0 for API contract compatibility
|
||||||
TOOL = enum.auto()
|
TOOL = 1 # must be 1 for API contract compatibility
|
||||||
|
|
||||||
def to_number(self):
|
def to_number(self):
|
||||||
return self.value
|
return self.value
|
||||||
|
|||||||
@@ -375,13 +375,14 @@ class WorkflowService:
|
|||||||
|
|
||||||
def _validate_llm_model_config(self, tenant_id: str, provider: str, model_name: str) -> None:
|
def _validate_llm_model_config(self, tenant_id: str, provider: str, model_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
Validate that an LLM model configuration can fetch valid credentials.
|
Validate that an LLM model configuration can fetch valid credentials and has active status.
|
||||||
|
|
||||||
This method attempts to get the model instance and validates that:
|
This method attempts to get the model instance and validates that:
|
||||||
1. The provider exists and is configured
|
1. The provider exists and is configured
|
||||||
2. The model exists in the provider
|
2. The model exists in the provider
|
||||||
3. Credentials can be fetched for the model
|
3. Credentials can be fetched for the model
|
||||||
4. The credentials pass policy compliance checks
|
4. The credentials pass policy compliance checks
|
||||||
|
5. The model status is ACTIVE (not NO_CONFIGURE, DISABLED, etc.)
|
||||||
|
|
||||||
:param tenant_id: The tenant ID
|
:param tenant_id: The tenant ID
|
||||||
:param provider: The provider name
|
:param provider: The provider name
|
||||||
@@ -391,6 +392,7 @@ class WorkflowService:
|
|||||||
try:
|
try:
|
||||||
from core.model_manager import ModelManager
|
from core.model_manager import ModelManager
|
||||||
from core.model_runtime.entities.model_entities import ModelType
|
from core.model_runtime.entities.model_entities import ModelType
|
||||||
|
from core.provider_manager import ProviderManager
|
||||||
|
|
||||||
# Get model instance to validate provider+model combination
|
# Get model instance to validate provider+model combination
|
||||||
model_manager = ModelManager()
|
model_manager = ModelManager()
|
||||||
@@ -402,6 +404,22 @@ class WorkflowService:
|
|||||||
# via ProviderConfiguration.get_current_credentials() -> _check_credential_policy_compliance()
|
# via ProviderConfiguration.get_current_credentials() -> _check_credential_policy_compliance()
|
||||||
# If it fails, an exception will be raised
|
# If it fails, an exception will be raised
|
||||||
|
|
||||||
|
# Additionally, check the model status to ensure it's ACTIVE
|
||||||
|
provider_manager = ProviderManager()
|
||||||
|
provider_configurations = provider_manager.get_configurations(tenant_id)
|
||||||
|
models = provider_configurations.get_models(provider=provider, model_type=ModelType.LLM)
|
||||||
|
|
||||||
|
target_model = None
|
||||||
|
for model in models:
|
||||||
|
if model.model == model_name and model.provider.provider == provider:
|
||||||
|
target_model = model
|
||||||
|
break
|
||||||
|
|
||||||
|
if target_model:
|
||||||
|
target_model.raise_for_status()
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Model {model_name} not found for provider {provider}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Failed to validate LLM model configuration (provider: {provider}, model: {model_name}): {str(e)}"
|
f"Failed to validate LLM model configuration (provider: {provider}, model: {model_name}): {str(e)}"
|
||||||
@@ -434,7 +452,8 @@ class WorkflowService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not default_provider:
|
if not default_provider:
|
||||||
raise ValueError("No default credential found")
|
# plugin does not require credentials, skip
|
||||||
|
return
|
||||||
|
|
||||||
# Check credential policy compliance using the default credential ID
|
# Check credential policy compliance using the default credential ID
|
||||||
from core.helper.credential_utils import check_credential_policy_compliance
|
from core.helper.credential_utils import check_credential_policy_compliance
|
||||||
|
|||||||
@@ -92,10 +92,10 @@ const CredentialItem = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
showAction && (
|
showAction && !credential.from_enterprise && (
|
||||||
<div className='ml-2 hidden shrink-0 items-center group-hover:flex'>
|
<div className='ml-2 hidden shrink-0 items-center group-hover:flex'>
|
||||||
{
|
{
|
||||||
!disableEdit && !credential.not_allowed_to_use && !credential.from_enterprise && (
|
!disableEdit && !credential.not_allowed_to_use && (
|
||||||
<Tooltip popupContent={t('common.operation.edit')}>
|
<Tooltip popupContent={t('common.operation.edit')}>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -110,7 +110,7 @@ const CredentialItem = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
!disableDelete && !credential.from_enterprise && (
|
!disableDelete && (
|
||||||
<Tooltip popupContent={disableDeleteWhenSelected ? disableDeleteTip : t('common.operation.delete')}>
|
<Tooltip popupContent={disableDeleteWhenSelected ? disableDeleteTip : t('common.operation.delete')}>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
className='hover:bg-transparent'
|
className='hover:bg-transparent'
|
||||||
|
|||||||
@@ -37,51 +37,57 @@ const SwitchCredentialInLoadBalancing = ({
|
|||||||
onRemove,
|
onRemove,
|
||||||
}: SwitchCredentialInLoadBalancingProps) => {
|
}: SwitchCredentialInLoadBalancingProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const notAllowCustomCredential = provider.allow_custom_token === false
|
||||||
const handleItemClick = useCallback((credential: Credential) => {
|
const handleItemClick = useCallback((credential: Credential) => {
|
||||||
setCustomModelCredential(credential)
|
setCustomModelCredential(credential)
|
||||||
}, [setCustomModelCredential])
|
}, [setCustomModelCredential])
|
||||||
|
|
||||||
const renderTrigger = useCallback(() => {
|
const renderTrigger = useCallback(() => {
|
||||||
const selectedCredentialId = customModelCredential?.credential_id
|
const selectedCredentialId = customModelCredential?.credential_id
|
||||||
const authRemoved = !selectedCredentialId && !!credentials?.length
|
const currentCredential = credentials?.find(c => c.credential_id === selectedCredentialId)
|
||||||
|
const empty = !credentials?.length
|
||||||
|
const authRemoved = selectedCredentialId && !currentCredential && !empty
|
||||||
|
const unavailable = currentCredential?.not_allowed_to_use
|
||||||
|
|
||||||
let color = 'green'
|
let color = 'green'
|
||||||
if (authRemoved && !customModelCredential?.not_allowed_to_use)
|
if (authRemoved || unavailable)
|
||||||
color = 'red'
|
color = 'red'
|
||||||
if (customModelCredential?.not_allowed_to_use)
|
|
||||||
color = 'gray'
|
|
||||||
|
|
||||||
const Item = (
|
const Item = (
|
||||||
<Button
|
<Button
|
||||||
variant='secondary'
|
variant='secondary'
|
||||||
className={cn(
|
className={cn(
|
||||||
'shrink-0 space-x-1',
|
'shrink-0 space-x-1',
|
||||||
authRemoved && 'text-components-button-destructive-secondary-text',
|
(authRemoved || unavailable) && 'text-components-button-destructive-secondary-text',
|
||||||
customModelCredential?.not_allowed_to_use && 'cursor-not-allowed opacity-50',
|
empty && 'cursor-not-allowed opacity-50',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Indicator
|
|
||||||
className='mr-2'
|
|
||||||
color={color as any}
|
|
||||||
/>
|
|
||||||
{
|
{
|
||||||
authRemoved && !customModelCredential?.not_allowed_to_use && t('common.modelProvider.auth.authRemoved')
|
!empty && (
|
||||||
|
<Indicator
|
||||||
|
className='mr-2'
|
||||||
|
color={color as any}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
!authRemoved && customModelCredential?.not_allowed_to_use && t('plugin.auth.credentialUnavailable')
|
authRemoved && t('common.modelProvider.auth.authRemoved')
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
!authRemoved && !customModelCredential?.not_allowed_to_use && customModelCredential?.credential_name
|
(unavailable || empty) && t('plugin.auth.credentialUnavailableInButton')
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
customModelCredential?.from_enterprise && (
|
!authRemoved && !unavailable && !empty && customModelCredential?.credential_name
|
||||||
|
}
|
||||||
|
{
|
||||||
|
currentCredential?.from_enterprise && (
|
||||||
<Badge className='ml-2'>Enterprise</Badge>
|
<Badge className='ml-2'>Enterprise</Badge>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<RiArrowDownSLine className='h-4 w-4' />
|
<RiArrowDownSLine className='h-4 w-4' />
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
if (customModelCredential?.not_allowed_to_use) {
|
if (empty && notAllowCustomCredential) {
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
asChild
|
asChild
|
||||||
@@ -92,7 +98,7 @@ const SwitchCredentialInLoadBalancing = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
return Item
|
return Item
|
||||||
}, [customModelCredential, t, credentials])
|
}, [customModelCredential, t, credentials, notAllowCustomCredential])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Authorized
|
<Authorized
|
||||||
@@ -123,6 +129,7 @@ const SwitchCredentialInLoadBalancing = ({
|
|||||||
enableAddModelCredential
|
enableAddModelCredential
|
||||||
showItemSelectedIcon
|
showItemSelectedIcon
|
||||||
popupTitle={t('common.modelProvider.auth.modelCredentials')}
|
popupTitle={t('common.modelProvider.auth.modelCredentials')}
|
||||||
|
triggerOnlyOpenModal={!credentials?.length}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ const ModelModal: FC<ModelModalProps> = ({
|
|||||||
const formRef1 = useRef<FormRefObject>(null)
|
const formRef1 = useRef<FormRefObject>(null)
|
||||||
const [selectedCredential, setSelectedCredential] = useState<Credential & { addNewCredential?: boolean } | undefined>()
|
const [selectedCredential, setSelectedCredential] = useState<Credential & { addNewCredential?: boolean } | undefined>()
|
||||||
const formRef2 = useRef<FormRefObject>(null)
|
const formRef2 = useRef<FormRefObject>(null)
|
||||||
const isEditMode = !!Object.keys(formValues).filter((key) => {
|
const isEditMode = !!credential && !!Object.keys(formSchemasValue || {}).filter((key) => {
|
||||||
return key !== '__model_name' && key !== '__model_type' && !!formValues[key]
|
return key !== '__model_name' && key !== '__model_type' && !!formValues[key]
|
||||||
}).length && isCurrentWorkspaceManager
|
}).length && isCurrentWorkspaceManager
|
||||||
|
|
||||||
@@ -376,16 +376,16 @@ const ModelModal: FC<ModelModalProps> = ({
|
|||||||
<a
|
<a
|
||||||
href={provider.help?.url[language] || provider.help?.url.en_US}
|
href={provider.help?.url[language] || provider.help?.url.en_US}
|
||||||
target='_blank' rel='noopener noreferrer'
|
target='_blank' rel='noopener noreferrer'
|
||||||
className='system-xs-regular mt-2 inline-flex items-center text-text-accent'
|
className='system-xs-regular mt-2 inline-block align-middle text-text-accent'
|
||||||
onClick={e => !provider.help.url && e.preventDefault()}
|
onClick={e => !provider.help.url && e.preventDefault()}
|
||||||
>
|
>
|
||||||
{provider.help.title?.[language] || provider.help.url[language] || provider.help.title?.en_US || provider.help.url.en_US}
|
{provider.help.title?.[language] || provider.help.url[language] || provider.help.title?.en_US || provider.help.url.en_US}
|
||||||
<LinkExternal02 className='ml-1 h-3 w-3' />
|
<LinkExternal02 className='ml-1 mt-[-2px] inline-block h-3 w-3' />
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
: <div />
|
: <div />
|
||||||
}
|
}
|
||||||
<div className='flex items-center justify-end space-x-2'>
|
<div className='ml-2 flex items-center justify-end space-x-2'>
|
||||||
{
|
{
|
||||||
isEditMode && (
|
isEditMode && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -36,14 +36,22 @@ const AuthorizedInNode = ({
|
|||||||
disabled,
|
disabled,
|
||||||
invalidPluginCredentialInfo,
|
invalidPluginCredentialInfo,
|
||||||
notAllowCustomCredential,
|
notAllowCustomCredential,
|
||||||
} = usePluginAuth(pluginPayload, isOpen || !!credentialId)
|
} = usePluginAuth(pluginPayload, true)
|
||||||
const renderTrigger = useCallback((open?: boolean) => {
|
const renderTrigger = useCallback((open?: boolean) => {
|
||||||
let label = ''
|
let label = ''
|
||||||
let removed = false
|
let removed = false
|
||||||
let unavailable = false
|
let unavailable = false
|
||||||
let color = 'green'
|
let color = 'green'
|
||||||
|
let defaultUnavailable = false
|
||||||
if (!credentialId) {
|
if (!credentialId) {
|
||||||
label = t('plugin.auth.workspaceDefault')
|
label = t('plugin.auth.workspaceDefault')
|
||||||
|
|
||||||
|
const defaultCredential = credentials.find(c => c.is_default)
|
||||||
|
|
||||||
|
if (defaultCredential?.not_allowed_to_use) {
|
||||||
|
color = 'gray'
|
||||||
|
defaultUnavailable = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const credential = credentials.find(c => c.id === credentialId)
|
const credential = credentials.find(c => c.id === credentialId)
|
||||||
@@ -63,6 +71,7 @@ const AuthorizedInNode = ({
|
|||||||
open && !removed && 'bg-components-button-ghost-bg-hover',
|
open && !removed && 'bg-components-button-ghost-bg-hover',
|
||||||
removed && 'bg-transparent text-text-destructive',
|
removed && 'bg-transparent text-text-destructive',
|
||||||
)}
|
)}
|
||||||
|
variant={(defaultUnavailable || unavailable) ? 'ghost' : 'secondary'}
|
||||||
>
|
>
|
||||||
<Indicator
|
<Indicator
|
||||||
className='mr-1.5'
|
className='mr-1.5'
|
||||||
@@ -70,7 +79,12 @@ const AuthorizedInNode = ({
|
|||||||
/>
|
/>
|
||||||
{label}
|
{label}
|
||||||
{
|
{
|
||||||
unavailable && t('plugin.auth.unavailable')
|
(unavailable || defaultUnavailable) && (
|
||||||
|
<>
|
||||||
|
|
||||||
|
{t('plugin.auth.unavailable')}
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
<RiArrowDownSLine
|
<RiArrowDownSLine
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -81,6 +95,7 @@ const AuthorizedInNode = ({
|
|||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}, [credentialId, credentials, t])
|
}, [credentialId, credentials, t])
|
||||||
|
const defaultUnavailable = credentials.find(c => c.is_default)?.not_allowed_to_use
|
||||||
const extraAuthorizationItems: Credential[] = [
|
const extraAuthorizationItems: Credential[] = [
|
||||||
{
|
{
|
||||||
id: '__workspace_default__',
|
id: '__workspace_default__',
|
||||||
@@ -88,6 +103,7 @@ const AuthorizedInNode = ({
|
|||||||
provider: '',
|
provider: '',
|
||||||
is_default: !credentialId,
|
is_default: !credentialId,
|
||||||
isWorkspaceDefault: true,
|
isWorkspaceDefault: true,
|
||||||
|
not_allowed_to_use: defaultUnavailable,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
const handleAuthorizationItemClick = useCallback((id: string) => {
|
const handleAuthorizationItemClick = useCallback((id: string) => {
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ const Authorized = ({
|
|||||||
}
|
}
|
||||||
}, [updatePluginCredential, notify, t, handleSetDoingAction, onUpdate])
|
}, [updatePluginCredential, notify, t, handleSetDoingAction, onUpdate])
|
||||||
const unavailableCredentials = credentials.filter(credential => credential.not_allowed_to_use)
|
const unavailableCredentials = credentials.filter(credential => credential.not_allowed_to_use)
|
||||||
|
const unavailableCredential = credentials.find(credential => credential.not_allowed_to_use && credential.is_default)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -197,7 +198,7 @@ const Authorized = ({
|
|||||||
'w-full',
|
'w-full',
|
||||||
isOpen && 'bg-components-button-secondary-bg-hover',
|
isOpen && 'bg-components-button-secondary-bg-hover',
|
||||||
)}>
|
)}>
|
||||||
<Indicator className='mr-2' />
|
<Indicator className='mr-2' color={unavailableCredential ? 'gray' : 'green'} />
|
||||||
{credentials.length}
|
{credentials.length}
|
||||||
{
|
{
|
||||||
credentials.length > 1
|
credentials.length > 1
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
import { useFavicon, useTitle } from 'ahooks'
|
import { useFavicon, useTitle } from 'ahooks'
|
||||||
import { basePath } from '@/utils/var'
|
import { basePath } from '@/utils/var'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
export default function useDocumentTitle(title: string) {
|
export default function useDocumentTitle(title: string) {
|
||||||
const isPending = useGlobalPublicStore(s => s.isGlobalPending)
|
const isPending = useGlobalPublicStore(s => s.isGlobalPending)
|
||||||
@@ -20,5 +21,24 @@ export default function useDocumentTitle(title: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
useTitle(titleStr)
|
useTitle(titleStr)
|
||||||
|
useEffect(() => {
|
||||||
|
let apple: HTMLLinkElement | null = null
|
||||||
|
if (systemFeatures.branding.favicon) {
|
||||||
|
document
|
||||||
|
.querySelectorAll(
|
||||||
|
'link[rel=\'icon\'], link[rel=\'shortcut icon\'], link[rel=\'apple-touch-icon\'], link[rel=\'mask-icon\']',
|
||||||
|
)
|
||||||
|
.forEach(n => n.parentNode?.removeChild(n))
|
||||||
|
|
||||||
|
apple = document.createElement('link')
|
||||||
|
apple.rel = 'apple-touch-icon'
|
||||||
|
apple.href = systemFeatures.branding.favicon
|
||||||
|
document.head.appendChild(apple)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
apple?.remove()
|
||||||
|
}
|
||||||
|
}, [systemFeatures.branding.favicon])
|
||||||
useFavicon(favicon)
|
useFavicon(favicon)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -298,6 +298,7 @@ const translation = {
|
|||||||
clientInfo: 'As no system client secrets found for this tool provider, setup it manually is required, for redirect_uri, please use',
|
clientInfo: 'As no system client secrets found for this tool provider, setup it manually is required, for redirect_uri, please use',
|
||||||
oauthClient: 'OAuth Client',
|
oauthClient: 'OAuth Client',
|
||||||
credentialUnavailable: 'Credentials currently unavailable. Please contact admin.',
|
credentialUnavailable: 'Credentials currently unavailable. Please contact admin.',
|
||||||
|
credentialUnavailableInButton: 'Credential unavailable',
|
||||||
customCredentialUnavailable: 'Custom credentials currently unavailable',
|
customCredentialUnavailable: 'Custom credentials currently unavailable',
|
||||||
unavailable: 'Unavailable',
|
unavailable: 'Unavailable',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -298,6 +298,7 @@ const translation = {
|
|||||||
clientInfo: '由于未找到此工具提供者的系统客户端密钥,因此需要手动设置,对于 redirect_uri,请使用',
|
clientInfo: '由于未找到此工具提供者的系统客户端密钥,因此需要手动设置,对于 redirect_uri,请使用',
|
||||||
oauthClient: 'OAuth 客户端',
|
oauthClient: 'OAuth 客户端',
|
||||||
credentialUnavailable: '自定义凭据当前不可用,请联系管理员。',
|
credentialUnavailable: '自定义凭据当前不可用,请联系管理员。',
|
||||||
|
credentialUnavailableInButton: '凭据不可用',
|
||||||
customCredentialUnavailable: '自定义凭据当前不可用',
|
customCredentialUnavailable: '自定义凭据当前不可用',
|
||||||
unavailable: '不可用',
|
unavailable: '不可用',
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user