mirror of
https://github.com/langgenius/dify.git
synced 2026-09-24 23:22:26 +08:00
feat(web): refine onboarding UI (#37433)
Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: yyh <yuanyouhuilyz@gmail.com> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: hjlarry <hjlarry@163.com> Co-authored-by: fatelei <fatelei@gmail.com> Co-authored-by: Asuka Minato <i@asukaminato.eu.org> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com> Co-authored-by: gigglewang <gigglewang@dify.ai> Co-authored-by: Yunlu Wen <yunlu.wen@dify.ai> Co-authored-by: chariri <w@chariri.moe> Co-authored-by: Evan <2869018789@qq.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
This commit is contained in:
co-authored by
yyh
Joel
hjlarry
fatelei
Asuka Minato
autofix-ci[bot]
dependabot[bot]
Xiyuan Chen
gigglewang
Yunlu Wen
chariri
Evan
yyh
parent
4b15b0e6a6
commit
9b74df21d0
@@ -70,6 +70,7 @@ from services.errors.account import (
|
||||
)
|
||||
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkspacesLimitExceededError
|
||||
from services.feature_service import FeatureService
|
||||
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
|
||||
from tasks.delete_account_task import delete_account_task
|
||||
from tasks.mail_account_deletion_task import send_account_deletion_verification_code
|
||||
from tasks.mail_change_mail_task import (
|
||||
@@ -263,6 +264,7 @@ class AccountService:
|
||||
|
||||
account.set_tenant_id(available_ta.tenant_id)
|
||||
available_ta.current = True
|
||||
available_ta.last_opened_at = naive_utc_now()
|
||||
db.session.commit()
|
||||
|
||||
AccountService._refresh_account_last_active(account)
|
||||
@@ -1167,15 +1169,17 @@ class TenantService:
|
||||
db.session.add(tenant)
|
||||
db.session.commit()
|
||||
|
||||
plugin_upgrade_strategy = TenantPluginAutoUpgradeStrategy(
|
||||
tenant_id=tenant.id,
|
||||
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
upgrade_time_of_day=0,
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
exclude_plugins=[],
|
||||
include_plugins=[],
|
||||
)
|
||||
db.session.add(plugin_upgrade_strategy)
|
||||
for category in TenantPluginAutoUpgradeStrategy.PluginCategory:
|
||||
plugin_upgrade_strategy = TenantPluginAutoUpgradeStrategy(
|
||||
tenant_id=tenant.id,
|
||||
category=category,
|
||||
strategy_setting=PluginAutoUpgradeService.default_strategy_setting_for_category(category),
|
||||
upgrade_time_of_day=PluginAutoUpgradeService.default_upgrade_time_of_day(tenant.id),
|
||||
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
exclude_plugins=[],
|
||||
include_plugins=[],
|
||||
)
|
||||
db.session.add(plugin_upgrade_strategy)
|
||||
db.session.commit()
|
||||
|
||||
tenant.encrypt_public_key = generate_key_pair(tenant.id)
|
||||
@@ -1447,6 +1451,7 @@ class TenantService:
|
||||
.values(current=False)
|
||||
)
|
||||
tenant_account_join.current = True
|
||||
tenant_account_join.last_opened_at = naive_utc_now()
|
||||
# Set the current tenant for the account
|
||||
account.set_tenant_id(tenant_account_join.tenant_id)
|
||||
db.session.commit()
|
||||
|
||||
+172
-42
@@ -23,7 +23,7 @@ from graphon.model_runtime.entities.model_entities import ModelPropertyKey, Mode
|
||||
from graphon.model_runtime.model_providers.base.large_language_model import LargeLanguageModel
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from libs.login import current_user
|
||||
from models import Account
|
||||
from models import Account, AppStar
|
||||
from models.agent import Agent, AgentIconType, AgentScope, AgentSource, AgentStatus
|
||||
from models.model import App, AppMode, AppModelConfig, IconType, Site
|
||||
from models.tools import ApiToolProvider
|
||||
@@ -36,19 +36,29 @@ from tasks.remove_app_and_related_data_task import remove_app_and_related_data_t
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
AppListSortBy = Literal["last_modified", "recently_created", "earliest_created"]
|
||||
|
||||
class AppListParams(BaseModel):
|
||||
|
||||
class AppListBaseParams(BaseModel):
|
||||
page: int = Field(default=1, ge=1)
|
||||
limit: int = Field(default=20, ge=1, le=100)
|
||||
mode: Literal["completion", "chat", "advanced-chat", "workflow", "agent-chat", "agent", "channel", "all"] = "all"
|
||||
sort_by: AppListSortBy = "last_modified"
|
||||
name: str | None = None
|
||||
tag_ids: list[str] | None = None
|
||||
creator_ids: list[str] | None = None
|
||||
is_created_by_me: bool | None = None
|
||||
|
||||
|
||||
class AppListParams(AppListBaseParams):
|
||||
status: str | None = None
|
||||
openapi_visible: bool = False
|
||||
|
||||
|
||||
class StarredAppListParams(AppListBaseParams):
|
||||
pass
|
||||
|
||||
|
||||
class CreateAppParams(BaseModel):
|
||||
name: str = Field(min_length=1)
|
||||
description: str | None = None
|
||||
@@ -62,6 +72,83 @@ class CreateAppParams(BaseModel):
|
||||
|
||||
|
||||
class AppService:
|
||||
@staticmethod
|
||||
def _build_app_list_filters(
|
||||
user_id: str, tenant_id: str, params: AppListBaseParams
|
||||
) -> list[sa.ColumnElement[bool]]:
|
||||
filters = [App.tenant_id == tenant_id, App.is_universal == False]
|
||||
|
||||
if params.mode == "workflow":
|
||||
filters.append(App.mode == AppMode.WORKFLOW)
|
||||
elif params.mode == "completion":
|
||||
filters.append(App.mode == AppMode.COMPLETION)
|
||||
elif params.mode == "chat":
|
||||
filters.append(App.mode == AppMode.CHAT)
|
||||
elif params.mode == "advanced-chat":
|
||||
filters.append(App.mode == AppMode.ADVANCED_CHAT)
|
||||
elif params.mode == "agent-chat":
|
||||
filters.append(App.mode == AppMode.AGENT_CHAT)
|
||||
elif params.mode == "agent":
|
||||
filters.append(App.mode == AppMode.AGENT)
|
||||
|
||||
if isinstance(params, AppListParams):
|
||||
if params.status:
|
||||
filters.append(App.status == params.status)
|
||||
# OpenAPI surface visibility gate. Pushed into the query so
|
||||
# `pagination.total` reflects only apps the openapi caller can
|
||||
# actually reach; post-filtering by enable_api after the page
|
||||
# arrives would make `total` page-dependent.
|
||||
if params.openapi_visible:
|
||||
filters.append(App.enable_api.is_(True))
|
||||
|
||||
if params.is_created_by_me:
|
||||
filters.append(App.created_by == user_id)
|
||||
if params.creator_ids:
|
||||
filters.append(App.created_by.in_(params.creator_ids))
|
||||
if params.name:
|
||||
from libs.helper import escape_like_pattern
|
||||
|
||||
name = params.name[:30]
|
||||
escaped_name = escape_like_pattern(name)
|
||||
filters.append(App.name.ilike(f"%{escaped_name}%", escape="\\"))
|
||||
if params.tag_ids and len(params.tag_ids) > 0:
|
||||
target_ids = TagService.get_target_ids_by_tag_ids("app", tenant_id, params.tag_ids, match_all=True)
|
||||
if target_ids and len(target_ids) > 0:
|
||||
filters.append(App.id.in_(target_ids))
|
||||
else:
|
||||
return []
|
||||
|
||||
return filters
|
||||
|
||||
@staticmethod
|
||||
def _build_app_list_order_by(sort_by: AppListSortBy) -> sa.ColumnElement[Any]:
|
||||
return {
|
||||
"last_modified": App.updated_at.desc(),
|
||||
"recently_created": App.created_at.desc(),
|
||||
"earliest_created": App.created_at.asc(),
|
||||
}[sort_by]
|
||||
|
||||
@staticmethod
|
||||
def get_starred_app_ids(
|
||||
session: Session | scoped_session,
|
||||
*,
|
||||
tenant_id: str,
|
||||
account_id: str,
|
||||
app_ids: Sequence[str],
|
||||
) -> set[str]:
|
||||
"""Return app IDs starred by this account within the tenant."""
|
||||
if not app_ids:
|
||||
return set()
|
||||
|
||||
starred_app_ids = session.scalars(
|
||||
select(AppStar.app_id).where(
|
||||
AppStar.tenant_id == tenant_id,
|
||||
AppStar.account_id == account_id,
|
||||
AppStar.app_id.in_(list(app_ids)),
|
||||
)
|
||||
).all()
|
||||
return set(starred_app_ids)
|
||||
|
||||
@staticmethod
|
||||
def get_app_by_id(
|
||||
session: Session | scoped_session,
|
||||
@@ -109,61 +196,104 @@ class AppService:
|
||||
|
||||
def get_paginate_apps(self, user_id: str, tenant_id: str, params: AppListParams) -> Pagination | None:
|
||||
"""
|
||||
Get app list with pagination
|
||||
Get app list with pagination, filters, and explicit sort order.
|
||||
:param user_id: user id
|
||||
:param tenant_id: tenant id
|
||||
:param params: query parameters
|
||||
:return:
|
||||
"""
|
||||
filters = [App.tenant_id == tenant_id, App.is_universal == False]
|
||||
filters = self._build_app_list_filters(user_id, tenant_id, params)
|
||||
if not filters:
|
||||
return None
|
||||
|
||||
if params.mode == "workflow":
|
||||
filters.append(App.mode == AppMode.WORKFLOW)
|
||||
elif params.mode == "completion":
|
||||
filters.append(App.mode == AppMode.COMPLETION)
|
||||
elif params.mode == "chat":
|
||||
filters.append(App.mode == AppMode.CHAT)
|
||||
elif params.mode == "advanced-chat":
|
||||
filters.append(App.mode == AppMode.ADVANCED_CHAT)
|
||||
elif params.mode == "agent-chat":
|
||||
filters.append(App.mode == AppMode.AGENT_CHAT)
|
||||
elif params.mode == "agent":
|
||||
filters.append(App.mode == AppMode.AGENT)
|
||||
|
||||
if params.status:
|
||||
filters.append(App.status == params.status)
|
||||
# OpenAPI surface visibility gate. Pushed into the query so
|
||||
# `pagination.total` reflects only apps the openapi caller can
|
||||
# actually reach — post-filtering by enable_api after the page
|
||||
# arrives would make `total` page-dependent.
|
||||
if params.openapi_visible:
|
||||
filters.append(App.enable_api.is_(True))
|
||||
if params.is_created_by_me:
|
||||
filters.append(App.created_by == user_id)
|
||||
if params.creator_ids:
|
||||
filters.append(App.created_by.in_(params.creator_ids))
|
||||
if params.name:
|
||||
from libs.helper import escape_like_pattern
|
||||
|
||||
name = params.name[:30]
|
||||
escaped_name = escape_like_pattern(name)
|
||||
filters.append(App.name.ilike(f"%{escaped_name}%", escape="\\"))
|
||||
if params.tag_ids and len(params.tag_ids) > 0:
|
||||
target_ids = TagService.get_target_ids_by_tag_ids("app", tenant_id, params.tag_ids, match_all=True)
|
||||
if target_ids and len(target_ids) > 0:
|
||||
filters.append(App.id.in_(target_ids))
|
||||
else:
|
||||
return None
|
||||
order_by = self._build_app_list_order_by(params.sort_by)
|
||||
|
||||
app_models = db.paginate(
|
||||
sa.select(App).where(*filters).order_by(App.created_at.desc()),
|
||||
sa.select(App).where(*filters).order_by(order_by),
|
||||
page=params.page,
|
||||
per_page=params.limit,
|
||||
error_out=False,
|
||||
)
|
||||
|
||||
app_ids = [str(app.id) for app in app_models.items]
|
||||
starred_app_ids = self.get_starred_app_ids(
|
||||
db.session,
|
||||
tenant_id=tenant_id,
|
||||
account_id=user_id,
|
||||
app_ids=app_ids,
|
||||
)
|
||||
for app in app_models.items:
|
||||
app.is_starred = str(app.id) in starred_app_ids
|
||||
|
||||
return app_models
|
||||
|
||||
def get_paginate_starred_apps(
|
||||
self, user_id: str, tenant_id: str, params: StarredAppListParams
|
||||
) -> Pagination | None:
|
||||
"""
|
||||
Get apps starred by the current account with pagination, filters, and explicit sort order.
|
||||
"""
|
||||
filters = self._build_app_list_filters(user_id, tenant_id, params)
|
||||
if not filters:
|
||||
return None
|
||||
|
||||
order_by = self._build_app_list_order_by(params.sort_by)
|
||||
app_models = db.paginate(
|
||||
sa.select(App)
|
||||
.join(
|
||||
AppStar,
|
||||
sa.and_(
|
||||
AppStar.tenant_id == App.tenant_id,
|
||||
AppStar.app_id == App.id,
|
||||
AppStar.account_id == user_id,
|
||||
),
|
||||
)
|
||||
.where(AppStar.tenant_id == tenant_id, *filters)
|
||||
.order_by(order_by),
|
||||
page=params.page,
|
||||
per_page=params.limit,
|
||||
error_out=False,
|
||||
)
|
||||
|
||||
for app in app_models.items:
|
||||
app.is_starred = True
|
||||
|
||||
return app_models
|
||||
|
||||
@staticmethod
|
||||
def star_app(session: Session, *, app: App, account_id: str) -> None:
|
||||
"""Create the account's app star if it does not already exist."""
|
||||
existing_star = session.scalar(
|
||||
select(AppStar)
|
||||
.where(
|
||||
AppStar.tenant_id == app.tenant_id,
|
||||
AppStar.app_id == app.id,
|
||||
AppStar.account_id == account_id,
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
if existing_star:
|
||||
return
|
||||
|
||||
session.add(AppStar(tenant_id=app.tenant_id, app_id=app.id, account_id=account_id))
|
||||
|
||||
@staticmethod
|
||||
def unstar_app(session: Session, *, app: App, account_id: str) -> None:
|
||||
"""Remove the account's app star if present."""
|
||||
existing_star = session.scalar(
|
||||
select(AppStar)
|
||||
.where(
|
||||
AppStar.tenant_id == app.tenant_id,
|
||||
AppStar.app_id == app.id,
|
||||
AppStar.account_id == account_id,
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
if not existing_star:
|
||||
return
|
||||
|
||||
session.delete(existing_star)
|
||||
|
||||
def create_app(self, tenant_id: str, params: CreateAppParams, account: Account) -> App:
|
||||
"""
|
||||
Create app
|
||||
|
||||
@@ -192,6 +192,27 @@ class SimpleProviderEntityResponse(BaseModel):
|
||||
return self
|
||||
|
||||
|
||||
class ProviderEntityResponse(BaseModel):
|
||||
"""Runtime provider response with codegen-safe model pricing schemas."""
|
||||
|
||||
provider: str
|
||||
provider_name: str = ""
|
||||
label: I18nObject
|
||||
description: I18nObject | None = None
|
||||
icon_small: I18nObject | None = None
|
||||
icon_small_dark: I18nObject | None = None
|
||||
background: str | None = None
|
||||
help: ProviderHelpEntity | None = None
|
||||
supported_model_types: Sequence[ModelType]
|
||||
configurate_methods: list[ConfigurateMethod]
|
||||
models: list[AIModelEntityResponse] = []
|
||||
provider_credential_schema: ProviderCredentialSchema | None = None
|
||||
model_credential_schema: ModelCredentialSchema | None = None
|
||||
position: dict[str, list[str]] | None = {}
|
||||
|
||||
model_config = ConfigDict(from_attributes=True, protected_namespaces=())
|
||||
|
||||
|
||||
class DefaultModelResponse(BaseModel):
|
||||
"""
|
||||
Default model entity.
|
||||
|
||||
@@ -1,18 +1,295 @@
|
||||
"""Manage tenant plugin auto-upgrade strategies.
|
||||
|
||||
The storage is category-scoped: each tenant can have one strategy per plugin
|
||||
category. Public mutation helpers require an explicit category so callers do
|
||||
not accidentally overwrite every plugin type with one workspace-level policy.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from hashlib import sha256
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.db.session_factory import session_factory
|
||||
from core.plugin.impl.plugin import PluginInstaller
|
||||
from models.account import TenantPluginAutoUpgradeStrategy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PluginCategory = TenantPluginAutoUpgradeStrategy.PluginCategory
|
||||
PLUGIN_CATEGORIES = tuple(PluginCategory)
|
||||
SECONDS_PER_DAY = 24 * 60 * 60
|
||||
AUTO_UPGRADE_CHECK_SLOT_SECONDS = 15 * 60
|
||||
AUTO_UPGRADE_CHECK_SLOT_COUNT = SECONDS_PER_DAY // AUTO_UPGRADE_CHECK_SLOT_SECONDS
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PluginAutoUpgradeBackfillResult:
|
||||
created_count: int
|
||||
normalized: bool
|
||||
|
||||
|
||||
class PluginAutoUpgradeService:
|
||||
@staticmethod
|
||||
def get_strategy(tenant_id: str) -> TenantPluginAutoUpgradeStrategy | None:
|
||||
with session_factory.create_session() as session:
|
||||
return session.scalar(
|
||||
select(TenantPluginAutoUpgradeStrategy)
|
||||
.where(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id)
|
||||
.limit(1)
|
||||
def default_strategy_setting_for_category(
|
||||
category: PluginCategory,
|
||||
) -> TenantPluginAutoUpgradeStrategy.StrategySetting:
|
||||
if category == PluginCategory.MODEL:
|
||||
return TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST
|
||||
return TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY
|
||||
|
||||
@staticmethod
|
||||
def default_upgrade_time_of_day(tenant_id: str) -> int:
|
||||
"""Spread default checks across 15-minute aligned slots by tenant."""
|
||||
hash_input = tenant_id.encode()
|
||||
slot = int.from_bytes(sha256(hash_input).digest()[:8], "big") % AUTO_UPGRADE_CHECK_SLOT_COUNT
|
||||
return slot * AUTO_UPGRADE_CHECK_SLOT_SECONDS
|
||||
|
||||
@staticmethod
|
||||
def _coerce_category(category: object) -> PluginCategory | None:
|
||||
"""Accept daemon enum/string categories and ignore unknown values."""
|
||||
category_value = getattr(category, "value", category)
|
||||
if category_value is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return PluginCategory(str(category_value))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_installed_plugin_categories(tenant_id: str) -> dict[str, PluginCategory]:
|
||||
"""Build a plugin_id -> category map for splitting legacy include/exclude lists."""
|
||||
installed_plugins = PluginInstaller().list_plugins(tenant_id)
|
||||
plugin_categories: dict[str, PluginCategory] = {}
|
||||
|
||||
for plugin in installed_plugins:
|
||||
plugin_category = PluginAutoUpgradeService._coerce_category(plugin.declaration.category)
|
||||
if plugin_category is not None:
|
||||
plugin_categories[plugin.plugin_id] = plugin_category
|
||||
|
||||
return plugin_categories
|
||||
|
||||
@staticmethod
|
||||
def _filter_plugin_ids_for_category(
|
||||
plugin_ids: list[str],
|
||||
category: PluginCategory,
|
||||
plugin_categories: dict[str, PluginCategory],
|
||||
) -> list[str]:
|
||||
return [plugin_id for plugin_id in plugin_ids if plugin_categories.get(plugin_id) == category]
|
||||
|
||||
@staticmethod
|
||||
def _log_unknown_plugin_ids(
|
||||
tenant_id: str,
|
||||
field_name: str,
|
||||
plugin_ids: list[str],
|
||||
plugin_categories: dict[str, PluginCategory],
|
||||
) -> None:
|
||||
unknown_plugin_ids = [plugin_id for plugin_id in plugin_ids if plugin_id not in plugin_categories]
|
||||
if not unknown_plugin_ids:
|
||||
return
|
||||
|
||||
logger.warning(
|
||||
"Skipped unknown plugin IDs while backfilling plugin auto-upgrade strategies: "
|
||||
"tenant_id=%s, field=%s, plugin_ids=%s",
|
||||
tenant_id,
|
||||
field_name,
|
||||
unknown_plugin_ids,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _has_default_strategy(strategy: TenantPluginAutoUpgradeStrategy) -> bool:
|
||||
return (
|
||||
strategy.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY
|
||||
and strategy.upgrade_time_of_day == 0
|
||||
and strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
|
||||
and not strategy.exclude_plugins
|
||||
and not strategy.include_plugins
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _strategy_setting_for_category(
|
||||
source_strategy: TenantPluginAutoUpgradeStrategy,
|
||||
category: PluginCategory,
|
||||
source_has_default_strategy: bool,
|
||||
) -> TenantPluginAutoUpgradeStrategy.StrategySetting:
|
||||
# Only pure legacy defaults adopt the new model=latest default. User-edited
|
||||
# strategies keep their original setting across all categories.
|
||||
if source_has_default_strategy:
|
||||
return PluginAutoUpgradeService.default_strategy_setting_for_category(category)
|
||||
return source_strategy.strategy_setting
|
||||
|
||||
@staticmethod
|
||||
def _upgrade_time_of_day_for_category(
|
||||
tenant_id: str,
|
||||
source_strategy: TenantPluginAutoUpgradeStrategy,
|
||||
source_has_default_strategy: bool,
|
||||
) -> int:
|
||||
# Pure legacy defaults are spread by tenant so all default rows do not
|
||||
# concentrate in the same scheduler window. User-edited schedules keep their time.
|
||||
if source_has_default_strategy:
|
||||
return PluginAutoUpgradeService.default_upgrade_time_of_day(tenant_id)
|
||||
return source_strategy.upgrade_time_of_day
|
||||
|
||||
@staticmethod
|
||||
def backfill_strategy_categories(
|
||||
tenant_id: str,
|
||||
) -> PluginAutoUpgradeBackfillResult:
|
||||
"""Create missing category strategies and split include/exclude lists when needed.
|
||||
|
||||
The historical row is treated as the workspace-level source strategy.
|
||||
New category rows copy it first, then plugin lists are narrowed by real
|
||||
plugin category when the source strategy contains include/exclude IDs.
|
||||
"""
|
||||
with session_factory.create_session() as session, session.begin():
|
||||
strategies = list(
|
||||
session.scalars(
|
||||
select(TenantPluginAutoUpgradeStrategy).where(
|
||||
TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id
|
||||
)
|
||||
).all()
|
||||
)
|
||||
if not strategies:
|
||||
return PluginAutoUpgradeBackfillResult(created_count=0, normalized=False)
|
||||
|
||||
# Schema migration marks the historical workspace-level row as tool.
|
||||
source_strategy = next(
|
||||
(strategy for strategy in strategies if strategy.category == PluginCategory.TOOL),
|
||||
strategies[0],
|
||||
)
|
||||
source_has_default_strategy = PluginAutoUpgradeService._has_default_strategy(source_strategy)
|
||||
strategies_by_category = {strategy.category: strategy for strategy in strategies}
|
||||
exclude_plugins = source_strategy.exclude_plugins
|
||||
include_plugins = source_strategy.include_plugins
|
||||
should_split_plugin_lists = bool(exclude_plugins or include_plugins)
|
||||
# Query daemon only for tenants that actually customized plugin lists.
|
||||
plugin_categories = (
|
||||
PluginAutoUpgradeService._get_installed_plugin_categories(tenant_id)
|
||||
if should_split_plugin_lists
|
||||
else {}
|
||||
)
|
||||
if should_split_plugin_lists:
|
||||
PluginAutoUpgradeService._log_unknown_plugin_ids(
|
||||
tenant_id,
|
||||
"exclude_plugins",
|
||||
exclude_plugins,
|
||||
plugin_categories,
|
||||
)
|
||||
PluginAutoUpgradeService._log_unknown_plugin_ids(
|
||||
tenant_id,
|
||||
"include_plugins",
|
||||
include_plugins,
|
||||
plugin_categories,
|
||||
)
|
||||
|
||||
created_count = 0
|
||||
for category in PLUGIN_CATEGORIES:
|
||||
strategy = strategies_by_category.get(category)
|
||||
if strategy is None:
|
||||
# Start from the legacy workspace-level behavior before narrowing lists.
|
||||
strategy = TenantPluginAutoUpgradeStrategy(
|
||||
tenant_id=tenant_id,
|
||||
category=category,
|
||||
strategy_setting=PluginAutoUpgradeService._strategy_setting_for_category(
|
||||
source_strategy, category, source_has_default_strategy
|
||||
),
|
||||
upgrade_time_of_day=PluginAutoUpgradeService._upgrade_time_of_day_for_category(
|
||||
tenant_id, source_strategy, source_has_default_strategy
|
||||
),
|
||||
upgrade_mode=source_strategy.upgrade_mode,
|
||||
exclude_plugins=source_strategy.exclude_plugins.copy(),
|
||||
include_plugins=source_strategy.include_plugins.copy(),
|
||||
)
|
||||
session.add(strategy)
|
||||
created_count += 1
|
||||
elif source_has_default_strategy:
|
||||
strategy.strategy_setting = PluginAutoUpgradeService.default_strategy_setting_for_category(
|
||||
strategy.category
|
||||
)
|
||||
strategy.upgrade_time_of_day = PluginAutoUpgradeService.default_upgrade_time_of_day(tenant_id)
|
||||
|
||||
if not should_split_plugin_lists:
|
||||
continue
|
||||
|
||||
# Narrow include/exclude lists to the current category after all rows exist.
|
||||
strategy.exclude_plugins = PluginAutoUpgradeService._filter_plugin_ids_for_category(
|
||||
exclude_plugins,
|
||||
strategy.category,
|
||||
plugin_categories,
|
||||
)
|
||||
strategy.include_plugins = PluginAutoUpgradeService._filter_plugin_ids_for_category(
|
||||
include_plugins,
|
||||
strategy.category,
|
||||
plugin_categories,
|
||||
)
|
||||
|
||||
return PluginAutoUpgradeBackfillResult(created_count=created_count, normalized=should_split_plugin_lists)
|
||||
|
||||
@staticmethod
|
||||
def _get_strategy(
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
category: PluginCategory,
|
||||
) -> TenantPluginAutoUpgradeStrategy | None:
|
||||
return session.scalar(
|
||||
select(TenantPluginAutoUpgradeStrategy)
|
||||
.where(
|
||||
TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id,
|
||||
TenantPluginAutoUpgradeStrategy.category == category,
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_strategy(
|
||||
tenant_id: str,
|
||||
category: PluginCategory,
|
||||
) -> TenantPluginAutoUpgradeStrategy | None:
|
||||
with session_factory.create_session() as session:
|
||||
return PluginAutoUpgradeService._get_strategy(session, tenant_id, category)
|
||||
|
||||
@staticmethod
|
||||
def get_strategies(tenant_id: str) -> list[TenantPluginAutoUpgradeStrategy]:
|
||||
with session_factory.create_session() as session:
|
||||
return list(
|
||||
session.scalars(
|
||||
select(TenantPluginAutoUpgradeStrategy).where(
|
||||
TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id
|
||||
)
|
||||
).all()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _change_strategy(
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
category: PluginCategory,
|
||||
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting,
|
||||
upgrade_time_of_day: int,
|
||||
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode,
|
||||
exclude_plugins: list[str],
|
||||
include_plugins: list[str],
|
||||
) -> None:
|
||||
exist_strategy = PluginAutoUpgradeService._get_strategy(session, tenant_id, category)
|
||||
if not exist_strategy:
|
||||
strategy = TenantPluginAutoUpgradeStrategy(
|
||||
tenant_id=tenant_id,
|
||||
category=category,
|
||||
strategy_setting=strategy_setting,
|
||||
upgrade_time_of_day=upgrade_time_of_day,
|
||||
upgrade_mode=upgrade_mode,
|
||||
exclude_plugins=exclude_plugins,
|
||||
include_plugins=include_plugins,
|
||||
)
|
||||
session.add(strategy)
|
||||
else:
|
||||
exist_strategy.strategy_setting = strategy_setting
|
||||
exist_strategy.upgrade_time_of_day = upgrade_time_of_day
|
||||
exist_strategy.upgrade_mode = upgrade_mode
|
||||
exist_strategy.exclude_plugins = exclude_plugins
|
||||
exist_strategy.include_plugins = include_plugins
|
||||
|
||||
@staticmethod
|
||||
def change_strategy(
|
||||
@@ -22,64 +299,72 @@ class PluginAutoUpgradeService:
|
||||
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode,
|
||||
exclude_plugins: list[str],
|
||||
include_plugins: list[str],
|
||||
category: PluginCategory,
|
||||
) -> bool:
|
||||
with session_factory.create_session() as session, session.begin():
|
||||
exist_strategy = session.scalar(
|
||||
select(TenantPluginAutoUpgradeStrategy)
|
||||
.where(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id)
|
||||
.limit(1)
|
||||
PluginAutoUpgradeService._change_strategy(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
category=category,
|
||||
strategy_setting=strategy_setting,
|
||||
upgrade_time_of_day=upgrade_time_of_day,
|
||||
upgrade_mode=upgrade_mode,
|
||||
exclude_plugins=exclude_plugins,
|
||||
include_plugins=include_plugins,
|
||||
)
|
||||
if not exist_strategy:
|
||||
strategy = TenantPluginAutoUpgradeStrategy(
|
||||
tenant_id=tenant_id,
|
||||
strategy_setting=strategy_setting,
|
||||
upgrade_time_of_day=upgrade_time_of_day,
|
||||
upgrade_mode=upgrade_mode,
|
||||
exclude_plugins=exclude_plugins,
|
||||
include_plugins=include_plugins,
|
||||
)
|
||||
session.add(strategy)
|
||||
else:
|
||||
exist_strategy.strategy_setting = strategy_setting
|
||||
exist_strategy.upgrade_time_of_day = upgrade_time_of_day
|
||||
exist_strategy.upgrade_mode = upgrade_mode
|
||||
exist_strategy.exclude_plugins = exclude_plugins
|
||||
exist_strategy.include_plugins = include_plugins
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def exclude_plugin(tenant_id: str, plugin_id: str) -> bool:
|
||||
with session_factory.create_session() as session, session.begin():
|
||||
exist_strategy = session.scalar(
|
||||
select(TenantPluginAutoUpgradeStrategy)
|
||||
.where(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id)
|
||||
.limit(1)
|
||||
def _exclude_plugin(
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
category: PluginCategory,
|
||||
plugin_id: str,
|
||||
) -> None:
|
||||
"""Remove one plugin from automatic updates for a single category strategy."""
|
||||
exist_strategy = PluginAutoUpgradeService._get_strategy(session, tenant_id, category)
|
||||
if not exist_strategy:
|
||||
PluginAutoUpgradeService._change_strategy(
|
||||
session,
|
||||
tenant_id,
|
||||
category,
|
||||
TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
0,
|
||||
TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
[plugin_id],
|
||||
[],
|
||||
)
|
||||
if not exist_strategy:
|
||||
# create for this tenant
|
||||
PluginAutoUpgradeService.change_strategy(
|
||||
tenant_id,
|
||||
TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||
0,
|
||||
TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||
[plugin_id],
|
||||
[],
|
||||
)
|
||||
return True
|
||||
else:
|
||||
if exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE:
|
||||
if plugin_id not in exist_strategy.exclude_plugins:
|
||||
new_exclude_plugins = exist_strategy.exclude_plugins.copy()
|
||||
new_exclude_plugins.append(plugin_id)
|
||||
exist_strategy.exclude_plugins = new_exclude_plugins
|
||||
elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL:
|
||||
if plugin_id in exist_strategy.include_plugins:
|
||||
new_include_plugins = exist_strategy.include_plugins.copy()
|
||||
new_include_plugins.remove(plugin_id)
|
||||
exist_strategy.include_plugins = new_include_plugins
|
||||
elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL:
|
||||
exist_strategy.upgrade_mode = TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
|
||||
exist_strategy.exclude_plugins = [plugin_id]
|
||||
else:
|
||||
if exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE:
|
||||
# In exclude mode, disabling one plugin means adding it to exclude_plugins.
|
||||
if plugin_id not in exist_strategy.exclude_plugins:
|
||||
new_exclude_plugins = exist_strategy.exclude_plugins.copy()
|
||||
new_exclude_plugins.append(plugin_id)
|
||||
exist_strategy.exclude_plugins = new_exclude_plugins
|
||||
elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL:
|
||||
# In partial mode, disabling one plugin means removing it from include_plugins.
|
||||
if plugin_id in exist_strategy.include_plugins:
|
||||
new_include_plugins = exist_strategy.include_plugins.copy()
|
||||
new_include_plugins.remove(plugin_id)
|
||||
exist_strategy.include_plugins = new_include_plugins
|
||||
elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL:
|
||||
# In all mode, switch to exclude mode so only this plugin is skipped.
|
||||
exist_strategy.upgrade_mode = TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
|
||||
exist_strategy.exclude_plugins = [plugin_id]
|
||||
|
||||
return True
|
||||
@staticmethod
|
||||
def exclude_plugin(
|
||||
tenant_id: str,
|
||||
plugin_id: str,
|
||||
category: PluginCategory,
|
||||
) -> bool:
|
||||
with session_factory.create_session() as session, session.begin():
|
||||
PluginAutoUpgradeService._exclude_plugin(
|
||||
session,
|
||||
tenant_id,
|
||||
category,
|
||||
plugin_id,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Any, TypedDict, override
|
||||
from typing import Any, NotRequired, TypedDict, override
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
@@ -22,6 +22,7 @@ class RecommendedAppItemDict(TypedDict):
|
||||
categories: list[str]
|
||||
position: int
|
||||
is_listed: bool
|
||||
can_trial: NotRequired[bool]
|
||||
|
||||
|
||||
class RecommendedAppsResultDict(TypedDict):
|
||||
@@ -64,14 +65,47 @@ class DatabaseRecommendAppRetrieval(RecommendAppRetrievalBase):
|
||||
:param language: language
|
||||
:return:
|
||||
"""
|
||||
recommended_apps = db.session.scalars(
|
||||
select(RecommendedApp).where(RecommendedApp.is_listed == True, RecommendedApp.language == language)
|
||||
).all()
|
||||
recommended_apps = cls._fetch_listed_recommended_apps(language)
|
||||
|
||||
if len(recommended_apps) == 0:
|
||||
recommended_apps = db.session.scalars(
|
||||
select(RecommendedApp).where(RecommendedApp.is_listed == True, RecommendedApp.language == languages[0])
|
||||
).all()
|
||||
recommended_apps = cls._fetch_listed_recommended_apps(languages[0])
|
||||
|
||||
return cls._format_recommended_apps(recommended_apps, language)
|
||||
|
||||
@classmethod
|
||||
def fetch_learn_dify_apps_from_db(cls, language: str) -> RecommendedAppsResultDict:
|
||||
"""
|
||||
Fetch listed recommended apps explicitly marked for the Learn Dify section.
|
||||
:param language: language
|
||||
:return:
|
||||
"""
|
||||
recommended_apps = cls._fetch_listed_recommended_apps(language, is_learn_dify=True)
|
||||
|
||||
if len(recommended_apps) == 0 and language != languages[0]:
|
||||
recommended_apps = cls._fetch_listed_recommended_apps(languages[0], is_learn_dify=True)
|
||||
|
||||
return cls._format_recommended_apps(recommended_apps, language)
|
||||
|
||||
@classmethod
|
||||
def _fetch_listed_recommended_apps(
|
||||
cls, language: str, *, is_learn_dify: bool | None = None
|
||||
) -> list[RecommendedApp]:
|
||||
filters = [RecommendedApp.is_listed.is_(True), RecommendedApp.language == language]
|
||||
if is_learn_dify is not None:
|
||||
filters.append(RecommendedApp.is_learn_dify.is_(is_learn_dify))
|
||||
|
||||
return list(db.session.scalars(select(RecommendedApp).where(*filters)).all())
|
||||
|
||||
@classmethod
|
||||
def _format_recommended_apps(
|
||||
cls, recommended_apps: list[RecommendedApp], language: str
|
||||
) -> RecommendedAppsResultDict:
|
||||
"""
|
||||
Serialize DB recommended app rows into the Explore list response shape.
|
||||
:param recommended_apps: recommended app rows
|
||||
:param language: language used for category ordering
|
||||
:return:
|
||||
"""
|
||||
|
||||
categories = set()
|
||||
recommended_apps_result: list[RecommendedAppItemDict] = []
|
||||
|
||||
@@ -6,6 +6,7 @@ from sqlalchemy.orm import scoped_session
|
||||
from configs import dify_config
|
||||
from models.model import AccountTrialAppRecord, TrialApp
|
||||
from services.feature_service import FeatureService
|
||||
from services.recommend_app.database.database_retrieval import DatabaseRecommendAppRetrieval
|
||||
from services.recommend_app.recommend_app_factory import RecommendAppRetrievalFactory
|
||||
|
||||
|
||||
@@ -31,13 +32,24 @@ class RecommendedAppService:
|
||||
apps = result["recommended_apps"]
|
||||
for app in apps:
|
||||
app_id = app["app_id"]
|
||||
trial_app_model = session.scalar(select(TrialApp).where(TrialApp.app_id == app_id).limit(1))
|
||||
if trial_app_model:
|
||||
app["can_trial"] = True
|
||||
else:
|
||||
app["can_trial"] = False
|
||||
app["can_trial"] = cls._can_trial_app(session, app_id)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def get_learn_dify_apps(cls, session: scoped_session, language: str) -> dict[str, Any]:
|
||||
"""
|
||||
Get database-backed recommended apps marked as Learn Dify.
|
||||
:param language: language
|
||||
:return:
|
||||
"""
|
||||
result = DatabaseRecommendAppRetrieval.fetch_learn_dify_apps_from_db(language)
|
||||
|
||||
if FeatureService.get_system_features().enable_trial_app:
|
||||
for app in result["recommended_apps"]:
|
||||
app["can_trial"] = cls._can_trial_app(session, app["app_id"])
|
||||
|
||||
return {"recommended_apps": result["recommended_apps"]}
|
||||
|
||||
@classmethod
|
||||
def get_recommend_app_detail(cls, session: scoped_session, app_id: str) -> dict[str, Any] | None:
|
||||
"""
|
||||
@@ -52,11 +64,7 @@ class RecommendedAppService:
|
||||
return None
|
||||
if FeatureService.get_system_features().enable_trial_app:
|
||||
app_id = result["id"]
|
||||
trial_app_model = session.scalar(select(TrialApp).where(TrialApp.app_id == app_id).limit(1))
|
||||
if trial_app_model:
|
||||
result["can_trial"] = True
|
||||
else:
|
||||
result["can_trial"] = False
|
||||
result["can_trial"] = cls._can_trial_app(session, app_id)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
@@ -77,3 +85,8 @@ class RecommendedAppService:
|
||||
else:
|
||||
session.add(AccountTrialAppRecord(app_id=app_id, count=1, account_id=account_id))
|
||||
session.commit()
|
||||
|
||||
@staticmethod
|
||||
def _can_trial_app(session: scoped_session, app_id: str) -> bool:
|
||||
trial_app_model = session.scalar(select(TrialApp).where(TrialApp.app_id == app_id).limit(1))
|
||||
return trial_app_model is not None
|
||||
|
||||
Reference in New Issue
Block a user