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:
Jingyi 2026-06-15 01:47:15 -07:00 committed by GitHub
parent 4b15b0e6a6
commit 9b74df21d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
995 changed files with 35090 additions and 10333 deletions

View File

@ -11,6 +11,7 @@ from .data_migration import (
migration_data_wizard,
)
from .plugin import (
backfill_plugin_auto_upgrade,
extract_plugins,
extract_unique_plugins,
install_plugins,
@ -49,6 +50,7 @@ from .vector import (
__all__ = [
"add_qdrant_index",
"archive_workflow_runs",
"backfill_plugin_auto_upgrade",
"clean_expired_messages",
"clean_workflow_runs",
"cleanup_orphaned_draft_variables",

View File

@ -1,10 +1,11 @@
import json
import logging
import time
from typing import Any, cast
import click
from pydantic import TypeAdapter
from sqlalchemy import delete, select
from sqlalchemy import delete, func, select
from sqlalchemy.engine import CursorResult
from configs import dify_config
@ -15,11 +16,13 @@ from core.plugin.plugin_service import PluginService
from core.tools.utils.system_encryption import encrypt_system_params
from extensions.ext_database import db
from models import Tenant
from models.account import TenantPluginAutoUpgradeStrategy
from models.oauth import DatasourceOauthParamConfig, DatasourceProvider
from models.provider_ids import DatasourceProviderID, ToolProviderID
from models.source import DataSourceApiKeyAuthBinding, DataSourceOauthBinding
from models.tools import ToolOAuthSystemClient
from services.plugin.data_migration import PluginDataMigration
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
from services.plugin.plugin_migration import PluginMigration
logger = logging.getLogger(__name__)
@ -402,6 +405,110 @@ def migrate_data_for_plugin():
click.echo(click.style("Migrate data for plugin completed.", fg="green"))
def _candidate_auto_upgrade_strategy_tenant_ids_stmt(limit: int | None = None):
category_count = len(TenantPluginAutoUpgradeStrategy.PluginCategory)
stmt = (
select(TenantPluginAutoUpgradeStrategy.tenant_id)
.group_by(TenantPluginAutoUpgradeStrategy.tenant_id)
.having(func.count(func.distinct(TenantPluginAutoUpgradeStrategy.category)) < category_count)
.order_by(TenantPluginAutoUpgradeStrategy.tenant_id)
)
if limit is not None:
stmt = stmt.limit(limit)
return stmt
def _count_auto_upgrade_strategy_tenant_ids(limit: int | None) -> int:
candidate_stmt = _candidate_auto_upgrade_strategy_tenant_ids_stmt(limit).subquery()
return db.session.scalar(select(func.count()).select_from(candidate_stmt)) or 0
def _iter_auto_upgrade_strategy_tenant_ids(limit: int | None):
stmt = _candidate_auto_upgrade_strategy_tenant_ids_stmt(limit).execution_options(yield_per=1000)
yield from db.session.scalars(stmt)
@click.command(
"backfill-plugin-auto-upgrade",
help="Backfill category-scoped plugin auto-upgrade strategies and normalize plugin lists.",
)
@click.option("--tenant-id", multiple=True, help="Tenant ID to backfill. Can be passed multiple times.")
@click.option("--limit", type=int, default=None, help="Maximum number of candidate tenants to process.")
@click.option("--batch-size", type=int, default=500, show_default=True, help="Progress reporting batch size.")
@click.option("--dry-run", is_flag=True, help="Only print candidate tenant count.")
def backfill_plugin_auto_upgrade(
tenant_id: tuple[str, ...],
limit: int | None,
batch_size: int,
dry_run: bool,
):
"""
Backfill historical auto-upgrade strategies after the category column exists.
Missing category rows are created from the tenant's tool/default row. Pure default
strategies become latest for model plugins and fix-only for all other categories.
Tenants with include/exclude plugin IDs are split
by installed plugin category using plugin daemon metadata.
"""
start_at = time.perf_counter()
candidate_count = len(tenant_id) if tenant_id else _count_auto_upgrade_strategy_tenant_ids(limit)
click.echo(click.style(f"Found {candidate_count} candidate tenants.", fg="yellow"))
if dry_run:
elapsed = time.perf_counter() - start_at
click.echo(click.style(f"Dry run completed. elapsed={elapsed:.2f}s", fg="green"))
return
tenant_ids = list(tenant_id) if tenant_id else _iter_auto_upgrade_strategy_tenant_ids(limit)
backfilled_count = 0
created_count = 0
normalized_count = 0
skipped_count = 0
failed_count = 0
for index, current_tenant_id in enumerate(tenant_ids, start=1):
try:
result = PluginAutoUpgradeService.backfill_strategy_categories(
current_tenant_id,
)
except Exception as e:
failed_count += 1
click.echo(click.style(f"Failed tenant {current_tenant_id}: {str(e)}", fg="red"))
continue
if result.created_count > 0:
backfilled_count += 1
created_count += result.created_count
elif not result.normalized:
skipped_count += 1
if result.normalized:
normalized_count += 1
if batch_size > 0 and index % batch_size == 0:
click.echo(
click.style(
f"Processed {index}/{candidate_count} tenants. "
f"backfilled={backfilled_count}, created_rows={created_count}, "
f"normalized={normalized_count}, skipped={skipped_count}, failed={failed_count}, "
f"elapsed={time.perf_counter() - start_at:.2f}s",
fg="yellow",
)
)
elapsed = time.perf_counter() - start_at
click.echo(
click.style(
f"Backfill plugin auto-upgrade strategy categories completed. "
f"backfilled={backfilled_count}, created_rows={created_count}, "
f"normalized={normalized_count}, skipped={skipped_count}, failed={failed_count}, "
f"elapsed={elapsed:.2f}s",
fg="green",
)
)
@click.command("extract-plugins", help="Extract plugins.")
@click.option("--output_file", prompt=True, help="The file to store the extracted plugins.", default="plugins.jsonl")
@click.option("--workers", prompt=True, help="The number of workers to extract plugins.", default=10)

View File

@ -1,6 +1,7 @@
import logging
import re
import uuid
from collections.abc import Sequence
from datetime import datetime
from typing import Any, Literal, cast
@ -41,12 +42,12 @@ from core.trigger.constants import TRIGGER_NODE_TYPES
from extensions.ext_database import db
from fields.base import ResponseModel
from graphon.enums import WorkflowExecutionStatus
from libs.helper import build_icon_url, to_timestamp
from libs.helper import build_icon_url, dump_response, to_timestamp
from libs.login import login_required
from models import Account, App, DatasetPermissionEnum, Workflow
from models.model import IconType
from services.app_dsl_service import AppDslService
from services.app_service import AppListParams, AppService, CreateAppParams
from services.app_service import AppListParams, AppListSortBy, AppService, CreateAppParams, StarredAppListParams
from services.enterprise.enterprise_service import EnterpriseService
from services.entities.dsl_entities import ImportMode, ImportStatus
from services.entities.knowledge_entities.knowledge_entities import (
@ -73,10 +74,14 @@ _CREATOR_IDS_BRACKET_PATTERN = re.compile(r"^creator_ids\[(\d+)\]$")
AppListMode = Literal["completion", "chat", "advanced-chat", "workflow", "agent-chat", "agent", "channel", "all"]
class AppListQuery(BaseModel):
class AppListBaseQuery(BaseModel):
page: int = Field(default=1, ge=1, le=99999, description="Page number (1-99999)")
limit: int = Field(default=20, ge=1, le=100, description="Page size (1-100)")
mode: AppListMode = Field(default=cast(AppListMode, "all"), description="App mode filter")
sort_by: AppListSortBy = Field(
default="last_modified",
description="Sort apps by last modified, recently created, or earliest created",
)
name: str | None = Field(default=None, description="Filter by app name")
tag_ids: list[str] | None = Field(default=None, description="Filter by tag IDs")
creator_ids: list[str] | None = Field(default=None, description="Filter by creator account IDs")
@ -119,6 +124,14 @@ class AppListQuery(BaseModel):
raise ValueError("Invalid UUID format in creator_ids.") from exc
class AppListQuery(AppListBaseQuery):
pass
class StarredAppListQuery(AppListBaseQuery):
pass
def _normalize_app_list_query_args(query_args: MultiDict[str, str]) -> dict[str, str | list[str]]:
normalized: dict[str, str | list[str]] = {}
indexed_tag_ids: list[tuple[int, str]] = []
@ -387,6 +400,7 @@ class AppPartial(ResponseModel):
create_user_name: str | None = None
author_name: str | None = None
has_draft_trigger: bool | None = None
is_starred: bool = False
@computed_field(return_type=str | None) # type: ignore
@property
@ -456,12 +470,54 @@ class AppExportResponse(ResponseModel):
data: str
def _enrich_app_list_items(session: Session, *, apps: Sequence[App], tenant_id: str) -> None:
if FeatureService.get_system_features().webapp_auth.enabled:
app_ids = [str(app.id) for app in apps]
res = EnterpriseService.WebAppAuth.batch_get_app_access_mode_by_id(app_ids=app_ids)
if len(res) != len(app_ids):
raise BadRequest("Invalid app id in webapp auth")
for app in apps:
if str(app.id) in res:
app.access_mode = res[str(app.id)].access_mode
workflow_capable_app_ids = [str(app.id) for app in apps if app.mode in {"workflow", "advanced-chat"}]
draft_trigger_app_ids: set[str] = set()
if workflow_capable_app_ids:
draft_workflows = (
session.execute(
select(Workflow).where(
Workflow.version == Workflow.VERSION_DRAFT,
Workflow.app_id.in_(workflow_capable_app_ids),
Workflow.tenant_id == tenant_id,
)
)
.scalars()
.all()
)
trigger_node_types = TRIGGER_NODE_TYPES
for workflow in draft_workflows:
node_id = None
try:
for node_id, node_data in workflow.walk_nodes():
if node_data.get("type") in trigger_node_types:
draft_trigger_app_ids.add(str(workflow.app_id))
break
except Exception:
_logger.exception("error while walking nodes, workflow_id=%s, node_id=%s", workflow.id, node_id)
continue
for app in apps:
app.has_draft_trigger = str(app.id) in draft_trigger_app_ids
register_enum_models(console_ns, RetrievalMethod, WorkflowExecutionStatus, DatasetPermissionEnum)
register_response_schema_models(console_ns, AppTraceResponse, RedirectUrlResponse, SimpleResultResponse)
register_schema_models(
console_ns,
AppListQuery,
StarredAppListQuery,
CreateAppPayload,
UpdateAppPayload,
CopyAppPayload,
@ -521,6 +577,7 @@ class AppListApi(Resource):
page=args.page,
limit=args.limit,
mode=args.mode,
sort_by=args.sort_by,
name=args.name,
tag_ids=args.tag_ids,
creator_ids=args.creator_ids,
@ -534,46 +591,7 @@ class AppListApi(Resource):
empty = AppPagination(page=args.page, limit=args.limit, total=0, has_more=False, data=[])
return empty.model_dump(mode="json"), 200
if FeatureService.get_system_features().webapp_auth.enabled:
app_ids = [str(app.id) for app in app_pagination.items]
res = EnterpriseService.WebAppAuth.batch_get_app_access_mode_by_id(app_ids=app_ids)
if len(res) != len(app_ids):
raise BadRequest("Invalid app id in webapp auth")
for app in app_pagination.items:
if str(app.id) in res:
app.access_mode = res[str(app.id)].access_mode
workflow_capable_app_ids = [
str(app.id) for app in app_pagination.items if app.mode in {"workflow", "advanced-chat"}
]
draft_trigger_app_ids: set[str] = set()
if workflow_capable_app_ids:
draft_workflows = (
session.execute(
select(Workflow).where(
Workflow.version == Workflow.VERSION_DRAFT,
Workflow.app_id.in_(workflow_capable_app_ids),
Workflow.tenant_id == current_tenant_id,
)
)
.scalars()
.all()
)
trigger_node_types = TRIGGER_NODE_TYPES
for workflow in draft_workflows:
node_id = None
try:
for node_id, node_data in workflow.walk_nodes():
if node_data.get("type") in trigger_node_types:
draft_trigger_app_ids.add(str(workflow.app_id))
break
except Exception:
_logger.exception("error while walking nodes, workflow_id=%s, node_id=%s", workflow.id, node_id)
continue
for app in app_pagination.items:
app.has_draft_trigger = str(app.id) in draft_trigger_app_ids
_enrich_app_list_items(session, apps=app_pagination.items, tenant_id=current_tenant_id)
pagination_model = AppPagination.model_validate(app_pagination, from_attributes=True)
return pagination_model.model_dump(mode="json"), 200
@ -609,6 +627,78 @@ class AppListApi(Resource):
return app_detail.model_dump(mode="json"), 201
@console_ns.route("/apps/starred")
class StarredAppListApi(Resource):
@console_ns.doc("list_starred_apps")
@console_ns.doc(description="Get applications starred by the current account")
@console_ns.doc(params=query_params_from_model(StarredAppListQuery))
@console_ns.response(200, "Success", console_ns.models[AppPagination.__name__])
@setup_required
@login_required
@account_initialization_required
@enterprise_license_required
@with_session(write=False)
@with_current_user_id
@with_current_tenant_id
def get(self, current_tenant_id: str, current_user_id: str, session: Session):
args = StarredAppListQuery.model_validate(_normalize_app_list_query_args(request.args))
params = StarredAppListParams(
page=args.page,
limit=args.limit,
mode=args.mode,
sort_by=args.sort_by,
name=args.name,
tag_ids=args.tag_ids,
creator_ids=args.creator_ids,
is_created_by_me=args.is_created_by_me,
)
app_pagination = AppService().get_paginate_starred_apps(current_user_id, current_tenant_id, params)
if not app_pagination:
empty = AppPagination(page=args.page, limit=args.limit, total=0, has_more=False, data=[])
return empty.model_dump(mode="json"), 200
_enrich_app_list_items(session, apps=app_pagination.items, tenant_id=current_tenant_id)
pagination_model = AppPagination.model_validate(app_pagination, from_attributes=True)
return pagination_model.model_dump(mode="json"), 200
@console_ns.route("/apps/<uuid:app_id>/star")
class AppStarApi(Resource):
@console_ns.doc("star_app")
@console_ns.doc(description="Star an application for the current account")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(404, "App not found")
@setup_required
@login_required
@account_initialization_required
@enterprise_license_required
@with_current_user_id
@with_session
@get_app_model(mode=None)
def post(self, session: Session, current_user_id: str, app_model: App):
AppService.star_app(session, app=app_model, account_id=current_user_id)
return dump_response(SimpleResultResponse, {"result": "success"})
@console_ns.doc("unstar_app")
@console_ns.doc(description="Remove the current account's star from an application")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(404, "App not found")
@setup_required
@login_required
@account_initialization_required
@enterprise_license_required
@with_current_user_id
@with_session
@get_app_model(mode=None)
def delete(self, session: Session, current_user_id: str, app_model: App):
AppService.unstar_app(session, app=app_model, account_id=current_user_id)
return dump_response(SimpleResultResponse, {"result": "success"})
@console_ns.route("/apps/<uuid:app_id>")
class AppApi(Resource):
@console_ns.doc("get_app_detail")
@ -628,7 +718,7 @@ class AppApi(Resource):
if FeatureService.get_system_features().webapp_auth.enabled:
app_setting = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=str(app_model.id))
app_model.access_mode = app_setting.access_mode # type: ignore[attr-defined]
app_model.access_mode = app_setting.access_mode
response_model = AppDetailWithSite.model_validate(app_model, from_attributes=True)
return response_model.model_dump(mode="json")

View File

@ -66,6 +66,10 @@ class RecommendedAppListResponse(ResponseModel):
categories: list[str]
class LearnDifyAppListResponse(ResponseModel):
recommended_apps: list[RecommendedAppResponse]
class RecommendedAppDetailResponse(RootModel[dict[str, Any]]):
root: dict[str, Any]
@ -76,10 +80,19 @@ register_schema_models(
RecommendedAppInfoResponse,
RecommendedAppResponse,
RecommendedAppListResponse,
LearnDifyAppListResponse,
)
register_response_schema_models(console_ns, RecommendedAppDetailResponse)
def _resolve_language(language: str | None, user: Account) -> str:
if language and language in languages:
return language
if user.interface_language:
return user.interface_language
return languages[0]
@console_ns.route("/explore/apps")
class RecommendedAppListApi(Resource):
@console_ns.doc(params=query_params_from_model(RecommendedAppsQuery))
@ -90,13 +103,7 @@ class RecommendedAppListApi(Resource):
def get(self, current_user: Account):
# language args
args = RecommendedAppsQuery.model_validate(request.args.to_dict(flat=True))
language = args.language
if language and language in languages:
language_prefix = language
elif current_user.interface_language:
language_prefix = current_user.interface_language
else:
language_prefix = languages[0]
language_prefix = _resolve_language(args.language, current_user)
return RecommendedAppListResponse.model_validate(
RecommendedAppService.get_recommended_apps_and_categories(db.session, language_prefix),
@ -104,6 +111,23 @@ class RecommendedAppListApi(Resource):
).model_dump(mode="json")
@console_ns.route("/explore/apps/learn-dify")
class LearnDifyAppListApi(Resource):
@console_ns.doc(params=query_params_from_model(RecommendedAppsQuery))
@console_ns.response(200, "Success", console_ns.models[LearnDifyAppListResponse.__name__])
@login_required
@account_initialization_required
@with_current_user
def get(self, current_user: Account):
args = RecommendedAppsQuery.model_validate(request.args.to_dict(flat=True))
language_prefix = _resolve_language(args.language, current_user)
return LearnDifyAppListResponse.model_validate(
RecommendedAppService.get_learn_dify_apps(db.session, language_prefix),
from_attributes=True,
).model_dump(mode="json")
@console_ns.route("/explore/apps/<uuid:app_id>")
class RecommendedAppApi(Resource):
@console_ns.response(200, "Success", console_ns.models[RecommendedAppDetailResponse.__name__])

View File

@ -1,10 +1,11 @@
import io
from collections.abc import Mapping
from typing import Any, Literal
from datetime import datetime
from typing import Any, Literal, TypedDict
from flask import request, send_file
from flask_restx import Resource
from pydantic import BaseModel, Field, RootModel
from pydantic import BaseModel, ConfigDict, Field, RootModel
from werkzeug.datastructures import FileStorage
from werkzeug.exceptions import Forbidden
@ -26,15 +27,33 @@ from controllers.console.wraps import (
with_current_user,
with_current_user_id,
)
from core.helper.position_helper import is_filtered
from core.plugin.entities.plugin import PluginCategory, PluginInstallationSource
from core.plugin.impl.exc import PluginDaemonClientSideError
from core.plugin.plugin_service import PluginService
from core.tools.builtin_tool.providers._positions import BuiltinToolProviderSort
from core.tools.entities.common_entities import I18nObject
from core.tools.entities.tool_entities import ToolProviderType
from core.tools.tool_manager import ToolManager
from fields.base import ResponseModel
from graphon.model_runtime.utils.encoders import jsonable_encoder
from libs.helper import dump_response
from libs.login import login_required
from models.account import Account, TenantPluginAutoUpgradeStrategy, TenantPluginPermission
from models.provider_ids import ToolProviderID
from services.entities.model_provider_entities import ProviderEntityResponse
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
from services.plugin.plugin_parameter_service import PluginParameterService
from services.plugin.plugin_permission_service import PluginPermissionService
from services.tools.tools_transform_service import ToolTransformService
class AutoUpgradeSettingsResponse(TypedDict):
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting
upgrade_time_of_day: int
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode
exclude_plugins: list[str]
include_plugins: list[str]
class ParserList(BaseModel):
@ -42,6 +61,11 @@ class ParserList(BaseModel):
page_size: int = Field(default=256, ge=1, le=256, description="Page size (1-256)")
class PluginCategoryListQuery(BaseModel):
page: int = Field(default=1, ge=1, description="Page number")
page_size: int = Field(default=256, ge=1, le=256, description="Page size (1-256)")
class ParserLatest(BaseModel):
plugin_ids: list[str]
@ -100,8 +124,8 @@ class ParserUninstall(BaseModel):
class ParserPermissionChange(BaseModel):
install_permission: TenantPluginPermission.InstallPermission
debug_permission: TenantPluginPermission.DebugPermission
install_permission: TenantPluginPermission.InstallPermission = TenantPluginPermission.InstallPermission.EVERYONE
debug_permission: TenantPluginPermission.DebugPermission = TenantPluginPermission.DebugPermission.EVERYONE
class ParserDynamicOptions(BaseModel):
@ -137,13 +161,64 @@ class PluginAutoUpgradeSettingsPayload(BaseModel):
include_plugins: list[str] = Field(default_factory=list)
class ParserPreferencesChange(BaseModel):
permission: PluginPermissionSettingsPayload
class PluginAutoUpgradeChangeResponse(ResponseModel):
success: bool
message: str | None = None
class PluginAutoUpgradeSettingsResponseModel(ResponseModel):
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting
upgrade_time_of_day: int
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode
exclude_plugins: list[str]
include_plugins: list[str]
class PluginAutoUpgradeFetchResponse(ResponseModel):
category: TenantPluginAutoUpgradeStrategy.PluginCategory
auto_upgrade: PluginAutoUpgradeSettingsResponseModel
class PluginDeclarationResponse(ResponseModel):
version: str
author: str | None
name: str
description: I18nObject
icon: str
icon_dark: str | None = None
label: I18nObject
category: PluginCategory
created_at: datetime
resource: Mapping[str, Any]
plugins: Mapping[str, list[str] | None]
tags: list[str] = Field(default_factory=list)
repo: str | None = None
verified: bool = False
tool: Mapping[str, Any] | None = None
model: ProviderEntityResponse | None = None
endpoint: Mapping[str, Any] | None = None
agent_strategy: Mapping[str, Any] | None = None
datasource: Mapping[str, Any] | None = None
trigger: Mapping[str, Any] | None = None
meta: Mapping[str, Any]
class ParserAutoUpgradeChange(BaseModel):
model_config = ConfigDict(extra="forbid")
category: TenantPluginAutoUpgradeStrategy.PluginCategory
auto_upgrade: PluginAutoUpgradeSettingsPayload
class ParserAutoUpgradeFetch(BaseModel):
category: TenantPluginAutoUpgradeStrategy.PluginCategory
class ParserExcludePlugin(BaseModel):
model_config = ConfigDict(extra="forbid")
plugin_id: str
category: TenantPluginAutoUpgradeStrategy.PluginCategory
class ParserReadme(BaseModel):
@ -157,6 +232,63 @@ class PluginDebuggingKeyResponse(ResponseModel):
port: int
class PluginCategoryInstalledPluginResponse(ResponseModel):
id: str
name: str
tenant_id: str
plugin_id: str
plugin_unique_identifier: str
endpoints_active: int
endpoints_setups: int
installation_id: str
declaration: PluginDeclarationResponse
runtime_type: str
version: str
created_at: datetime
updated_at: datetime
source: PluginInstallationSource
checksum: str
meta: Mapping[str, Any]
class PluginCategoryBuiltinToolResponse(ResponseModel):
model_config = ConfigDict(extra="allow")
author: str
name: str
label: I18nObject
description: I18nObject
parameters: list[Mapping[str, Any]] | None = None
labels: list[str]
output_schema: Mapping[str, object]
class PluginCategoryBuiltinToolProviderResponse(ResponseModel):
model_config = ConfigDict(extra="allow")
id: str
author: str
name: str
plugin_id: str | None
plugin_unique_identifier: str | None
description: I18nObject
icon: str | Mapping[str, str]
icon_dark: str | Mapping[str, str] | None
label: I18nObject
type: ToolProviderType
team_credentials: Mapping[str, object]
is_team_authorization: bool
allow_delete: bool
tools: list[PluginCategoryBuiltinToolResponse]
labels: list[str]
class PluginCategoryListResponse(ResponseModel):
plugins: list[PluginCategoryInstalledPluginResponse]
builtin_tools: list[PluginCategoryBuiltinToolProviderResponse]
has_more: bool
class PluginDaemonOperationResponse(RootModel[Any]):
root: Any
@ -200,11 +332,6 @@ class PluginOperationSuccessResponse(ResponseModel):
message: str | None = None
class PluginPreferencesResponse(ResponseModel):
permission: PluginPermissionSettingsPayload
auto_upgrade: PluginAutoUpgradeSettingsPayload
class PluginReadmeResponse(ResponseModel):
readme: str
@ -212,6 +339,7 @@ class PluginReadmeResponse(ResponseModel):
register_schema_models(
console_ns,
ParserList,
PluginCategoryListQuery,
PluginAutoUpgradeSettingsPayload,
PluginPermissionSettingsPayload,
ParserLatest,
@ -228,13 +356,21 @@ register_schema_models(
ParserPermissionChange,
ParserDynamicOptions,
ParserDynamicOptionsWithCredentials,
ParserPreferencesChange,
ParserAutoUpgradeChange,
ParserAutoUpgradeFetch,
ParserExcludePlugin,
ParserReadme,
)
register_response_schema_models(
console_ns,
PluginAutoUpgradeChangeResponse,
PluginAutoUpgradeFetchResponse,
PluginAutoUpgradeSettingsResponseModel,
BinaryFileResponse,
PluginCategoryBuiltinToolProviderResponse,
PluginCategoryBuiltinToolResponse,
PluginCategoryInstalledPluginResponse,
PluginCategoryListResponse,
PluginDaemonOperationResponse,
PluginDebuggingKeyResponse,
PluginDynamicOptionsResponse,
@ -243,7 +379,6 @@ register_response_schema_models(
PluginManifestResponse,
PluginOperationSuccessResponse,
PluginPermissionResponse,
PluginPreferencesResponse,
PluginReadmeResponse,
PluginTaskResponse,
PluginTasksResponse,
@ -254,12 +389,36 @@ register_response_schema_models(
register_enum_models(
console_ns,
TenantPluginPermission.DebugPermission,
TenantPluginAutoUpgradeStrategy.PluginCategory,
TenantPluginAutoUpgradeStrategy.UpgradeMode,
TenantPluginAutoUpgradeStrategy.StrategySetting,
TenantPluginPermission.InstallPermission,
)
def _default_auto_upgrade_settings(
tenant_id: str,
category: TenantPluginAutoUpgradeStrategy.PluginCategory,
) -> AutoUpgradeSettingsResponse:
return {
"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": [],
}
def _auto_upgrade_settings_to_dict(strategy: TenantPluginAutoUpgradeStrategy) -> AutoUpgradeSettingsResponse:
return {
"strategy_setting": strategy.strategy_setting,
"upgrade_time_of_day": strategy.upgrade_time_of_day,
"upgrade_mode": strategy.upgrade_mode,
"exclude_plugins": strategy.exclude_plugins,
"include_plugins": strategy.include_plugins,
}
def _read_upload_content(file: FileStorage, max_size: int) -> bytes:
"""
Read the uploaded file and validate its actual size before delegating to the plugin service.
@ -274,6 +433,33 @@ def _read_upload_content(file: FileStorage, max_size: int) -> bytes:
return content
def _list_hardcoded_builtin_tool_providers(tenant_id: str) -> list[dict[str, Any]]:
db_builtin_providers = {
str(ToolProviderID(provider.provider)): provider
for provider in ToolManager.list_default_builtin_providers(tenant_id)
}
builtin_providers = []
for provider in ToolManager.list_hardcoded_providers():
if is_filtered(
include_set=dify_config.POSITION_TOOL_INCLUDES_SET,
exclude_set=dify_config.POSITION_TOOL_EXCLUDES_SET,
data=provider,
name_func=lambda provider_controller: provider_controller.entity.identity.name,
):
continue
user_provider = ToolTransformService.builtin_provider_to_user_provider(
provider_controller=provider,
db_provider=db_builtin_providers.get(provider.entity.identity.name),
decrypt_credentials=False,
)
ToolTransformService.repack_provider(tenant_id=tenant_id, provider=user_provider)
builtin_providers.append(user_provider)
return [provider.to_dict() for provider in BuiltinToolProviderSort.sort(builtin_providers)]
@console_ns.route("/workspaces/current/plugin/debugging-key")
class PluginDebuggingKeyApi(Resource):
@console_ns.response(200, "Success", console_ns.models[PluginDebuggingKeyResponse.__name__])
@ -312,6 +498,41 @@ class PluginListApi(Resource):
return jsonable_encoder({"plugins": plugins_with_total.list, "total": plugins_with_total.total})
@console_ns.route("/workspaces/current/plugin/<string:category>/list")
class PluginCategoryListApi(Resource):
@console_ns.doc(params=query_params_from_model(PluginCategoryListQuery))
@console_ns.response(200, "Success", console_ns.models[PluginCategoryListResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str, category: str):
args = PluginCategoryListQuery.model_validate(request.args.to_dict(flat=True))
try:
plugin_category = PluginCategory(category)
except ValueError:
return {"code": "invalid_param", "message": "invalid plugin category"}, 400
try:
plugins = PluginService.list_by_category(tenant_id, plugin_category, args.page, args.page_size)
except PluginDaemonClientSideError as e:
return {"code": "plugin_error", "message": e.description}, 400
builtin_tools = []
if plugin_category == PluginCategory.Tool:
builtin_tools = _list_hardcoded_builtin_tool_providers(tenant_id)
return dump_response(
PluginCategoryListResponse,
{
"plugins": jsonable_encoder(plugins.list),
"builtin_tools": builtin_tools,
"has_more": plugins.has_more,
},
)
@console_ns.route("/workspaces/current/plugin/list/latest-versions")
class PluginListLatestVersionsApi(Resource):
@console_ns.expect(console_ns.models[ParserLatest.__name__])
@ -713,11 +934,13 @@ class PluginChangePermissionApi(Resource):
args = ParserPermissionChange.model_validate(console_ns.payload)
return {
"success": PluginPermissionService.change_permission(
tenant_id, args.install_permission, args.debug_permission
)
}
set_permission_result = PluginPermissionService.change_permission(
tenant_id, args.install_permission, args.debug_permission
)
if not set_permission_result:
return jsonable_encoder({"success": False, "message": "Failed to set permission"})
return jsonable_encoder({"success": True})
@console_ns.route("/workspaces/current/plugin/permission/fetch")
@ -806,10 +1029,10 @@ class PluginFetchDynamicSelectOptionsWithCredentialsApi(Resource):
return jsonable_encoder({"options": options})
@console_ns.route("/workspaces/current/plugin/preferences/change")
class PluginChangePreferencesApi(Resource):
@console_ns.expect(console_ns.models[ParserPreferencesChange.__name__])
@console_ns.response(200, "Success", console_ns.models[PluginOperationSuccessResponse.__name__])
@console_ns.route("/workspaces/current/plugin/auto-upgrade/change")
class PluginChangeAutoUpgradeApi(Resource):
@console_ns.expect(console_ns.models[ParserAutoUpgradeChange.__name__])
@console_ns.response(200, "Success", console_ns.models[PluginAutoUpgradeChangeResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -819,38 +1042,17 @@ class PluginChangePreferencesApi(Resource):
if not user.is_admin_or_owner:
raise Forbidden()
args = ParserPreferencesChange.model_validate(console_ns.payload)
permission = args.permission
install_permission = permission.install_permission
debug_permission = permission.debug_permission
args = ParserAutoUpgradeChange.model_validate(console_ns.payload)
auto_upgrade = args.auto_upgrade
strategy_setting = auto_upgrade.strategy_setting
upgrade_time_of_day = auto_upgrade.upgrade_time_of_day
upgrade_mode = auto_upgrade.upgrade_mode
exclude_plugins = auto_upgrade.exclude_plugins
include_plugins = auto_upgrade.include_plugins
# set permission
set_permission_result = PluginPermissionService.change_permission(
tenant_id,
install_permission,
debug_permission,
)
if not set_permission_result:
return jsonable_encoder({"success": False, "message": "Failed to set permission"})
# set auto upgrade strategy
set_auto_upgrade_strategy_result = PluginAutoUpgradeService.change_strategy(
tenant_id,
strategy_setting,
upgrade_time_of_day,
upgrade_mode,
exclude_plugins,
include_plugins,
auto_upgrade.strategy_setting,
auto_upgrade.upgrade_time_of_day,
auto_upgrade.upgrade_mode,
auto_upgrade.exclude_plugins,
auto_upgrade.include_plugins,
category=args.category,
)
if not set_auto_upgrade_strategy_result:
return jsonable_encoder({"success": False, "message": "Failed to set auto upgrade strategy"})
@ -858,49 +1060,35 @@ class PluginChangePreferencesApi(Resource):
return jsonable_encoder({"success": True})
@console_ns.route("/workspaces/current/plugin/preferences/fetch")
class PluginFetchPreferencesApi(Resource):
@console_ns.response(200, "Success", console_ns.models[PluginPreferencesResponse.__name__])
@console_ns.route("/workspaces/current/plugin/auto-upgrade/fetch")
class PluginFetchAutoUpgradeApi(Resource):
@console_ns.doc(params=query_params_from_model(ParserAutoUpgradeFetch))
@console_ns.response(200, "Success", console_ns.models[PluginAutoUpgradeFetchResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str):
permission = PluginPermissionService.get_permission(tenant_id)
permission_dict = {
"install_permission": TenantPluginPermission.InstallPermission.EVERYONE,
"debug_permission": TenantPluginPermission.DebugPermission.EVERYONE,
}
args = ParserAutoUpgradeFetch.model_validate(request.args.to_dict(flat=True))
auto_upgrade = PluginAutoUpgradeService.get_strategy(tenant_id, args.category)
auto_upgrade_dict = (
_auto_upgrade_settings_to_dict(auto_upgrade)
if auto_upgrade
else _default_auto_upgrade_settings(tenant_id, args.category)
)
if permission:
permission_dict["install_permission"] = permission.install_permission
permission_dict["debug_permission"] = permission.debug_permission
auto_upgrade = PluginAutoUpgradeService.get_strategy(tenant_id)
auto_upgrade_dict = {
"strategy_setting": TenantPluginAutoUpgradeStrategy.StrategySetting.DISABLED,
"upgrade_time_of_day": 0,
"upgrade_mode": TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
"exclude_plugins": [],
"include_plugins": [],
}
if auto_upgrade:
auto_upgrade_dict = {
"strategy_setting": auto_upgrade.strategy_setting,
"upgrade_time_of_day": auto_upgrade.upgrade_time_of_day,
"upgrade_mode": auto_upgrade.upgrade_mode,
"exclude_plugins": auto_upgrade.exclude_plugins,
"include_plugins": auto_upgrade.include_plugins,
return jsonable_encoder(
{
"category": args.category,
"auto_upgrade": auto_upgrade_dict,
}
return jsonable_encoder({"permission": permission_dict, "auto_upgrade": auto_upgrade_dict})
)
@console_ns.route("/workspaces/current/plugin/preferences/autoupgrade/exclude")
@console_ns.route("/workspaces/current/plugin/auto-upgrade/exclude")
class PluginAutoUpgradeExcludePluginApi(Resource):
@console_ns.expect(console_ns.models[ParserExcludePlugin.__name__])
@console_ns.response(200, "Success", console_ns.models[PluginOperationSuccessResponse.__name__])
@console_ns.response(200, "Success", console_ns.models[SuccessResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -909,7 +1097,9 @@ class PluginAutoUpgradeExcludePluginApi(Resource):
# exclude one single plugin
args = ParserExcludePlugin.model_validate(console_ns.payload)
return jsonable_encoder({"success": PluginAutoUpgradeService.exclude_plugin(tenant_id, args.plugin_id)})
return jsonable_encoder(
{"success": PluginAutoUpgradeService.exclude_plugin(tenant_id, args.plugin_id, args.category)}
)
@console_ns.route("/workspaces/current/plugin/readme")

View File

@ -31,9 +31,9 @@ from controllers.console.wraps import (
from enums.cloud_plan import CloudPlan
from extensions.ext_database import db
from fields.base import ResponseModel
from libs.helper import TimestampField, dump_response, to_timestamp
from libs.helper import OptionalTimestampField, TimestampField, dump_response, to_timestamp
from libs.login import login_required
from models.account import Account, Tenant, TenantCustomConfigDict, TenantStatus
from models.account import Account, Tenant, TenantAccountJoin, TenantCustomConfigDict, TenantStatus
from services.account_service import TenantService
from services.billing_service import BillingService, SubscriptionPlan
from services.enterprise.enterprise_service import EnterpriseService
@ -219,6 +219,7 @@ tenants_fields = {
"plan": fields.String,
"status": fields.String,
"created_at": TimestampField,
"last_opened_at": OptionalTimestampField,
"current": fields.Boolean,
}
@ -234,7 +235,12 @@ class TenantListApi(Resource):
@with_current_user
@with_current_tenant_id
def get(self, current_tenant_id: str, current_user: Account):
tenants = TenantService.get_join_tenants(current_user)
tenant_rows: list[tuple[Tenant, TenantAccountJoin]] = [
(tenant, membership)
for tenant, membership in TenantService.get_workspaces_for_account(db.session, current_user.id)
if tenant.status == TenantStatus.NORMAL
]
tenants = [tenant for tenant, _ in tenant_rows]
tenant_dicts = []
is_enterprise_only = dify_config.ENTERPRISE_ENABLED and not dify_config.BILLING_ENABLED
is_saas = dify_config.EDITION == "CLOUD" and dify_config.BILLING_ENABLED
@ -247,7 +253,7 @@ class TenantListApi(Resource):
if not tenant_plans:
logger.warning("get_plan_bulk returned empty result, falling back to legacy feature path")
for tenant in tenants:
for tenant, membership in tenant_rows:
plan: str = CloudPlan.SANDBOX
if is_saas:
tenant_plan = tenant_plans.get(tenant.id)
@ -266,6 +272,7 @@ class TenantListApi(Resource):
"name": tenant.name,
"status": tenant.status,
"created_at": tenant.created_at,
"last_opened_at": membership.last_opened_at,
"plan": plan,
"current": tenant.id == current_tenant_id if current_tenant_id else False,
}

View File

@ -168,6 +168,7 @@ class PluginInstallTask(BasePluginEntity):
class PluginInstallTaskStartResponse(BaseModel):
all_installed: bool = Field(description="Whether all plugins are installed.")
task_id: str = Field(description="The ID of the install task.")
task: PluginInstallTask | None = Field(default=None, description="The install task.")
class PluginVerification(BaseModel):
@ -206,6 +207,11 @@ class PluginListResponse(BaseModel):
total: int
class PluginListWithoutTotalResponse(BaseModel):
list: list[PluginEntity]
has_more: bool
class PluginDynamicSelectOptionsResponse(BaseModel):
options: Sequence[PluginParameterOption] = Field(description="The options of the dynamic select.")

View File

@ -6,6 +6,7 @@ from requests import HTTPError
from core.plugin.entities.bundle import PluginBundleDependency
from core.plugin.entities.plugin import (
MissingPluginDependency,
PluginCategory,
PluginDeclaration,
PluginEntity,
PluginInstallation,
@ -16,6 +17,7 @@ from core.plugin.entities.plugin_daemon import (
PluginInstallTask,
PluginInstallTaskStartResponse,
PluginListResponse,
PluginListWithoutTotalResponse,
PluginReadmeResponse,
)
from core.plugin.impl.base import BasePluginClient
@ -74,6 +76,16 @@ class PluginInstaller(BasePluginClient):
params={"page": page, "page_size": page_size, "response_type": "paged"},
)
def list_plugins_by_category(
self, tenant_id: str, category: PluginCategory, page: int, page_size: int
) -> PluginListWithoutTotalResponse:
return self._request_with_plugin_daemon_response(
"GET",
f"plugin/{tenant_id}/management/{category.value}/list",
PluginListWithoutTotalResponse,
params={"page": page, "page_size": page_size, "response_type": "paged"},
)
def upload_pkg(
self,
tenant_id: str,

View File

@ -31,6 +31,7 @@ from core.helper.marketplace import download_plugin_pkg
from core.helper.model_provider_cache import ProviderCredentialsCache, ProviderCredentialsCacheType
from core.plugin.entities.bundle import PluginBundleDependency
from core.plugin.entities.plugin import (
PluginCategory,
PluginDeclaration,
PluginEntity,
PluginInstallation,
@ -41,6 +42,7 @@ from core.plugin.entities.plugin_daemon import (
PluginInstallTask,
PluginInstallTaskStatus,
PluginListResponse,
PluginListWithoutTotalResponse,
PluginModelProviderEntity,
PluginVerification,
)
@ -437,6 +439,19 @@ class PluginService:
PluginService._reconcile_endpoint_counts(tenant_id, user_id, plugins.list)
return plugins
@staticmethod
def list_by_category(
tenant_id: str, category: PluginCategory, page: int, page_size: int
) -> PluginListWithoutTotalResponse:
"""
List plugins in one category with a has-more cursor signal and without calculating total.
The daemon scans tenant installations in the existing list order and stops once it finds one extra match.
This keeps pagination usable before category is persisted on installation rows.
"""
manager = PluginInstaller()
return manager.list_plugins_by_category(tenant_id, category, page, page_size)
@staticmethod
def _normalize_endpoint_count(value: object) -> int:
"""Convert daemon endpoint counters to safe non-negative integers.

View File

@ -5,6 +5,7 @@ def init_app(app: DifyApp):
from commands import (
add_qdrant_index,
archive_workflow_runs,
backfill_plugin_auto_upgrade,
clean_expired_messages,
clean_workflow_runs,
cleanup_orphaned_draft_variables,
@ -53,6 +54,7 @@ def init_app(app: DifyApp):
upgrade_db,
fix_app_site_missing,
migrate_data_for_plugin,
backfill_plugin_auto_upgrade,
extract_plugins,
extract_unique_plugins,
install_plugins,

View File

@ -0,0 +1,26 @@
"""add tenant account join last opened at
Revision ID: b7c2d9e8a1f4
Revises: 9f4b7c2d1a80
Create Date: 2026-06-05 11:00:00.000000
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "b7c2d9e8a1f4"
down_revision = "9f4b7c2d1a80"
branch_labels = None
depends_on = None
def upgrade():
with op.batch_alter_table("tenant_account_joins", schema=None) as batch_op:
batch_op.add_column(sa.Column("last_opened_at", sa.DateTime(), nullable=True))
def downgrade():
with op.batch_alter_table("tenant_account_joins", schema=None) as batch_op:
batch_op.drop_column("last_opened_at")

View File

@ -0,0 +1,42 @@
"""add plugin auto upgrade category
Revision ID: f6a7b8c9d012
Revises: b7c2d9e8a1f4
Create Date: 2026-05-15 12:00:00.000000
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "f6a7b8c9d012"
down_revision = "b7c2d9e8a1f4"
branch_labels = None
depends_on = None
LEGACY_CATEGORY = "tool"
UNIQUE_CONSTRAINT_NAME = "unique_tenant_plugin_auto_upgrade_strategy"
UPGRADE_TIME_INDEX_NAME = "idx_tenant_plugin_auto_upgrade_strategy_time"
STRATEGY_TABLE_NAME = "tenant_plugin_auto_upgrade_strategies"
def upgrade():
with op.batch_alter_table(STRATEGY_TABLE_NAME, schema=None) as batch_op:
batch_op.add_column(
sa.Column("category", sa.String(length=32), server_default=LEGACY_CATEGORY, nullable=False)
)
batch_op.drop_constraint(UNIQUE_CONSTRAINT_NAME, type_="unique")
batch_op.create_unique_constraint(UNIQUE_CONSTRAINT_NAME, ["tenant_id", "category"])
batch_op.create_index(UPGRADE_TIME_INDEX_NAME, ["upgrade_time_of_day"])
def downgrade():
op.execute(sa.text(f"DELETE FROM {STRATEGY_TABLE_NAME} WHERE category != '{LEGACY_CATEGORY}'"))
with op.batch_alter_table(STRATEGY_TABLE_NAME, schema=None) as batch_op:
batch_op.drop_index(UPGRADE_TIME_INDEX_NAME)
batch_op.drop_constraint(UNIQUE_CONSTRAINT_NAME, type_="unique")
batch_op.drop_column("category")
batch_op.create_unique_constraint(UNIQUE_CONSTRAINT_NAME, ["tenant_id"])

View File

@ -0,0 +1,26 @@
"""add learn dify flag to recommended apps
Revision ID: f5e8a9c0d2b3
Revises: f6a7b8c9d012
Create Date: 2026-05-18 15:00:00.000000
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "f5e8a9c0d2b3"
down_revision = "f6a7b8c9d012"
branch_labels = None
depends_on = None
def upgrade():
with op.batch_alter_table("recommended_apps", schema=None) as batch_op:
batch_op.add_column(sa.Column("is_learn_dify", sa.Boolean(), server_default=sa.text("false"), nullable=False))
def downgrade():
with op.batch_alter_table("recommended_apps", schema=None) as batch_op:
batch_op.drop_column("is_learn_dify")

View File

@ -0,0 +1,38 @@
"""add app stars
Revision ID: c4d5e6f7a8b9
Revises: f5e8a9c0d2b3
Create Date: 2026-06-08 12:00:00.000000
"""
import sqlalchemy as sa
from alembic import op
import models.types
# revision identifiers, used by Alembic.
revision = "c4d5e6f7a8b9"
down_revision = "f5e8a9c0d2b3"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
"app_stars",
sa.Column("id", models.types.StringUUID(), nullable=False),
sa.Column("tenant_id", models.types.StringUUID(), nullable=False),
sa.Column("app_id", models.types.StringUUID(), nullable=False),
sa.Column("account_id", models.types.StringUUID(), nullable=False),
sa.Column("created_at", sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False),
sa.PrimaryKeyConstraint("id", name="app_star_pkey"),
sa.UniqueConstraint("tenant_id", "account_id", "app_id", name="app_star_tenant_account_app_unique"),
)
with op.batch_alter_table("app_stars", schema=None) as batch_op:
batch_op.create_index("app_star_tenant_account_idx", ["tenant_id", "account_id"], unique=False)
batch_op.create_index("app_star_app_idx", ["app_id"], unique=False)
def downgrade() -> None:
op.drop_table("app_stars")

View File

@ -73,6 +73,7 @@ from .model import (
AppMCPServer,
AppMode,
AppModelConfig,
AppStar,
Conversation,
DatasetRetrieverResource,
DifySetup,
@ -175,6 +176,7 @@ __all__ = [
"AppMCPServer",
"AppMode",
"AppModelConfig",
"AppStar",
"AppTrigger",
"AppTriggerStatus",
"AppTriggerType",

View File

@ -301,6 +301,7 @@ class TenantAccountJoin(TypeBase):
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.current_timestamp(), nullable=False, init=False, onupdate=func.current_timestamp()
)
last_opened_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, default=None)
class AccountIntegrate(TypeBase):
@ -389,6 +390,14 @@ class TenantPluginPermission(TypeBase):
class TenantPluginAutoUpgradeStrategy(TypeBase):
class PluginCategory(enum.StrEnum):
TOOL = "tool"
MODEL = "model"
EXTENSION = "extension"
AGENT_STRATEGY = "agent-strategy"
DATASOURCE = "datasource"
TRIGGER = "trigger"
class StrategySetting(enum.StrEnum):
DISABLED = "disabled"
FIX_ONLY = "fix_only"
@ -402,13 +411,20 @@ class TenantPluginAutoUpgradeStrategy(TypeBase):
__tablename__ = "tenant_plugin_auto_upgrade_strategies"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="tenant_plugin_auto_upgrade_strategy_pkey"),
sa.UniqueConstraint("tenant_id", name="unique_tenant_plugin_auto_upgrade_strategy"),
sa.UniqueConstraint("tenant_id", "category", name="unique_tenant_plugin_auto_upgrade_strategy"),
sa.Index("idx_tenant_plugin_auto_upgrade_strategy_time", "upgrade_time_of_day"),
)
id: Mapped[str] = mapped_column(
StringUUID, insert_default=lambda: str(uuid4()), default_factory=lambda: str(uuid4()), init=False
)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
category: Mapped[PluginCategory] = mapped_column(
EnumText(PluginCategory, length=32),
nullable=False,
server_default="tool",
default=PluginCategory.TOOL,
)
strategy_setting: Mapped[StrategySetting] = mapped_column(
EnumText(StrategySetting, length=16),
nullable=False,

View File

@ -397,6 +397,12 @@ class App(Base):
__tablename__ = "apps"
__table_args__ = (sa.PrimaryKeyConstraint("id", name="app_pkey"), sa.Index("app_tenant_id_idx", "tenant_id"))
if TYPE_CHECKING:
# Response-only attributes attached by app list/detail enrichers.
access_mode: str | None
has_draft_trigger: bool
is_starred: bool
id: Mapped[str] = mapped_column(StringUUID, default=lambda: str(uuid4()))
tenant_id: Mapped[str] = mapped_column(StringUUID)
name: Mapped[str] = mapped_column(String(255))
@ -654,6 +660,28 @@ class App(Base):
return None
class AppStar(Base):
"""Account-scoped star marker for apps in a workspace."""
__tablename__ = "app_stars"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="app_star_pkey"),
sa.UniqueConstraint("tenant_id", "account_id", "app_id", name="app_star_tenant_account_app_unique"),
sa.Index("app_star_tenant_account_idx", "tenant_id", "account_id"),
sa.Index("app_star_app_idx", "app_id"),
)
id: Mapped[str] = mapped_column(StringUUID, default=lambda: str(uuidv7()))
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
account_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
created_at: Mapped[datetime] = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
@override
def __repr__(self) -> str:
return f"<AppStar app_id={self.app_id} account_id={self.account_id}>"
class AppModelConfig(TypeBase):
__tablename__ = "app_model_configs"
__table_args__ = (sa.PrimaryKeyConstraint("id", name="app_model_config_pkey"), sa.Index("app_app_id_idx", "app_id"))
@ -907,6 +935,9 @@ class RecommendedApp(TypeBase):
custom_disclaimer: Mapped[str] = mapped_column(LongText, default="")
position: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0)
is_listed: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=True)
is_learn_dify: Mapped[bool] = mapped_column(
sa.Boolean, nullable=False, server_default=sa.text("false"), default=False
)
install_count: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0)
language: Mapped[str] = mapped_column(
String(255),

View File

@ -531,6 +531,7 @@ Get list of applications with pagination and filtering
| mode | query | App mode filter | No | string, <br>**Available values:** "advanced-chat", "agent", "agent-chat", "all", "channel", "chat", "completion", "workflow", <br>**Default:** all |
| name | query | Filter by app name | No | string |
| page | query | Page number (1-99999) | No | integer, <br>**Default:** 1 |
| sort_by | query | Sort apps by last modified, recently created, or earliest created | No | string, <br>**Available values:** "earliest_created", "last_modified", "recently_created", <br>**Default:** last_modified |
| tag_ids | query | Filter by tag IDs | No | [ string ] |
#### Responses
@ -600,6 +601,28 @@ Create a new application
| 200 | Import confirmed | **application/json**: [Import](#import)<br> |
| 400 | Import failed | **application/json**: [Import](#import)<br> |
### [GET] /apps/starred
Get applications starred by the current account
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| creator_ids | query | Filter by creator account IDs | No | [ string ] |
| is_created_by_me | query | Filter by creator | No | boolean |
| limit | query | Page size (1-100) | No | integer, <br>**Default:** 20 |
| mode | query | App mode filter | No | string, <br>**Available values:** "advanced-chat", "agent", "agent-chat", "all", "channel", "chat", "completion", "workflow", <br>**Default:** all |
| name | query | Filter by app name | No | string |
| page | query | Page number (1-99999) | No | integer, <br>**Default:** 1 |
| sort_by | query | Sort apps by last modified, recently created, or earliest created | No | string, <br>**Available values:** "earliest_created", "last_modified", "recently_created", <br>**Default:** last_modified |
| tag_ids | query | Filter by tag IDs | No | [ string ] |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [AppPagination](#apppagination)<br> |
### [POST] /apps/workflows/online-users
Get workflow online users
@ -2045,6 +2068,38 @@ Reset access token for application site
| 403 | Insufficient permissions (admin/owner required) | |
| 404 | App or site not found | |
### [DELETE] /apps/{app_id}/star
Remove the current account's star from an application
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| app_id | path | Application ID | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
| 404 | App not found | |
### [POST] /apps/{app_id}/star
Star an application for the current account
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| app_id | path | Application ID | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
| 404 | App not found | |
### [GET] /apps/{app_id}/statistics/average-response-time
Get average response time statistics for an application
@ -5573,6 +5628,19 @@ Check if dataset is in use
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [RecommendedAppListResponse](#recommendedapplistresponse)<br> |
### [GET] /explore/apps/learn-dify
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| language | query | Language code for recommended app localization | No | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [LearnDifyAppListResponse](#learndifyapplistresponse)<br> |
### [GET] /explore/apps/{app_id}
#### Parameters
@ -9391,6 +9459,45 @@ Returns permission flags that control workspace features like member invitations
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [BinaryFileResponse](#binaryfileresponse)<br> |
### [POST] /workspaces/current/plugin/auto-upgrade/change
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [ParserAutoUpgradeChange](#parserautoupgradechange)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [PluginAutoUpgradeChangeResponse](#pluginautoupgradechangeresponse)<br> |
### [POST] /workspaces/current/plugin/auto-upgrade/exclude
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [ParserExcludePlugin](#parserexcludeplugin)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [SuccessResponse](#successresponse)<br> |
### [GET] /workspaces/current/plugin/auto-upgrade/fetch
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| category | query | | Yes | string, <br>**Available values:** "agent-strategy", "datasource", "extension", "model", "tool", "trigger" |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [PluginAutoUpgradeFetchResponse](#pluginautoupgradefetchresponse)<br> |
### [GET] /workspaces/current/plugin/debugging-key
#### Responses
@ -9570,39 +9677,6 @@ Returns permission flags that control workspace features like member invitations
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [PluginPermissionResponse](#pluginpermissionresponse)<br> |
### [POST] /workspaces/current/plugin/preferences/autoupgrade/exclude
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [ParserExcludePlugin](#parserexcludeplugin)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [PluginOperationSuccessResponse](#pluginoperationsuccessresponse)<br> |
### [POST] /workspaces/current/plugin/preferences/change
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [ParserPreferencesChange](#parserpreferenceschange)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [PluginOperationSuccessResponse](#pluginoperationsuccessresponse)<br> |
### [GET] /workspaces/current/plugin/preferences/fetch
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [PluginPreferencesResponse](#pluginpreferencesresponse)<br> |
### [GET] /workspaces/current/plugin/readme
#### Parameters
@ -9744,6 +9818,21 @@ Returns permission flags that control workspace features like member invitations
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [PluginDaemonOperationResponse](#plugindaemonoperationresponse)<br> |
### [GET] /workspaces/current/plugin/{category}/list
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| page | query | Page number | No | integer, <br>**Default:** 1 |
| page_size | query | Page size (1-256) | No | integer, <br>**Default:** 256 |
| category | path | | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [PluginCategoryListResponse](#plugincategorylistresponse)<br> |
### [GET] /workspaces/current/tool-labels
#### Responses
@ -10641,7 +10730,7 @@ Default namespace
| deprecated | boolean | | No |
| features | [ [ModelFeature](#modelfeature) ] | | No |
| fetch_from | [FetchFrom](#fetchfrom) | | Yes |
| label | [I18nObject](#i18nobject) | | Yes |
| label | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes |
| model | string | | Yes |
| model_properties | object | | Yes |
| model_type | [ModelType](#modeltype) | | Yes |
@ -12167,6 +12256,7 @@ Enum class for api provider schema type.
| mode | string, <br>**Available values:** "advanced-chat", "agent", "agent-chat", "all", "channel", "chat", "completion", "workflow", <br>**Default:** all | App mode filter<br>*Enum:* `"advanced-chat"`, `"agent"`, `"agent-chat"`, `"all"`, `"channel"`, `"chat"`, `"completion"`, `"workflow"` | No |
| name | string | Filter by app name | No |
| page | integer, <br>**Default:** 1 | Page number (1-99999) | No |
| sort_by | string, <br>**Available values:** "earliest_created", "last_modified", "recently_created", <br>**Default:** last_modified | Sort apps by last modified, recently created, or earliest created<br>*Enum:* `"earliest_created"`, `"last_modified"`, `"recently_created"` | No |
| tag_ids | [ string ] | Filter by tag IDs | No |
#### AppMCPServerResponse
@ -12222,6 +12312,7 @@ AppMCPServer Status Enum
| icon_background | string | | No |
| icon_type | string | | No |
| id | string | | Yes |
| is_starred | boolean | | No |
| max_active_requests | integer | | No |
| mode_compatible_with_agent | string | | Yes |
| name | string | | Yes |
@ -13050,10 +13141,10 @@ Model class for credential form schema.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| default | string | | No |
| label | [I18nObject](#i18nobject) | | Yes |
| label | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes |
| max_length | integer | | No |
| options | [ [FormOption](#formoption) ] | | No |
| placeholder | [I18nObject](#i18nobject) | | No |
| placeholder | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | No |
| required | boolean, <br>**Default:** true | | No |
| show_on | [ [FormShowOnObject](#formshowonobject) ], <br>**Default:** | | No |
| type | [FormType](#formtype) | | Yes |
@ -14473,8 +14564,8 @@ Enum class for fetch from.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| label | [I18nObject](#i18nobject) | | Yes |
| placeholder | [I18nObject](#i18nobject) | | No |
| label | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes |
| placeholder | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | No |
#### FileInfo
@ -14605,7 +14696,7 @@ Model class for form option.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| label | [I18nObject](#i18nobject) | | Yes |
| label | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes |
| show_on | [ [FormShowOnObject](#formshowonobject) ], <br>**Default:** | | No |
| value | string | | Yes |
@ -14837,6 +14928,8 @@ Model class for i18n object.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| en_US | string | | Yes |
| ja_JP | string | | No |
| pt_BR | string | | No |
| zh_Hans | string | | No |
#### IconInfo
@ -15098,6 +15191,12 @@ Enum class for large language model mode.
| ---- | ---- | ----------- | -------- |
| LLMMode | string | Enum class for large language model mode. | |
#### LearnDifyAppListResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| recommended_apps | [ [RecommendedAppResponse](#recommendedappresponse) ] | | Yes |
#### LegacyEndpointUpdatePayload
| Name | Type | Description | Required |
@ -15932,8 +16031,8 @@ Model class for parameter rule.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| default | | | No |
| help | [I18nObject](#i18nobject) | | No |
| label | [I18nObject](#i18nobject) | | Yes |
| help | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | No |
| label | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes |
| max | number | | No |
| min | number | | No |
| name | string | | Yes |
@ -15983,6 +16082,19 @@ Enum class for parameter type.
| file_name | string | | Yes |
| plugin_unique_identifier | string | | Yes |
#### ParserAutoUpgradeChange
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| auto_upgrade | [PluginAutoUpgradeSettingsPayload](#pluginautoupgradesettingspayload) | | Yes |
| category | [PluginCategory](#plugincategory) | | Yes |
#### ParserAutoUpgradeFetch
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| category | [PluginCategory](#plugincategory) | | Yes |
#### ParserCreateCredential
| Name | Type | Description | Required |
@ -16079,6 +16191,7 @@ Enum class for parameter type.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| category | [PluginCategory](#plugincategory) | | Yes |
| plugin_id | string | | Yes |
#### ParserGetCredentials
@ -16166,8 +16279,8 @@ Enum class for parameter type.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| debug_permission | [DebugPermission](#debugpermission) | | Yes |
| install_permission | [InstallPermission](#installpermission) | | Yes |
| debug_permission | [DebugPermission](#debugpermission) | | No |
| install_permission | [InstallPermission](#installpermission) | | No |
#### ParserPluginIdentifierQuery
@ -16197,13 +16310,6 @@ Enum class for parameter type.
| model | string | | Yes |
| model_type | [ModelType](#modeltype) | | Yes |
#### ParserPreferencesChange
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| auto_upgrade | [PluginAutoUpgradeSettingsPayload](#pluginautoupgradesettingspayload) | | Yes |
| permission | [PluginPermissionSettingsPayload](#pluginpermissionsettingspayload) | | Yes |
#### ParserPreferredProviderType
| Name | Type | Description | Required |
@ -16348,6 +16454,20 @@ Shared permission levels for resources (datasets, credentials, etc.)
| unit | string | | No |
| variable | string | | Yes |
#### PluginAutoUpgradeChangeResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| message | string | | No |
| success | boolean | | Yes |
#### PluginAutoUpgradeFetchResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| auto_upgrade | [PluginAutoUpgradeSettingsResponseModel](#pluginautoupgradesettingsresponsemodel) | | Yes |
| category | [PluginCategory](#plugincategory) | | Yes |
#### PluginAutoUpgradeSettingsPayload
| Name | Type | Description | Required |
@ -16358,6 +16478,90 @@ Shared permission levels for resources (datasets, credentials, etc.)
| upgrade_mode | [UpgradeMode](#upgrademode) | | No |
| upgrade_time_of_day | integer | | No |
#### PluginAutoUpgradeSettingsResponseModel
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| exclude_plugins | [ string ] | | Yes |
| include_plugins | [ string ] | | Yes |
| strategy_setting | [StrategySetting](#strategysetting) | | Yes |
| upgrade_mode | [UpgradeMode](#upgrademode) | | Yes |
| upgrade_time_of_day | integer | | Yes |
#### PluginCategory
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| PluginCategory | string | | |
#### PluginCategoryBuiltinToolProviderResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| allow_delete | boolean | | Yes |
| author | string | | Yes |
| description | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | | Yes |
| icon | string<br>object | | Yes |
| icon_dark | string<br>object | | Yes |
| id | string | | Yes |
| is_team_authorization | boolean | | Yes |
| label | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | | Yes |
| labels | [ string ] | | Yes |
| name | string | | Yes |
| plugin_id | string | | Yes |
| plugin_unique_identifier | string | | Yes |
| team_credentials | object | | Yes |
| tools | [ [PluginCategoryBuiltinToolResponse](#plugincategorybuiltintoolresponse) ] | | Yes |
| type | [ToolProviderType](#toolprovidertype) | | Yes |
#### PluginCategoryBuiltinToolResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| author | string | | Yes |
| description | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | | Yes |
| label | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | | Yes |
| labels | [ string ] | | Yes |
| name | string | | Yes |
| output_schema | object | | Yes |
| parameters | [ object ] | | No |
#### PluginCategoryInstalledPluginResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| checksum | string | | Yes |
| created_at | dateTime | | Yes |
| declaration | [PluginDeclarationResponse](#plugindeclarationresponse) | | Yes |
| endpoints_active | integer | | Yes |
| endpoints_setups | integer | | Yes |
| id | string | | Yes |
| installation_id | string | | Yes |
| meta | object | | Yes |
| name | string | | Yes |
| plugin_id | string | | Yes |
| plugin_unique_identifier | string | | Yes |
| runtime_type | string | | Yes |
| source | [PluginInstallationSource](#plugininstallationsource) | | Yes |
| tenant_id | string | | Yes |
| updated_at | dateTime | | Yes |
| version | string | | Yes |
#### PluginCategoryListQuery
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| page | integer, <br>**Default:** 1 | Page number | No |
| page_size | integer, <br>**Default:** 256 | Page size (1-256) | No |
#### PluginCategoryListResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| builtin_tools | [ [PluginCategoryBuiltinToolProviderResponse](#plugincategorybuiltintoolproviderresponse) ] | | Yes |
| has_more | boolean | | Yes |
| plugins | [ [PluginCategoryInstalledPluginResponse](#plugincategoryinstalledpluginresponse) ] | | Yes |
#### PluginDaemonOperationResponse
| Name | Type | Description | Required |
@ -16372,6 +16576,32 @@ Shared permission levels for resources (datasets, credentials, etc.)
| key | string | | Yes |
| port | integer | | Yes |
#### PluginDeclarationResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| agent_strategy | object | | No |
| author | string | | Yes |
| category | [PluginCategory](#plugincategory) | | Yes |
| created_at | dateTime | | Yes |
| datasource | object | | No |
| description | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | | Yes |
| endpoint | object | | No |
| icon | string | | Yes |
| icon_dark | string | | No |
| label | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | | Yes |
| meta | object | | Yes |
| model | [ProviderEntityResponse](#providerentityresponse) | | No |
| name | string | | Yes |
| plugins | object | | Yes |
| repo | string | | No |
| resource | object | | Yes |
| tags | [ string ] | | No |
| tool | object | | No |
| trigger | object | | No |
| verified | boolean | | No |
| version | string | | Yes |
#### PluginDependency
| Name | Type | Description | Required |
@ -16405,6 +16635,12 @@ Shared permission levels for resources (datasets, credentials, etc.)
| ---- | ---- | ----------- | -------- |
| PluginInstallationScope | string | | |
#### PluginInstallationSource
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| PluginInstallationSource | string | | |
#### PluginInstallationsResponse
| Name | Type | Description | Required |
@ -16457,13 +16693,6 @@ Shared permission levels for resources (datasets, credentials, etc.)
| debug_permission | [DebugPermission](#debugpermission) | | No |
| install_permission | [InstallPermission](#installpermission) | | No |
#### PluginPreferencesResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| auto_upgrade | [PluginAutoUpgradeSettingsPayload](#pluginautoupgradesettingspayload) | | Yes |
| permission | [PluginPermissionSettingsPayload](#pluginpermissionsettingspayload) | | Yes |
#### PluginReadmeResponse
| Name | Type | Description | Required |
@ -16550,14 +16779,35 @@ Model class for provider credential schema.
| error | string | | No |
| result | string, <br>**Available values:** "error", "success" | *Enum:* `"error"`, `"success"` | Yes |
#### ProviderEntityResponse
Runtime provider response with codegen-safe model pricing schemas.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| background | string | | No |
| configurate_methods | [ [ConfigurateMethod](#configuratemethod) ] | | Yes |
| description | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | No |
| help | [ProviderHelpEntity](#providerhelpentity) | | No |
| icon_small | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | No |
| icon_small_dark | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | No |
| label | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes |
| model_credential_schema | [ModelCredentialSchema](#modelcredentialschema) | | No |
| models | [ [AIModelEntityResponse](#aimodelentityresponse) ], <br>**Default:** | | No |
| position | object | | No |
| provider | string | | Yes |
| provider_credential_schema | [ProviderCredentialSchema](#providercredentialschema) | | No |
| provider_name | string | | No |
| supported_model_types | [ [ModelType](#modeltype) ] | | Yes |
#### ProviderHelpEntity
Model class for provider help.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| title | [I18nObject](#i18nobject) | | Yes |
| url | [I18nObject](#i18nobject) | | Yes |
| title | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes |
| url | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes |
#### ProviderModelWithStatusEntity
@ -17473,6 +17723,19 @@ Query parameters for listing snippet published workflows.
| updated_by | [SimpleAccount](#simpleaccount) | | No |
| version | string | | Yes |
#### StarredAppListQuery
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| creator_ids | [ string ] | Filter by creator account IDs | No |
| is_created_by_me | boolean | Filter by creator | No |
| limit | integer, <br>**Default:** 20 | Page size (1-100) | No |
| mode | string, <br>**Available values:** "advanced-chat", "agent", "agent-chat", "all", "channel", "chat", "completion", "workflow", <br>**Default:** all | App mode filter<br>*Enum:* `"advanced-chat"`, `"agent"`, `"agent-chat"`, `"all"`, `"channel"`, `"chat"`, `"completion"`, `"workflow"` | No |
| name | string | Filter by app name | No |
| page | integer, <br>**Default:** 1 | Page number (1-99999) | No |
| sort_by | string, <br>**Available values:** "earliest_created", "last_modified", "recently_created", <br>**Default:** last_modified | Sort apps by last modified, recently created, or earliest created<br>*Enum:* `"earliest_created"`, `"last_modified"`, `"recently_created"` | No |
| tag_ids | [ string ] | Filter by tag IDs | No |
#### StatisticTimeRangeQuery
| Name | Type | Description | Required |
@ -17817,6 +18080,14 @@ Tag type
| ---- | ---- | ----------- | -------- |
| ToolProviderOpaqueResponse | | | |
#### ToolProviderType
Enum class for tool provider
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| ToolProviderType | string | Enum class for tool provider | |
#### TraceAppConfigResponse
| Name | Type | Description | Required |
@ -19236,6 +19507,26 @@ Workflow tool configuration
| use_count | integer | | No |
| version | integer | | No |
#### core__tools__entities__common_entities__I18nObject
Model class for i18n object.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| en_US | string | | Yes |
| ja_JP | string | | No |
| pt_BR | string | | No |
| zh_Hans | string | | No |
#### graphon__model_runtime__entities__common_entities__I18nObject
Model class for i18n object.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| en_US | string | | Yes |
| zh_Hans | string | | No |
## FastOpenAPI Preview (OpenAPI 3.1)
### Dify API (FastOpenAPI PoC)

View File

@ -73,6 +73,7 @@ def check_upgradable_plugin_task():
strategy.upgrade_mode,
strategy.exclude_plugins,
strategy.include_plugins,
strategy.category,
)
# Only sleep if batch_interval_time > 0.0001 AND current batch is not the last one

View File

@ -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()

View File

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

View File

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

View File

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

View File

@ -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] = []

View File

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

View File

@ -7,7 +7,7 @@ import click
from celery import shared_task
from core.plugin.entities.marketplace import MarketplacePluginSnapshot
from core.plugin.entities.plugin import PluginInstallationSource
from core.plugin.entities.plugin import PluginInstallation, PluginInstallationSource
from core.plugin.impl.plugin import PluginInstaller
from core.plugin.plugin_service import PluginService
from extensions.ext_redis import redis_client
@ -15,6 +15,7 @@ from models.account import TenantPluginAutoUpgradeStrategy
logger = logging.getLogger(__name__)
PluginCategory = TenantPluginAutoUpgradeStrategy.PluginCategory
RETRY_TIMES_OF_ONE_PLUGIN_IN_ONE_TENANT = 3
CACHE_REDIS_KEY_PREFIX = "plugin_autoupgrade_check_task:cached_plugin_snapshot:"
CACHE_REDIS_TTL = 60 * 60 # 1 hour
@ -72,6 +73,25 @@ def marketplace_batch_fetch_plugin_manifests(
return result
def _normalize_category(category: PluginCategory | str | None) -> str | None:
if category is None:
return None
if isinstance(category, PluginCategory):
return category.value
return str(category)
def _plugin_matches_category(plugin: PluginInstallation, category: str | None) -> bool:
"""Return whether an installed plugin should be checked by a category strategy."""
if category is None:
return True
declaration = getattr(plugin, "declaration", None)
plugin_category = getattr(declaration, "category", None)
plugin_category_value = getattr(plugin_category, "value", plugin_category)
return plugin_category_value == category
@shared_task(queue="plugin")
def process_tenant_plugin_autoupgrade_check_task(
tenant_id: str,
@ -80,13 +100,15 @@ def process_tenant_plugin_autoupgrade_check_task(
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode,
exclude_plugins: list[str],
include_plugins: list[str],
category: PluginCategory | str | None = None,
):
try:
manager = PluginInstaller()
category_value = _normalize_category(category)
click.echo(
click.style(
f"Checking upgradable plugin for tenant: {tenant_id}",
f"Checking upgradable plugin for tenant: {tenant_id}, category: {category_value or 'all'}",
fg="green",
)
)
@ -102,7 +124,11 @@ def process_tenant_plugin_autoupgrade_check_task(
all_plugins = manager.list_plugins(tenant_id)
for plugin in all_plugins:
if plugin.source == PluginInstallationSource.Marketplace and plugin.plugin_id in include_plugins:
if (
plugin.source == PluginInstallationSource.Marketplace
and plugin.plugin_id in include_plugins
and _plugin_matches_category(plugin, category_value)
):
plugin_ids.append(
(
plugin.plugin_id,
@ -117,7 +143,9 @@ def process_tenant_plugin_autoupgrade_check_task(
plugin_ids = [
(plugin.plugin_id, plugin.version, plugin.plugin_unique_identifier)
for plugin in all_plugins
if plugin.source == PluginInstallationSource.Marketplace and plugin.plugin_id not in exclude_plugins
if plugin.source == PluginInstallationSource.Marketplace
and plugin.plugin_id not in exclude_plugins
and _plugin_matches_category(plugin, category_value)
]
elif upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL:
all_plugins = manager.list_plugins(tenant_id)
@ -125,6 +153,7 @@ def process_tenant_plugin_autoupgrade_check_task(
(plugin.plugin_id, plugin.version, plugin.plugin_unique_identifier)
for plugin in all_plugins
if plugin.source == PluginInstallationSource.Marketplace
and _plugin_matches_category(plugin, category_value)
]
if not plugin_ids:

View File

@ -22,6 +22,7 @@ from models import (
AppDatasetJoin,
AppMCPServer,
AppModelConfig,
AppStar,
AppTrigger,
Conversation,
EndUser,
@ -64,6 +65,7 @@ def remove_app_and_related_data_task(self, tenant_id: str, app_id: str):
_delete_app_mcp_servers(tenant_id, app_id)
_delete_app_api_tokens(tenant_id, app_id)
_delete_installed_apps(tenant_id, app_id)
_delete_app_stars(tenant_id, app_id)
_delete_recommended_apps(tenant_id, app_id)
_delete_app_annotation_data(tenant_id, app_id)
_delete_app_dataset_joins(tenant_id, app_id)
@ -173,6 +175,18 @@ def _delete_installed_apps(tenant_id: str, app_id: str):
)
def _delete_app_stars(tenant_id: str, app_id: str):
def del_app_star(session, app_star_id: str):
session.execute(delete(AppStar).where(AppStar.id == app_star_id).execution_options(synchronize_session=False))
_delete_records(
"""select id from app_stars where tenant_id=:tenant_id and app_id=:app_id limit 1000""",
{"tenant_id": tenant_id, "app_id": app_id},
del_app_star,
"app star",
)
def _delete_recommended_apps(tenant_id: str, app_id: str):
def del_recommended_app(session, recommended_app_id: str):
session.execute(

View File

@ -7,6 +7,8 @@ from models.account import TenantPluginAutoUpgradeStrategy, TenantPluginPermissi
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
from services.plugin.plugin_permission_service import PluginPermissionService
PLUGIN_CATEGORY = TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL
@pytest.fixture
def tenant(flask_req_ctx):
@ -71,7 +73,7 @@ class TestPluginPermissionLifecycle:
class TestPluginAutoUpgradeLifecycle:
def test_get_returns_none_for_new_tenant(self, tenant):
assert PluginAutoUpgradeService.get_strategy(tenant) is None
assert PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY) is None
def test_change_creates_row(self, tenant):
result = PluginAutoUpgradeService.change_strategy(
@ -81,10 +83,11 @@ class TestPluginAutoUpgradeLifecycle:
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
exclude_plugins=[],
include_plugins=[],
category=PLUGIN_CATEGORY,
)
assert result is True
strategy = PluginAutoUpgradeService.get_strategy(tenant)
strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY)
assert strategy is not None
assert strategy.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST
assert strategy.upgrade_time_of_day == 3
@ -97,6 +100,7 @@ class TestPluginAutoUpgradeLifecycle:
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
exclude_plugins=[],
include_plugins=[],
category=PLUGIN_CATEGORY,
)
PluginAutoUpgradeService.change_strategy(
tenant,
@ -105,9 +109,10 @@ class TestPluginAutoUpgradeLifecycle:
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL,
exclude_plugins=[],
include_plugins=["plugin-a"],
category=PLUGIN_CATEGORY,
)
strategy = PluginAutoUpgradeService.get_strategy(tenant)
strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY)
assert strategy is not None
assert strategy.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST
assert strategy.upgrade_time_of_day == 12
@ -115,9 +120,9 @@ class TestPluginAutoUpgradeLifecycle:
assert strategy.include_plugins == ["plugin-a"]
def test_exclude_plugin_creates_strategy_when_none_exists(self, tenant):
PluginAutoUpgradeService.exclude_plugin(tenant, "my-plugin")
PluginAutoUpgradeService.exclude_plugin(tenant, "my-plugin", PLUGIN_CATEGORY)
strategy = PluginAutoUpgradeService.get_strategy(tenant)
strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY)
assert strategy is not None
assert strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
assert "my-plugin" in strategy.exclude_plugins
@ -130,10 +135,11 @@ class TestPluginAutoUpgradeLifecycle:
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
exclude_plugins=["existing"],
include_plugins=[],
category=PLUGIN_CATEGORY,
)
PluginAutoUpgradeService.exclude_plugin(tenant, "new-plugin")
PluginAutoUpgradeService.exclude_plugin(tenant, "new-plugin", PLUGIN_CATEGORY)
strategy = PluginAutoUpgradeService.get_strategy(tenant)
strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY)
assert strategy is not None
assert "existing" in strategy.exclude_plugins
assert "new-plugin" in strategy.exclude_plugins
@ -146,10 +152,11 @@ class TestPluginAutoUpgradeLifecycle:
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
exclude_plugins=["same-plugin"],
include_plugins=[],
category=PLUGIN_CATEGORY,
)
PluginAutoUpgradeService.exclude_plugin(tenant, "same-plugin")
PluginAutoUpgradeService.exclude_plugin(tenant, "same-plugin", PLUGIN_CATEGORY)
strategy = PluginAutoUpgradeService.get_strategy(tenant)
strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY)
assert strategy is not None
assert strategy.exclude_plugins.count("same-plugin") == 1
@ -161,10 +168,11 @@ class TestPluginAutoUpgradeLifecycle:
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL,
exclude_plugins=[],
include_plugins=["p1", "p2"],
category=PLUGIN_CATEGORY,
)
PluginAutoUpgradeService.exclude_plugin(tenant, "p1")
PluginAutoUpgradeService.exclude_plugin(tenant, "p1", PLUGIN_CATEGORY)
strategy = PluginAutoUpgradeService.get_strategy(tenant)
strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY)
assert strategy is not None
assert "p1" not in strategy.include_plugins
assert "p2" in strategy.include_plugins
@ -177,10 +185,11 @@ class TestPluginAutoUpgradeLifecycle:
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
exclude_plugins=[],
include_plugins=[],
category=PLUGIN_CATEGORY,
)
PluginAutoUpgradeService.exclude_plugin(tenant, "excluded-plugin")
PluginAutoUpgradeService.exclude_plugin(tenant, "excluded-plugin", PLUGIN_CATEGORY)
strategy = PluginAutoUpgradeService.get_strategy(tenant)
strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY)
assert strategy is not None
assert strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
assert "excluded-plugin" in strategy.exclude_plugins

View File

@ -51,6 +51,7 @@ def _create_recommended_app(
categories: list[str] | None = None,
language: str = "en-US",
is_listed: bool = True,
is_learn_dify: bool = False,
position: int = 1,
) -> RecommendedApp:
rec = RecommendedApp(
@ -62,6 +63,7 @@ def _create_recommended_app(
categories=[category] if categories is None else categories,
language=language,
is_listed=is_listed,
is_learn_dify=is_learn_dify,
position=position,
)
rec.id = str(uuid4())
@ -205,6 +207,65 @@ class TestFetchRecommendedAppsFromDb:
app_ids = {r["app_id"] for r in result["recommended_apps"]}
assert app1.id not in app_ids
def test_fetch_learn_dify_apps_uses_flag_not_categories(
self,
flask_app_with_containers,
db_session_with_containers: Session,
):
tenant_id = str(uuid4())
learn_dify_app = _create_app(db_session_with_containers, tenant_id=tenant_id)
_create_site(db_session_with_containers, app_id=learn_dify_app.id)
_create_recommended_app(
db_session_with_containers,
app_id=learn_dify_app.id,
category="workflow",
categories=["Workflow"],
is_learn_dify=True,
)
category_only_app = _create_app(db_session_with_containers, tenant_id=tenant_id)
_create_site(db_session_with_containers, app_id=category_only_app.id)
_create_recommended_app(
db_session_with_containers,
app_id=category_only_app.id,
category="Learn Dify",
categories=["Learn Dify"],
is_learn_dify=False,
)
db_session_with_containers.expire_all()
result = DatabaseRecommendAppRetrieval.fetch_learn_dify_apps_from_db("en-US")
app_ids = {r["app_id"] for r in result["recommended_apps"]}
assert learn_dify_app.id in app_ids
assert category_only_app.id not in app_ids
recommended_app = next(item for item in result["recommended_apps"] if item["app_id"] == learn_dify_app.id)
assert recommended_app["categories"] == ["Workflow"]
def test_fetch_learn_dify_apps_falls_back_to_default_language(
self,
flask_app_with_containers,
db_session_with_containers: Session,
):
tenant_id = str(uuid4())
learn_dify_app = _create_app(db_session_with_containers, tenant_id=tenant_id)
_create_site(db_session_with_containers, app_id=learn_dify_app.id)
_create_recommended_app(
db_session_with_containers,
app_id=learn_dify_app.id,
categories=["Workflow"],
is_learn_dify=True,
language="en-US",
)
db_session_with_containers.expire_all()
result = DatabaseRecommendAppRetrieval.fetch_learn_dify_apps_from_db("fr-FR")
app_ids = {r["app_id"] for r in result["recommended_apps"]}
assert learn_dify_app.id in app_ids
class TestFetchRecommendedAppDetailFromDb:
def test_returns_none_when_not_listed(self, flask_app_with_containers: Flask, db_session_with_containers: Session):

View File

@ -1,6 +1,8 @@
from datetime import datetime
from unittest.mock import create_autospec, patch
import pytest
import sqlalchemy as sa
from faker import Faker
from pydantic import ValidationError
from sqlalchemy.orm import Session
@ -245,6 +247,236 @@ class TestAppService:
assert app.tenant_id == tenant.id
assert app.mode == "chat"
def test_get_paginate_apps_sorts_by_modified_and_created_times(
self, db_session_with_containers: Session, mock_external_service_dependencies
):
"""
Test app list sort options for modified time and creation time.
"""
fake = Faker()
account = AccountService.create_account(
email=fake.email(),
name=fake.name(),
interface_language="en-US",
password=generate_valid_password(fake),
)
TenantService.create_owner_tenant_if_not_exist(account, name=fake.company())
tenant = account.current_tenant
from services.app_service import AppListParams, AppService, CreateAppParams
app_service = AppService()
oldest_created = app_service.create_app(
tenant.id,
CreateAppParams(name="Oldest Created", mode="chat", icon_type="emoji", icon="1"),
account,
)
newest_modified = app_service.create_app(
tenant.id,
CreateAppParams(name="Newest Modified", mode="chat", icon_type="emoji", icon="2"),
account,
)
newest_created = app_service.create_app(
tenant.id,
CreateAppParams(name="Newest Created", mode="chat", icon_type="emoji", icon="3"),
account,
)
timestamp_by_app_id = {
oldest_created.id: (datetime(2026, 1, 1, 10, 0, 0), datetime(2026, 1, 1, 10, 0, 0)),
newest_modified.id: (datetime(2026, 1, 2, 10, 0, 0), datetime(2026, 1, 4, 10, 0, 0)),
newest_created.id: (datetime(2026, 1, 3, 10, 0, 0), datetime(2026, 1, 3, 10, 0, 0)),
}
for app_id, (created_at, updated_at) in timestamp_by_app_id.items():
db_session_with_containers.execute(
sa.update(App).where(App.id == app_id).values(created_at=created_at, updated_at=updated_at)
)
db_session_with_containers.commit()
last_modified_apps = app_service.get_paginate_apps(
account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat")
)
assert last_modified_apps is not None
assert [app.name for app in last_modified_apps.items] == [
"Newest Modified",
"Newest Created",
"Oldest Created",
]
recently_created_apps = app_service.get_paginate_apps(
account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat", sort_by="recently_created")
)
assert recently_created_apps is not None
assert [app.name for app in recently_created_apps.items] == [
"Newest Created",
"Newest Modified",
"Oldest Created",
]
earliest_created_apps = app_service.get_paginate_apps(
account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat", sort_by="earliest_created")
)
assert earliest_created_apps is not None
assert [app.name for app in earliest_created_apps.items] == [
"Oldest Created",
"Newest Modified",
"Newest Created",
]
def test_get_paginate_apps_marks_starred_apps(
self, db_session_with_containers: Session, mock_external_service_dependencies
):
"""
Test app list marks apps starred by the current account.
"""
fake = Faker()
account = AccountService.create_account(
email=fake.email(),
name=fake.name(),
interface_language="en-US",
password=generate_valid_password(fake),
)
TenantService.create_owner_tenant_if_not_exist(account, name=fake.company())
tenant = account.current_tenant
from models import AppStar
from services.app_service import AppListParams, AppService, CreateAppParams
app_service = AppService()
starred_app = app_service.create_app(
tenant.id,
CreateAppParams(name="Starred App", mode="chat", icon_type="emoji", icon="1"),
account,
)
unstarred_app = app_service.create_app(
tenant.id,
CreateAppParams(name="Unstarred App", mode="chat", icon_type="emoji", icon="2"),
account,
)
app_service.star_app(db_session_with_containers, app=starred_app, account_id=account.id)
app_service.star_app(db_session_with_containers, app=starred_app, account_id=account.id)
db_session_with_containers.commit()
star_count = db_session_with_containers.scalar(
sa.select(sa.func.count()).select_from(AppStar).where(AppStar.app_id == starred_app.id)
)
assert star_count == 1
paginated_apps = app_service.get_paginate_apps(
account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat")
)
assert paginated_apps is not None
starred_by_app_id = {app.id: app.is_starred for app in paginated_apps.items}
assert starred_by_app_id[starred_app.id] is True
assert starred_by_app_id[unstarred_app.id] is False
app_service.unstar_app(db_session_with_containers, app=starred_app, account_id=account.id)
db_session_with_containers.commit()
paginated_apps = app_service.get_paginate_apps(
account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat")
)
assert paginated_apps is not None
starred_by_app_id = {app.id: app.is_starred for app in paginated_apps.items}
assert starred_by_app_id[starred_app.id] is False
assert starred_by_app_id[unstarred_app.id] is False
def test_get_paginate_starred_apps_returns_only_starred_apps_with_requested_sort(
self, db_session_with_containers: Session, mock_external_service_dependencies
):
"""
Test starred app list returns only starred apps ordered by requested app sort.
"""
fake = Faker()
account = AccountService.create_account(
email=fake.email(),
name=fake.name(),
interface_language="en-US",
password=generate_valid_password(fake),
)
TenantService.create_owner_tenant_if_not_exist(account, name=fake.company())
tenant = account.current_tenant
from services.app_service import AppService, CreateAppParams, StarredAppListParams
app_service = AppService()
oldest_created_app = app_service.create_app(
tenant.id,
CreateAppParams(name="Oldest Created Starred App", mode="chat", icon_type="emoji", icon="1"),
account,
)
newest_modified_app = app_service.create_app(
tenant.id,
CreateAppParams(name="Newest Modified Starred App", mode="chat", icon_type="emoji", icon="2"),
account,
)
newest_created_app = app_service.create_app(
tenant.id,
CreateAppParams(name="Newest Created Starred App", mode="chat", icon_type="emoji", icon="3"),
account,
)
unstarred_app = app_service.create_app(
tenant.id,
CreateAppParams(name="Unstarred App", mode="chat", icon_type="emoji", icon="4"),
account,
)
app_service.star_app(db_session_with_containers, app=oldest_created_app, account_id=account.id)
app_service.star_app(db_session_with_containers, app=newest_modified_app, account_id=account.id)
app_service.star_app(db_session_with_containers, app=newest_created_app, account_id=account.id)
timestamp_by_app_id = {
oldest_created_app.id: (datetime(2026, 1, 1, 10, 0, 0), datetime(2026, 1, 1, 10, 0, 0)),
newest_modified_app.id: (datetime(2026, 1, 2, 10, 0, 0), datetime(2026, 1, 4, 10, 0, 0)),
newest_created_app.id: (datetime(2026, 1, 3, 10, 0, 0), datetime(2026, 1, 3, 10, 0, 0)),
unstarred_app.id: (datetime(2026, 1, 5, 10, 0, 0), datetime(2026, 1, 5, 10, 0, 0)),
}
for app_id, (created_at, updated_at) in timestamp_by_app_id.items():
db_session_with_containers.execute(
sa.update(App).where(App.id == app_id).values(created_at=created_at, updated_at=updated_at)
)
db_session_with_containers.commit()
last_modified_apps = app_service.get_paginate_starred_apps(
account.id, tenant.id, StarredAppListParams(page=1, limit=10, mode="chat")
)
assert last_modified_apps is not None
assert [app.name for app in last_modified_apps.items] == [
"Newest Modified Starred App",
"Newest Created Starred App",
"Oldest Created Starred App",
]
assert all(app.is_starred for app in last_modified_apps.items)
assert unstarred_app.id not in {app.id for app in last_modified_apps.items}
recently_created_apps = app_service.get_paginate_starred_apps(
account.id,
tenant.id,
StarredAppListParams(page=1, limit=10, mode="chat", sort_by="recently_created"),
)
assert recently_created_apps is not None
assert [app.name for app in recently_created_apps.items] == [
"Newest Created Starred App",
"Newest Modified Starred App",
"Oldest Created Starred App",
]
earliest_created_apps = app_service.get_paginate_starred_apps(
account.id,
tenant.id,
StarredAppListParams(page=1, limit=10, mode="chat", sort_by="earliest_created"),
)
assert earliest_created_apps is not None
assert [app.name for app in earliest_created_apps.items] == [
"Oldest Created Starred App",
"Newest Modified Starred App",
"Newest Created Starred App",
]
def test_get_paginate_apps_with_filters(
self, db_session_with_containers: Session, mock_external_service_dependencies
):

View File

@ -263,6 +263,54 @@ class TestRecommendedAppServiceGetDetail:
mock_factory_class.get_recommend_app_factory.assert_called_with(mode)
# ── Pure logic tests: get_learn_dify_apps ──────────────────────────────
class TestRecommendedAppServiceGetLearnDifyApps:
def test_returns_database_learn_dify_apps_without_remote_factory(self, monkeypatch: pytest.MonkeyPatch) -> None:
expected_app = RecommendedAppPayload(app_id="app-1", category="Workflow")
mock_database_retrieval = MagicMock()
mock_database_retrieval.fetch_learn_dify_apps_from_db.return_value = {
"recommended_apps": [expected_app],
"categories": ["Workflow"],
}
monkeypatch.setattr(service_module, "DatabaseRecommendAppRetrieval", mock_database_retrieval)
monkeypatch.setattr(
service_module.FeatureService,
"get_system_features",
MagicMock(return_value=SimpleNamespace(enable_trial_app=False)),
)
factory_mock = MagicMock()
monkeypatch.setattr(service_module.RecommendAppRetrievalFactory, "get_recommend_app_factory", factory_mock)
result = RecommendedAppService.get_learn_dify_apps(db.session, "en-US")
assert result == {"recommended_apps": [expected_app]}
mock_database_retrieval.fetch_learn_dify_apps_from_db.assert_called_once_with("en-US")
factory_mock.assert_not_called()
def test_sets_can_trial_when_trial_feature_enabled(self, monkeypatch: pytest.MonkeyPatch) -> None:
app = RecommendedAppPayload(app_id="app-1", category="Workflow")
mock_database_retrieval = MagicMock()
mock_database_retrieval.fetch_learn_dify_apps_from_db.return_value = {
"recommended_apps": [app],
"categories": ["Workflow"],
}
monkeypatch.setattr(service_module, "DatabaseRecommendAppRetrieval", mock_database_retrieval)
monkeypatch.setattr(
service_module.FeatureService,
"get_system_features",
MagicMock(return_value=SimpleNamespace(enable_trial_app=True)),
)
can_trial_mock = MagicMock(return_value=True)
monkeypatch.setattr(RecommendedAppService, "_can_trial_app", can_trial_mock)
result = RecommendedAppService.get_learn_dify_apps(db.session, "en-US")
assert result["recommended_apps"][0]["can_trial"] is True
can_trial_mock.assert_called_once_with(db.session, "app-1")
# ── Integration tests: trial app features (real DB) ────────────────────

View File

@ -79,6 +79,46 @@ class TestRecommendedAppListApi:
assert result == result_data
class TestLearnDifyAppListApi:
def test_get_with_language_param(self, app: Flask):
api = module.LearnDifyAppListApi()
method = unwrap(api.get)
result_data = {"recommended_apps": []}
with (
app.test_request_context("/", query_string={"language": "en-US"}),
patch.object(
module.RecommendedAppService,
"get_learn_dify_apps",
return_value=result_data,
) as service_mock,
):
result = method(api, make_account("fr-FR"))
service_mock.assert_called_once_with(ANY, "en-US")
assert result == result_data
def test_get_fallback_to_user_language(self, app: Flask):
api = module.LearnDifyAppListApi()
method = unwrap(api.get)
result_data = {"recommended_apps": []}
with (
app.test_request_context("/", query_string={"language": "invalid"}),
patch.object(
module.RecommendedAppService,
"get_learn_dify_apps",
return_value=result_data,
) as service_mock,
):
result = method(api, make_account("fr-FR"))
service_mock.assert_called_once_with(ANY, "fr-FR")
assert result == result_data
class TestRecommendedAppApi:
def test_get_success(self, app: Flask):
api = module.RecommendedAppApi()
@ -144,3 +184,29 @@ class TestRecommendedAppResponseModels:
assert response["recommended_apps"][0]["app_id"] == "app-1"
assert response["recommended_apps"][0]["categories"] == ["cat", "other"]
assert response["categories"] == ["cat"]
def test_learn_dify_app_list_response_serialization(self):
response = module.LearnDifyAppListResponse.model_validate(
{
"recommended_apps": [
{
"app": {
"id": "app-1",
"name": "App",
"mode": "chat",
"icon": "icon.png",
"icon_type": "emoji",
"icon_background": "#fff",
},
"app_id": "app-1",
"description": "desc",
"categories": ["Workflow"],
"position": 1,
"is_listed": True,
}
],
}
).model_dump(mode="json")
assert response["recommended_apps"][0]["app_id"] == "app-1"
assert response["recommended_apps"][0]["categories"] == ["Workflow"]

View File

@ -1,5 +1,7 @@
import io
from datetime import datetime
from inspect import unwrap
from typing import Any
from unittest.mock import MagicMock, patch
import pytest
@ -10,12 +12,14 @@ from werkzeug.exceptions import Forbidden
from controllers.console.workspace.plugin import (
PluginAssetApi,
PluginAutoUpgradeExcludePluginApi,
PluginCategoryListApi,
PluginChangeAutoUpgradeApi,
PluginChangePermissionApi,
PluginChangePreferencesApi,
PluginDebuggingKeyApi,
PluginDeleteAllInstallTaskItemsApi,
PluginDeleteInstallTaskApi,
PluginDeleteInstallTaskItemApi,
PluginFetchAutoUpgradeApi,
PluginFetchDynamicSelectOptionsApi,
PluginFetchDynamicSelectOptionsWithCredentialsApi,
PluginFetchInstallTaskApi,
@ -23,7 +27,6 @@ from controllers.console.workspace.plugin import (
PluginFetchManifestApi,
PluginFetchMarketplacePkgApi,
PluginFetchPermissionApi,
PluginFetchPreferencesApi,
PluginIconApi,
PluginInstallFromGithubApi,
PluginInstallFromMarketplaceApi,
@ -43,6 +46,69 @@ from core.plugin.impl.exc import PluginDaemonClientSideError
from models.account import Account, TenantAccountRole, TenantPluginAutoUpgradeStrategy, TenantPluginPermission
def _plugin_category_list_item(category: str = "tool") -> dict[str, Any]:
now = datetime(2023, 1, 1, 0, 0, 0)
return {
"id": "entity-1",
"created_at": now,
"updated_at": now,
"tenant_id": "t1",
"endpoints_setups": 0,
"endpoints_active": 0,
"runtime_type": "remote",
"source": "marketplace",
"meta": {},
"plugin_id": "test-author/test-plugin",
"plugin_unique_identifier": "test-author/test-plugin:1.0.0@checksum",
"version": "1.0.0",
"checksum": "checksum",
"name": "test-plugin",
"installation_id": "entity-1",
"declaration": {
"version": "1.0.0",
"author": "test-author",
"name": "test-plugin",
"description": {"en_US": "Test plugin"},
"icon": "icon.svg",
"label": {"en_US": "Test Plugin"},
"category": category,
"created_at": now,
"resource": {"memory": 268435456, "permission": None},
"plugins": {"tools": ["provider/test.yaml"]},
"meta": {"version": "1.0.0"},
"tool": {
"identity": {
"author": "test-author",
"name": "test-plugin",
"description": {"en_US": "Test plugin"},
"icon": "icon.svg",
"label": {"en_US": "Test Plugin"},
}
},
},
}
def _builtin_tool_provider_item() -> dict[str, Any]:
return {
"id": "builtin",
"author": "dify",
"name": "builtin",
"plugin_id": "",
"plugin_unique_identifier": "",
"description": {"en_US": "Builtin tool provider"},
"icon": "icon.svg",
"icon_dark": "",
"label": {"en_US": "Builtin"},
"type": "builtin",
"team_credentials": {},
"is_team_authorization": False,
"allow_delete": True,
"tools": [],
"labels": [],
}
def _account(role: TenantAccountRole = TenantAccountRole.OWNER) -> Account:
account = Account(name="Test User", email="u1@example.com")
account.id = "u1"
@ -142,6 +208,83 @@ class TestPluginListApi:
mock_list_with_total.assert_called_once_with("t1", "u1", 1, 10)
class TestPluginCategoryListApi:
def test_plugin_category_list(self, app: Flask):
api = PluginCategoryListApi()
method = unwrap(api.get)
plugin_item = _plugin_category_list_item()
builtin_item = _builtin_tool_provider_item()
mock_list = MagicMock(list=[plugin_item], has_more=True)
with (
app.test_request_context("/?page=2&page_size=10"),
patch(
"controllers.console.workspace.plugin.PluginService.list_by_category", return_value=mock_list
) as list_mock,
patch(
"controllers.console.workspace.plugin._list_hardcoded_builtin_tool_providers",
return_value=[builtin_item],
) as builtin_mock,
):
result = method(api, "t1", "tool")
list_mock.assert_called_once()
assert list_mock.call_args.args[0] == "t1"
assert list_mock.call_args.args[1] == "tool"
assert list_mock.call_args.args[2] == 2
assert list_mock.call_args.args[3] == 10
assert result["plugins"][0]["id"] == "entity-1"
assert result["plugins"][0]["plugin_unique_identifier"] == "test-author/test-plugin:1.0.0@checksum"
assert result["builtin_tools"][0]["id"] == "builtin"
assert result["builtin_tools"][0]["type"] == "builtin"
assert result["has_more"] is True
assert "total" not in result
builtin_mock.assert_called_once_with("t1")
def test_non_tool_category_does_not_include_builtin_tools(self, app: Flask):
api = PluginCategoryListApi()
method = unwrap(api.get)
mock_list = MagicMock(list=[_plugin_category_list_item(category="datasource")], has_more=False)
with (
app.test_request_context("/?page=1&page_size=10"),
patch("controllers.console.workspace.plugin.PluginService.list_by_category", return_value=mock_list),
patch("controllers.console.workspace.plugin._list_hardcoded_builtin_tool_providers") as builtin_mock,
):
result = method(api, "t1", "datasource")
assert result["plugins"][0]["id"] == "entity-1"
assert result["builtin_tools"] == []
assert result["has_more"] is False
builtin_mock.assert_not_called()
def test_invalid_category(self, app: Flask):
api = PluginCategoryListApi()
method = unwrap(api.get)
with (
app.test_request_context("/?page=1&page_size=10"),
):
result = method(api, "t1", "unknown")
assert result == ({"code": "invalid_param", "message": "invalid plugin category"}, 400)
def test_daemon_error(self, app: Flask):
api = PluginCategoryListApi()
method = unwrap(api.get)
with (
app.test_request_context("/?page=1&page_size=10"),
patch(
"controllers.console.workspace.plugin.PluginService.list_by_category",
side_effect=PluginDaemonClientSideError("error"),
),
):
result = method(api, "t1", "tool")
assert result == ({"code": "plugin_error", "message": "error"}, 400)
class TestPluginIconApi:
def test_plugin_icon(self, app: Flask):
api = PluginIconApi()
@ -857,18 +1000,15 @@ class TestPluginFetchDynamicSelectOptionsWithCredentialsApi:
assert result == ({"code": "plugin_error", "message": "error"}, 400)
class TestPluginChangePreferencesApi:
class TestPluginChangeAutoUpgradeApi:
def test_success(self, app: Flask):
api = PluginChangePreferencesApi()
api = PluginChangeAutoUpgradeApi()
method = unwrap(api.post)
user = _account()
payload = {
"permission": {
"install_permission": TenantPluginPermission.InstallPermission.EVERYONE,
"debug_permission": TenantPluginPermission.DebugPermission.EVERYONE,
},
"category": TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value,
"auto_upgrade": {
"strategy_setting": TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
"upgrade_time_of_day": 0,
@ -880,24 +1020,52 @@ class TestPluginChangePreferencesApi:
with (
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.plugin.PluginPermissionService.change_permission", return_value=True),
patch("controllers.console.workspace.plugin.PluginAutoUpgradeService.change_strategy", return_value=True),
patch(
"controllers.console.workspace.plugin.PluginAutoUpgradeService.change_strategy", return_value=True
) as change,
):
result = method(api, "t1", user)
assert result["success"] is True
change.assert_called_once()
def test_permission_fail(self, app: Flask):
api = PluginChangePreferencesApi()
def test_success_with_model_category_auto_upgrade(self, app: Flask):
api = PluginChangeAutoUpgradeApi()
method = unwrap(api.post)
user = _account()
payload = {
"permission": {
"install_permission": TenantPluginPermission.InstallPermission.EVERYONE,
"debug_permission": TenantPluginPermission.DebugPermission.EVERYONE,
"category": TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL.value,
"auto_upgrade": {
"strategy_setting": TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST,
"upgrade_time_of_day": 3600,
"upgrade_mode": TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
"exclude_plugins": [],
"include_plugins": [],
},
}
with (
app.test_request_context("/", json=payload),
patch(
"controllers.console.workspace.plugin.PluginAutoUpgradeService.change_strategy", return_value=True
) as change,
):
result = method(api, "t1", user)
assert result["success"] is True
change.assert_called_once()
assert change.call_args.kwargs["category"] == TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL
def test_auto_upgrade_fail(self, app: Flask):
api = PluginChangeAutoUpgradeApi()
method = unwrap(api.post)
user = MagicMock(is_admin_or_owner=True)
payload = {
"category": TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value,
"auto_upgrade": {
"strategy_setting": TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
"upgrade_time_of_day": 0,
@ -909,24 +1077,20 @@ class TestPluginChangePreferencesApi:
with (
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.plugin.PluginPermissionService.change_permission", return_value=False),
patch("controllers.console.workspace.plugin.PluginAutoUpgradeService.change_strategy", return_value=False),
):
result = method(api, "t1", user)
assert result["success"] is False
class TestPluginFetchPreferencesApi:
class TestPluginFetchAutoUpgradeApi:
def test_success(self, app: Flask):
api = PluginFetchPreferencesApi()
api = PluginFetchAutoUpgradeApi()
method = unwrap(api.get)
permission = MagicMock(
install_permission=TenantPluginPermission.InstallPermission.EVERYONE,
debug_permission=TenantPluginPermission.DebugPermission.EVERYONE,
)
auto_upgrade = MagicMock(
category=TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL,
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
upgrade_time_of_day=1,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
@ -935,18 +1099,16 @@ class TestPluginFetchPreferencesApi:
)
with (
app.test_request_context("/"),
app.test_request_context(f"/?category={TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value}"),
patch(
"controllers.console.workspace.plugin.PluginPermissionService.get_permission", return_value=permission
),
patch(
"controllers.console.workspace.plugin.PluginAutoUpgradeService.get_strategy", return_value=auto_upgrade
"controllers.console.workspace.plugin.PluginAutoUpgradeService.get_strategy",
return_value=auto_upgrade,
),
):
result = method(api, "t1")
assert "permission" in result
assert "auto_upgrade" in result
assert result["category"] == TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL
assert result["auto_upgrade"]["upgrade_time_of_day"] == 1
class TestPluginAutoUpgradeExcludePluginApi:
@ -954,7 +1116,7 @@ class TestPluginAutoUpgradeExcludePluginApi:
api = PluginAutoUpgradeExcludePluginApi()
method = unwrap(api.post)
payload = {"plugin_id": "p"}
payload = {"plugin_id": "p", "category": TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value}
with (
app.test_request_context("/", json=payload),
@ -968,7 +1130,7 @@ class TestPluginAutoUpgradeExcludePluginApi:
api = PluginAutoUpgradeExcludePluginApi()
method = unwrap(api.post)
payload = {"plugin_id": "p"}
payload = {"plugin_id": "p", "category": TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value}
with (
app.test_request_context("/", json=payload),

View File

@ -53,6 +53,12 @@ def make_tenant(
return tenant
def make_membership(*, last_opened_at=None) -> MagicMock:
membership = MagicMock()
membership.last_opened_at = last_opened_at
return membership
def make_account_with_tenant(tenant: Tenant) -> Account:
account = make_account()
account._current_tenant = tenant
@ -66,13 +72,17 @@ class TestTenantListApi:
tenant1 = make_tenant("t1", name="Tenant 1")
tenant2 = make_tenant("t2", name="Tenant 2")
last_opened_at = naive_utc_now()
user = make_account()
with (
app.test_request_context("/workspaces"),
patch(
"controllers.console.workspace.workspace.TenantService.get_join_tenants",
return_value=[tenant1, tenant2],
"controllers.console.workspace.workspace.TenantService.get_workspaces_for_account",
return_value=[
(tenant1, make_membership(last_opened_at=last_opened_at)),
(tenant2, make_membership()),
],
),
patch("controllers.console.workspace.workspace.dify_config.ENTERPRISE_ENABLED", False),
patch("controllers.console.workspace.workspace.dify_config.BILLING_ENABLED", True),
@ -92,7 +102,9 @@ class TestTenantListApi:
assert len(result["workspaces"]) == 2
assert result["workspaces"][0]["current"] is True
assert result["workspaces"][0]["plan"] == CloudPlan.TEAM
assert result["workspaces"][0]["last_opened_at"] == int(last_opened_at.timestamp())
assert result["workspaces"][1]["plan"] == CloudPlan.PROFESSIONAL
assert result["workspaces"][1]["last_opened_at"] is None
get_plan_bulk_mock.assert_called_once_with(["t1", "t2"])
get_features_mock.assert_not_called()
@ -116,8 +128,8 @@ class TestTenantListApi:
with (
app.test_request_context("/workspaces"),
patch(
"controllers.console.workspace.workspace.TenantService.get_join_tenants",
return_value=[tenant1, tenant2],
"controllers.console.workspace.workspace.TenantService.get_workspaces_for_account",
return_value=[(tenant1, make_membership()), (tenant2, make_membership())],
),
patch("controllers.console.workspace.workspace.dify_config.ENTERPRISE_ENABLED", False),
patch("controllers.console.workspace.workspace.dify_config.BILLING_ENABLED", True),
@ -159,8 +171,8 @@ class TestTenantListApi:
with (
app.test_request_context("/workspaces"),
patch(
"controllers.console.workspace.workspace.TenantService.get_join_tenants",
return_value=[tenant1, tenant2],
"controllers.console.workspace.workspace.TenantService.get_workspaces_for_account",
return_value=[(tenant1, make_membership()), (tenant2, make_membership())],
),
patch("controllers.console.workspace.workspace.dify_config.ENTERPRISE_ENABLED", False),
patch("controllers.console.workspace.workspace.dify_config.BILLING_ENABLED", True),
@ -198,8 +210,8 @@ class TestTenantListApi:
with (
app.test_request_context("/workspaces"),
patch(
"controllers.console.workspace.workspace.TenantService.get_join_tenants",
return_value=[tenant],
"controllers.console.workspace.workspace.TenantService.get_workspaces_for_account",
return_value=[(tenant, make_membership())],
),
patch("controllers.console.workspace.workspace.dify_config.ENTERPRISE_ENABLED", False),
patch("controllers.console.workspace.workspace.dify_config.BILLING_ENABLED", False),
@ -226,8 +238,8 @@ class TestTenantListApi:
with (
app.test_request_context("/workspaces"),
patch(
"controllers.console.workspace.workspace.TenantService.get_join_tenants",
return_value=[tenant1, tenant2],
"controllers.console.workspace.workspace.TenantService.get_workspaces_for_account",
return_value=[(tenant1, make_membership()), (tenant2, make_membership())],
),
patch("controllers.console.workspace.workspace.dify_config.ENTERPRISE_ENABLED", True),
patch("controllers.console.workspace.workspace.dify_config.BILLING_ENABLED", False),
@ -251,7 +263,7 @@ class TestTenantListApi:
with (
app.test_request_context("/workspaces"),
patch(
"controllers.console.workspace.workspace.TenantService.get_join_tenants",
"controllers.console.workspace.workspace.TenantService.get_workspaces_for_account",
return_value=[],
),
patch("controllers.console.workspace.workspace.dify_config.ENTERPRISE_ENABLED", True),

View File

@ -1,5 +1,6 @@
"""OpenAPI JSON rendering tests for Flask-RESTX API blueprints."""
import json
from collections.abc import Iterator
import pytest
@ -187,3 +188,39 @@ def test_console_account_avatar_query_param_renders_as_query(monkeypatch: pytest
assert "payload" not in params
assert params["avatar"]["in"] == "query"
assert params["avatar"]["required"] is True
def test_console_plugin_category_list_exported_schema_uses_typed_items(tmp_path):
from dev.generate_swagger_specs import generate_specs
written_paths = generate_specs(tmp_path)
console_openapi_path = next(path for path in written_paths if path.name == "console-openapi.json")
payload = json.loads(console_openapi_path.read_text(encoding="utf-8"))
operation = payload["paths"]["/workspaces/current/plugin/{category}/list"]["get"]
response_ref = operation["responses"]["200"]["content"]["application/json"]["schema"]["$ref"].removeprefix(
"#/components/schemas/"
)
schemas = payload["components"]["schemas"]
response_schema = schemas[response_ref]
assert response_schema["properties"]["plugins"]["items"]["$ref"] == (
"#/components/schemas/PluginCategoryInstalledPluginResponse"
)
assert response_schema["properties"]["builtin_tools"]["items"]["$ref"] == (
"#/components/schemas/PluginCategoryBuiltinToolProviderResponse"
)
installed_plugin_schema = schemas["PluginCategoryInstalledPluginResponse"]
for field in (
"plugin_unique_identifier",
"source",
"version",
"declaration",
"endpoints_active",
"endpoints_setups",
):
assert field in installed_plugin_schema["properties"]
builtin_tool_schema = schemas["PluginCategoryBuiltinToolProviderResponse"]
for field in ("plugin_unique_identifier", "team_credentials", "type", "tools"):
assert field in builtin_tool_schema["properties"]

View File

@ -33,6 +33,7 @@ from core.plugin.entities.plugin_daemon import (
PluginInstallTaskStartResponse,
PluginInstallTaskStatus,
PluginListResponse,
PluginListWithoutTotalResponse,
PluginReadmeResponse,
PluginVerification,
)
@ -123,6 +124,26 @@ class TestPluginDiscovery:
assert call_args[1]["params"]["page_size"] == 5
assert result.total == 10
def test_list_plugins_by_category(self, plugin_installer, mock_plugin_entity):
"""Test category plugin listing without total."""
mock_response = PluginListWithoutTotalResponse(list=[mock_plugin_entity], has_more=True)
with patch.object(
plugin_installer, "_request_with_plugin_daemon_response", return_value=mock_response
) as mock_request:
result = plugin_installer.list_plugins_by_category(
"test-tenant", category=PluginCategory.Tool, page=2, page_size=10
)
mock_request.assert_called_once()
call_args = mock_request.call_args
assert call_args.args[1] == "plugin/test-tenant/management/tool/list"
assert call_args.args[2] is PluginListWithoutTotalResponse
assert call_args.kwargs["params"]["page"] == 2
assert call_args.kwargs["params"]["page_size"] == 10
assert result.list == [mock_plugin_entity]
assert result.has_more is True
def test_list_plugins_empty_result(self, plugin_installer):
"""Test plugin listing when no plugins are installed."""
# Arrange: Mock empty response

View File

@ -1,8 +1,10 @@
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
from models.account import TenantPluginAutoUpgradeStrategy
MODULE = "services.plugin.plugin_auto_upgrade_service"
PLUGIN_CATEGORY = TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL
def _patched_session():
@ -25,7 +27,7 @@ class TestGetStrategy:
with p1:
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
result = PluginAutoUpgradeService.get_strategy("t1")
result = PluginAutoUpgradeService.get_strategy("t1", PLUGIN_CATEGORY)
assert result is strategy
@ -36,7 +38,7 @@ class TestGetStrategy:
with p1:
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
result = PluginAutoUpgradeService.get_strategy("t1")
result = PluginAutoUpgradeService.get_strategy("t1", PLUGIN_CATEGORY)
assert result is None
@ -57,6 +59,7 @@ class TestChangeStrategy:
TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
[],
[],
category=PLUGIN_CATEGORY,
)
assert result is True
@ -77,6 +80,7 @@ class TestChangeStrategy:
TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL,
["p1"],
["p2"],
category=PLUGIN_CATEGORY,
)
assert result is True
@ -96,17 +100,19 @@ class TestExcludePlugin:
p1,
patch(f"{MODULE}.select"),
patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls,
patch(f"{MODULE}.PluginAutoUpgradeService.change_strategy") as cs,
):
strat_cls.StrategySetting.FIX_ONLY = "fix_only"
strat_cls.UpgradeMode.EXCLUDE = "exclude"
cs.return_value = True
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
result = PluginAutoUpgradeService.exclude_plugin("t1", "plugin-1")
result = PluginAutoUpgradeService.exclude_plugin(
"t1",
"plugin-1",
PLUGIN_CATEGORY,
)
assert result is True
cs.assert_called_once()
session.add.assert_called_once()
def test_appends_to_exclude_list_in_exclude_mode(self):
p1, session = _patched_session()
@ -121,7 +127,7 @@ class TestExcludePlugin:
strat_cls.UpgradeMode.ALL = "all"
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
result = PluginAutoUpgradeService.exclude_plugin("t1", "p-new")
result = PluginAutoUpgradeService.exclude_plugin("t1", "p-new", PLUGIN_CATEGORY)
assert result is True
assert existing.exclude_plugins == ["p-existing", "p-new"]
@ -139,7 +145,7 @@ class TestExcludePlugin:
strat_cls.UpgradeMode.ALL = "all"
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
result = PluginAutoUpgradeService.exclude_plugin("t1", "p1")
result = PluginAutoUpgradeService.exclude_plugin("t1", "p1", PLUGIN_CATEGORY)
assert result is True
assert existing.include_plugins == ["p2"]
@ -156,7 +162,7 @@ class TestExcludePlugin:
strat_cls.UpgradeMode.ALL = "all"
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
result = PluginAutoUpgradeService.exclude_plugin("t1", "p1")
result = PluginAutoUpgradeService.exclude_plugin("t1", "p1", PLUGIN_CATEGORY)
assert result is True
assert existing.upgrade_mode == "exclude"
@ -175,6 +181,101 @@ class TestExcludePlugin:
strat_cls.UpgradeMode.ALL = "all"
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
PluginAutoUpgradeService.exclude_plugin("t1", "p1")
PluginAutoUpgradeService.exclude_plugin("t1", "p1", PLUGIN_CATEGORY)
assert existing.exclude_plugins == ["p1"]
class TestBackfillStrategyCategories:
def test_creates_default_missing_categories_without_fetching_daemon(self):
p1, session = _patched_session()
tool_strategy = SimpleNamespace(
category=TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL,
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
upgrade_time_of_day=0,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
exclude_plugins=[],
include_plugins=[],
)
session.scalars.return_value.all.return_value = [tool_strategy]
installer = MagicMock()
with p1, patch(f"{MODULE}.PluginInstaller", return_value=installer):
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
result = PluginAutoUpgradeService.backfill_strategy_categories("t1")
expected_time = PluginAutoUpgradeService.default_upgrade_time_of_day("t1")
assert result.created_count == len(TenantPluginAutoUpgradeStrategy.PluginCategory) - 1
assert result.normalized is False
installer.list_plugins.assert_not_called()
assert tool_strategy.upgrade_time_of_day == expected_time
created_strategies = [call.args[0] for call in session.add.call_args_list]
model_strategy = next(
strategy
for strategy in created_strategies
if strategy.category == TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL
)
assert model_strategy.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST
assert model_strategy.upgrade_time_of_day == expected_time
def test_default_upgrade_time_is_aligned_to_fifteen_minutes(self):
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
default_time = PluginAutoUpgradeService.default_upgrade_time_of_day("t1")
assert default_time % (15 * 60) == 0
assert 0 <= default_time < 24 * 60 * 60
def test_creates_missing_categories_and_splits_known_plugins(self):
p1, session = _patched_session()
tool_strategy = SimpleNamespace(
category=TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL,
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
upgrade_time_of_day=0,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
exclude_plugins=["tool-plugin", "model-plugin", "unknown-plugin"],
include_plugins=["model-plugin", "tool-plugin"],
)
model_strategy = SimpleNamespace(
category=TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL,
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
upgrade_time_of_day=0,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
exclude_plugins=["tool-plugin", "model-plugin", "unknown-plugin"],
include_plugins=["model-plugin", "tool-plugin"],
)
session.scalars.return_value.all.return_value = [tool_strategy, model_strategy]
installed_plugins = [
SimpleNamespace(
plugin_id="tool-plugin",
declaration=SimpleNamespace(category=TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL),
),
SimpleNamespace(
plugin_id="model-plugin",
declaration=SimpleNamespace(category=TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL),
),
]
installer = MagicMock()
installer.list_plugins.return_value = installed_plugins
with p1, patch(f"{MODULE}.PluginInstaller", return_value=installer), patch(f"{MODULE}.logger") as logger:
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
result = PluginAutoUpgradeService.backfill_strategy_categories("t1")
assert result.created_count == len(TenantPluginAutoUpgradeStrategy.PluginCategory) - 2
assert result.normalized is True
assert session.add.call_count == len(TenantPluginAutoUpgradeStrategy.PluginCategory) - 2
assert tool_strategy.exclude_plugins == ["tool-plugin"]
assert tool_strategy.include_plugins == ["tool-plugin"]
assert model_strategy.exclude_plugins == ["model-plugin"]
assert model_strategy.include_plugins == ["model-plugin"]
logger.warning.assert_called_once_with(
"Skipped unknown plugin IDs while backfilling plugin auto-upgrade strategies: "
"tenant_id=%s, field=%s, plugin_ids=%s",
"t1",
"exclude_plugins",
["unknown-plugin"],
)

View File

@ -65,6 +65,7 @@ class TestAccountAssociatedDataFactory:
tenant_join.account_id = account_id
tenant_join.current = current
tenant_join.role = role
tenant_join.last_opened_at = kwargs.pop("last_opened_at", None)
for key, value in kwargs.items():
setattr(tenant_join, key, value)
return tenant_join
@ -489,11 +490,13 @@ class TestAccountService:
# Mock datetime
with (
patch("services.account_service.datetime") as mock_datetime,
patch("services.account_service.naive_utc_now") as mock_naive_utc_now,
patch.object(AccountService, "_refresh_account_last_active") as mock_refresh_last_active,
):
mock_now = datetime.now()
mock_datetime.now.return_value = mock_now
mock_datetime.UTC = "UTC"
mock_naive_utc_now.return_value = mock_now
# Execute test
result = AccountService.load_user("user-123")
@ -501,6 +504,7 @@ class TestAccountService:
# Verify results
assert result == mock_account
assert mock_available_tenant.current is True
assert mock_available_tenant.last_opened_at == mock_now
self._assert_database_operations_called(mock_db_dependencies["db"])
mock_refresh_last_active.assert_called_once_with(mock_account)
@ -922,11 +926,16 @@ class TestTenantService:
# Mock scalar for the join query
mock_db.session.scalar.return_value = mock_tenant_join
# Execute test
TenantService.switch_tenant(mock_account, "tenant-456")
with patch("services.account_service.naive_utc_now") as mock_naive_utc_now:
mock_now = datetime(2026, 6, 5, 11, 0, 0)
mock_naive_utc_now.return_value = mock_now
# Execute test
TenantService.switch_tenant(mock_account, "tenant-456")
# Verify tenant was switched
assert mock_tenant_join.current is True
assert mock_tenant_join.last_opened_at == mock_now
self._assert_database_operations_called(mock_db)
def test_switch_tenant_no_tenant_id(self):

View File

@ -4,19 +4,25 @@ from types import SimpleNamespace
from unittest.mock import MagicMock, patch
from core.plugin.entities.marketplace import MarketplacePluginSnapshot
from core.plugin.entities.plugin import PluginInstallationSource
from core.plugin.entities.plugin import PluginCategory, PluginInstallationSource
from models.account import TenantPluginAutoUpgradeStrategy
MODULE = "tasks.process_tenant_plugin_autoupgrade_check_task"
def _make_plugin(plugin_id: str, version: str, source=PluginInstallationSource.Marketplace):
def _make_plugin(
plugin_id: str,
version: str,
source=PluginInstallationSource.Marketplace,
category: PluginCategory = PluginCategory.Tool,
):
"""Build a minimal stand-in for a PluginInstallation entry returned by manager.list_plugins."""
return SimpleNamespace(
plugin_id=plugin_id,
version=version,
plugin_unique_identifier=f"{plugin_id}:{version}@deadbeef",
source=source,
declaration=SimpleNamespace(category=category),
)
@ -39,6 +45,7 @@ def _run_task(
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
exclude_plugins=None,
include_plugins=None,
category=None,
):
"""
Execute the celery task synchronously with mocks for the plugin manager,
@ -72,6 +79,7 @@ def _run_task(
upgrade_mode,
exclude_plugins or [],
include_plugins or [],
category,
)
return upgrade_mock, upgrade_calls
@ -246,6 +254,26 @@ class TestUpgradeMode:
assert upgrade_mock.call_count == 1
assert calls[0][1] == plugins[0].plugin_unique_identifier
def test_category_strategy_only_upgrades_matching_category(self):
plugins = [
_make_plugin("acme/model-provider", "1.0.0", category=PluginCategory.Model),
_make_plugin("acme/tool-provider", "1.0.0", category=PluginCategory.Tool),
]
manifests = [
_make_manifest("acme/model-provider", "1.0.1"),
_make_manifest("acme/tool-provider", "1.0.1"),
]
upgrade_mock, calls = _run_task(
plugins=plugins,
manifests=manifests,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
category=TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL,
)
upgrade_mock.assert_called_once()
assert calls[0][1] == plugins[0].plugin_unique_identifier
class TestErrorIsolation:
def test_one_plugin_failure_does_not_block_others(self):

View File

@ -4,6 +4,7 @@ import pytest
from libs.archive_storage import ArchiveStorageNotConfiguredError
from tasks.remove_app_and_related_data_task import (
_delete_app_stars,
_delete_app_workflow_archive_logs,
_delete_archived_workflow_run_files,
_delete_draft_variable_offload_data,
@ -89,6 +90,27 @@ class TestDeleteWorkflowArchiveLogs:
mock_session.execute.assert_called_once()
class TestDeleteAppStars:
@patch("tasks.remove_app_and_related_data_task._delete_records")
def test_delete_app_stars_calls_delete_records(self, mock_delete_records):
tenant_id = "tenant-1"
app_id = "app-1"
_delete_app_stars(tenant_id, app_id)
mock_delete_records.assert_called_once()
query_sql, params, delete_func, name = mock_delete_records.call_args[0]
assert "app_stars" in query_sql
assert params == {"tenant_id": tenant_id, "app_id": app_id}
assert name == "app star"
mock_session = MagicMock()
delete_func(mock_session, "star-1")
mock_session.execute.assert_called_once()
class TestDeleteArchivedWorkflowRunFiles:
@patch("tasks.remove_app_and_related_data_task.get_archive_storage")
@patch("tasks.remove_app_and_related_data_task.logger")

View File

@ -4,5 +4,4 @@ Feature: Authenticated app console
Given I am signed in as the default E2E admin
When I open the apps console
Then I should stay on the apps console
And I should see the "Create from Blank" button
And I should not see the "Sign in" button

View File

@ -4,4 +4,3 @@ Feature: Fresh installation bootstrap
Given the last authentication bootstrap came from a fresh install
When I open the apps console
Then I should stay on the apps console
And I should see the "Create from Blank" button

View File

@ -1,12 +1,10 @@
import type { DifyWorld } from '../../support/world'
import { Then, When } from '@cucumber/cucumber'
import { expect } from '@playwright/test'
import { openBlankAppCreation } from '../../../support/apps'
When('I start creating a blank app', async function (this: DifyWorld) {
const page = this.getPage()
await expect(page.getByRole('button', { name: 'Create from Blank' })).toBeVisible()
await page.getByRole('button', { name: 'Create from Blank' }).click()
await openBlankAppCreation(this.getPage())
})
When('I enter a unique E2E app name', async function (this: DifyWorld) {

View File

@ -29,7 +29,7 @@ Then('the app should no longer appear in the apps console', async function (this
)
}
await expect(this.getPage().getByTitle(appName)).not.toBeVisible({
await expect(this.getPage().getByRole('link', { name: appName, exact: true })).not.toBeVisible({
timeout: 10_000,
})
})

View File

@ -1,5 +1,6 @@
import type { DifyWorld } from '../../support/world'
import { Given, When } from '@cucumber/cucumber'
import { expect } from '@playwright/test'
import { createTestApp } from '../../../support/api'
Given('there is an existing E2E app available for testing', async function (this: DifyWorld) {
@ -15,14 +16,13 @@ When('I open the options menu for the last created E2E app', async function (thi
throw new Error('No app name stored. Run "I enter a unique E2E app name" first.')
const page = this.getPage()
// Scope to the specific card: the card root is the innermost div that contains
// both the unique app name text and a More button (they are in separate branches,
// so no child div satisfies both). .last() picks the deepest match in DOM order.
const appLink = page.getByRole('link', { name: appName, exact: true })
const appCard = page
.locator('div')
.filter({ has: page.getByText(appName, { exact: true }) })
.filter({ has: appLink })
.filter({ has: page.getByRole('button', { name: 'More' }) })
.last()
await expect(appLink).toBeVisible()
await appCard.hover()
await appCard.getByRole('button', { name: 'More' }).click()
})

View File

@ -4,7 +4,7 @@ import { expect } from '@playwright/test'
import { adminCredentials } from '../../../fixtures/auth'
When('I open the sign-in page', async function (this: DifyWorld) {
await this.getPage().goto('/signin')
await this.getPage().goto('/signin?redirect_url=%2Fapps')
})
When('I sign in as the default E2E admin', async function (this: DifyWorld) {

View File

@ -2,6 +2,7 @@ import type { DifyWorld } from '../../support/world'
import { Given, When } from '@cucumber/cucumber'
import { expect } from '@playwright/test'
import { createTestApp, syncMinimalWorkflowDraft } from '../../../support/api'
import { waitForAppsConsole } from '../../../support/apps'
Given('a {string} app has been created via API', async function (this: DifyWorld, mode: string) {
const app = await createTestApp(`E2E ${Date.now()}`, mode)
@ -17,6 +18,8 @@ Given('a minimal workflow draft has been synced', async function (this: DifyWorl
When('I open the app from the app list', async function (this: DifyWorld) {
const page = this.getPage()
await page.goto('/apps')
await expect(page.getByRole('button', { name: 'Create from Blank' })).toBeVisible()
await page.getByText(this.lastCreatedAppName!).click()
await waitForAppsConsole(page)
const appLink = page.getByRole('link', { name: this.lastCreatedAppName!, exact: true })
await expect(appLink).toBeVisible()
await appLink.click()
})

View File

@ -1,13 +1,14 @@
import type { DifyWorld } from '../../support/world'
import { Then, When } from '@cucumber/cucumber'
import { expect } from '@playwright/test'
import { waitForAppsConsole } from '../../../support/apps'
When('I open the apps console', async function (this: DifyWorld) {
await this.getPage().goto('/apps')
})
Then('I should stay on the apps console', async function (this: DifyWorld) {
await expect(this.getPage()).toHaveURL(/\/apps(?:\?.*)?$/)
await waitForAppsConsole(this.getPage())
})
Then('I should be redirected to the signin page', async function (this: DifyWorld) {

View File

@ -1,9 +1,10 @@
import type { Browser, Page } from '@playwright/test'
import type { APIResponse, Browser, BrowserContext } from '@playwright/test'
import { Buffer } from 'node:buffer'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { expect } from '@playwright/test'
import { defaultBaseURL, defaultLocale } from '../test-env'
import { waitForAppsConsole } from '../support/apps'
import { apiURL, defaultBaseURL, defaultLocale } from '../test-env'
export type AuthSessionMetadata = {
adminEmail: string
@ -12,7 +13,8 @@ export type AuthSessionMetadata = {
usedInitPassword: boolean
}
export const AUTH_BOOTSTRAP_TIMEOUT_MS = 120_000
export const AUTH_BOOTSTRAP_TIMEOUT_MS = 180_000
const AUTH_FLOW_TIMEOUT_MS = AUTH_BOOTSTRAP_TIMEOUT_MS - 30_000
const e2eRoot = fileURLToPath(new URL('..', import.meta.url))
export const authDir = path.join(e2eRoot, '.auth')
@ -35,89 +37,106 @@ export const readAuthSessionMetadata = async () => {
return JSON.parse(content) as AuthSessionMetadata
}
const escapeRegex = (value: string) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&')
const appURL = (baseURL: string, pathname: string) => new URL(pathname, baseURL).toString()
const apiEndpoint = (pathname: string) => new URL(pathname, apiURL).toString()
type AuthPageState = 'install' | 'login' | 'init'
type SetupStatusResponse = {
step: 'not_started' | 'finished'
}
type InitStatusResponse = {
status: 'not_started' | 'finished'
}
type AuthBootstrapResult = {
mode: AuthSessionMetadata['mode']
usedInitPassword: boolean
}
const getRemainingTimeout = (deadline: number) => Math.max(deadline - Date.now(), 1)
const waitForPageState = async (page: Page, deadline: number): Promise<AuthPageState> => {
const installHeading = page.getByRole('heading', { name: 'Setting up an admin account' })
const signInButton = page.getByRole('button', { name: 'Sign in' })
const initPasswordField = page.getByLabel('Admin initialization password')
const encodeField = (value: string) => Buffer.from(value, 'utf8').toString('base64')
try {
return await Promise.any<AuthPageState>([
installHeading
.waitFor({ state: 'visible', timeout: getRemainingTimeout(deadline) })
.then(() => 'install'),
signInButton
.waitFor({ state: 'visible', timeout: getRemainingTimeout(deadline) })
.then(() => 'login'),
initPasswordField
.waitFor({ state: 'visible', timeout: getRemainingTimeout(deadline) })
.then(() => 'init'),
])
}
catch {
throw new Error(`Unable to determine auth page state for ${page.url()}`)
}
const assertAPIResponse = async (response: APIResponse, action: string) => {
if (response.ok())
return
const body = await response.text().catch(() => '')
throw new Error(
`${action} failed with ${response.status()} ${response.statusText()}${body ? `: ${body}` : ''}`,
)
}
const completeInitPasswordIfNeeded = async (page: Page, deadline: number) => {
const initPasswordField = page.getByLabel('Admin initialization password')
const needsInitPassword = await initPasswordField
.waitFor({ state: 'visible', timeout: Math.min(getRemainingTimeout(deadline), 3_000) })
.then(() => true)
.catch(() => false)
if (!needsInitPassword)
return false
await initPasswordField.fill(initPassword)
await page.getByRole('button', { name: 'Validate' }).click()
await expect(page.getByRole('heading', { name: 'Setting up an admin account' })).toBeVisible({
const getConsoleAPI = async <T>(context: BrowserContext, pathname: string, deadline: number) => {
const response = await context.request.get(apiEndpoint(pathname), {
timeout: getRemainingTimeout(deadline),
})
await assertAPIResponse(response, `GET ${pathname}`)
return response.json() as Promise<T>
}
const postConsoleAPI = async (
context: BrowserContext,
pathname: string,
deadline: number,
data: Record<string, unknown>,
) => {
const response = await context.request.post(apiEndpoint(pathname), {
data,
timeout: getRemainingTimeout(deadline),
})
await assertAPIResponse(response, `POST ${pathname}`)
}
const validateInitPasswordIfNeeded = async (context: BrowserContext, deadline: number) => {
const initStatus = await getConsoleAPI<InitStatusResponse>(context, '/console/api/init', deadline)
if (initStatus.status === 'finished')
return false
console.warn('[e2e] auth bootstrap: validating init password')
await postConsoleAPI(context, '/console/api/init', deadline, { password: initPassword })
return true
}
const completeInstall = async (page: Page, baseURL: string, deadline: number) => {
await expect(page.getByRole('heading', { name: 'Setting up an admin account' })).toBeVisible({
timeout: getRemainingTimeout(deadline),
})
const ensureAdminAccount = async (
context: BrowserContext,
deadline: number,
): Promise<AuthBootstrapResult> => {
const setupStatus = await getConsoleAPI<SetupStatusResponse>(
context,
'/console/api/setup',
deadline,
)
let usedInitPassword = false
await page.getByLabel('Email address').fill(adminCredentials.email)
await page.getByLabel('Username').fill(adminCredentials.name)
await page.getByLabel('Password').fill(adminCredentials.password)
await page.getByRole('button', { name: 'Set up' }).click()
if (setupStatus.step === 'not_started') {
usedInitPassword = await validateInitPasswordIfNeeded(context, deadline)
console.warn('[e2e] auth bootstrap: creating admin account')
await postConsoleAPI(context, '/console/api/setup', deadline, {
email: adminCredentials.email,
name: adminCredentials.name,
password: adminCredentials.password,
language: defaultLocale,
})
await expect(page).toHaveURL(new RegExp(`^${escapeRegex(baseURL)}/apps(?:\\?.*)?$`), {
timeout: getRemainingTimeout(deadline),
})
return { mode: 'install', usedInitPassword }
}
return { mode: 'login', usedInitPassword }
}
const completeLogin = async (page: Page, baseURL: string, deadline: number) => {
await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible({
timeout: getRemainingTimeout(deadline),
})
await page.getByLabel('Email address').fill(adminCredentials.email)
await page.getByLabel('Password').fill(adminCredentials.password)
await page.getByRole('button', { name: 'Sign in' }).click()
await expect(page).toHaveURL(new RegExp(`^${escapeRegex(baseURL)}/apps(?:\\?.*)?$`), {
timeout: getRemainingTimeout(deadline),
const loginAdmin = async (context: BrowserContext, deadline: number) => {
console.warn('[e2e] auth bootstrap: logging in admin')
await postConsoleAPI(context, '/console/api/login', deadline, {
email: adminCredentials.email,
password: encodeField(adminCredentials.password),
remember_me: true,
})
}
export const ensureAuthenticatedState = async (browser: Browser, configuredBaseURL?: string) => {
const baseURL = resolveBaseURL(configuredBaseURL)
const deadline = Date.now() + AUTH_BOOTSTRAP_TIMEOUT_MS
const deadline = Date.now() + AUTH_FLOW_TIMEOUT_MS
await mkdir(authDir, { recursive: true })
@ -128,37 +147,22 @@ export const ensureAuthenticatedState = async (browser: Browser, configuredBaseU
const page = await context.newPage()
try {
await page.goto(appURL(baseURL, '/install'), {
const { mode, usedInitPassword } = await ensureAdminAccount(context, deadline)
await loginAdmin(context, deadline)
console.warn('[e2e] auth bootstrap: verifying apps console')
await page.goto(appURL(baseURL, '/apps'), {
timeout: getRemainingTimeout(deadline),
waitUntil: 'domcontentloaded',
})
let usedInitPassword = await completeInitPasswordIfNeeded(page, deadline)
let pageState = await waitForPageState(page, deadline)
while (pageState === 'init') {
const completedInitPassword = await completeInitPasswordIfNeeded(page, deadline)
if (!completedInitPassword)
throw new Error(`Unable to validate initialization password for ${page.url()}`)
usedInitPassword = true
pageState = await waitForPageState(page, deadline)
}
if (pageState === 'install')
await completeInstall(page, baseURL, deadline)
else await completeLogin(page, baseURL, deadline)
await expect(page.getByRole('button', { name: 'Create from Blank' })).toBeVisible({
timeout: getRemainingTimeout(deadline),
})
await waitForAppsConsole(page, getRemainingTimeout(deadline))
await context.storageState({ path: authStatePath })
const metadata: AuthSessionMetadata = {
adminEmail: adminCredentials.email,
baseURL,
mode: pageState,
mode,
usedInitPassword,
}

24
e2e/support/apps.ts Normal file
View File

@ -0,0 +1,24 @@
import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
export const waitForAppsConsole = async (page: Page, timeout?: number) => {
await expect(page).toHaveURL(/\/apps(?:\?.*)?$/, timeout === undefined ? undefined : { timeout })
await expect(page.getByRole('heading', { name: 'Studio' })).toBeVisible(
timeout === undefined ? undefined : { timeout },
)
}
export const openBlankAppCreation = async (page: Page) => {
const createFromBlankButton = page.getByRole('button', { name: 'Create from Blank' }).first()
const isDirectCreateVisible = await createFromBlankButton
.isVisible({ timeout: 3_000 })
.catch(() => false)
if (isDirectCreateVisible) {
await createFromBlankButton.click()
return
}
await page.getByRole('button', { name: 'Create' }).click()
await page.getByRole('menuitem', { name: 'Create from Blank' }).click()
}

View File

@ -152,17 +152,6 @@
"count": 1
}
},
"web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx": {
"no-restricted-globals": {
"count": 1
},
"react/set-state-in-effect": {
"count": 2
},
"ts/no-explicit-any": {
"count": 1
}
},
"web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
@ -396,9 +385,6 @@
},
"web/app/components/app-sidebar/index.tsx": {
"no-restricted-globals": {
"count": 2
},
"ts/no-explicit-any": {
"count": 1
}
},
@ -990,9 +976,6 @@
},
"jsx-a11y/no-static-element-interactions": {
"count": 1
},
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/app/overview/trigger-card.tsx": {
@ -1051,10 +1034,10 @@
},
"web/app/components/apps/app-card.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 2
"count": 1
},
"jsx-a11y/no-static-element-interactions": {
"count": 2
"count": 1
}
},
"web/app/components/apps/import-from-marketplace-template-modal.tsx": {
@ -1065,17 +1048,6 @@
"count": 1
}
},
"web/app/components/apps/new-app-card.tsx": {
"react-hooks-extra/no-direct-set-state-in-use-effect": {
"count": 1
},
"react/set-state-in-effect": {
"count": 1
},
"ts/no-explicit-any": {
"count": 1
}
},
"web/app/components/base/action-button/index.tsx": {
"erasable-syntax-only/enums": {
"count": 1
@ -1886,7 +1858,7 @@
},
"web/app/components/base/icons/src/vender/line/alertsAndFeedback/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 2
"count": 1
}
},
"web/app/components/base/icons/src/vender/line/arrows/index.ts": {
@ -1921,7 +1893,7 @@
},
"web/app/components/base/icons/src/vender/line/financeAndECommerce/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 4
"count": 3
}
},
"web/app/components/base/icons/src/vender/line/general/index.ts": {
@ -3474,6 +3446,11 @@
"count": 1
}
},
"web/app/components/datasets/list/__tests__/header.spec.tsx": {
"jsx-a11y/label-has-associated-control": {
"count": 1
}
},
"web/app/components/datasets/list/dataset-card/__tests__/index.spec.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
@ -3628,11 +3605,6 @@
"count": 2
}
},
"web/app/components/explore/app-list/index.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/explore/banner/__tests__/indicator-button.spec.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
@ -3647,12 +3619,6 @@
},
"jsx-a11y/no-static-element-interactions": {
"count": 1
},
"react-hooks-extra/no-direct-set-state-in-use-effect": {
"count": 1
},
"react/set-state-in-effect": {
"count": 1
}
},
"web/app/components/explore/banner/indicator-button.tsx": {
@ -3663,14 +3629,6 @@
"count": 2
}
},
"web/app/components/explore/category.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 2
},
"jsx-a11y/no-static-element-interactions": {
"count": 2
}
},
"web/app/components/explore/item-operation/__tests__/index.spec.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
@ -3679,12 +3637,17 @@
"count": 1
}
},
"web/app/components/explore/learn-dify/item.tsx": {
"jsx-a11y/no-noninteractive-element-interactions": {
"count": 1
}
},
"web/app/components/explore/sidebar/app-nav-item/index.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 2
"count": 1
},
"jsx-a11y/no-static-element-interactions": {
"count": 2
"count": 1
}
},
"web/app/components/explore/try-app/app/text-generation.tsx": {
@ -3700,34 +3663,16 @@
"count": 2
}
},
"web/app/components/goto-anything/actions/commands/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 2
}
},
"web/app/components/goto-anything/actions/commands/registry.ts": {
"ts/no-explicit-any": {
"count": 3
}
},
"web/app/components/goto-anything/actions/commands/slash.tsx": {
"react-refresh/only-export-components": {
"count": 1
},
"ts/no-explicit-any": {
"count": 1
}
},
"web/app/components/goto-anything/actions/commands/types.ts": {
"ts/no-explicit-any": {
"count": 1
}
},
"web/app/components/goto-anything/actions/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 2
}
},
"web/app/components/goto-anything/actions/plugin.tsx": {
"no-restricted-imports": {
"count": 1
@ -3743,11 +3688,6 @@
"count": 2
}
},
"web/app/components/goto-anything/command-selector.tsx": {
"react/unsupported-syntax": {
"count": 2
}
},
"web/app/components/goto-anything/components/__tests__/search-input.spec.tsx": {
"jsx-a11y/no-autofocus": {
"count": 1
@ -3779,11 +3719,6 @@
"count": 4
}
},
"web/app/components/goto-anything/hooks/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 4
}
},
"web/app/components/goto-anything/hooks/use-goto-anything-results.ts": {
"@tanstack/query/exhaustive-deps": {
"count": 1
@ -3798,10 +3733,10 @@
}
},
"web/app/components/header/account-setting/data-source-page-new/card.tsx": {
"jsx-a11y/alt-text": {
"count": 1
"jsx-a11y/click-events-have-key-events": {
"count": 2
},
"ts/no-explicit-any": {
"jsx-a11y/no-static-element-interactions": {
"count": 2
}
},
@ -3815,16 +3750,11 @@
"count": 1
}
},
"web/app/components/header/account-setting/data-source-page-new/install-from-marketplace.tsx": {
"ts/no-explicit-any": {
"count": 1
}
},
"web/app/components/header/account-setting/data-source-page-new/item.tsx": {
"no-restricted-imports": {
"web/app/components/header/account-setting/data-source-page-new/plugin-actions.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
},
"ts/no-explicit-any": {
"jsx-a11y/no-static-element-interactions": {
"count": 1
}
},
@ -4027,6 +3957,11 @@
"count": 2
}
},
"web/app/components/header/account-setting/model-provider-page/model-provider-page-body.tsx": {
"jsx-a11y/anchor-has-content": {
"count": 1
}
},
"web/app/components/header/account-setting/model-provider-page/provider-added-card/cooldown-timer.tsx": {
"react/set-state-in-effect": {
"count": 1
@ -4096,8 +4031,13 @@
"count": 1
}
},
"web/app/components/plugins/card/index.tsx": {
"ts/no-non-null-asserted-optional-chain": {
"web/app/components/main-nav/components/web-apps-section.tsx": {
"jsx-a11y/no-autofocus": {
"count": 1
}
},
"web/app/components/main-nav/components/workspace-switcher.tsx": {
"jsx-a11y/no-autofocus": {
"count": 1
}
},
@ -4490,11 +4430,6 @@
"count": 1
}
},
"web/app/components/plugins/plugin-page/empty/index.tsx": {
"react/set-state-in-effect": {
"count": 2
}
},
"web/app/components/plugins/plugin-page/filter-management/__tests__/index.spec.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
@ -4513,11 +4448,6 @@
"count": 1
}
},
"web/app/components/plugins/plugin-page/filter-management/tag-filter.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/index.spec.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
@ -4555,11 +4485,6 @@
"count": 25
}
},
"web/app/components/plugins/update-plugin/from-market-place.tsx": {
"erasable-syntax-only/enums": {
"count": 1
}
},
"web/app/components/plugins/update-plugin/plugin-version-picker.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
@ -4940,32 +4865,11 @@
"count": 2
}
},
"web/app/components/tools/mcp/create-card.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
},
"jsx-a11y/no-static-element-interactions": {
"count": 1
},
"ts/no-explicit-any": {
"count": 1
}
},
"web/app/components/tools/mcp/headers-input.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/tools/mcp/mcp-server-param-item.tsx": {
"ts/no-explicit-any": {
"count": 1
}
},
"web/app/components/tools/mcp/modal.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/tools/mcp/provider-card.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 2
@ -4977,40 +4881,8 @@
"count": 3
}
},
"web/app/components/tools/mcp/sections/authentication-section.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/tools/mcp/sections/configurations-section.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/tools/provider-list.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
},
"jsx-a11y/no-static-element-interactions": {
"count": 1
},
"no-restricted-imports": {
"count": 1
},
"ts/no-explicit-any": {
"count": 1
}
},
"web/app/components/tools/provider/custom-create-card.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
},
"jsx-a11y/no-static-element-interactions": {
"count": 1
}
},
"web/app/components/tools/provider/empty.tsx": {
"ts/no-explicit-any": {
"web/app/components/tools/provider/detail.tsx": {
"jsx-a11y/anchor-has-content": {
"count": 1
}
},
@ -5027,6 +4899,14 @@
"count": 3
}
},
"web/app/components/tools/tool-provider-grid.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
},
"jsx-a11y/no-static-element-interactions": {
"count": 1
}
},
"web/app/components/tools/types.ts": {
"erasable-syntax-only/enums": {
"count": 4
@ -5334,11 +5214,6 @@
"count": 1
}
},
"web/app/components/workflow/header/__tests__/index.spec.tsx": {
"react/static-components": {
"count": 2
}
},
"web/app/components/workflow/header/online-users.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 2
@ -5448,16 +5323,6 @@
"count": 1
}
},
"web/app/components/workflow/hooks/use-workflow-canvas-maximize.ts": {
"no-restricted-globals": {
"count": 1
}
},
"web/app/components/workflow/hooks/use-workflow-interactions.ts": {
"no-barrel-files/no-barrel-files": {
"count": 5
}
},
"web/app/components/workflow/hooks/use-workflow-run-event/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 19
@ -7406,11 +7271,6 @@
"count": 2
}
},
"web/app/components/workflow/store/workflow/layout-slice.ts": {
"no-restricted-properties": {
"count": 1
}
},
"web/app/components/workflow/store/workflow/workflow-draft-slice.ts": {
"ts/no-explicit-any": {
"count": 1
@ -7728,11 +7588,6 @@
"count": 3
}
},
"web/context/provider-context-provider.tsx": {
"ts/no-explicit-any": {
"count": 1
}
},
"web/context/web-app-context.tsx": {
"react-refresh/only-export-components": {
"count": 1
@ -7799,46 +7654,6 @@
"count": 1
}
},
"web/i18n/de-DE/billing.json": {
"no-irregular-whitespace": {
"count": 1
}
},
"web/i18n/en-US/app-debug.json": {
"no-irregular-whitespace": {
"count": 1
}
},
"web/i18n/fr-FR/app-debug.json": {
"no-irregular-whitespace": {
"count": 1
}
},
"web/i18n/fr-FR/plugin-trigger.json": {
"no-irregular-whitespace": {
"count": 1
}
},
"web/i18n/fr-FR/tools.json": {
"no-irregular-whitespace": {
"count": 1
}
},
"web/i18n/pt-BR/common.json": {
"no-irregular-whitespace": {
"count": 1
}
},
"web/i18n/ru-RU/common.json": {
"no-irregular-whitespace": {
"count": 2
}
},
"web/i18n/uk-UA/app-debug.json": {
"no-irregular-whitespace": {
"count": 1
}
},
"web/models/access-control.ts": {
"erasable-syntax-only/enums": {
"count": 2
@ -7849,14 +7664,6 @@
"count": 2
}
},
"web/models/common.ts": {
"erasable-syntax-only/enums": {
"count": 2
},
"ts/no-explicit-any": {
"count": 3
}
},
"web/models/datasets.ts": {
"erasable-syntax-only/enums": {
"count": 7
@ -7971,7 +7778,7 @@
"count": 1
},
"ts/no-explicit-any": {
"count": 29
"count": 27
}
},
"web/service/datasets.ts": {
@ -8167,20 +7974,6 @@
"count": 4
}
},
"web/service/use-plugins.ts": {
"no-restricted-imports": {
"count": 1
},
"react/set-state-in-effect": {
"count": 1
},
"regexp/no-unused-capturing-group": {
"count": 1
},
"ts/no-explicit-any": {
"count": 3
}
},
"web/service/use-snippet-workflows.ts": {
"no-restricted-imports": {
"count": 1

File diff suppressed because it is too large Load Diff

View File

@ -1191,6 +1191,7 @@ export type AppPartial = {
icon_background?: string | null
icon_type?: string | null
id: string
is_starred?: boolean
max_active_requests?: number | null
mode_compatible_with_agent: string
name: string
@ -2683,6 +2684,7 @@ export type GetAppsData = {
| 'workflow'
name?: string
page?: number
sort_by?: 'earliest_created' | 'last_modified' | 'recently_created'
tag_ids?: Array<string>
}
url: '/apps'
@ -2771,6 +2773,36 @@ export type PostAppsImportsByImportIdConfirmResponses = {
export type PostAppsImportsByImportIdConfirmResponse
= PostAppsImportsByImportIdConfirmResponses[keyof PostAppsImportsByImportIdConfirmResponses]
export type GetAppsStarredData = {
body?: never
path?: never
query?: {
creator_ids?: Array<string>
is_created_by_me?: boolean
limit?: number
mode?:
| 'advanced-chat'
| 'agent'
| 'agent-chat'
| 'all'
| 'channel'
| 'chat'
| 'completion'
| 'workflow'
name?: string
page?: number
sort_by?: 'earliest_created' | 'last_modified' | 'recently_created'
tag_ids?: Array<string>
}
url: '/apps/starred'
}
export type GetAppsStarredResponses = {
200: AppPagination
}
export type GetAppsStarredResponse = GetAppsStarredResponses[keyof GetAppsStarredResponses]
export type PostAppsWorkflowsOnlineUsersData = {
body: WorkflowOnlineUsersPayload
path?: never
@ -4250,6 +4282,46 @@ export type PostAppsByAppIdSiteAccessTokenResetResponses = {
export type PostAppsByAppIdSiteAccessTokenResetResponse
= PostAppsByAppIdSiteAccessTokenResetResponses[keyof PostAppsByAppIdSiteAccessTokenResetResponses]
export type DeleteAppsByAppIdStarData = {
body?: never
path: {
app_id: string
}
query?: never
url: '/apps/{app_id}/star'
}
export type DeleteAppsByAppIdStarErrors = {
404: unknown
}
export type DeleteAppsByAppIdStarResponses = {
200: SimpleResultResponse
}
export type DeleteAppsByAppIdStarResponse
= DeleteAppsByAppIdStarResponses[keyof DeleteAppsByAppIdStarResponses]
export type PostAppsByAppIdStarData = {
body?: never
path: {
app_id: string
}
query?: never
url: '/apps/{app_id}/star'
}
export type PostAppsByAppIdStarErrors = {
404: unknown
}
export type PostAppsByAppIdStarResponses = {
200: SimpleResultResponse
}
export type PostAppsByAppIdStarResponse
= PostAppsByAppIdStarResponses[keyof PostAppsByAppIdStarResponses]
export type GetAppsByAppIdStatisticsAverageResponseTimeData = {
body?: never
path: {

View File

@ -2000,6 +2000,7 @@ export const zAppPartial = z.object({
icon_background: z.string().nullish(),
icon_type: z.string().nullish(),
id: z.string(),
is_starred: z.boolean().optional().default(false),
max_active_requests: z.int().nullish(),
mode_compatible_with_agent: z.string(),
name: z.string(),
@ -3625,6 +3626,10 @@ export const zGetAppsQuery = z.object({
.default('all'),
name: z.string().optional(),
page: z.int().gte(1).lte(99999).optional().default(1),
sort_by: z
.enum(['earliest_created', 'last_modified', 'recently_created'])
.optional()
.default('last_modified'),
tag_ids: z.array(z.string()).optional(),
})
@ -3665,6 +3670,37 @@ export const zPostAppsImportsByImportIdConfirmPath = z.object({
*/
export const zPostAppsImportsByImportIdConfirmResponse = zImport
export const zGetAppsStarredQuery = z.object({
creator_ids: z.array(z.string()).optional(),
is_created_by_me: z.boolean().optional(),
limit: z.int().gte(1).lte(100).optional().default(20),
mode: z
.enum([
'advanced-chat',
'agent',
'agent-chat',
'all',
'channel',
'chat',
'completion',
'workflow',
])
.optional()
.default('all'),
name: z.string().optional(),
page: z.int().gte(1).lte(99999).optional().default(1),
sort_by: z
.enum(['earliest_created', 'last_modified', 'recently_created'])
.optional()
.default('last_modified'),
tag_ids: z.array(z.string()).optional(),
})
/**
* Success
*/
export const zGetAppsStarredResponse = zAppPagination
export const zPostAppsWorkflowsOnlineUsersBody = zWorkflowOnlineUsersPayload
/**
@ -4541,6 +4577,24 @@ export const zPostAppsByAppIdSiteAccessTokenResetPath = z.object({
*/
export const zPostAppsByAppIdSiteAccessTokenResetResponse = zAppSiteResponse
export const zDeleteAppsByAppIdStarPath = z.object({
app_id: z.string(),
})
/**
* Success
*/
export const zDeleteAppsByAppIdStarResponse = zSimpleResultResponse
export const zPostAppsByAppIdStarPath = z.object({
app_id: z.string(),
})
/**
* Success
*/
export const zPostAppsByAppIdStarResponse = zSimpleResultResponse
export const zGetAppsByAppIdStatisticsAverageResponseTimePath = z.object({
app_id: z.string(),
})

View File

@ -6,6 +6,8 @@ import * as z from 'zod'
import {
zGetExploreAppsByAppIdPath,
zGetExploreAppsByAppIdResponse,
zGetExploreAppsLearnDifyQuery,
zGetExploreAppsLearnDifyResponse,
zGetExploreAppsQuery,
zGetExploreAppsResponse,
zGetExploreBannersQuery,
@ -13,6 +15,21 @@ import {
} from './zod.gen'
export const get = oc
.route({
inputStructure: 'detailed',
method: 'GET',
operationId: 'getExploreAppsLearnDify',
path: '/explore/apps/learn-dify',
tags: ['console'],
})
.input(z.object({ query: zGetExploreAppsLearnDifyQuery.optional() }))
.output(zGetExploreAppsLearnDifyResponse)
export const learnDify = {
get,
}
export const get2 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -24,10 +41,10 @@ export const get = oc
.output(zGetExploreAppsByAppIdResponse)
export const byAppId = {
get,
get: get2,
}
export const get2 = oc
export const get3 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -39,14 +56,15 @@ export const get2 = oc
.output(zGetExploreAppsResponse)
export const apps = {
get: get2,
get: get3,
learnDify,
byAppId,
}
/**
* Get banner list
*/
export const get3 = oc
export const get4 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -59,7 +77,7 @@ export const get3 = oc
.output(zGetExploreBannersResponse)
export const banners = {
get: get3,
get: get4,
}
export const explore = {

View File

@ -9,6 +9,10 @@ export type RecommendedAppListResponse = {
recommended_apps: Array<RecommendedAppResponse>
}
export type LearnDifyAppListResponse = {
recommended_apps: Array<RecommendedAppResponse>
}
export type RecommendedAppDetailResponse = {
[key: string]: unknown
}
@ -61,6 +65,22 @@ export type GetExploreAppsResponses = {
export type GetExploreAppsResponse = GetExploreAppsResponses[keyof GetExploreAppsResponses]
export type GetExploreAppsLearnDifyData = {
body?: never
path?: never
query?: {
language?: string
}
url: '/explore/apps/learn-dify'
}
export type GetExploreAppsLearnDifyResponses = {
200: LearnDifyAppListResponse
}
export type GetExploreAppsLearnDifyResponse
= GetExploreAppsLearnDifyResponses[keyof GetExploreAppsLearnDifyResponses]
export type GetExploreAppsByAppIdData = {
body?: never
path: {

View File

@ -60,6 +60,13 @@ export const zRecommendedAppListResponse = z.object({
recommended_apps: z.array(zRecommendedAppResponse),
})
/**
* LearnDifyAppListResponse
*/
export const zLearnDifyAppListResponse = z.object({
recommended_apps: z.array(zRecommendedAppResponse),
})
export const zGetExploreAppsQuery = z.object({
language: z.string().optional(),
})
@ -69,6 +76,15 @@ export const zGetExploreAppsQuery = z.object({
*/
export const zGetExploreAppsResponse = zRecommendedAppListResponse
export const zGetExploreAppsLearnDifyQuery = z.object({
language: z.string().optional(),
})
/**
* Success
*/
export const zGetExploreAppsLearnDifyResponse = zLearnDifyAppListResponse
export const zGetExploreAppsByAppIdPath = z.object({
app_id: z.string(),
})

View File

@ -67,6 +67,11 @@ import {
zGetWorkspacesCurrentPermissionResponse,
zGetWorkspacesCurrentPluginAssetQuery,
zGetWorkspacesCurrentPluginAssetResponse,
zGetWorkspacesCurrentPluginAutoUpgradeFetchQuery,
zGetWorkspacesCurrentPluginAutoUpgradeFetchResponse,
zGetWorkspacesCurrentPluginByCategoryListPath,
zGetWorkspacesCurrentPluginByCategoryListQuery,
zGetWorkspacesCurrentPluginByCategoryListResponse,
zGetWorkspacesCurrentPluginDebuggingKeyResponse,
zGetWorkspacesCurrentPluginFetchManifestQuery,
zGetWorkspacesCurrentPluginFetchManifestResponse,
@ -79,7 +84,6 @@ import {
zGetWorkspacesCurrentPluginParametersDynamicOptionsQuery,
zGetWorkspacesCurrentPluginParametersDynamicOptionsResponse,
zGetWorkspacesCurrentPluginPermissionFetchResponse,
zGetWorkspacesCurrentPluginPreferencesFetchResponse,
zGetWorkspacesCurrentPluginReadmeQuery,
zGetWorkspacesCurrentPluginReadmeResponse,
zGetWorkspacesCurrentPluginTasksByTaskIdPath,
@ -214,6 +218,10 @@ import {
zPostWorkspacesCurrentModelProvidersByProviderPreferredProviderTypeBody,
zPostWorkspacesCurrentModelProvidersByProviderPreferredProviderTypePath,
zPostWorkspacesCurrentModelProvidersByProviderPreferredProviderTypeResponse,
zPostWorkspacesCurrentPluginAutoUpgradeChangeBody,
zPostWorkspacesCurrentPluginAutoUpgradeChangeResponse,
zPostWorkspacesCurrentPluginAutoUpgradeExcludeBody,
zPostWorkspacesCurrentPluginAutoUpgradeExcludeResponse,
zPostWorkspacesCurrentPluginInstallGithubBody,
zPostWorkspacesCurrentPluginInstallGithubResponse,
zPostWorkspacesCurrentPluginInstallMarketplaceBody,
@ -228,10 +236,6 @@ import {
zPostWorkspacesCurrentPluginParametersDynamicOptionsWithCredentialsResponse,
zPostWorkspacesCurrentPluginPermissionChangeBody,
zPostWorkspacesCurrentPluginPermissionChangeResponse,
zPostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeBody,
zPostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeResponse,
zPostWorkspacesCurrentPluginPreferencesChangeBody,
zPostWorkspacesCurrentPluginPreferencesChangeResponse,
zPostWorkspacesCurrentPluginTasksByTaskIdDeleteByIdentifierPath,
zPostWorkspacesCurrentPluginTasksByTaskIdDeleteByIdentifierResponse,
zPostWorkspacesCurrentPluginTasksByTaskIdDeletePath,
@ -1487,7 +1491,58 @@ export const asset = {
get: get20,
}
export const post26 = oc
.route({
inputStructure: 'detailed',
method: 'POST',
operationId: 'postWorkspacesCurrentPluginAutoUpgradeChange',
path: '/workspaces/current/plugin/auto-upgrade/change',
tags: ['console'],
})
.input(z.object({ body: zPostWorkspacesCurrentPluginAutoUpgradeChangeBody }))
.output(zPostWorkspacesCurrentPluginAutoUpgradeChangeResponse)
export const change = {
post: post26,
}
export const post27 = oc
.route({
inputStructure: 'detailed',
method: 'POST',
operationId: 'postWorkspacesCurrentPluginAutoUpgradeExclude',
path: '/workspaces/current/plugin/auto-upgrade/exclude',
tags: ['console'],
})
.input(z.object({ body: zPostWorkspacesCurrentPluginAutoUpgradeExcludeBody }))
.output(zPostWorkspacesCurrentPluginAutoUpgradeExcludeResponse)
export const exclude = {
post: post27,
}
export const get21 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
operationId: 'getWorkspacesCurrentPluginAutoUpgradeFetch',
path: '/workspaces/current/plugin/auto-upgrade/fetch',
tags: ['console'],
})
.input(z.object({ query: zGetWorkspacesCurrentPluginAutoUpgradeFetchQuery }))
.output(zGetWorkspacesCurrentPluginAutoUpgradeFetchResponse)
export const fetch_ = {
get: get21,
}
export const autoUpgrade = {
change,
exclude,
fetch: fetch_,
}
export const get22 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -1498,10 +1553,10 @@ export const get21 = oc
.output(zGetWorkspacesCurrentPluginDebuggingKeyResponse)
export const debuggingKey = {
get: get21,
get: get22,
}
export const get22 = oc
export const get23 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -1513,10 +1568,10 @@ export const get22 = oc
.output(zGetWorkspacesCurrentPluginFetchManifestResponse)
export const fetchManifest = {
get: get22,
get: get23,
}
export const get23 = oc
export const get24 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -1528,10 +1583,10 @@ export const get23 = oc
.output(zGetWorkspacesCurrentPluginIconResponse)
export const icon = {
get: get23,
get: get24,
}
export const post26 = oc
export const post28 = oc
.route({
inputStructure: 'detailed',
method: 'POST',
@ -1543,10 +1598,10 @@ export const post26 = oc
.output(zPostWorkspacesCurrentPluginInstallGithubResponse)
export const github = {
post: post26,
post: post28,
}
export const post27 = oc
export const post29 = oc
.route({
inputStructure: 'detailed',
method: 'POST',
@ -1558,10 +1613,10 @@ export const post27 = oc
.output(zPostWorkspacesCurrentPluginInstallMarketplaceResponse)
export const marketplace = {
post: post27,
post: post29,
}
export const post28 = oc
export const post30 = oc
.route({
inputStructure: 'detailed',
method: 'POST',
@ -1573,7 +1628,7 @@ export const post28 = oc
.output(zPostWorkspacesCurrentPluginInstallPkgResponse)
export const pkg = {
post: post28,
post: post30,
}
export const install = {
@ -1582,7 +1637,7 @@ export const install = {
pkg,
}
export const post29 = oc
export const post31 = oc
.route({
inputStructure: 'detailed',
method: 'POST',
@ -1594,14 +1649,14 @@ export const post29 = oc
.output(zPostWorkspacesCurrentPluginListInstallationsIdsResponse)
export const ids = {
post: post29,
post: post31,
}
export const installations = {
ids,
}
export const post30 = oc
export const post32 = oc
.route({
inputStructure: 'detailed',
method: 'POST',
@ -1613,10 +1668,10 @@ export const post30 = oc
.output(zPostWorkspacesCurrentPluginListLatestVersionsResponse)
export const latestVersions = {
post: post30,
post: post32,
}
export const get24 = oc
export const get25 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -1628,12 +1683,12 @@ export const get24 = oc
.output(zGetWorkspacesCurrentPluginListResponse)
export const list2 = {
get: get24,
get: get25,
installations,
latestVersions,
}
export const get25 = oc
export const get26 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -1645,14 +1700,14 @@ export const get25 = oc
.output(zGetWorkspacesCurrentPluginMarketplacePkgResponse)
export const pkg2 = {
get: get25,
get: get26,
}
export const marketplace2 = {
pkg: pkg2,
}
export const get26 = oc
export const get27 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -1664,13 +1719,13 @@ export const get26 = oc
.output(zGetWorkspacesCurrentPluginParametersDynamicOptionsResponse)
export const dynamicOptions = {
get: get26,
get: get27,
}
/**
* Fetch dynamic options using credentials directly (for edit mode)
*/
export const post31 = oc
export const post33 = oc
.route({
inputStructure: 'detailed',
method: 'POST',
@ -1685,7 +1740,7 @@ export const post31 = oc
.output(zPostWorkspacesCurrentPluginParametersDynamicOptionsWithCredentialsResponse)
export const dynamicOptionsWithCredentials = {
post: post31,
post: post33,
}
export const parameters = {
@ -1693,7 +1748,7 @@ export const parameters = {
dynamicOptionsWithCredentials,
}
export const post32 = oc
export const post34 = oc
.route({
inputStructure: 'detailed',
method: 'POST',
@ -1704,11 +1759,11 @@ export const post32 = oc
.input(z.object({ body: zPostWorkspacesCurrentPluginPermissionChangeBody }))
.output(zPostWorkspacesCurrentPluginPermissionChangeResponse)
export const change = {
post: post32,
export const change2 = {
post: post34,
}
export const get27 = oc
export const get28 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -1718,65 +1773,11 @@ export const get27 = oc
})
.output(zGetWorkspacesCurrentPluginPermissionFetchResponse)
export const fetch_ = {
get: get27,
}
export const permission2 = {
change,
fetch: fetch_,
}
export const post33 = oc
.route({
inputStructure: 'detailed',
method: 'POST',
operationId: 'postWorkspacesCurrentPluginPreferencesAutoupgradeExclude',
path: '/workspaces/current/plugin/preferences/autoupgrade/exclude',
tags: ['console'],
})
.input(z.object({ body: zPostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeBody }))
.output(zPostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeResponse)
export const exclude = {
post: post33,
}
export const autoupgrade = {
exclude,
}
export const post34 = oc
.route({
inputStructure: 'detailed',
method: 'POST',
operationId: 'postWorkspacesCurrentPluginPreferencesChange',
path: '/workspaces/current/plugin/preferences/change',
tags: ['console'],
})
.input(z.object({ body: zPostWorkspacesCurrentPluginPreferencesChangeBody }))
.output(zPostWorkspacesCurrentPluginPreferencesChangeResponse)
export const change2 = {
post: post34,
}
export const get28 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
operationId: 'getWorkspacesCurrentPluginPreferencesFetch',
path: '/workspaces/current/plugin/preferences/fetch',
tags: ['console'],
})
.output(zGetWorkspacesCurrentPluginPreferencesFetchResponse)
export const fetch2 = {
get: get28,
}
export const preferences = {
autoupgrade,
export const permission2 = {
change: change2,
fetch: fetch2,
}
@ -1973,8 +1974,33 @@ export const upload = {
pkg: pkg3,
}
export const get32 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
operationId: 'getWorkspacesCurrentPluginByCategoryList',
path: '/workspaces/current/plugin/{category}/list',
tags: ['console'],
})
.input(
z.object({
params: zGetWorkspacesCurrentPluginByCategoryListPath,
query: zGetWorkspacesCurrentPluginByCategoryListQuery.optional(),
}),
)
.output(zGetWorkspacesCurrentPluginByCategoryListResponse)
export const list3 = {
get: get32,
}
export const byCategory = {
list: list3,
}
export const plugin2 = {
asset,
autoUpgrade,
debuggingKey,
fetchManifest,
icon,
@ -1983,15 +2009,15 @@ export const plugin2 = {
marketplace: marketplace2,
parameters,
permission: permission2,
preferences,
readme,
tasks,
uninstall,
upgrade,
upload,
byCategory,
}
export const get32 = oc
export const get33 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2002,7 +2028,7 @@ export const get32 = oc
.output(zGetWorkspacesCurrentToolLabelsResponse)
export const toolLabels = {
get: get32,
get: get33,
}
export const post44 = oc
@ -2035,7 +2061,7 @@ export const delete9 = {
post: post45,
}
export const get33 = oc
export const get34 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2046,11 +2072,11 @@ export const get33 = oc
.input(z.object({ query: zGetWorkspacesCurrentToolProviderApiGetQuery }))
.output(zGetWorkspacesCurrentToolProviderApiGetResponse)
export const get34 = {
get: get33,
export const get35 = {
get: get34,
}
export const get35 = oc
export const get36 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2062,7 +2088,7 @@ export const get35 = oc
.output(zGetWorkspacesCurrentToolProviderApiRemoteResponse)
export const remote = {
get: get35,
get: get36,
}
export const post46 = oc
@ -2099,7 +2125,7 @@ export const test = {
pre,
}
export const get36 = oc
export const get37 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2111,7 +2137,7 @@ export const get36 = oc
.output(zGetWorkspacesCurrentToolProviderApiToolsResponse)
export const tools = {
get: get36,
get: get37,
}
export const post48 = oc
@ -2132,7 +2158,7 @@ export const update2 = {
export const api = {
add,
delete: delete9,
get: get34,
get: get35,
remote,
schema,
test,
@ -2160,7 +2186,7 @@ export const add2 = {
post: post49,
}
export const get37 = oc
export const get38 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2177,10 +2203,10 @@ export const get37 = oc
.output(zGetWorkspacesCurrentToolProviderBuiltinByProviderCredentialInfoResponse)
export const info = {
get: get37,
get: get38,
}
export const get38 = oc
export const get39 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2200,7 +2226,7 @@ export const get38 = oc
)
export const byCredentialType = {
get: get38,
get: get39,
}
export const schema2 = {
@ -2212,7 +2238,7 @@ export const credential = {
schema: schema2,
}
export const get39 = oc
export const get40 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2229,7 +2255,7 @@ export const get39 = oc
.output(zGetWorkspacesCurrentToolProviderBuiltinByProviderCredentialsResponse)
export const credentials3 = {
get: get39,
get: get40,
}
export const post50 = oc
@ -2272,7 +2298,7 @@ export const delete10 = {
post: post51,
}
export const get40 = oc
export const get41 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2284,10 +2310,10 @@ export const get40 = oc
.output(zGetWorkspacesCurrentToolProviderBuiltinByProviderIconResponse)
export const icon2 = {
get: get40,
get: get41,
}
export const get41 = oc
export const get42 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2299,10 +2325,10 @@ export const get41 = oc
.output(zGetWorkspacesCurrentToolProviderBuiltinByProviderInfoResponse)
export const info2 = {
get: get41,
get: get42,
}
export const get42 = oc
export const get43 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2316,7 +2342,7 @@ export const get42 = oc
.output(zGetWorkspacesCurrentToolProviderBuiltinByProviderOauthClientSchemaResponse)
export const clientSchema = {
get: get42,
get: get43,
}
export const delete11 = oc
@ -2334,7 +2360,7 @@ export const delete11 = oc
)
.output(zDeleteWorkspacesCurrentToolProviderBuiltinByProviderOauthCustomClientResponse)
export const get43 = oc
export const get44 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2365,7 +2391,7 @@ export const post52 = oc
export const customClient = {
delete: delete11,
get: get43,
get: get44,
post: post52,
}
@ -2374,7 +2400,7 @@ export const oauth = {
customClient,
}
export const get44 = oc
export const get45 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2386,7 +2412,7 @@ export const get44 = oc
.output(zGetWorkspacesCurrentToolProviderBuiltinByProviderToolsResponse)
export const tools2 = {
get: get44,
get: get45,
}
export const post53 = oc
@ -2441,7 +2467,7 @@ export const auth = {
post: post54,
}
export const get45 = oc
export const get46 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2453,14 +2479,14 @@ export const get45 = oc
.output(zGetWorkspacesCurrentToolProviderMcpToolsByProviderIdResponse)
export const byProviderId = {
get: get45,
get: get46,
}
export const tools3 = {
byProviderId,
}
export const get46 = oc
export const get47 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2472,7 +2498,7 @@ export const get46 = oc
.output(zGetWorkspacesCurrentToolProviderMcpUpdateByProviderIdResponse)
export const byProviderId2 = {
get: get46,
get: get47,
}
export const update4 = {
@ -2551,7 +2577,7 @@ export const delete13 = {
post: post57,
}
export const get47 = oc
export const get48 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2562,11 +2588,11 @@ export const get47 = oc
.input(z.object({ query: zGetWorkspacesCurrentToolProviderWorkflowGetQuery.optional() }))
.output(zGetWorkspacesCurrentToolProviderWorkflowGetResponse)
export const get48 = {
get: get47,
export const get49 = {
get: get48,
}
export const get49 = oc
export const get50 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2578,7 +2604,7 @@ export const get49 = oc
.output(zGetWorkspacesCurrentToolProviderWorkflowToolsResponse)
export const tools4 = {
get: get49,
get: get50,
}
export const post58 = oc
@ -2599,7 +2625,7 @@ export const update5 = {
export const workflow = {
create: create2,
delete: delete13,
get: get48,
get: get49,
tools: tools4,
update: update5,
}
@ -2611,7 +2637,7 @@ export const toolProvider = {
workflow,
}
export const get50 = oc
export const get51 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2623,10 +2649,10 @@ export const get50 = oc
.output(zGetWorkspacesCurrentToolProvidersResponse)
export const toolProviders = {
get: get50,
get: get51,
}
export const get51 = oc
export const get52 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2637,10 +2663,10 @@ export const get51 = oc
.output(zGetWorkspacesCurrentToolsApiResponse)
export const api2 = {
get: get51,
get: get52,
}
export const get52 = oc
export const get53 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2651,10 +2677,10 @@ export const get52 = oc
.output(zGetWorkspacesCurrentToolsBuiltinResponse)
export const builtin2 = {
get: get52,
get: get53,
}
export const get53 = oc
export const get54 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2665,10 +2691,10 @@ export const get53 = oc
.output(zGetWorkspacesCurrentToolsMcpResponse)
export const mcp2 = {
get: get53,
get: get54,
}
export const get54 = oc
export const get55 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2679,7 +2705,7 @@ export const get54 = oc
.output(zGetWorkspacesCurrentToolsWorkflowResponse)
export const workflow2 = {
get: get54,
get: get55,
}
export const tools5 = {
@ -2689,7 +2715,7 @@ export const tools5 = {
workflow: workflow2,
}
export const get55 = oc
export const get56 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2701,13 +2727,13 @@ export const get55 = oc
.output(zGetWorkspacesCurrentTriggerProviderByProviderIconResponse)
export const icon3 = {
get: get55,
get: get56,
}
/**
* Get info for a trigger provider
*/
export const get56 = oc
export const get57 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2720,7 +2746,7 @@ export const get56 = oc
.output(zGetWorkspacesCurrentTriggerProviderByProviderInfoResponse)
export const info3 = {
get: get56,
get: get57,
}
/**
@ -2741,7 +2767,7 @@ export const delete14 = oc
/**
* Get OAuth client configuration for a provider
*/
export const get57 = oc
export const get58 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2775,7 +2801,7 @@ export const post59 = oc
export const client = {
delete: delete14,
get: get57,
get: get58,
post: post59,
}
@ -2842,7 +2868,7 @@ export const create3 = {
/**
* Get the request logs for a subscription instance for a trigger provider
*/
export const get58 = oc
export const get59 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2863,7 +2889,7 @@ export const get58 = oc
)
export const bySubscriptionBuilderId2 = {
get: get58,
get: get59,
}
export const logs = {
@ -2937,7 +2963,7 @@ export const verifyAndUpdate = {
/**
* Get a subscription instance for a trigger provider
*/
export const get59 = oc
export const get60 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2958,7 +2984,7 @@ export const get59 = oc
)
export const bySubscriptionBuilderId5 = {
get: get59,
get: get60,
}
export const builder = {
@ -2973,7 +2999,7 @@ export const builder = {
/**
* List all trigger subscriptions for the current tenant's provider
*/
export const get60 = oc
export const get61 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -2985,14 +3011,14 @@ export const get60 = oc
.input(z.object({ params: zGetWorkspacesCurrentTriggerProviderByProviderSubscriptionsListPath }))
.output(zGetWorkspacesCurrentTriggerProviderByProviderSubscriptionsListResponse)
export const list3 = {
get: get60,
export const list4 = {
get: get61,
}
/**
* Initiate OAuth authorization flow for a trigger provider
*/
export const get61 = oc
export const get62 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -3009,7 +3035,7 @@ export const get61 = oc
.output(zGetWorkspacesCurrentTriggerProviderByProviderSubscriptionsOauthAuthorizeResponse)
export const authorize = {
get: get61,
get: get62,
}
export const oauth3 = {
@ -3050,7 +3076,7 @@ export const verify = {
export const subscriptions = {
builder,
list: list3,
list: list4,
oauth: oauth3,
verify,
}
@ -3126,7 +3152,7 @@ export const triggerProvider = {
/**
* List all trigger providers for the current tenant
*/
export const get62 = oc
export const get63 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -3138,7 +3164,7 @@ export const get62 = oc
.output(zGetWorkspacesCurrentTriggersResponse)
export const triggers = {
get: get62,
get: get63,
}
export const post67 = oc
@ -3237,7 +3263,7 @@ export const switch3 = {
post: post71,
}
export const get63 = oc
export const get64 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -3249,7 +3275,7 @@ export const get63 = oc
.output(zGetWorkspacesByTenantIdModelProvidersByProviderByIconTypeByLangResponse)
export const byLang = {
get: get63,
get: get64,
}
export const byIconType = {
@ -3268,7 +3294,7 @@ export const byTenantId = {
modelProviders: modelProviders2,
}
export const get64 = oc
export const get65 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -3279,7 +3305,7 @@ export const get64 = oc
.output(zGetWorkspacesResponse)
export const workspaces = {
get: get64,
get: get65,
current,
customConfig,
info: info4,

View File

@ -376,6 +376,30 @@ export type WorkspacePermissionResponse = {
export type BinaryFileResponse = Blob | File
export type ParserAutoUpgradeChange = {
auto_upgrade: PluginAutoUpgradeSettingsPayload
category: PluginCategory
}
export type PluginAutoUpgradeChangeResponse = {
message?: string | null
success: boolean
}
export type ParserExcludePlugin = {
category: PluginCategory
plugin_id: string
}
export type SuccessResponse = {
success: boolean
}
export type PluginAutoUpgradeFetchResponse = {
auto_upgrade: PluginAutoUpgradeSettingsResponseModel
category: PluginCategory
}
export type PluginDebuggingKeyResponse = {
host: string
key: string
@ -432,12 +456,8 @@ export type ParserDynamicOptionsWithCredentials = {
}
export type ParserPermissionChange = {
debug_permission: DebugPermission
install_permission: InstallPermission
}
export type SuccessResponse = {
success: boolean
debug_permission?: DebugPermission
install_permission?: InstallPermission
}
export type PluginPermissionResponse = {
@ -445,25 +465,6 @@ export type PluginPermissionResponse = {
install_permission: InstallPermission
}
export type ParserExcludePlugin = {
plugin_id: string
}
export type PluginOperationSuccessResponse = {
message?: string | null
success: boolean
}
export type ParserPreferencesChange = {
auto_upgrade: PluginAutoUpgradeSettingsPayload
permission: PluginPermissionSettingsPayload
}
export type PluginPreferencesResponse = {
auto_upgrade: PluginAutoUpgradeSettingsPayload
permission: PluginPermissionSettingsPayload
}
export type PluginReadmeResponse = {
readme: string
}
@ -499,6 +500,12 @@ export type ParserGithubUpload = {
version: string
}
export type PluginCategoryListResponse = {
builtin_tools: Array<PluginCategoryBuiltinToolProviderResponse>
has_more: boolean
plugins: Array<PluginCategoryInstalledPluginResponse>
}
export type ToolProviderOpaqueResponse = unknown
export type ApiToolProviderAddPayload = {
@ -901,8 +908,8 @@ export type ModelCredentialLoadBalancingResponse = {
export type ParameterRule = {
default?: unknown | null
help?: I18nObject | null
label: I18nObject
help?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject | null
label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
max?: number | null
min?: number | null
name: string
@ -923,10 +930,6 @@ export type ProviderWithModelsResponse = {
tenant_id: string
}
export type DebugPermission = 'admins' | 'everyone' | 'noone'
export type InstallPermission = 'admins' | 'everyone' | 'noone'
export type PluginAutoUpgradeSettingsPayload = {
exclude_plugins?: Array<string>
include_plugins?: Array<string>
@ -935,9 +938,75 @@ export type PluginAutoUpgradeSettingsPayload = {
upgrade_time_of_day?: number
}
export type PluginPermissionSettingsPayload = {
debug_permission?: DebugPermission
install_permission?: InstallPermission
export type PluginCategory
= | 'agent-strategy'
| 'datasource'
| 'extension'
| 'model'
| 'tool'
| 'trigger'
export type PluginAutoUpgradeSettingsResponseModel = {
exclude_plugins: Array<string>
include_plugins: Array<string>
strategy_setting: StrategySetting
upgrade_mode: UpgradeMode
upgrade_time_of_day: number
}
export type DebugPermission = 'admins' | 'everyone' | 'noone'
export type InstallPermission = 'admins' | 'everyone' | 'noone'
export type PluginCategoryBuiltinToolProviderResponse = {
allow_delete: boolean
author: string
description: CoreToolsEntitiesCommonEntitiesI18nObject
icon:
| string
| {
[key: string]: string
}
icon_dark:
| string
| {
[key: string]: string
}
| null
id: string
is_team_authorization: boolean
label: CoreToolsEntitiesCommonEntitiesI18nObject
labels: Array<string>
name: string
plugin_id: string | null
plugin_unique_identifier: string | null
team_credentials: {
[key: string]: unknown
}
tools: Array<PluginCategoryBuiltinToolResponse>
type: ToolProviderType
[key: string]: unknown
}
export type PluginCategoryInstalledPluginResponse = {
checksum: string
created_at: string
declaration: PluginDeclarationResponse
endpoints_active: number
endpoints_setups: number
id: string
installation_id: string
meta: {
[key: string]: unknown
}
name: string
plugin_id: string
plugin_unique_identifier: string
runtime_type: string
source: PluginInstallationSource
tenant_id: string
updated_at: string
version: string
}
export type ApiProviderSchemaType = 'openai_actions' | 'openai_plugin' | 'openapi' | 'swagger'
@ -976,12 +1045,14 @@ export type CustomConfigurationResponse = {
export type I18nObject = {
en_US: string
ja_JP?: string | null
pt_BR?: string | null
zh_Hans?: string | null
}
export type ProviderHelpEntity = {
title: I18nObject
url: I18nObject
title: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
url: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
}
export type ModelCredentialSchema = {
@ -1036,6 +1107,11 @@ export type ModelStatus
| 'no-permission'
| 'quota-exceeded'
export type GraphonModelRuntimeEntitiesCommonEntitiesI18nObject = {
en_US: string
zh_Hans?: string | null
}
export type ParameterType = 'boolean' | 'float' | 'int' | 'string' | 'text'
export type ProviderModelWithStatusEntity = {
@ -1059,13 +1135,86 @@ export type StrategySetting = 'disabled' | 'fix_only' | 'latest'
export type UpgradeMode = 'all' | 'exclude' | 'partial'
export type CoreToolsEntitiesCommonEntitiesI18nObject = {
en_US: string
ja_JP?: string | null
pt_BR?: string | null
zh_Hans?: string | null
}
export type PluginCategoryBuiltinToolResponse = {
author: string
description: CoreToolsEntitiesCommonEntitiesI18nObject
label: CoreToolsEntitiesCommonEntitiesI18nObject
labels: Array<string>
name: string
output_schema: {
[key: string]: unknown
}
parameters?: Array<{
[key: string]: unknown
}> | null
[key: string]: unknown
}
export type ToolProviderType
= | 'api'
| 'app'
| 'builtin'
| 'dataset-retrieval'
| 'mcp'
| 'plugin'
| 'workflow'
export type PluginDeclarationResponse = {
agent_strategy?: {
[key: string]: unknown
} | null
author: string | null
category: PluginCategory
created_at: string
datasource?: {
[key: string]: unknown
} | null
description: CoreToolsEntitiesCommonEntitiesI18nObject
endpoint?: {
[key: string]: unknown
} | null
icon: string
icon_dark?: string | null
label: CoreToolsEntitiesCommonEntitiesI18nObject
meta: {
[key: string]: unknown
}
model?: ProviderEntityResponse | null
name: string
plugins: {
[key: string]: Array<string> | null
}
repo?: string | null
resource: {
[key: string]: unknown
}
tags?: Array<string>
tool?: {
[key: string]: unknown
} | null
trigger?: {
[key: string]: unknown
} | null
verified?: boolean
version: string
}
export type PluginInstallationSource = 'github' | 'marketplace' | 'package' | 'remote'
export type ToolParameterForm = 'form' | 'llm' | 'schema'
export type AiModelEntityResponse = {
deprecated?: boolean
features?: Array<ModelFeature> | null
fetch_from: FetchFrom
label: I18nObject
label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
model: string
model_properties: {
[key in ModelPropertyKey]?: unknown
@ -1094,10 +1243,10 @@ export type CustomModelConfiguration = {
export type CredentialFormSchema = {
default?: string | null
label: I18nObject
label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
max_length?: number
options?: Array<FormOption> | null
placeholder?: I18nObject | null
placeholder?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject | null
required?: boolean
show_on?: Array<FormShowOnObject>
type: FormType
@ -1105,8 +1254,8 @@ export type CredentialFormSchema = {
}
export type FieldModelSchema = {
label: I18nObject
placeholder?: I18nObject | null
label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
placeholder?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject | null
}
export type ProviderQuotaType = 'free' | 'paid' | 'trial'
@ -1120,6 +1269,25 @@ export type QuotaConfiguration = {
restrict_models?: Array<RestrictModel>
}
export type ProviderEntityResponse = {
background?: string | null
configurate_methods: Array<ConfigurateMethod>
description?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject | null
help?: ProviderHelpEntity | null
icon_small?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject | null
icon_small_dark?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject | null
label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
model_credential_schema?: ModelCredentialSchema | null
models?: Array<AiModelEntityResponse>
position?: {
[key: string]: Array<string>
} | null
provider: string
provider_credential_schema?: ProviderCredentialSchema | null
provider_name?: string
supported_model_types: Array<ModelType>
}
export type PriceConfigResponse = {
currency: string
input: string
@ -1128,7 +1296,7 @@ export type PriceConfigResponse = {
}
export type FormOption = {
label: I18nObject
label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
show_on?: Array<FormShowOnObject>
value: string
}
@ -2166,6 +2334,50 @@ export type GetWorkspacesCurrentPluginAssetResponses = {
export type GetWorkspacesCurrentPluginAssetResponse
= GetWorkspacesCurrentPluginAssetResponses[keyof GetWorkspacesCurrentPluginAssetResponses]
export type PostWorkspacesCurrentPluginAutoUpgradeChangeData = {
body: ParserAutoUpgradeChange
path?: never
query?: never
url: '/workspaces/current/plugin/auto-upgrade/change'
}
export type PostWorkspacesCurrentPluginAutoUpgradeChangeResponses = {
200: PluginAutoUpgradeChangeResponse
}
export type PostWorkspacesCurrentPluginAutoUpgradeChangeResponse
= PostWorkspacesCurrentPluginAutoUpgradeChangeResponses[keyof PostWorkspacesCurrentPluginAutoUpgradeChangeResponses]
export type PostWorkspacesCurrentPluginAutoUpgradeExcludeData = {
body: ParserExcludePlugin
path?: never
query?: never
url: '/workspaces/current/plugin/auto-upgrade/exclude'
}
export type PostWorkspacesCurrentPluginAutoUpgradeExcludeResponses = {
200: SuccessResponse
}
export type PostWorkspacesCurrentPluginAutoUpgradeExcludeResponse
= PostWorkspacesCurrentPluginAutoUpgradeExcludeResponses[keyof PostWorkspacesCurrentPluginAutoUpgradeExcludeResponses]
export type GetWorkspacesCurrentPluginAutoUpgradeFetchData = {
body?: never
path?: never
query: {
category: 'agent-strategy' | 'datasource' | 'extension' | 'model' | 'tool' | 'trigger'
}
url: '/workspaces/current/plugin/auto-upgrade/fetch'
}
export type GetWorkspacesCurrentPluginAutoUpgradeFetchResponses = {
200: PluginAutoUpgradeFetchResponse
}
export type GetWorkspacesCurrentPluginAutoUpgradeFetchResponse
= GetWorkspacesCurrentPluginAutoUpgradeFetchResponses[keyof GetWorkspacesCurrentPluginAutoUpgradeFetchResponses]
export type GetWorkspacesCurrentPluginDebuggingKeyData = {
body?: never
path?: never
@ -2379,48 +2591,6 @@ export type GetWorkspacesCurrentPluginPermissionFetchResponses = {
export type GetWorkspacesCurrentPluginPermissionFetchResponse
= GetWorkspacesCurrentPluginPermissionFetchResponses[keyof GetWorkspacesCurrentPluginPermissionFetchResponses]
export type PostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeData = {
body: ParserExcludePlugin
path?: never
query?: never
url: '/workspaces/current/plugin/preferences/autoupgrade/exclude'
}
export type PostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeResponses = {
200: PluginOperationSuccessResponse
}
export type PostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeResponse
= PostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeResponses[keyof PostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeResponses]
export type PostWorkspacesCurrentPluginPreferencesChangeData = {
body: ParserPreferencesChange
path?: never
query?: never
url: '/workspaces/current/plugin/preferences/change'
}
export type PostWorkspacesCurrentPluginPreferencesChangeResponses = {
200: PluginOperationSuccessResponse
}
export type PostWorkspacesCurrentPluginPreferencesChangeResponse
= PostWorkspacesCurrentPluginPreferencesChangeResponses[keyof PostWorkspacesCurrentPluginPreferencesChangeResponses]
export type GetWorkspacesCurrentPluginPreferencesFetchData = {
body?: never
path?: never
query?: never
url: '/workspaces/current/plugin/preferences/fetch'
}
export type GetWorkspacesCurrentPluginPreferencesFetchResponses = {
200: PluginPreferencesResponse
}
export type GetWorkspacesCurrentPluginPreferencesFetchResponse
= GetWorkspacesCurrentPluginPreferencesFetchResponses[keyof GetWorkspacesCurrentPluginPreferencesFetchResponses]
export type GetWorkspacesCurrentPluginReadmeData = {
body?: never
path?: never
@ -2602,6 +2772,25 @@ export type PostWorkspacesCurrentPluginUploadPkgResponses = {
export type PostWorkspacesCurrentPluginUploadPkgResponse
= PostWorkspacesCurrentPluginUploadPkgResponses[keyof PostWorkspacesCurrentPluginUploadPkgResponses]
export type GetWorkspacesCurrentPluginByCategoryListData = {
body?: never
path: {
category: string
}
query?: {
page?: number
page_size?: number
}
url: '/workspaces/current/plugin/{category}/list'
}
export type GetWorkspacesCurrentPluginByCategoryListResponses = {
200: PluginCategoryListResponse
}
export type GetWorkspacesCurrentPluginByCategoryListResponse
= GetWorkspacesCurrentPluginByCategoryListResponses[keyof GetWorkspacesCurrentPluginByCategoryListResponses]
export type GetWorkspacesCurrentToolLabelsData = {
body?: never
path?: never

View File

@ -289,6 +289,21 @@ export const zWorkspacePermissionResponse = z.object({
*/
export const zBinaryFileResponse = z.custom<Blob | File>()
/**
* PluginAutoUpgradeChangeResponse
*/
export const zPluginAutoUpgradeChangeResponse = z.object({
message: z.string().nullish(),
success: z.boolean(),
})
/**
* SuccessResponse
*/
export const zSuccessResponse = z.object({
success: z.boolean(),
})
/**
* PluginDebuggingKeyResponse
*/
@ -375,28 +390,6 @@ export const zParserDynamicOptionsWithCredentials = z.object({
provider: z.string(),
})
/**
* SuccessResponse
*/
export const zSuccessResponse = z.object({
success: z.boolean(),
})
/**
* ParserExcludePlugin
*/
export const zParserExcludePlugin = z.object({
plugin_id: z.string(),
})
/**
* PluginOperationSuccessResponse
*/
export const zPluginOperationSuccessResponse = z.object({
message: z.string().nullish(),
success: z.boolean(),
})
/**
* PluginReadmeResponse
*/
@ -1022,6 +1015,26 @@ export const zModelCredentialResponse = z.object({
load_balancing: zModelCredentialLoadBalancingResponse,
})
/**
* PluginCategory
*/
export const zPluginCategory = z.enum([
'agent-strategy',
'datasource',
'extension',
'model',
'tool',
'trigger',
])
/**
* ParserExcludePlugin
*/
export const zParserExcludePlugin = z.object({
category: zPluginCategory,
plugin_id: z.string(),
})
/**
* DebugPermission
*/
@ -1036,8 +1049,8 @@ export const zInstallPermission = z.enum(['admins', 'everyone', 'noone'])
* ParserPermissionChange
*/
export const zParserPermissionChange = z.object({
debug_permission: zDebugPermission,
install_permission: zInstallPermission,
debug_permission: zDebugPermission.optional().default('everyone'),
install_permission: zInstallPermission.optional().default('everyone'),
})
/**
@ -1048,14 +1061,6 @@ export const zPluginPermissionResponse = z.object({
install_permission: zInstallPermission,
})
/**
* PluginPermissionSettingsPayload
*/
export const zPluginPermissionSettingsPayload = z.object({
debug_permission: zDebugPermission.optional().default('everyone'),
install_permission: zInstallPermission.optional().default('everyone'),
})
/**
* ApiProviderSchemaType
*
@ -1178,19 +1183,11 @@ export const zConfigurateMethod = z.enum(['customizable-model', 'predefined-mode
*/
export const zI18nObject = z.object({
en_US: z.string(),
ja_JP: z.string().nullish(),
pt_BR: z.string().nullish(),
zh_Hans: z.string().nullish(),
})
/**
* ProviderHelpEntity
*
* Model class for provider help.
*/
export const zProviderHelpEntity = z.object({
title: zI18nObject,
url: zI18nObject,
})
/**
* ProviderType
*/
@ -1254,6 +1251,26 @@ export const zModelStatus = z.enum([
'quota-exceeded',
])
/**
* I18nObject
*
* Model class for i18n object.
*/
export const zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject = z.object({
en_US: z.string(),
zh_Hans: z.string().nullish(),
})
/**
* ProviderHelpEntity
*
* Model class for provider help.
*/
export const zProviderHelpEntity = z.object({
title: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject,
url: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject,
})
/**
* ParameterType
*
@ -1268,8 +1285,8 @@ export const zParameterType = z.enum(['boolean', 'float', 'int', 'string', 'text
*/
export const zParameterRule = z.object({
default: z.unknown().nullish(),
help: zI18nObject.nullish(),
label: zI18nObject,
help: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.nullish(),
label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject,
max: z.number().nullish(),
min: z.number().nullish(),
name: z.string(),
@ -1356,21 +1373,98 @@ export const zPluginAutoUpgradeSettingsPayload = z.object({
})
/**
* ParserPreferencesChange
* ParserAutoUpgradeChange
*/
export const zParserPreferencesChange = z.object({
export const zParserAutoUpgradeChange = z.object({
auto_upgrade: zPluginAutoUpgradeSettingsPayload,
permission: zPluginPermissionSettingsPayload,
category: zPluginCategory,
})
/**
* PluginPreferencesResponse
* PluginAutoUpgradeSettingsResponseModel
*/
export const zPluginPreferencesResponse = z.object({
auto_upgrade: zPluginAutoUpgradeSettingsPayload,
permission: zPluginPermissionSettingsPayload,
export const zPluginAutoUpgradeSettingsResponseModel = z.object({
exclude_plugins: z.array(z.string()),
include_plugins: z.array(z.string()),
strategy_setting: zStrategySetting,
upgrade_mode: zUpgradeMode,
upgrade_time_of_day: z.int(),
})
/**
* PluginAutoUpgradeFetchResponse
*/
export const zPluginAutoUpgradeFetchResponse = z.object({
auto_upgrade: zPluginAutoUpgradeSettingsResponseModel,
category: zPluginCategory,
})
/**
* I18nObject
*
* Model class for i18n object.
*/
export const zCoreToolsEntitiesCommonEntitiesI18nObject = z.object({
en_US: z.string(),
ja_JP: z.string().nullish(),
pt_BR: z.string().nullish(),
zh_Hans: z.string().nullish(),
})
/**
* PluginCategoryBuiltinToolResponse
*/
export const zPluginCategoryBuiltinToolResponse = z.object({
author: z.string(),
description: zCoreToolsEntitiesCommonEntitiesI18nObject,
label: zCoreToolsEntitiesCommonEntitiesI18nObject,
labels: z.array(z.string()),
name: z.string(),
output_schema: z.record(z.string(), z.unknown()),
parameters: z.array(z.record(z.string(), z.unknown())).nullish(),
})
/**
* ToolProviderType
*
* Enum class for tool provider
*/
export const zToolProviderType = z.enum([
'api',
'app',
'builtin',
'dataset-retrieval',
'mcp',
'plugin',
'workflow',
])
/**
* PluginCategoryBuiltinToolProviderResponse
*/
export const zPluginCategoryBuiltinToolProviderResponse = z.object({
allow_delete: z.boolean(),
author: z.string(),
description: zCoreToolsEntitiesCommonEntitiesI18nObject,
icon: z.union([z.string(), z.record(z.string(), z.string())]),
icon_dark: z.union([z.string(), z.record(z.string(), z.string())]).nullable(),
id: z.string(),
is_team_authorization: z.boolean(),
label: zCoreToolsEntitiesCommonEntitiesI18nObject,
labels: z.array(z.string()),
name: z.string(),
plugin_id: z.string().nullable(),
plugin_unique_identifier: z.string().nullable(),
team_credentials: z.record(z.string(), z.unknown()),
tools: z.array(zPluginCategoryBuiltinToolResponse),
type: zToolProviderType,
})
/**
* PluginInstallationSource
*/
export const zPluginInstallationSource = z.enum(['github', 'marketplace', 'package', 'remote'])
/**
* ToolParameterForm
*/
@ -1458,8 +1552,8 @@ export const zCustomConfigurationResponse = z.object({
* FieldModelSchema
*/
export const zFieldModelSchema = z.object({
label: zI18nObject,
placeholder: zI18nObject.nullish(),
label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject,
placeholder: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.nullish(),
})
/**
@ -1489,7 +1583,7 @@ export const zAiModelEntityResponse = z.object({
deprecated: z.boolean().optional().default(false),
features: z.array(zModelFeature).nullish(),
fetch_from: zFetchFrom,
label: zI18nObject,
label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject,
model: z.string(),
model_properties: z.record(z.string(), z.unknown()),
model_type: zModelType,
@ -1573,7 +1667,7 @@ export const zFormShowOnObject = z.object({
* Model class for form option.
*/
export const zFormOption = z.object({
label: zI18nObject,
label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject,
show_on: z.array(zFormShowOnObject).optional().default([]),
value: z.string(),
})
@ -1592,10 +1686,10 @@ export const zFormType = z.enum(['radio', 'secret-input', 'select', 'switch', 't
*/
export const zCredentialFormSchema = z.object({
default: z.string().nullish(),
label: zI18nObject,
label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject,
max_length: z.int().optional().default(0),
options: z.array(zFormOption).nullish(),
placeholder: zI18nObject.nullish(),
placeholder: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.nullish(),
required: z.boolean().optional().default(true),
show_on: z.array(zFormShowOnObject).optional().default([]),
type: zFormType,
@ -1621,6 +1715,86 @@ export const zProviderCredentialSchema = z.object({
credential_form_schemas: z.array(zCredentialFormSchema),
})
/**
* ProviderEntityResponse
*
* Runtime provider response with codegen-safe model pricing schemas.
*/
export const zProviderEntityResponse = z.object({
background: z.string().nullish(),
configurate_methods: z.array(zConfigurateMethod),
description: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.nullish(),
help: zProviderHelpEntity.nullish(),
icon_small: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.nullish(),
icon_small_dark: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.nullish(),
label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject,
model_credential_schema: zModelCredentialSchema.nullish(),
models: z.array(zAiModelEntityResponse).optional().default([]),
position: z.record(z.string(), z.array(z.string())).nullish().default({}),
provider: z.string(),
provider_credential_schema: zProviderCredentialSchema.nullish(),
provider_name: z.string().optional().default(''),
supported_model_types: z.array(zModelType),
})
/**
* PluginDeclarationResponse
*/
export const zPluginDeclarationResponse = z.object({
agent_strategy: z.record(z.string(), z.unknown()).nullish(),
author: z.string().nullable(),
category: zPluginCategory,
created_at: z.iso.datetime(),
datasource: z.record(z.string(), z.unknown()).nullish(),
description: zCoreToolsEntitiesCommonEntitiesI18nObject,
endpoint: z.record(z.string(), z.unknown()).nullish(),
icon: z.string(),
icon_dark: z.string().nullish(),
label: zCoreToolsEntitiesCommonEntitiesI18nObject,
meta: z.record(z.string(), z.unknown()),
model: zProviderEntityResponse.nullish(),
name: z.string(),
plugins: z.record(z.string(), z.array(z.string()).nullable()),
repo: z.string().nullish(),
resource: z.record(z.string(), z.unknown()),
tags: z.array(z.string()).optional(),
tool: z.record(z.string(), z.unknown()).nullish(),
trigger: z.record(z.string(), z.unknown()).nullish(),
verified: z.boolean().optional().default(false),
version: z.string(),
})
/**
* PluginCategoryInstalledPluginResponse
*/
export const zPluginCategoryInstalledPluginResponse = z.object({
checksum: z.string(),
created_at: z.iso.datetime(),
declaration: zPluginDeclarationResponse,
endpoints_active: z.int(),
endpoints_setups: z.int(),
id: z.string(),
installation_id: z.string(),
meta: z.record(z.string(), z.unknown()),
name: z.string(),
plugin_id: z.string(),
plugin_unique_identifier: z.string(),
runtime_type: z.string(),
source: zPluginInstallationSource,
tenant_id: z.string(),
updated_at: z.iso.datetime(),
version: z.string(),
})
/**
* PluginCategoryListResponse
*/
export const zPluginCategoryListResponse = z.object({
builtin_tools: z.array(zPluginCategoryBuiltinToolProviderResponse),
has_more: z.boolean(),
plugins: z.array(zPluginCategoryInstalledPluginResponse),
})
/**
* QuotaUnit
*/
@ -2294,6 +2468,30 @@ export const zGetWorkspacesCurrentPluginAssetQuery = z.object({
*/
export const zGetWorkspacesCurrentPluginAssetResponse = zBinaryFileResponse
export const zPostWorkspacesCurrentPluginAutoUpgradeChangeBody = zParserAutoUpgradeChange
/**
* Success
*/
export const zPostWorkspacesCurrentPluginAutoUpgradeChangeResponse
= zPluginAutoUpgradeChangeResponse
export const zPostWorkspacesCurrentPluginAutoUpgradeExcludeBody = zParserExcludePlugin
/**
* Success
*/
export const zPostWorkspacesCurrentPluginAutoUpgradeExcludeResponse = zSuccessResponse
export const zGetWorkspacesCurrentPluginAutoUpgradeFetchQuery = z.object({
category: z.enum(['agent-strategy', 'datasource', 'extension', 'model', 'tool', 'trigger']),
})
/**
* Success
*/
export const zGetWorkspacesCurrentPluginAutoUpgradeFetchResponse = zPluginAutoUpgradeFetchResponse
/**
* Success
*/
@ -2408,26 +2606,6 @@ export const zPostWorkspacesCurrentPluginPermissionChangeResponse = zSuccessResp
*/
export const zGetWorkspacesCurrentPluginPermissionFetchResponse = zPluginPermissionResponse
export const zPostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeBody = zParserExcludePlugin
/**
* Success
*/
export const zPostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeResponse
= zPluginOperationSuccessResponse
export const zPostWorkspacesCurrentPluginPreferencesChangeBody = zParserPreferencesChange
/**
* Success
*/
export const zPostWorkspacesCurrentPluginPreferencesChangeResponse = zPluginOperationSuccessResponse
/**
* Success
*/
export const zGetWorkspacesCurrentPluginPreferencesFetchResponse = zPluginPreferencesResponse
export const zGetWorkspacesCurrentPluginReadmeQuery = z.object({
language: z.string().optional().default('en-US'),
plugin_unique_identifier: z.string(),
@ -2519,6 +2697,20 @@ export const zPostWorkspacesCurrentPluginUploadGithubResponse = zPluginDaemonOpe
*/
export const zPostWorkspacesCurrentPluginUploadPkgResponse = zPluginDaemonOperationResponse
export const zGetWorkspacesCurrentPluginByCategoryListPath = z.object({
category: z.string(),
})
export const zGetWorkspacesCurrentPluginByCategoryListQuery = z.object({
page: z.int().gte(1).optional().default(1),
page_size: z.int().gte(1).lte(256).optional().default(256),
})
/**
* Success
*/
export const zGetWorkspacesCurrentPluginByCategoryListResponse = zPluginCategoryListResponse
/**
* Success
*/

View File

@ -265,6 +265,12 @@
line-height: 1.2;
}
@utility title-5xl-semi-bold {
font-size: 30px;
font-weight: 600;
line-height: 1.2;
}
@utility title-5xl-bold {
font-size: 30px;
font-weight: 700;

View File

@ -156,6 +156,19 @@ html[data-theme="dark"] {
--color-components-main-nav-nav-button-bg-active: rgb(200 206 218 / 0.14);
--color-components-main-nav-nav-button-border: rgb(255 255 255 / 0.08);
--color-components-main-nav-nav-button-bg-hover: rgb(200 206 218 / 0.04);
--color-components-main-nav-glass-text-glow: #3146ff2e;
--color-components-main-nav-glass-surface-first: #0033ff14;
--color-components-main-nav-glass-surface-middle-1: #0033ff1f;
--color-components-main-nav-glass-surface-middle-2: #0033ff1a;
--color-components-main-nav-glass-surface-end: #0033ff14;
--color-components-main-nav-glass-edge-highlight-first: #fffffffa;
--color-components-main-nav-glass-edge-highlight-end: #ffffff6b;
--color-components-main-nav-glass-edge-reflection-first: #0033ff00;
--color-components-main-nav-glass-edge-reflection-middle: #0033ff99;
--color-components-main-nav-glass-edge-reflection-end: #0033ff00;
--color-components-main-nav-glass-inner-glow: #ffffff4d;
--color-components-main-nav-glass-shadow-reflection: #0033ff0a;
--color-components-main-nav-glass-shadow-reflection-glow: #ffffff00;
--color-components-main-nav-nav-user-border: rgb(255 255 255 / 0.05);

View File

@ -156,6 +156,19 @@ html[data-theme="light"] {
--color-components-main-nav-nav-button-bg-active: #fcfcfd;
--color-components-main-nav-nav-button-border: rgb(255 255 255 / 0.95);
--color-components-main-nav-nav-button-bg-hover: rgb(16 24 40 / 0.04);
--color-components-main-nav-glass-text-glow: #3146ff2e;
--color-components-main-nav-glass-surface-first: #0033ff14;
--color-components-main-nav-glass-surface-middle-1: #0033ff1f;
--color-components-main-nav-glass-surface-middle-2: #0033ff1a;
--color-components-main-nav-glass-surface-end: #0033ff14;
--color-components-main-nav-glass-edge-highlight-first: #fffffffa;
--color-components-main-nav-glass-edge-highlight-end: #ffffff6b;
--color-components-main-nav-glass-edge-reflection-first: #0033ff00;
--color-components-main-nav-glass-edge-reflection-middle: #0033ff99;
--color-components-main-nav-glass-edge-reflection-end: #0033ff00;
--color-components-main-nav-glass-inner-glow: #ffffff4d;
--color-components-main-nav-glass-shadow-reflection: #0033ff0a;
--color-components-main-nav-glass-shadow-reflection-glow: #ffffff00;
--color-components-main-nav-nav-user-border: #ffffff;

View File

@ -163,6 +163,19 @@
--color-components-main-nav-nav-button-bg-active: var(--color-components-main-nav-nav-button-bg-active);
--color-components-main-nav-nav-button-border: var(--color-components-main-nav-nav-button-border);
--color-components-main-nav-nav-button-bg-hover: var(--color-components-main-nav-nav-button-bg-hover);
--color-components-main-nav-glass-text-glow: var(--color-components-main-nav-glass-text-glow);
--color-components-main-nav-glass-surface-first: var(--color-components-main-nav-glass-surface-first);
--color-components-main-nav-glass-surface-middle-1: var(--color-components-main-nav-glass-surface-middle-1);
--color-components-main-nav-glass-surface-middle-2: var(--color-components-main-nav-glass-surface-middle-2);
--color-components-main-nav-glass-surface-end: var(--color-components-main-nav-glass-surface-end);
--color-components-main-nav-glass-edge-highlight-first: var(--color-components-main-nav-glass-edge-highlight-first);
--color-components-main-nav-glass-edge-highlight-end: var(--color-components-main-nav-glass-edge-highlight-end);
--color-components-main-nav-glass-edge-reflection-first: var(--color-components-main-nav-glass-edge-reflection-first);
--color-components-main-nav-glass-edge-reflection-middle: var(--color-components-main-nav-glass-edge-reflection-middle);
--color-components-main-nav-glass-edge-reflection-end: var(--color-components-main-nav-glass-edge-reflection-end);
--color-components-main-nav-glass-inner-glow: var(--color-components-main-nav-glass-inner-glow);
--color-components-main-nav-glass-shadow-reflection: var(--color-components-main-nav-glass-shadow-reflection);
--color-components-main-nav-glass-shadow-reflection-glow: var(--color-components-main-nav-glass-shadow-reflection-glow);
--color-components-main-nav-nav-user-border: var(--color-components-main-nav-nav-user-border);

View File

@ -0,0 +1,43 @@
# @dify/iconify-collections
Pre-generated Iconify collections for Dify custom SVG icons. The web app imports these collections from this package so Tailwind does not need to scan and build custom SVG icon data from the old `web/app/components/base/icons/src` tree during dev startup.
## Adding Custom SVG Icons
Add new SVG source files under one of these directories:
- `assets/public/...` for multi-color or public brand-like icons.
- `assets/vender/...` for UI vendor icons that should render with `currentColor`.
After adding or changing SVG files, regenerate the packaged collections:
```bash
pnpm --filter @dify/iconify-collections generate
```
Then run the dimension guard:
```bash
pnpm --filter @dify/iconify-collections check:dimensions
```
This protects existing icon groups with layout-sensitive intrinsic sizes, such as the `main-nav-*` icons that must remain `20x20` after collection flattening.
Commit both the SVG source files and the generated package files under `custom-public/` or `custom-vender/`.
Restart the web dev server after regenerating icons. Tailwind loads this plugin collection at startup, so an already-running dev server may not render newly-added `i-custom-*` classes until it restarts.
Use the generated icons through Tailwind icon classes in frontend code. For example:
```text
assets/vender/integrations/mcp.svg
```
becomes:
```tsx
<span aria-hidden className="i-custom-vender-integrations-mcp size-4" />
```
Do not add new generated React icon components or JSON files under `web/app/components/base/icons/src/...` for new custom SVG icons. That path is legacy; new custom icons should flow through this package and be consumed as `i-custom-*` classes.
When reviewing generated `icons.json` diffs, check that unrelated existing icon groups did not lose or change their intrinsic `width` and `height`. If a group is layout-sensitive, add it to `scripts/check-icon-dimensions.ts`.

View File

@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 15.3333 14.6667" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M13.1423 4.75207L12.9779 5.12919C12.8576 5.40528 12.4757 5.40528 12.3554 5.12919L12.1911 4.75207C11.8981 4.07965 11.3703 3.54427 10.7118 3.25139L10.2053 3.02615C9.93153 2.90435 9.93153 2.50587 10.2053 2.38408L10.6835 2.17143C11.3589 1.87101 11.8961 1.31582 12.1841 0.620552L12.3529 0.213023C12.4705 -0.0710075 12.8628 -0.0710075 12.9804 0.213023L13.1492 0.620552C13.4372 1.31582 13.9744 1.87101 14.6499 2.17143L15.1279 2.38408C15.4018 2.50587 15.4018 2.90435 15.1279 3.02615L14.6215 3.25139C13.963 3.54427 13.4353 4.07965 13.1423 4.75207ZM5.33333 1.33333C8.045 1.33333 10.284 3.35708 10.6225 5.97663L12.1228 8.3358C12.2216 8.49113 12.2017 8.72313 11.9729 8.82113L10.6667 9.38067V11.3333C10.6667 12.0697 10.0697 12.6667 9.33333 12.6667H8.00067L8 14.6667H2L2.00017 12.2041C2.00022 11.4168 1.70901 10.6725 1.17033 10.0007C0.438047 9.08753 0 7.92827 0 6.66667C0 3.72115 2.38781 1.33333 5.33333 1.33333ZM13.4357 12.0683L12.3262 11.3286C12.9624 10.3761 13.3333 9.2314 13.3333 8.00007C13.3333 7.65933 13.3049 7.3252 13.2504 7L14.5457 6.66667C14.6252 7.09907 14.6667 7.54467 14.6667 8.00007C14.6667 9.50507 14.2133 10.9041 13.4357 12.0683Z" fill="var(--fill-0, #18222F)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 15.3333 14.6667" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M13.1423 4.75207L12.9779 5.12919C12.8576 5.40528 12.4757 5.40528 12.3554 5.12919L12.1911 4.75207C11.8981 4.07965 11.3703 3.54427 10.7118 3.25139L10.2053 3.02615C9.93153 2.90435 9.93153 2.50587 10.2053 2.38408L10.6835 2.17143C11.3589 1.87101 11.8961 1.31582 12.1841 0.620552L12.3529 0.213023C12.4705 -0.0710075 12.8628 -0.0710075 12.9804 0.213023L13.1492 0.620552C13.4372 1.31582 13.9744 1.87101 14.6499 2.17143L15.1279 2.38408C15.4018 2.50587 15.4018 2.90435 15.1279 3.02615L14.6215 3.25139C13.963 3.54427 13.4353 4.07965 13.1423 4.75207ZM5.33333 1.33333C8.045 1.33333 10.284 3.35708 10.6225 5.97663L12.1228 8.3358C12.2216 8.49113 12.2017 8.72313 11.9729 8.82113L10.6667 9.38067V11.3333C10.6667 12.0697 10.0697 12.6667 9.33333 12.6667H8.00067L8 14.6667H2L2.00017 12.2041C2.00022 11.4168 1.70901 10.6725 1.17033 10.0007C0.438047 9.08753 0 7.92827 0 6.66667C0 3.72115 2.38781 1.33333 5.33333 1.33333ZM5.33333 2.66667C3.12419 2.66667 1.33333 4.45753 1.33333 6.66667C1.33333 7.58993 1.64545 8.46193 2.21052 9.1666C2.93977 10.076 3.33357 11.1115 3.3335 12.2042L3.33342 13.3333H6.66713L6.6678 11.3333H9.33333V8.50127L10.3665 8.05873L9.33813 6.44175L9.30007 6.14745C9.04433 4.16761 7.34953 2.66667 5.33333 2.66667ZM12.3262 11.3286L13.4357 12.0683C14.2133 10.9041 14.6667 9.50507 14.6667 8.00007C14.6667 7.54467 14.6252 7.09907 14.5457 6.66667L13.2504 7C13.3049 7.3252 13.3333 7.65933 13.3333 8.00007C13.3333 9.2314 12.9624 10.3761 12.3262 11.3286Z" fill="var(--fill-0, #495464)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,10 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Icon">
<g id="Vector">
<path d="M5.92578 11.0095C5.92578 10.0175 5.12163 9.21267 4.12956 9.21267C3.13752 9.21271 2.33333 10.0175 2.33333 11.0095C2.33349 12.0015 3.13762 12.8057 4.12956 12.8058C5.12153 12.8058 5.92562 12.0015 5.92578 11.0095ZM13.6667 11.0095C13.6667 10.0175 12.8625 9.21271 11.8704 9.21267C10.8784 9.21267 10.0742 10.0175 10.0742 11.0095C10.0744 12.0015 10.8785 12.8058 11.8704 12.8058C12.8624 12.8057 13.6665 12.0015 13.6667 11.0095ZM9.79622 4.324C9.79619 3.33197 8.99205 2.52778 8 2.52778C7.00795 2.52778 6.20382 3.33197 6.20378 4.324C6.20378 5.31607 7.00793 6.12023 8 6.12023C8.99207 6.12023 9.79622 5.31607 9.79622 4.324ZM11.1296 4.324C11.1296 5.82362 10.0748 7.07639 8.66667 7.38194V7.9197L9.74284 8.71398C10.3012 8.19618 11.0489 7.87934 11.8704 7.87934C13.5989 7.87938 15 9.28112 15 11.0095C14.9998 12.7378 13.5988 14.1391 11.8704 14.1391C10.1421 14.1391 8.74104 12.7379 8.74089 11.0095C8.74089 10.5838 8.82585 10.1777 8.97982 9.80772L8 9.08377L7.01953 9.80772C7.17356 10.1778 7.25911 10.5837 7.25911 11.0095C7.25896 12.7379 5.85791 14.1391 4.12956 14.1391C2.40124 14.1391 1.00016 12.7378 1 11.0095C1 9.28112 2.40114 7.87938 4.12956 7.87934C4.95094 7.87934 5.69819 8.19637 6.25651 8.71398L7.33333 7.9197V7.38194C5.92523 7.07639 4.87044 5.82362 4.87044 4.324C4.87048 2.59559 6.27158 1.19444 8 1.19444C9.72842 1.19444 11.1295 2.59559 11.1296 4.324Z" fill="var(--fill-0, #18222F)"/>
<path d="M9.79622 4.324C9.79619 3.33197 8.99205 2.52778 8 2.52778C7.00795 2.52778 6.20382 3.33197 6.20378 4.324C6.20378 5.31607 7.00793 6.12023 8 6.12023C8.99207 6.12023 9.79622 5.31607 9.79622 4.324Z" fill="var(--fill-0, #18222F)"/>
<path d="M5.92578 11.0095C5.92578 10.0175 5.12163 9.21267 4.12956 9.21267C3.13752 9.21271 2.33333 10.0175 2.33333 11.0095C2.33349 12.0015 3.13762 12.8057 4.12956 12.8058C5.12153 12.8058 5.92562 12.0015 5.92578 11.0095Z" fill="var(--fill-0, #18222F)"/>
<path d="M13.6667 11.0095C13.6667 10.0175 12.8625 9.21271 11.8704 9.21267C10.8784 9.21267 10.0742 10.0175 10.0742 11.0095C10.0744 12.0015 10.8785 12.8058 11.8704 12.8058C12.8624 12.8057 13.6665 12.0015 13.6667 11.0095Z" fill="var(--fill-0, #18222F)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 14 12.9447" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M4.92578 9.8151C4.92578 8.82303 4.12163 8.01823 3.12956 8.01823C2.13752 8.01827 1.33333 8.82306 1.33333 9.8151C1.33349 10.807 2.13762 11.6113 3.12956 11.6113C4.12153 11.6113 4.92563 10.807 4.92578 9.8151ZM12.6667 9.8151C12.6667 8.82306 11.8625 8.01827 10.8704 8.01823C9.87837 8.01823 9.07422 8.82303 9.07422 9.8151C9.07438 10.807 9.87847 11.6113 10.8704 11.6113C11.8624 11.6113 12.6665 10.807 12.6667 9.8151ZM8.79622 3.12956C8.79619 2.13752 7.99205 1.33333 7 1.33333C6.00795 1.33333 5.20382 2.13752 5.20378 3.12956C5.20378 4.12163 6.00793 4.92578 7 4.92578C7.99207 4.92578 8.79622 4.12163 8.79622 3.12956ZM10.1296 3.12956C10.1296 4.62918 9.07477 5.88194 7.66667 6.1875V6.72526L8.74284 7.51953C9.3012 7.00174 10.0489 6.6849 10.8704 6.6849C12.5989 6.68493 14 8.08668 14 9.8151C13.9998 11.5434 12.5988 12.9446 10.8704 12.9447C9.14209 12.9447 7.74104 11.5434 7.74089 9.8151C7.74089 9.38937 7.82585 8.98325 7.97982 8.61328L7 7.88932L6.01953 8.61328C6.17356 8.98332 6.25911 9.38929 6.25911 9.8151C6.25896 11.5434 4.85791 12.9447 3.12956 12.9447C1.40124 12.9446 0.000156326 11.5434 0 9.8151C0 8.08668 1.40114 6.68493 3.12956 6.6849C3.95094 6.6849 4.69819 7.00193 5.25651 7.51953L6.33333 6.72526V6.1875C4.92523 5.88194 3.87044 4.62918 3.87044 3.12956C3.87048 1.40114 5.27158 0 7 0C8.72843 0 10.1295 1.40114 10.1296 3.12956Z" fill="var(--fill-0, #495464)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12.6667 14.2807" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M12.0014 3.2815L6.33333 0L0.665287 3.2815L6.33333 6.563L12.0014 3.2815ZM0 4.437V11L5.66667 14.2807V7.71767L0 4.437ZM7 14.2807L12.6667 11V4.437L7 7.71767V14.2807Z" fill="var(--fill-0, #18222F)"/>
</svg>

After

Width:  |  Height:  |  Size: 403 B

View File

@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12.6667 14.6667" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M6.33333 0L12.6667 3.66667V11L6.33333 14.6667L0 11V3.66667L6.33333 0ZM1.99592 4.0518L6.3334 6.56293L10.6708 4.05183L6.33333 1.54067L1.99592 4.0518ZM1.33333 5.20886V10.2313L5.66673 12.7401V7.71767L1.33333 5.20886ZM7.00007 12.74L11.3333 10.2313V5.20891L7.00007 7.71767V12.74Z" fill="var(--fill-0, #495464)"/>
</svg>

After

Width:  |  Height:  |  Size: 515 B

View File

@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12 13.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M3.33333 2C3.33333 0.895433 4.22877 0 5.33333 0C6.43793 0 7.33333 0.895433 7.33333 2C7.33333 2.23376 7.2932 2.45815 7.21953 2.66667H11.3333C11.7015 2.66667 12 2.96515 12 3.33333V5.41735C12 5.62345 11.9047 5.81796 11.7418 5.94423C11.5789 6.07053 11.3667 6.11433 11.1671 6.063C11.0079 6.022 10.8403 6 10.6667 6C9.56207 6 8.66667 6.8954 8.66667 8C8.66667 9.1046 9.56207 10 10.6667 10C10.8403 10 11.0079 9.978 11.1671 9.937C11.3667 9.88567 11.5789 9.92947 11.7418 10.0557C11.9047 10.1821 12 10.3765 12 10.5827V12.6667C12 13.0349 11.7015 13.3333 11.3333 13.3333H0.666667C0.29848 13.3333 0 13.0349 0 12.6667V3.33333C0 2.96515 0.29848 2.66667 0.666667 2.66667H3.44714C3.37343 2.45815 3.33333 2.23376 3.33333 2Z" fill="var(--fill-0, #18222F)"/>
</svg>

After

Width:  |  Height:  |  Size: 940 B

View File

@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12 13.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M2.66667 2.66667C2.66667 1.19391 3.86057 0 5.33333 0C6.80607 0 8 1.19391 8 2.66667H11.3333C11.7015 2.66667 12 2.96515 12 3.33333V6.1138C12 6.3302 11.8949 6.53313 11.7183 6.65813C11.5415 6.78307 11.3152 6.81447 11.1112 6.74233C10.973 6.69353 10.8237 6.66667 10.6667 6.66667C9.93027 6.66667 9.33333 7.2636 9.33333 8C9.33333 8.7364 9.93027 9.33333 10.6667 9.33333C10.8237 9.33333 10.973 9.30647 11.1112 9.25767C11.3152 9.18553 11.5415 9.21693 11.7183 9.34187C11.8949 9.46687 12 9.6698 12 9.8862V12.6667C12 13.0349 11.7015 13.3333 11.3333 13.3333H0.666667C0.29848 13.3333 0 13.0349 0 12.6667V3.33333C0 2.96515 0.29848 2.66667 0.666667 2.66667H2.66667ZM5.33333 1.33333C4.59695 1.33333 4 1.93029 4 2.66667C4 2.82369 4.02687 2.97301 4.0757 3.11117C4.14781 3.31521 4.11641 3.54157 3.99145 3.71826C3.86649 3.89495 3.66355 4 3.44714 4H1.33333V12H10.6667V10.6667C9.19393 10.6667 8 9.47273 8 8C8 6.52727 9.19393 5.33333 10.6667 5.33333V4H7.21953C7.00313 4 6.8002 3.89495 6.6752 3.71826C6.55027 3.54157 6.51887 3.31521 6.591 3.11117C6.6398 2.97301 6.66667 2.8237 6.66667 2.66667C6.66667 1.93029 6.06973 1.33333 5.33333 1.33333Z" fill="var(--fill-0, #495464)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,3 @@
<svg width="13.3333" height="13.3333" viewBox="0 0 13.3333 13.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6709 1.83301C12.754 1.83301 12.833 1.90708 12.833 2.00195V9.125L9.125 12.8311L2.00098 12.832C1.90424 12.8318 1.83301 12.7562 1.83301 12.6709V7.16699H2.16699V12.5H8.5V8.66699C8.5 8.57647 8.57647 8.5 8.66699 8.5L12 8.49902H12.5V2.16699H7.16699V1.83301H12.6709ZM11.4473 8.83301H8.83301V12.6533L9.68652 11.7998L11.8008 9.68652L12.6553 8.83203L11.4473 8.83301ZM2.83301 0.5V2.5H4.83301V2.83301H2.83301V4.83301H2.5V2.83301H0.5V2.5H2.5V0.5H2.83301Z" stroke="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 602 B

View File

@ -0,0 +1,3 @@
<svg width="11.6416" height="13.086" viewBox="0 0 11.6416 13.086" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.0996 0.512695C10.1337 0.518171 10.156 0.524407 10.168 0.52832C10.2148 0.543681 10.2522 0.579318 10.2705 0.625C10.5581 1.34398 10.5694 1.95842 10.4502 2.44824L10.3955 2.67578L10.5342 2.86328C10.9293 3.39873 11.1416 4.03345 11.1416 4.76172C11.1416 5.94177 10.8789 6.76774 10.4385 7.34668C9.99983 7.92308 9.33952 8.3137 8.42773 8.53711L7.91602 8.66309L8.06738 9.16699C8.1351 9.39201 8.17383 9.65236 8.17383 9.94336L8.16895 11.2803C8.16815 11.4068 8.16748 11.5418 8.16602 11.75L8.16309 12.1553L8.55762 12.2422C8.62114 12.2562 8.67222 12.3062 8.68555 12.3721C8.7036 12.4623 8.64494 12.5503 8.55469 12.5684C8.30873 12.6174 8.13226 12.5563 8.02148 12.4668C7.90812 12.3752 7.83118 12.2287 7.83105 12.043C7.83105 11.9847 7.83103 11.8999 7.83203 11.748C7.83356 11.5396 7.83507 11.4046 7.83594 11.2783C7.83901 10.8059 7.84082 10.3843 7.84082 9.94336C7.84082 9.41961 7.70678 8.93648 7.38281 8.65723C7.27271 8.56225 7.32913 8.38146 7.47363 8.36523C8.5007 8.24979 9.36433 7.98413 9.96094 7.37695C10.5669 6.76005 10.8086 5.884 10.8086 4.76172C10.8086 4.00303 10.5554 3.35601 10.0693 2.82227C10.0264 2.77514 10.0144 2.70773 10.0381 2.64844C10.1874 2.27509 10.24 1.81114 10.126 1.28125L10.0137 0.759766L9.50098 0.905273L9.49414 0.907227C9.09648 1.01979 8.63458 1.25105 8.11035 1.60742C8.06963 1.63503 8.01887 1.64314 7.97168 1.62988C7.37871 1.46312 6.74527 1.37892 6.1084 1.37891C5.4715 1.37891 4.83803 1.46315 4.24512 1.62988C4.19802 1.64313 4.14702 1.63474 4.10645 1.60742C3.57968 1.25283 3.11619 1.02297 2.71777 0.910156L2.20703 0.765625L2.09277 1.28418C1.97674 1.813 2.02979 2.27614 2.17871 2.64844C2.20225 2.70762 2.19033 2.77513 2.14746 2.82227C1.66395 3.35317 1.4082 4.00895 1.4082 4.76172C1.40822 5.88234 1.64983 6.75778 2.25391 7.375C2.84913 7.98307 3.71022 8.25023 4.7334 8.36523C4.87762 8.38143 4.93358 8.56104 4.82422 8.65625C4.66044 8.79875 4.55302 9.0215 4.48828 9.20898C4.41614 9.41793 4.36621 9.67286 4.36621 9.94336V12.043C4.36598 12.3722 4.10732 12.6499 3.64648 12.5693C3.55582 12.5535 3.49488 12.4666 3.51074 12.376C3.52276 12.3083 3.57458 12.2564 3.63965 12.2422L4.0332 12.1562V10.5586L3.49902 10.5947C2.9627 10.6308 2.58298 10.5389 2.30859 10.3555C2.16409 10.2588 2.0263 10.1279 1.83984 9.90527C1.80759 9.86673 1.71954 9.75814 1.64258 9.66211C1.60519 9.61545 1.57219 9.5735 1.55176 9.54785C1.54666 9.54146 1.54226 9.53528 1.53906 9.53125L1.53711 9.5293C1.53789 9.53032 1.54048 9.53427 1.54395 9.53906C1.54454 9.53989 1.55212 9.55023 1.56055 9.56348C1.56664 9.57332 1.58653 9.60864 1.59863 9.63477C1.62166 9.72473 1.52906 9.94237 1.35645 10.1016L1.14648 9.84082L1.53613 9.52734C1.53304 9.52351 1.52837 9.51827 1.52539 9.51465C1.52488 9.51403 1.52438 9.51285 1.52344 9.51172C1.52279 9.51094 1.51991 9.50772 1.5166 9.50391C1.51604 9.50326 1.51446 9.50199 1.5127 9.5C1.21539 9.13323 0.948869 8.85872 0.610352 8.7373C0.523946 8.70626 0.479023 8.61085 0.509766 8.52441C0.540776 8.43794 0.636154 8.39219 0.722656 8.42285C1.0883 8.55402 1.35448 8.77291 1.7793 9.29785C1.78078 9.29986 1.78248 9.30178 1.7832 9.30273C1.78551 9.30578 1.78769 9.30808 1.78809 9.30859C1.79204 9.31367 1.79973 9.32481 1.80859 9.33594C1.82821 9.36058 1.8612 9.401 1.89746 9.44629L2.0957 9.69141C2.2267 9.84782 2.35754 9.98732 2.49316 10.0781C2.78339 10.2725 3.19313 10.2925 3.58887 10.2529L4.01172 10.2109L4.03809 9.78711C4.05126 9.57333 4.09056 9.36582 4.15039 9.17578L4.31055 8.66699L3.79199 8.54004C2.88088 8.31734 2.21997 7.92594 1.78027 7.34863C1.33864 6.76857 1.07521 5.94109 1.0752 4.76172C1.0752 4.0396 1.28837 3.39917 1.68262 2.86328L1.82129 2.67578L1.76562 2.44824C1.64647 1.95845 1.65776 1.34391 1.94531 0.625C1.96354 0.57943 2.00137 0.544774 2.04785 0.529297C2.07893 0.520398 2.08893 0.517862 2.11621 0.513672C2.47591 0.45859 3.10496 0.581722 4.05176 1.1748L4.22852 1.28516L4.43164 1.2373C4.97053 1.11008 5.53743 1.04492 6.1084 1.04492C6.67867 1.04494 7.24479 1.11034 7.7832 1.2373L7.9873 1.28516L8.16504 1.17285C9.10977 0.576084 9.73863 0.454651 10.0996 0.512695ZM2.39551 9.2666C2.28256 9.36997 2.13716 9.45037 1.96484 9.45117C1.91043 9.42066 1.8462 9.37172 1.83301 9.35937C1.82701 9.35346 1.81779 9.34341 1.81445 9.33984C1.81233 9.33754 1.80911 9.3337 1.80762 9.33203C1.80466 9.3287 1.80177 9.32635 1.80078 9.3252L1.79687 9.32031L1.80078 9.32422L2.19043 9.01172C2.22028 9.0493 2.31937 9.17218 2.39551 9.2666Z" stroke="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,3 @@
<svg width="12" height="13.3333" viewBox="0 0 12 13.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.332 0.5C11.426 0.500094 11.5 0.580915 11.5 0.661133V12.6719C11.5 12.7601 11.4282 12.8329 11.3379 12.833H0.662109C0.578569 12.8329 0.5 12.7632 0.5 12.6621V4.20703L4.20898 0.5H11.332ZM4.83301 4.66699C4.83283 4.75878 4.75878 4.83283 4.66699 4.83301H0.833008V12.5H11.167V0.833008H4.83301V4.66699ZM3.64648 1.5332L1.53223 3.64648L0.678711 4.5H4.5V0.680664L3.64648 1.5332Z" stroke="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 517 B

View File

@ -0,0 +1,3 @@
<svg width="14.6667" height="13.3333" viewBox="0 0 14.6667 13.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.13867 6.72949C8.67785 7.21017 8.03241 7.5 7.33301 7.5C6.63364 7.49991 5.9881 7.21021 5.52734 6.72949L5.16699 6.35254L4.80566 6.72949C4.34483 7.21027 3.69949 7.5 3 7.5C2.90569 7.5 2.81278 7.49447 2.72168 7.48438L2.16699 7.42285V12.5H12.5V7.42285L11.9453 7.48438C11.8543 7.49446 11.7612 7.49999 11.667 7.5C10.9676 7.5 10.3221 7.21017 9.86133 6.72949L9.5 6.35254L9.13867 6.72949ZM2.75977 1.08301L1.14258 3.88379C0.941179 4.21836 0.833008 4.60162 0.833008 5C0.833008 6.19661 1.80338 7.16699 3 7.16699C3.89354 7.16699 4.68499 6.62068 5.01172 5.80566C5.06765 5.66613 5.26532 5.66617 5.32129 5.80566C5.64794 6.62063 6.43956 7.16686 7.33301 7.16699C8.22659 7.16699 9.019 6.62072 9.3457 5.80566C9.4017 5.66633 9.5983 5.66633 9.6543 5.80566C9.981 6.62072 10.7734 7.16699 11.667 7.16699C12.8635 7.16682 13.833 6.1965 13.833 5C13.833 4.59984 13.725 4.21734 13.5186 3.87402H13.5176L11.9072 1.08301L11.7627 0.833008H2.90332L2.75977 1.08301ZM1.83301 7.22754L1.61133 7.0791C0.94019 6.62981 0.5 5.86624 0.5 5C0.5 4.53874 0.625339 4.09665 0.850586 3.72266L0.855469 3.71484L2.66309 0.583008C2.69288 0.531554 2.74815 0.5 2.80762 0.5H11.8594C11.9188 0.500086 11.9742 0.531575 12.0039 0.583008L13.8057 3.7041L13.8096 3.71191C14.0416 4.09734 14.167 4.53978 14.167 5C14.167 5.86614 13.7267 6.62979 13.0557 7.0791L12.833 7.22754V12.5H13.5V12.833H1.16699V12.5H1.83301V7.22754Z" stroke="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,6 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 13.4445 14.6667" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Vector">
<path d="M7.873 1.01976C8.28503 1.01976 8.68078 1.18055 8.97605 1.46791C9.12039 1.60841 9.23516 1.77638 9.31357 1.96193C9.39199 2.14747 9.43248 2.34683 9.43265 2.54827C9.43282 2.7497 9.39267 2.94913 9.31457 3.13481C9.23647 3.32048 9.12199 3.48865 8.97788 3.62939L4.53331 7.98841C4.48512 8.03528 4.44681 8.09133 4.42065 8.15326C4.39449 8.21519 4.38102 8.28173 4.38102 8.34896C4.38102 8.41619 4.39449 8.48273 4.42065 8.54466C4.44681 8.60659 4.48512 8.66264 4.53331 8.70951C4.63178 8.80538 4.76378 8.85902 4.9012 8.85902C5.03862 8.85902 5.17062 8.80538 5.26908 8.70951L5.32897 8.65024L5.33019 8.64901L9.71304 4.3505C10.0085 4.06393 10.404 3.90381 10.8155 3.90415C11.2271 3.90449 11.6224 4.06527 11.9173 4.35233L11.9479 4.38228C12.0924 4.52293 12.2072 4.69111 12.2856 4.87689C12.3641 5.06267 12.4045 5.26228 12.4045 5.46393C12.4045 5.66559 12.3641 5.8652 12.2856 6.05098C12.2072 6.23676 12.0924 6.40494 11.9479 6.54559L6.62757 11.7632C6.51503 11.8726 6.42557 12.0034 6.36448 12.1479C6.30339 12.2925 6.27191 12.4478 6.27191 12.6047C6.27191 12.7616 6.30339 12.9169 6.36448 13.0615C6.42557 13.206 6.51503 13.3368 6.62757 13.4462L7.72023 14.5175C7.81867 14.6131 7.95053 14.6667 8.08781 14.6667C8.22508 14.6667 8.35695 14.6131 8.45539 14.5175C8.50358 14.4706 8.54189 14.4145 8.56805 14.3526C8.59421 14.2907 8.60769 14.2241 8.60769 14.1569C8.60769 14.0897 8.59421 14.0231 8.56805 13.9612C8.54189 13.8993 8.50358 13.8432 8.45539 13.7964L7.36273 12.7245C7.34667 12.7089 7.33391 12.6902 7.32519 12.6696C7.31647 12.649 7.31198 12.6268 7.31198 12.6044C7.31198 12.582 7.31647 12.5598 7.32519 12.5392C7.33391 12.5186 7.34667 12.4999 7.36273 12.4843L12.683 7.2673C12.924 7.03296 13.1155 6.75268 13.2463 6.44304C13.3771 6.1334 13.4445 5.80068 13.4445 5.46454C13.4445 5.12841 13.3771 4.79569 13.2463 4.48605C13.1155 4.17641 12.924 3.89613 12.683 3.66178L12.6525 3.63123C12.3646 3.35023 12.016 3.13908 11.6336 3.01405C11.2512 2.88903 10.8453 2.85347 10.447 2.91012C10.5038 2.51705 10.4668 2.1161 10.3389 1.74009C10.211 1.36408 9.99593 1.0237 9.71121 0.746808C9.21914 0.267942 8.55962 0 7.873 0C7.18639 0 6.52687 0.267942 6.0348 0.746808L0.152297 6.51564C0.104103 6.56251 0.0657945 6.61857 0.039636 6.6805C0.0134776 6.74243 0 6.80897 0 6.8762C0 6.94342 0.0134776 7.00997 0.039636 7.0719C0.0657945 7.13382 0.104103 7.18988 0.152297 7.23675C0.250735 7.33243 0.382601 7.38595 0.519877 7.38595C0.657153 7.38595 0.789019 7.33243 0.887457 7.23675L6.76996 1.46791C7.06523 1.18055 7.46098 1.01976 7.873 1.01976Z" fill="var(--fill-0, #495464)"/>
<path d="M8.35362 2.74526C8.32747 2.80719 8.28916 2.86324 8.24096 2.91011L3.88989 7.17685C3.74539 7.3175 3.63053 7.48568 3.5521 7.67146C3.47367 7.85724 3.43327 8.05685 3.43327 8.25851C3.43327 8.46016 3.47367 8.65977 3.5521 8.84555C3.63053 9.03133 3.74539 9.19951 3.88989 9.34017C4.18516 9.62753 4.58092 9.78832 4.99294 9.78832C5.40496 9.78832 5.80072 9.62753 6.09598 9.34017L10.4464 5.07343C10.5449 4.97756 10.6769 4.92392 10.8143 4.92392C10.9518 4.92392 11.0837 4.97756 11.1822 5.07343C11.2304 5.1203 11.2687 5.17635 11.2949 5.23828C11.321 5.30021 11.3345 5.36675 11.3345 5.43398C11.3345 5.5012 11.321 5.56775 11.2949 5.62968C11.2687 5.69161 11.2304 5.74766 11.1822 5.79453L6.83114 10.0613C6.339 10.54 5.67951 10.8078 4.99294 10.8078C4.30636 10.8078 3.64688 10.54 3.15473 10.0613C2.91376 9.82692 2.72222 9.54664 2.59143 9.237C2.46064 8.92736 2.39325 8.59464 2.39325 8.25851C2.39325 7.92238 2.46064 7.58966 2.59143 7.28001C2.72222 6.97037 2.91376 6.69009 3.15473 6.45575L7.50519 2.18901C7.60366 2.09315 7.73566 2.03951 7.87308 2.03951C8.0105 2.03951 8.1425 2.09315 8.24096 2.18901C8.28916 2.23588 8.32747 2.29193 8.35362 2.35386C8.37978 2.41579 8.39326 2.48233 8.39326 2.54956C8.39326 2.61679 8.37978 2.68333 8.35362 2.74526Z" fill="var(--fill-0, #495464)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 14.5 14.5" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M5.08333 0.75V13.75M2.19444 0.75H12.3056C13.1033 0.75 13.75 1.3967 13.75 2.19444V12.3056C13.75 13.1033 13.1033 13.75 12.3056 13.75H2.19444C1.3967 13.75 0.75 13.1033 0.75 12.3056V2.19444C0.75 1.3967 1.3967 0.75 2.19444 0.75Z" stroke="var(--stroke-0, black)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 527 B

View File

@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12.3333 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M9.66667 4V0H11.6667C12.0349 0 12.3333 0.29848 12.3333 0.666667V3.33333C12.3333 3.70152 12.0349 4 11.6667 4H9.66667ZM8.33333 13.3333C8.33333 13.7015 8.03487 14 7.66667 14H5C4.63181 14 4.33333 13.7015 4.33333 13.3333V4H0V2.71625C0 2.47913 0.12594 2.25987 0.330753 2.14039L4 0H8.33333V13.3333Z" fill="var(--fill-0, #18222F)"/>
</svg>

After

Width:  |  Height:  |  Size: 528 B

View File

@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12.3333 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M11.6667 0C12.0349 0 12.3333 0.29848 12.3333 0.666667V4C12.3333 4.36819 12.0349 4.66667 11.6667 4.66667H8.33333V13.3333C8.33333 13.7015 8.03487 14 7.66667 14H5C4.63181 14 4.33333 13.7015 4.33333 13.3333V4.66667H0.666667C0.29848 4.66667 0 4.36819 0 4V2.41202C0 2.15951 0.142667 1.92867 0.368527 1.81574L4 0H11.6667ZM8.33333 1.33333H4.31476L1.33333 2.82405V3.33333H5.66667V12.6667H7V3.33333H8.33333V1.33333ZM11 1.33333H9.66667V3.33333H11V1.33333Z" fill="var(--fill-0, #495464)"/>
</svg>

After

Width:  |  Height:  |  Size: 681 B

View File

@ -0,0 +1,10 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 13.325 13.325" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Vector">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.81641 5.01888L5.91797 5.04883L12.8913 7.70573L12.9837 7.7487C13.3889 7.97463 13.4445 8.54581 13.0905 8.8457L13.0085 8.9056L10.4837 10.4837L8.9056 13.0085C8.62921 13.4507 7.99075 13.4178 7.7487 12.9837L7.70573 12.8913L5.04883 5.91797C4.85479 5.40863 5.31088 4.90871 5.81641 5.01888Z" fill="var(--fill-0, #18222F)"/>
<path d="M3.87891 9.06445L2.22917 10.7142L1.28646 9.77148L2.9362 8.12175L3.87891 9.06445Z" fill="var(--fill-0, #18222F)"/>
<path d="M2.33333 6.66667H0V5.33333H2.33333V6.66667Z" fill="var(--fill-0, #18222F)"/>
<path d="M3.87891 2.93555L2.9362 3.87826L1.28646 2.22852L2.22917 1.28581L3.87891 2.93555Z" fill="var(--fill-0, #18222F)"/>
<path d="M10.7142 2.22852L9.06445 3.87826L8.12175 2.93555L9.77148 1.28581L10.7142 2.22852Z" fill="var(--fill-0, #18222F)"/>
<path d="M6.66667 2.33333H5.33333V0H6.66667V2.33333Z" fill="var(--fill-0, #18222F)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,10 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 13.325 13.325" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Vector">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.81641 5.01888L5.91797 5.04883L12.8913 7.70573L12.9837 7.7487C13.3889 7.97463 13.4445 8.54581 13.0905 8.8457L13.0085 8.9056L10.4837 10.4837L8.9056 13.0085C8.62921 13.4507 7.99075 13.4178 7.7487 12.9837L7.70573 12.8913L5.04883 5.91797C4.85479 5.40863 5.31088 4.90871 5.81641 5.01888ZM8.47852 11.1751L9.43359 9.64779L9.47786 9.58529C9.52536 9.52564 9.5828 9.47422 9.64779 9.43359L11.1751 8.47852L6.81901 6.81901L8.47852 11.1751Z" fill="var(--fill-0, #495464)"/>
<path d="M3.87891 9.06445L2.22917 10.7142L1.28646 9.77148L2.9362 8.12175L3.87891 9.06445Z" fill="var(--fill-0, #495464)"/>
<path d="M2.33333 6.66667H0V5.33333H2.33333V6.66667Z" fill="var(--fill-0, #495464)"/>
<path d="M3.87891 2.93555L2.9362 3.87826L1.28646 2.22852L2.22917 1.28581L3.87891 2.93555Z" fill="var(--fill-0, #495464)"/>
<path d="M10.7142 2.22852L9.06445 3.87826L8.12175 2.93555L9.77148 1.28581L10.7142 2.22852Z" fill="var(--fill-0, #495464)"/>
<path d="M6.66667 2.33333H5.33333V0H6.66667V2.33333Z" fill="var(--fill-0, #495464)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,9 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12.1 11.4333" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Vector">
<path d="M1.71667 0C0.768578 0 0 0.768578 0 1.71667C0 2.66476 0.768578 3.43333 1.71667 3.43333H3.71667C4.66475 3.43333 5.43333 2.66476 5.43333 1.71667C5.43333 0.768578 4.66476 0 3.71667 0H1.71667Z" fill="var(--fill-0, #18222F)"/>
<path d="M8.38333 4C7.43524 4 6.66667 4.76858 6.66667 5.71667C6.66667 6.66476 7.43524 7.43333 8.38333 7.43333H10.3833C11.3314 7.43333 12.1 6.66476 12.1 5.71667C12.1 4.76858 11.3314 4 10.3833 4H8.38333Z" fill="var(--fill-0, #18222F)"/>
<path d="M0 9.71667C0 8.76858 0.768578 8 1.71667 8H3.71667C4.66476 8 5.43333 8.76858 5.43333 9.71667C5.43333 10.6648 4.66475 11.4333 3.71667 11.4333H1.71667C0.768578 11.4333 0 10.6648 0 9.71667Z" fill="var(--fill-0, #18222F)"/>
<path d="M7.05001 2.38334H6.38334V1.05001H7.05001C8.15458 1.05001 9.05001 1.94544 9.05001 3.05001H7.71667C7.71667 2.68182 7.4182 2.38334 7.05001 2.38334Z" fill="var(--fill-0, #18222F)"/>
<path d="M9.05001 8.38334C9.05001 9.48791 8.15458 10.3833 7.05001 10.3833H6.38334V9.05001H7.05001C7.4182 9.05001 7.71667 8.75153 7.71667 8.38334H9.05001Z" fill="var(--fill-0, #18222F)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.3333 2C13.7015 2 14 2.29848 14 2.66667V6C14 6.36819 13.7015 6.66667 13.3333 6.66667H10C9.63181 6.66667 9.33333 6.36819 9.33333 6V5H6.63086C5.94959 5.00026 5.70734 5.90184 6.29688 6.24349L10.3717 8.60221C12.1411 9.62681 11.4138 12.3331 9.36914 12.3333H6.66667V13.3333C6.66667 13.7015 6.36819 14 6 14H2.66667C2.29848 14 2 13.7015 2 13.3333V10C2 9.63181 2.29848 9.33333 2.66667 9.33333H6C6.36819 9.33333 6.66667 9.63181 6.66667 10V11H9.36914C10.0504 10.9997 10.2929 10.0981 9.70378 9.75651L5.62891 7.39779C3.85933 6.37322 4.58611 3.66693 6.63086 3.66667H9.33333V2.66667C9.33333 2.29848 9.63181 2 10 2H13.3333ZM3.33333 12.6667H5.33333V10.6667H3.33333V12.6667ZM10.6667 5.33333H12.6667V3.33333H10.6667V5.33333Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 861 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.66667 6.17435C2.66667 5.98785 2.66667 5.89459 2.69021 5.80837C2.71107 5.73198 2.74537 5.65992 2.7915 5.59556C2.84357 5.52292 2.91595 5.46411 3.0607 5.3465L7.3274 1.87983C7.56713 1.68502 7.687 1.58762 7.82027 1.55031C7.93787 1.51741 8.06213 1.51741 8.17973 1.55031C8.313 1.58762 8.43287 1.68502 8.6726 1.87983L12.9393 5.3465C13.0841 5.46411 13.1564 5.52292 13.2085 5.59556C13.2547 5.65992 13.2889 5.73198 13.3098 5.80837C13.3333 5.89459 13.3333 5.98785 13.3333 6.17435V12.2667C13.3333 12.64 13.3333 12.8267 13.2607 12.9693C13.1967 13.0947 13.0948 13.1967 12.9693 13.2607C12.8267 13.3333 12.6401 13.3333 12.2667 13.3333H3.73333C3.35997 13.3333 3.17328 13.3333 3.03067 13.2607C2.90523 13.1967 2.80325 13.0947 2.73933 12.9693C2.66667 12.8267 2.66667 12.64 2.66667 12.2667V6.17435Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
<path d="M10 13.3333V9.66667C10 9.11438 9.55228 8.66667 9 8.66667H7C6.44772 8.66667 6 9.11438 6 9.66667V13.3333" stroke="currentColor" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(3 3) scale(1.5)">
<path d="M7 10.5C8.933 10.5 10.5 8.4853 10.5 6C10.5 3.51472 8.933 1.5 7 1.5M7 10.5C5.067 10.5 3.5 8.4853 3.5 6C3.5 3.51472 5.067 1.5 7 1.5M7 10.5H5C3.06701 10.5 1.5 8.4853 1.5 6C1.5 3.51472 3.06701 1.5 5 1.5H7" stroke="currentColor" stroke-width="1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 410 B

View File

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 15.25C12.4142 15.25 12.75 15.5858 12.75 16V16.0098C12.75 16.424 12.4142 16.7598 12 16.7598C11.5858 16.7598 11.25 16.424 11.25 16.0098V16C11.25 15.5858 11.5858 15.25 12 15.25Z" fill="currentColor"/>
<path d="M14 7.25C14.4142 7.25 14.75 7.58579 14.75 8V10.5C14.75 10.7359 14.6389 10.958 14.4502 11.0996L12.75 12.374V13C12.75 13.4142 12.4142 13.75 12 13.75C11.5858 13.75 11.25 13.4142 11.25 13V12C11.25 11.7641 11.3611 11.542 11.5498 11.4004L13.25 10.125V8.75H10.75V9C10.75 9.41421 10.4142 9.75 10 9.75C9.58579 9.75 9.25 9.41421 9.25 9V8C9.25 7.58579 9.58579 7.25 10 7.25H14Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 716 B

View File

@ -0,0 +1,6 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-5.5 -6.95)">
<path d="M14.449 8.37314C15.0613 7.87562 15.9387 7.87562 16.551 8.37314L22.3843 13.1127C22.7738 13.4292 23 13.9044 23 14.4063V22.7596C23 23.6801 22.2538 24.4263 21.3333 24.4263H18.8333V17.7599C18.8333 17.2997 18.4602 16.9266 18 16.9266H13C12.5398 16.9266 12.1667 17.2997 12.1667 17.7599V24.4263H9.66667C8.74619 24.4263 8 23.6801 8 22.7596V14.4063C8 13.9044 8.22616 13.4292 8.61568 13.1127L14.449 8.37314Z" fill="currentColor"/>
<path d="M13.833 24.4263H17.1663V18.5933H13.833V24.4263Z" fill="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 666 B

View File

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.33301 7.71788C3.33301 7.48475 3.33301 7.36818 3.36243 7.2604C3.38851 7.16492 3.43138 7.07484 3.48905 6.99439C3.55414 6.90359 3.64461 6.83008 3.82555 6.68307L9.15892 2.34973C9.45859 2.10622 9.60842 1.98447 9.77501 1.93783C9.92201 1.8967 10.0773 1.8967 10.2243 1.93783C10.3909 1.98447 10.5408 2.10622 10.8404 2.34973L16.1738 6.68307C16.3548 6.83008 16.4452 6.90359 16.5103 6.99439C16.568 7.07484 16.6108 7.16492 16.6369 7.2604C16.6663 7.36818 16.6663 7.48475 16.6663 7.71788V15.3333C16.6663 15.7999 16.6663 16.0334 16.5755 16.2116C16.4956 16.3684 16.3682 16.4959 16.2113 16.5758C16.0331 16.6666 15.7998 16.6666 15.333 16.6666H4.66634C4.19963 16.6666 3.96627 16.6666 3.78802 16.5758C3.63122 16.4959 3.50373 16.3684 3.42383 16.2116C3.33301 16.0334 3.33301 15.7999 3.33301 15.3333V7.71788Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
<path d="M12.5 16.6666V11.8333C12.5 11.281 12.0523 10.8333 11.5 10.8333H8.5C7.94772 10.8333 7.5 11.281 7.5 11.8333V16.6666" stroke="currentColor" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,7 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-6.33 -5.55)">
<path d="M9.66602 8.83333C9.66602 8.3731 10.0391 8 10.4993 8H14.666C15.1263 8 15.4993 8.3731 15.4993 8.83333V10.5H9.66602V8.83333Z" fill="currentColor"/>
<path d="M17.166 8.83333C17.166 8.3731 17.5391 8 17.9993 8H22.166C22.6263 8 22.9993 8.3731 22.9993 8.83333V10.5H17.166V8.83333Z" fill="currentColor"/>
<path d="M8 13.0001C8 12.5398 8.3731 12.1667 8.83333 12.1667H23.8333C24.2936 12.1667 24.6667 12.5398 24.6667 13.0001V21.3334C24.6667 21.7937 24.2936 22.1667 23.8333 22.1667H8.83333C8.3731 22.1667 8 21.7937 8 21.3334V13.0001Z" fill="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 716 B

View File

@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.5 7.50008C2.5 7.03984 2.8731 6.66675 3.33333 6.66675H16.6667C17.1269 6.66675 17.5 7.03985 17.5 7.50008V15.0001C17.5 15.4603 17.1269 15.8334 16.6667 15.8334H3.33333C2.8731 15.8334 2.5 15.4603 2.5 15.0001V7.50008Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.16699 6.66659V4.58325C4.16699 3.89289 4.72663 3.33325 5.41699 3.33325H7.08366C7.77402 3.33325 8.33366 3.89289 8.33366 4.58325V6.66659" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.667 6.66659V4.16659C11.667 3.70635 12.0401 3.33325 12.5003 3.33325H15.0003C15.4606 3.33325 15.8337 3.70635 15.8337 4.16659V6.66659" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 896 B

View File

@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-4.667 -6.333)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5 8C9.11929 8 8 9.11929 8 10.5V22.1667C8 23.5474 9.11929 24.6667 10.5 24.6667H20.5C20.9602 24.6667 21.3333 24.2936 21.3333 23.8333V8.83333C21.3333 8.3731 20.9602 8 20.5 8H10.5ZM9.66667 22.1667C9.66667 22.6269 10.0398 23 10.5 23H19.6667V21.3333H10.5C10.0398 21.3333 9.66667 21.7064 9.66667 22.1667ZM12.1667 11.3333C11.7064 11.3333 11.3333 11.7064 11.3333 12.1667C11.3333 12.6269 11.7064 13 12.1667 13H17.1667C17.6269 13 18 12.6269 18 12.1667C18 11.7064 17.6269 11.3333 17.1667 11.3333H12.1667ZM11.3333 15.5C11.3333 15.0397 11.7064 14.6667 12.1667 14.6667H14.6667C15.1269 14.6667 15.5 15.0397 15.5 15.5C15.5 15.9602 15.1269 16.3333 14.6667 16.3333H12.1667C11.7064 16.3333 11.3333 15.9602 11.3333 15.5Z" fill="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 933 B

View File

@ -0,0 +1,6 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.5 9.16675H10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.5 5.83325H12.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.16699 4.16667C4.16699 3.24619 4.91318 2.5 5.83366 2.5H15.417C15.6471 2.5 15.8337 2.68655 15.8337 2.91667V17.5H5.83366C4.91318 17.5 4.16699 16.7538 4.16699 15.8333V4.16667Z" stroke="currentColor" stroke-width="1.5"/>
<path d="M4.16699 15.8334C4.16699 14.9129 4.91318 14.1667 5.83366 14.1667H15.8337V17.5001H5.83366C4.91318 17.5001 4.16699 16.7539 4.16699 15.8334Z" stroke="currentColor" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 764 B

View File

@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-6.3 -5.5)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.25369 8.58477C9.3624 8.23688 9.6846 8 10.0491 8H22.5491C22.9136 8 23.2357 8.23688 23.3445 8.58477L24.4481 12.1162C24.8042 13.256 24.5023 14.4059 23.7991 15.2164V22.1667C23.7991 22.6269 23.426 23 22.9657 23H9.63242C9.17219 23 8.79909 22.6269 8.79909 22.1667V15.2164C8.09588 14.4059 7.79393 13.256 8.15013 12.1162L9.25369 8.58477ZM18.0271 12.7092L17.6467 9.66667H14.9514L14.5711 12.7092C14.4412 13.7486 15.2516 14.6667 16.2991 14.6667C17.3465 14.6667 18.1568 13.7485 18.0271 12.7092ZM13.2718 9.66667H10.6617L9.74093 12.6133C9.42266 13.6317 10.1835 14.6667 11.2505 14.6667C12.0482 14.6667 12.721 14.0728 12.82 13.2812L13.2718 9.66667ZM19.3264 9.66667L19.6809 12.5025L19.7782 13.2812C19.8772 14.0728 20.55 14.6667 21.3477 14.6667C22.4147 14.6667 23.1755 13.6317 22.8572 12.6133L21.9364 9.66667H19.3264Z" fill="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.667 9.99992V16.6666H3.33366V9.99992M7.91699 3.33325H12.0837M7.91699 3.33325L7.44543 7.10578C7.25334 8.6425 8.45158 9.99992 10.0003 9.99992C11.5491 9.99992 12.7473 8.6425 12.5552 7.10578L12.0837 3.33325M7.91699 3.33325H3.75033L2.64677 6.86465C2.16081 8.41975 3.32257 9.99992 4.95179 9.99992C6.1697 9.99992 7.19703 9.093 7.34809 7.88451L7.91699 3.33325ZM12.0837 3.33325H16.2503L17.3539 6.86465C17.8398 8.41975 16.6781 9.99992 15.0489 9.99992C13.831 9.99992 12.8037 9.093 12.6526 7.88451L12.0837 3.33325Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 712 B

View File

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.0004 1.875C17.0398 1.87509 21.1246 5.9602 21.1249 10.9995C21.1249 13.1138 20.4037 15.0582 19.1972 16.6055L21.7958 19.2041C22.235 19.6433 22.2348 20.3556 21.7958 20.7949C21.3565 21.2343 20.6443 21.2343 20.205 20.7949L17.6064 18.1963C16.3417 19.1831 14.8123 19.8466 13.14 20.0552C12.5235 20.132 11.9616 19.6932 11.8847 19.0767C11.8081 18.4603 12.2454 17.8981 12.8617 17.8213C16.2516 17.3983 18.8749 14.5044 18.8749 10.9995C18.8746 7.20283 15.7971 4.12509 12.0004 4.125C8.4954 4.12505 5.60139 6.74948 5.17862 10.1396C5.10154 10.7559 4.53955 11.1934 3.92325 11.1167C3.30688 11.0398 2.86963 10.4777 2.9462 9.86133C3.50765 5.35896 7.34631 1.87505 12.0004 1.875Z" fill="currentColor"/>
<path d="M3.70727 16.1747L7.91781 11.2624C8.24038 10.8861 8.85505 11.158 8.79357 11.6498L8.49979 14.0001H10.9127C11.3399 14.0001 11.5703 14.5012 11.2923 14.8255L7.08177 19.7378C6.7592 20.1141 6.14453 19.8422 6.20601 19.3504L6.49979 17.0001H4.0869C3.65972 17.0001 3.42927 16.499 3.70727 16.1747Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,8 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-7.14 -6.38)">
<path d="M13.7969 17.1665C14.465 17.1665 15.1067 17.2803 15.7045 17.4872L14.7247 20.9165H12.9636C11.8131 20.9167 10.8803 21.8493 10.8803 22.9998C10.8803 23.2963 10.9436 23.5778 11.0552 23.8332H8.79695C8.33682 23.833 7.95953 23.4587 8.00349 23.0007C8.30296 19.8813 10.2937 17.1666 13.7969 17.1665Z" fill="currentColor"/>
<path d="M25.4632 16.3333C25.7246 16.3333 25.9715 16.4558 26.1289 16.6645C26.2668 16.8473 26.3224 17.0777 26.286 17.3008L26.2648 17.3953L24.5981 23.2286C24.4959 23.5863 24.1686 23.8333 23.7965 23.8333H12.9632C12.5031 23.8331 12.1299 23.4601 12.1299 22.9999C12.1299 22.5398 12.5031 22.1668 12.9632 22.1666H15.6675L17.1616 16.9379L17.2105 16.8093C17.3465 16.5223 17.6376 16.3333 17.9632 16.3333H25.4632Z" fill="currentColor"/>
<path d="M14.2132 9.25C16.0541 9.25 17.5465 10.7424 17.5465 12.5833C17.5465 14.4243 16.0541 15.9167 14.2132 15.9167C12.3724 15.9165 10.8799 14.4242 10.8799 12.5833C10.8799 10.7425 12.3724 9.25013 14.2132 9.25Z" fill="currentColor"/>
<path d="M22.5474 8C22.7539 8.00029 22.9276 8.15533 22.951 8.36052C23.0941 9.62402 23.8103 10.3975 25.093 10.5114C25.3029 10.53 25.4635 10.7067 25.4632 10.9175C25.4628 11.128 25.3019 11.3037 25.0921 11.3219C23.8276 11.4314 23.0613 12.1977 22.9518 13.4622C22.9335 13.672 22.758 13.833 22.5474 13.8333C22.3367 13.8336 22.1609 13.6728 22.1421 13.4631C22.0281 12.1805 21.2546 11.4635 19.9912 11.3203C19.786 11.2971 19.6303 11.124 19.6299 10.9175C19.6297 10.7108 19.7851 10.5367 19.9904 10.513C21.2719 10.3651 21.9951 9.64128 22.1429 8.3597C22.1667 8.15453 22.3408 7.99979 22.5474 8Z" fill="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,6 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.8206 2.0275C15.7973 1.82217 15.6238 1.66696 15.4171 1.66675C15.2104 1.66654 15.0365 1.82139 15.0128 2.02667C14.865 3.30836 14.1416 4.03176 12.8599 4.17959C12.6547 4.20326 12.4998 4.37719 12.5 4.58383C12.5003 4.79047 12.6554 4.96408 12.8608 4.98733C14.1243 5.13046 14.8978 5.84689 15.0117 7.12955C15.0304 7.33946 15.2064 7.50032 15.4171 7.50008C15.6278 7.49984 15.8035 7.33859 15.8217 7.12863C15.9311 5.86411 16.6973 5.09787 17.9619 4.98841C18.1718 4.97023 18.3331 4.79461 18.3333 4.58387C18.3336 4.37313 18.1728 4.19715 17.9628 4.17851C16.6802 4.06457 15.9637 3.29101 15.8206 2.0275Z" fill="currentColor"/>
<path d="M7.29167 9.16659C8.9025 9.16659 10.2083 7.86075 10.2083 6.24992C10.2083 4.63909 8.9025 3.33325 7.29167 3.33325C5.68084 3.33325 4.375 4.63909 4.375 6.24992C4.375 7.86075 5.68084 9.16659 7.29167 9.16659Z" stroke="currentColor" stroke-width="1.5"/>
<path d="M1.66699 16.6667C1.66699 13.9053 3.90557 11.6667 6.66699 11.6667H7.08366" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.16634 16.6666L10.833 10.8333H18.333L16.6663 16.6666H9.16634ZM9.16634 16.6666H5.83301" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(2 2.333)">
<path d="M1.33333 2.33333C1.33333 1.78105 1.78105 1.33333 2.33333 1.33333C2.88562 1.33333 3.33333 1.78105 3.33333 2.33333C3.33333 2.88562 2.88562 3.33333 2.33333 3.33333C1.78105 3.33333 1.33333 2.88562 1.33333 2.33333ZM2.33333 0C1.04467 0 0 1.04467 0 2.33333C0 3.622 1.04467 4.66667 2.33333 4.66667C3.622 4.66667 4.66667 3.622 4.66667 2.33333C4.66667 1.04467 3.622 0 2.33333 0ZM6 3H11.3333V1.66667H6V3ZM8.66667 9C8.66667 8.44773 9.1144 8 9.66667 8C10.2189 8 10.6667 8.44773 10.6667 9C10.6667 9.55227 10.2189 10 9.66667 10C9.1144 10 8.66667 9.55227 8.66667 9ZM9.66667 6.66667C8.378 6.66667 7.33333 7.71133 7.33333 9C7.33333 10.2887 8.378 11.3333 9.66667 11.3333C10.9553 11.3333 12 10.2887 12 9C12 7.71133 10.9553 6.66667 9.66667 6.66667ZM0.666667 8.33333V9.66667H6V8.33333H0.666667Z" fill="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 956 B

Some files were not shown because too many files have changed in this diff Show More