diff --git a/api/core/helper/name_generator.py b/api/core/helper/name_generator.py new file mode 100644 index 0000000000..2465e0ddfb --- /dev/null +++ b/api/core/helper/name_generator.py @@ -0,0 +1,42 @@ +import logging +import re +from collections.abc import Sequence +from typing import Any + +from core.tools.entities.tool_entities import CredentialType + +logger = logging.getLogger(__name__) + + +def generate_provider_name( + providers: Sequence[Any], credential_type: CredentialType, fallback_context: str = "provider" +) -> str: + try: + return generate_incremental_name( + [provider.name for provider in providers], + f"{credential_type.get_name()}", + ) + except Exception as e: + logger.warning(f"Error generating next provider name for {fallback_context}: {str(e)}") + return f"{credential_type.get_name()} 1" + + +def generate_incremental_name( + names: Sequence[str], + default_pattern: str, +) -> str: + pattern = rf"^{re.escape(default_pattern)}\s+(\d+)$" + numbers = [] + + for name in names: + if not name: + continue + match = re.match(pattern, name.strip()) + if match: + numbers.append(int(match.group(1))) + + if not numbers: + return f"{default_pattern} 1" + + max_number = max(numbers) + return f"{default_pattern} {max_number + 1}" diff --git a/api/core/helper/provider_name_generator.py b/api/core/helper/provider_name_generator.py deleted file mode 100644 index dd5816aa24..0000000000 --- a/api/core/helper/provider_name_generator.py +++ /dev/null @@ -1,35 +0,0 @@ -import logging -import re -from collections.abc import Sequence -from typing import Any - -from core.tools.entities.tool_entities import CredentialType - -logger = logging.getLogger(__name__) - - -def generate_provider_name( - providers: Sequence[Any], - credential_type: CredentialType, - fallback_context: str = "provider" -) -> str: - try: - default_pattern = f"{credential_type.get_name()}" - - pattern = rf"^{re.escape(default_pattern)}\s+(\d+)$" - numbers = [] - - for provider in providers: - if provider.name: - match = re.match(pattern, provider.name.strip()) - if match: - numbers.append(int(match.group(1))) - - if not numbers: - return f"{default_pattern} 1" - - max_number = max(numbers) - return f"{default_pattern} {max_number + 1}" - except Exception as e: - logger.warning(f"Error generating next provider name for {fallback_context}: {str(e)}") - return f"{credential_type.get_name()} 1" \ No newline at end of file diff --git a/api/migrations/versions/2025_07_18_2134-fcb46171d891_add_pipeline_info_13.py b/api/migrations/versions/2025_07_18_2134-fcb46171d891_add_pipeline_info_13.py new file mode 100644 index 0000000000..440fe462b5 --- /dev/null +++ b/api/migrations/versions/2025_07_18_2134-fcb46171d891_add_pipeline_info_13.py @@ -0,0 +1,33 @@ +"""add_pipeline_info_13 + +Revision ID: fcb46171d891 +Revises: 2008609cf2bb +Create Date: 2025-07-18 21:34:31.914500 + +""" +from alembic import op +import models as models +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'fcb46171d891' +down_revision = '2008609cf2bb' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('datasource_providers', schema=None) as batch_op: + batch_op.create_unique_constraint('datasource_provider_unique_name', ['tenant_id', 'plugin_id', 'provider', 'name']) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('datasource_providers', schema=None) as batch_op: + batch_op.drop_constraint('datasource_provider_unique_name', type_='unique') + + # ### end Alembic commands ### diff --git a/api/models/oauth.py b/api/models/oauth.py index aa4f7fdab0..3fe59bb650 100644 --- a/api/models/oauth.py +++ b/api/models/oauth.py @@ -25,6 +25,7 @@ class DatasourceProvider(Base): __tablename__ = "datasource_providers" __table_args__ = ( db.PrimaryKeyConstraint("id", name="datasource_provider_pkey"), + db.UniqueConstraint("tenant_id", "plugin_id", "provider", "name", name="datasource_provider_unique_name"), db.Index("datasource_provider_auth_type_provider_idx", "tenant_id", "plugin_id", "provider"), ) id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) @@ -35,6 +36,6 @@ class DatasourceProvider(Base): auth_type: Mapped[str] = db.Column(db.String(255), nullable=False) encrypted_credentials: Mapped[dict] = db.Column(JSONB, nullable=False) avatar_url: Mapped[str] = db.Column(db.String(255), nullable=True) - + created_at: Mapped[datetime] = db.Column(db.DateTime, nullable=False, default=datetime.now) updated_at: Mapped[datetime] = db.Column(db.DateTime, nullable=False, default=datetime.now) diff --git a/api/services/datasource_provider_service.py b/api/services/datasource_provider_service.py index e2019c70fd..87735563f7 100644 --- a/api/services/datasource_provider_service.py +++ b/api/services/datasource_provider_service.py @@ -5,7 +5,7 @@ from sqlalchemy.orm import Session from constants import HIDDEN_VALUE from core.helper import encrypter -from core.helper.provider_name_generator import generate_provider_name +from core.helper.name_generator import generate_incremental_name from core.model_runtime.entities.provider_entities import FormType from core.model_runtime.errors.validate import CredentialsValidateFailedError from core.plugin.entities.plugin import DatasourceProviderID @@ -40,7 +40,10 @@ class DatasourceProviderService: ) .all() ) - return generate_provider_name(db_providers, credential_type, f"datasource provider {provider_id}") + return generate_incremental_name( + [provider.name for provider in db_providers], + f"{credential_type.get_name()}", + ) def add_datasource_oauth_provider( self, @@ -57,15 +60,33 @@ class DatasourceProviderService: with Session(db.engine) as session: lock = f"datasource_provider_create_lock:{tenant_id}_{provider_id}_{credential_type.value}" with redis_client.lock(lock, timeout=20): - db_provider_name = name or self.generate_next_datasource_provider_name( - session=session, - tenant_id=tenant_id, - provider_id=provider_id, - credential_type=credential_type, - ) - - if session.query(DatasourceProvider).filter_by(tenant_id=tenant_id, name=db_provider_name).count() > 0: - raise ValueError("name is already exists") + db_provider_name = name + if not db_provider_name: + db_provider_name = self.generate_next_datasource_provider_name( + session=session, + tenant_id=tenant_id, + provider_id=provider_id, + credential_type=credential_type, + ) + else: + if session.query(DatasourceProvider).filter_by( + tenant_id=tenant_id, + name=db_provider_name, + provider=provider_id.provider_name, + plugin_id=provider_id.plugin_id, + auth_type=credential_type.value, + ).count() > 0: + db_provider_name = generate_incremental_name( + [ + provider.name + for provider in session.query(DatasourceProvider).filter_by( + tenant_id=tenant_id, + provider=provider_id.provider_name, + plugin_id=provider_id.plugin_id, + ) + ], + db_provider_name, + ) provider_credential_secret_variables = self.extract_secret_variables( tenant_id=tenant_id, provider_id=f"{provider_id}" diff --git a/api/services/tools/builtin_tools_manage_service.py b/api/services/tools/builtin_tools_manage_service.py index f12371f77c..04c8af78c8 100644 --- a/api/services/tools/builtin_tools_manage_service.py +++ b/api/services/tools/builtin_tools_manage_service.py @@ -8,9 +8,9 @@ from sqlalchemy.orm import Session from configs import dify_config from constants import HIDDEN_VALUE, UNKNOWN_VALUE +from core.helper.name_generator import generate_incremental_name from core.helper.position_helper import is_filtered from core.helper.provider_cache import NoOpProviderCredentialCache, ToolProviderCredentialsCache -from core.helper.provider_name_generator import generate_provider_name from core.plugin.entities.plugin import ToolProviderID from core.tools.builtin_tool.provider import BuiltinToolProviderController from core.tools.builtin_tool.providers._positions import BuiltinToolProviderSort @@ -309,8 +309,10 @@ class BuiltinToolManageService: .order_by(BuiltinToolProvider.created_at.desc()) .all() ) - - return generate_provider_name(db_providers, credential_type, f"builtin tool provider {provider}") + return generate_incremental_name( + [provider.name for provider in db_providers], + f"{credential_type.get_name()}", + ) @staticmethod def get_builtin_tool_provider_credentials(