Compare commits

...

16 Commits

Author SHA1 Message Date
GareArc
7f6d48175a Refactor WorkflowService to handle missing default credentials gracefully
Some checks failed
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Has been cancelled
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Has been cancelled
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Has been cancelled
2025-09-18 23:56:04 -07:00
GareArc
ac9e9b26d0 Enhance LLM model configuration validation to include active status check
Some checks failed
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Has been cancelled
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Has been cancelled
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Has been cancelled
2025-09-15 02:16:59 -07:00
zxhlyh
07ca4071d4 Fix/enterprise model credential (#25703) 2025-09-15 11:51:20 +08:00
zxhlyh
34c05bcd1a fix: favicon 2025-09-15 11:37:27 +08:00
hjlarry
8f7ed7df8c add credential status migration file 2025-09-15 10:35:03 +08:00
autofix-ci[bot]
548884c71b [autofix.ci] apply automated fixes 2025-09-15 02:24:44 +00:00
GareArc
aec72ba2d5 Merge remote-tracking branch 'origin/main' into release/e-1.8.1 2025-09-14 19:23:37 -07:00
zxhlyh
941ad3b4a0 Fix/enterprise model credential (#25611) 2025-09-12 18:35:45 +08:00
zxhlyh
d97e936590 fix: model modal 2025-09-12 18:34:32 +08:00
GareArc
50fec8fc3c fix: add comments for API contract compatibility in PluginCredentialType enum 2025-09-12 02:57:38 -07:00
zxhlyh
2f3d09c1ca fix: credential item style 2025-09-12 17:11:26 +08:00
zxhlyh
134dfe7c9b fix: model modal 2025-09-12 16:50:23 +08:00
zxhlyh
8a43aedc15 Fix/enterprise model credential (#25591) 2025-09-12 15:42:16 +08:00
zxhlyh
f07abfb781 fix: unavailable credential in plugin detail 2025-09-11 17:01:40 +08:00
zxhlyh
ba1a20ee59 fix: help url in model modal
Some checks are pending
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Waiting to run
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Waiting to run
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Blocked by required conditions
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Blocked by required conditions
2025-09-10 17:47:31 +08:00
zxhlyh
3c463c1e3a fix: credential in tool node & switch credential in load balancing 2025-09-10 15:49:49 +08:00
11 changed files with 130 additions and 32 deletions

View File

@@ -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 ###

View File

@@ -9,9 +9,9 @@ from services.errors.base import BaseServiceError
logger = logging.getLogger(__name__)
class PluginCredentialType(enum.IntEnum):
MODEL = enum.auto()
TOOL = enum.auto()
class PluginCredentialType(enum.Enum):
MODEL = 0 # must be 0 for API contract compatibility
TOOL = 1 # must be 1 for API contract compatibility
def to_number(self):
return self.value

View File

@@ -375,13 +375,14 @@ class WorkflowService:
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:
1. The provider exists and is configured
2. The model exists in the provider
3. Credentials can be fetched for the model
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 provider: The provider name
@@ -391,6 +392,7 @@ class WorkflowService:
try:
from core.model_manager import ModelManager
from core.model_runtime.entities.model_entities import ModelType
from core.provider_manager import ProviderManager
# Get model instance to validate provider+model combination
model_manager = ModelManager()
@@ -402,6 +404,22 @@ class WorkflowService:
# via ProviderConfiguration.get_current_credentials() -> _check_credential_policy_compliance()
# 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:
raise ValueError(
f"Failed to validate LLM model configuration (provider: {provider}, model: {model_name}): {str(e)}"
@@ -434,7 +452,8 @@ class WorkflowService:
)
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
from core.helper.credential_utils import check_credential_policy_compliance

View File

@@ -92,10 +92,10 @@ const CredentialItem = ({
)
}
{
showAction && (
showAction && !credential.from_enterprise && (
<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')}>
<ActionButton
disabled={disabled}
@@ -110,7 +110,7 @@ const CredentialItem = ({
)
}
{
!disableDelete && !credential.from_enterprise && (
!disableDelete && (
<Tooltip popupContent={disableDeleteWhenSelected ? disableDeleteTip : t('common.operation.delete')}>
<ActionButton
className='hover:bg-transparent'

View File

@@ -37,51 +37,57 @@ const SwitchCredentialInLoadBalancing = ({
onRemove,
}: SwitchCredentialInLoadBalancingProps) => {
const { t } = useTranslation()
const notAllowCustomCredential = provider.allow_custom_token === false
const handleItemClick = useCallback((credential: Credential) => {
setCustomModelCredential(credential)
}, [setCustomModelCredential])
const renderTrigger = useCallback(() => {
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'
if (authRemoved && !customModelCredential?.not_allowed_to_use)
if (authRemoved || unavailable)
color = 'red'
if (customModelCredential?.not_allowed_to_use)
color = 'gray'
const Item = (
<Button
variant='secondary'
className={cn(
'shrink-0 space-x-1',
authRemoved && 'text-components-button-destructive-secondary-text',
customModelCredential?.not_allowed_to_use && 'cursor-not-allowed opacity-50',
(authRemoved || unavailable) && 'text-components-button-destructive-secondary-text',
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>
)
}
<RiArrowDownSLine className='h-4 w-4' />
</Button>
)
if (customModelCredential?.not_allowed_to_use) {
if (empty && notAllowCustomCredential) {
return (
<Tooltip
asChild
@@ -92,7 +98,7 @@ const SwitchCredentialInLoadBalancing = ({
)
}
return Item
}, [customModelCredential, t, credentials])
}, [customModelCredential, t, credentials, notAllowCustomCredential])
return (
<Authorized
@@ -123,6 +129,7 @@ const SwitchCredentialInLoadBalancing = ({
enableAddModelCredential
showItemSelectedIcon
popupTitle={t('common.modelProvider.auth.modelCredentials')}
triggerOnlyOpenModal={!credentials?.length}
/>
)
}

View File

@@ -114,7 +114,7 @@ const ModelModal: FC<ModelModalProps> = ({
const formRef1 = useRef<FormRefObject>(null)
const [selectedCredential, setSelectedCredential] = useState<Credential & { addNewCredential?: boolean } | undefined>()
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]
}).length && isCurrentWorkspaceManager
@@ -376,16 +376,16 @@ const ModelModal: FC<ModelModalProps> = ({
<a
href={provider.help?.url[language] || provider.help?.url.en_US}
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()}
>
{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>
)
: <div />
}
<div className='flex items-center justify-end space-x-2'>
<div className='ml-2 flex items-center justify-end space-x-2'>
{
isEditMode && (
<Button

View File

@@ -36,14 +36,22 @@ const AuthorizedInNode = ({
disabled,
invalidPluginCredentialInfo,
notAllowCustomCredential,
} = usePluginAuth(pluginPayload, isOpen || !!credentialId)
} = usePluginAuth(pluginPayload, true)
const renderTrigger = useCallback((open?: boolean) => {
let label = ''
let removed = false
let unavailable = false
let color = 'green'
let defaultUnavailable = false
if (!credentialId) {
label = t('plugin.auth.workspaceDefault')
const defaultCredential = credentials.find(c => c.is_default)
if (defaultCredential?.not_allowed_to_use) {
color = 'gray'
defaultUnavailable = true
}
}
else {
const credential = credentials.find(c => c.id === credentialId)
@@ -63,6 +71,7 @@ const AuthorizedInNode = ({
open && !removed && 'bg-components-button-ghost-bg-hover',
removed && 'bg-transparent text-text-destructive',
)}
variant={(defaultUnavailable || unavailable) ? 'ghost' : 'secondary'}
>
<Indicator
className='mr-1.5'
@@ -70,7 +79,12 @@ const AuthorizedInNode = ({
/>
{label}
{
unavailable && t('plugin.auth.unavailable')
(unavailable || defaultUnavailable) && (
<>
&nbsp;
{t('plugin.auth.unavailable')}
</>
)
}
<RiArrowDownSLine
className={cn(
@@ -81,6 +95,7 @@ const AuthorizedInNode = ({
</Button>
)
}, [credentialId, credentials, t])
const defaultUnavailable = credentials.find(c => c.is_default)?.not_allowed_to_use
const extraAuthorizationItems: Credential[] = [
{
id: '__workspace_default__',
@@ -88,6 +103,7 @@ const AuthorizedInNode = ({
provider: '',
is_default: !credentialId,
isWorkspaceDefault: true,
not_allowed_to_use: defaultUnavailable,
},
]
const handleAuthorizationItemClick = useCallback((id: string) => {

View File

@@ -174,6 +174,7 @@ const Authorized = ({
}
}, [updatePluginCredential, notify, t, handleSetDoingAction, onUpdate])
const unavailableCredentials = credentials.filter(credential => credential.not_allowed_to_use)
const unavailableCredential = credentials.find(credential => credential.not_allowed_to_use && credential.is_default)
return (
<>
@@ -197,7 +198,7 @@ const Authorized = ({
'w-full',
isOpen && 'bg-components-button-secondary-bg-hover',
)}>
<Indicator className='mr-2' />
<Indicator className='mr-2' color={unavailableCredential ? 'gray' : 'green'} />
{credentials.length}&nbsp;
{
credentials.length > 1

View File

@@ -2,6 +2,7 @@
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useFavicon, useTitle } from 'ahooks'
import { basePath } from '@/utils/var'
import { useEffect } from 'react'
export default function useDocumentTitle(title: string) {
const isPending = useGlobalPublicStore(s => s.isGlobalPending)
@@ -20,5 +21,24 @@ export default function useDocumentTitle(title: string) {
}
}
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)
}

View File

@@ -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',
oauthClient: 'OAuth Client',
credentialUnavailable: 'Credentials currently unavailable. Please contact admin.',
credentialUnavailableInButton: 'Credential unavailable',
customCredentialUnavailable: 'Custom credentials currently unavailable',
unavailable: 'Unavailable',
},

View File

@@ -298,6 +298,7 @@ const translation = {
clientInfo: '由于未找到此工具提供者的系统客户端密钥,因此需要手动设置,对于 redirect_uri请使用',
oauthClient: 'OAuth 客户端',
credentialUnavailable: '自定义凭据当前不可用,请联系管理员。',
credentialUnavailableInButton: '凭据不可用',
customCredentialUnavailable: '自定义凭据当前不可用',
unavailable: '不可用',
},