Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions api/.importlinter
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,24 @@ forbidden_modules =
sqlalchemy
werkzeug

[importlinter:contract:web-app-runtime-query-service-boundary]
name = Web app runtime query application service does not directly depend on configuration, feature implementation, transport, or ORM modules
type = forbidden
source_modules =
services.web_app_runtime_query_service
forbidden_modules =
configs
controllers
extensions
flask
models
repositories
services.feature_service
services.feature_service_gateway
sqlalchemy
werkzeug
allow_indirect_imports = True

[importlinter:contract:feature-query-service-boundary]
name = Feature query application service is framework and persistence neutral
type = forbidden
Expand Down
65 changes: 23 additions & 42 deletions api/controllers/web/site.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
from typing import Any, Self

from pydantic import AliasChoices, Field
from sqlalchemy import select
from werkzeug.exceptions import Forbidden

from configs import dify_config
from controllers.common.schema import register_response_schema_models
from controllers.web import web_ns
from controllers.web.wraps import WebApiResource
from enums import DeploymentEdition
from extensions.ext_database import db
from extensions.storage.storage_type import StorageType
from extensions.ext_application_services import application_services
from fields.base import ResponseModel
from libs.helper import build_icon_url
from models.account import Tenant, TenantStatus
from models.model import App, AppMode, EndUser, IconType, Site
from libs.helper import build_icon_url, dump_response
from models.account import Tenant
from models.model import App, AppMode, EndUser, Site
from services.entities.feature_entities import FeatureModel
from services.feature_service import FeatureService
from services.file_service import FileService
from services.web_app_runtime_query_service import WebAppRuntimeUnavailableError


class WebSiteResponse(ResponseModel):
Expand Down Expand Up @@ -128,17 +124,6 @@ def from_app_site(
)


def _build_site_icon_url(*, site: Site, tenant_id: str) -> str | None:
"""Use direct S3 URLs only in Cloud Mode and preserve preview URLs elsewhere."""
if site.icon_type != IconType.IMAGE or not site.icon:
return None
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and (
StorageType(dify_config.STORAGE_TYPE) == StorageType.S3
):
return FileService(db.engine).get_file_presigned_url(file_id=site.icon, tenant_id=tenant_id)
return build_icon_url(site.icon_type, site.icon)


@web_ns.route("/site")
class AppSiteApi(WebApiResource):
@web_ns.doc("Get App Site Info")
Expand All @@ -156,25 +141,21 @@ class AppSiteApi(WebApiResource):
@web_ns.response(200, "Success", web_ns.models[WebAppSiteResponse.__name__])
def get(self, app_model: App, end_user: EndUser):
"""Retrieve app site info."""
# get site
site = db.session.scalar(select(Site).where(Site.app_id == app_model.id).limit(1))

if site is None:
raise Forbidden()

tenant = app_model.tenant
if tenant is None or tenant.status == TenantStatus.ARCHIVE:
raise Forbidden()

features = FeatureService.get_features(app_model.tenant_id, exclude_vector_space=True)

return WebAppSiteResponse.from_app_site(
tenant=tenant,
app_model=app_model,
mode=AppMode.value_of(app_model.mode_compatible_with_agent_with_session(session=db.session())),
site=site,
end_user_id=end_user.id,
features=features,
can_replace_logo=features.can_replace_logo,
icon_url=_build_site_icon_url(site=site, tenant_id=tenant.id),
).model_dump(mode="json")
try:
bootstrap = application_services().web_app_runtime.get_bootstrap(app_model.id)
except WebAppRuntimeUnavailableError:
raise Forbidden() from None

return dump_response(
WebAppSiteResponse,
{
"app_id": bootstrap.app_id,
"mode": bootstrap.mode,
"end_user_id": end_user.id,
"enable_site": bootstrap.enable_site,
"site": bootstrap.site,
"plan": bootstrap.plan,
"can_replace_logo": bootstrap.can_replace_logo,
"custom_config": bootstrap.custom_config,
},
)
15 changes: 13 additions & 2 deletions api/extensions/ext_application_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@
from services.feature_query_service import FeatureQueryService
from services.feature_service import FeatureService
from services.feature_service_gateway import FeatureServiceGateway
from services.file_service import FileService
from services.init_validation_service import InitValidationService
from services.schema_definition_service import SchemaDefinitionService
from services.setup_adapters import RedisSetupLock, RegisterServiceAccountProvisioner
from services.setup_service import SetupService
from services.web_app_runtime_query_service import WebAppRuntimeQueryService
from services.webapp_access_query_service import (
WebAppAccessQueryService,
WebAppAccessUnavailableError,
Expand Down Expand Up @@ -82,6 +84,7 @@ class ApplicationServices:
app_definitions: AppDefinitionQueryService
data_source_api_key_auth: DataSourceApiKeyAuthService
webapp_access: WebAppAccessQueryService
web_app_runtime: WebAppRuntimeQueryService
explore_banner_queries: ExploreBannerQueryService
schema_definitions: SchemaDefinitionService
setup: SetupService
Expand All @@ -100,6 +103,8 @@ def build_application_services(
) -> ApplicationServices:
installation_state = InstallationStateRepository(client=database_client)
data_source_api_key_auth_bindings = SQLAlchemyDataSourceApiKeyAuthBindingRepository(session_factory=database_client)
app_definition_repository = AppDefinitionQueryRepository(session_factory=database_client)
feature_gateway = FeatureServiceGateway()
return ApplicationServices(
account_activation=AccountActivationService(
tokens=RegisterServiceInvitationTokenStore(),
Expand All @@ -113,7 +118,7 @@ def build_application_services(
),
),
app_definitions=AppDefinitionQueryService(
definitions=AppDefinitionQueryRepository(session_factory=database_client),
definitions=app_definition_repository,
builtin_icon_url_prefix=(
dify_config.CONSOLE_API_URL + "/console/api/workspaces/current/tool-provider/builtin/"
),
Expand All @@ -129,6 +134,12 @@ def build_application_services(
access_mode_for_app=_get_enterprise_webapp_access_mode,
is_user_allowed_for_app=_is_user_allowed_to_access_webapp,
),
web_app_runtime=WebAppRuntimeQueryService(
runtime=app_definition_repository,
file_service=FileService(database_client),
workspace_features=feature_gateway.get_workspace_features,
files_url=dify_config.FILES_URL,
),
explore_banner_queries=ExploreBannerQueryService(
banners=ExploreBannerQueryRepository(client=database_client),
enabled=FeatureService.is_explore_banner_enabled(),
Expand All @@ -141,7 +152,7 @@ def build_application_services(
setup_required=deployment_edition != DeploymentEdition.CLOUD,
),
feature_queries=FeatureQueryService(
features=FeatureServiceGateway(),
features=feature_gateway,
trial_models=FeatureService.get_trial_models(),
app_dsl_version=CURRENT_APP_DSL_VERSION,
),
Expand Down
72 changes: 57 additions & 15 deletions api/repositories/app_definition_query_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from core.app.apps.agent_app.app_feature_projection import merge_agent_app_features
from core.app.apps.agent_app.app_variable_projection import agent_app_variables_to_user_input_form
from core.app.apps.agent_app.errors import AgentAppGeneratorError, AgentAppNotPublishedError
from models.account import Tenant, TenantStatus
from models.agent import AgentConfigSnapshot
from models.agent_config_entities import AgentSoulConfig
from models.model import App, AppMode, AppModelConfig, Site, load_annotation_reply_config
Expand All @@ -21,6 +22,27 @@
AppSiteConfiguration,
AppToolIconSource,
)
from services.web_app_runtime_query_service import WebAppRuntimeRecord


def _map_site_configuration(site: Site) -> AppSiteConfiguration:
return AppSiteConfiguration(
title=site.title,
chat_color_theme=site.chat_color_theme,
chat_color_theme_inverted=site.chat_color_theme_inverted,
icon_type=site.icon_type.value if site.icon_type is not None else None,
icon=site.icon,
icon_background=site.icon_background,
description=site.description,
copyright=site.copyright,
privacy_policy=site.privacy_policy,
input_placeholder=site.input_placeholder,
custom_disclaimer=site.custom_disclaimer,
default_language=site.default_language,
prompt_public=site.prompt_public,
show_workflow_steps=site.show_workflow_steps,
use_icon_as_answer_icon=site.use_icon_as_answer_icon,
)


def _get_public_agent_parameter_config(app: App, *, session: Session) -> AppParameterConfig:
Expand Down Expand Up @@ -153,21 +175,41 @@ def get_site_configuration(self, app_id: str) -> AppSiteConfiguration | None:
if site is None:
return None

return AppSiteConfiguration(
title=site.title,
chat_color_theme=site.chat_color_theme,
chat_color_theme_inverted=site.chat_color_theme_inverted,
icon_type=site.icon_type.value if site.icon_type is not None else None,
icon=site.icon,
icon_background=site.icon_background,
description=site.description,
copyright=site.copyright,
privacy_policy=site.privacy_policy,
input_placeholder=site.input_placeholder,
custom_disclaimer=site.custom_disclaimer,
default_language=site.default_language,
show_workflow_steps=site.show_workflow_steps,
use_icon_as_answer_icon=site.use_icon_as_answer_icon,
return _map_site_configuration(site)

def get_runtime_record(self, app_id: str) -> WebAppRuntimeRecord | None:
with self._session_factory() as session:
app = session.get(App, app_id)
if app is None:
return None

site = session.scalar(select(Site).where(Site.app_id == app_id).limit(1))
if site is None:
return None

tenant = session.get(Tenant, app.tenant_id)
if tenant is None:
return None

app_id = app.id
tenant_id = app.tenant_id
enable_site = app.enable_site
site_configuration = _map_site_configuration(site)
plan = tenant.plan
tenant_status = tenant.status.value
tenant_custom_config_json = tenant.custom_config
mode = AppMode.value_of(app.mode).value
if tenant.status != TenantStatus.ARCHIVE:
mode = AppMode.value_of(app.mode_compatible_with_agent_with_session(session=session)).value
return WebAppRuntimeRecord(
app_id=app_id,
tenant_id=tenant_id,
mode=mode,
enable_site=enable_site,
site=site_configuration,
plan=plan,
tenant_status=tenant_status,
tenant_custom_config_json=tenant_custom_config_json,
)

@staticmethod
Expand Down
1 change: 1 addition & 0 deletions api/services/app_definition_query_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class AppSiteConfiguration(NamedTuple):
input_placeholder: str | None
custom_disclaimer: str | None
default_language: str
prompt_public: bool
show_workflow_steps: bool
use_icon_as_answer_icon: bool

Expand Down
8 changes: 8 additions & 0 deletions api/services/file_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
VIDEO_EXTENSIONS,
)
from core.rag.extractor.extract_processor import ExtractProcessor
from enums import DeploymentEdition
from extensions.ext_storage import storage
from extensions.storage.storage_type import StorageType
from graphon.file import helpers as file_helpers
Expand Down Expand Up @@ -179,6 +180,13 @@ def get_file_presigned_url(self, *, file_id: str, tenant_id: str) -> str:
content_type=content_type,
)

def get_icon_url(self, file_id: str, tenant_id: str) -> str:
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and (
StorageType(dify_config.STORAGE_TYPE) == StorageType.S3
):
return self.get_file_presigned_url(file_id=file_id, tenant_id=tenant_id)
return file_helpers.get_signed_file_url(upload_file_id=file_id)

def upload_text(self, text: str, text_name: str, user_id: str, tenant_id: str) -> UploadFile:
if len(text_name) > 200:
text_name = text_name[:200]
Expand Down
Loading