From ca75a1c9a366d92f29ed72e9e5b244951cac57c7 Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Mon, 22 Sep 2025 10:44:08 +0800 Subject: [PATCH 01/32] add: trial api and trial table --- api/configs/feature/__init__.py | 10 + api/controllers/console/__init__.py | 4 + api/controllers/console/admin.py | 94 ++++- api/controllers/console/explore/banner.py | 34 ++ api/controllers/console/explore/error.py | 22 ++ .../console/explore/recommended_app.py | 1 + api/controllers/console/explore/trial.py | 349 ++++++++++++++++++ api/controllers/console/explore/wraps.py | 68 +++- ...db42_add_table_explore_banner_and_trial.py | 79 ++++ api/models/__init__.py | 6 + api/models/model.py | 57 +++ api/services/feature_service.py | 4 + api/services/recommended_app_service.py | 37 ++ 13 files changed, 763 insertions(+), 2 deletions(-) create mode 100644 api/controllers/console/explore/banner.py create mode 100644 api/controllers/console/explore/trial.py create mode 100644 api/migrations/versions/2025_09_19_1442-1b435d90db42_add_table_explore_banner_and_trial.py diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index a02f8a4d49..b47b980575 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -841,6 +841,16 @@ class MailConfig(BaseSettings): default=None, ) + ENABLE_TRIAL_APP: bool = Field( + description="Enable trial app", + default=False, + ) + + ENABLE_EXPLORE_BANNER: bool = Field( + description="Enable explore banner", + default=False, + ) + class RagEtlConfig(BaseSettings): """ diff --git a/api/controllers/console/__init__.py b/api/controllers/console/__init__.py index 621f5066e4..ee1c482397 100644 --- a/api/controllers/console/__init__.py +++ b/api/controllers/console/__init__.py @@ -106,10 +106,12 @@ from .datasets.rag_pipeline import ( # Import explore controllers from .explore import ( + banner, installed_app, parameter, recommended_app, saved_message, + trial, ) # Import tag controllers @@ -143,6 +145,7 @@ __all__ = [ "apikey", "app", "audio", + "banner", "billing", "bp", "completion", @@ -196,6 +199,7 @@ __all__ = [ "statistic", "tags", "tool_providers", + "trial", "version", "website", "workflow", diff --git a/api/controllers/console/admin.py b/api/controllers/console/admin.py index 2c4d8709eb..8d021e0f9d 100644 --- a/api/controllers/console/admin.py +++ b/api/controllers/console/admin.py @@ -16,7 +16,7 @@ from controllers.console import api, console_ns from controllers.console.wraps import only_edition_cloud from extensions.ext_database import db from libs.token import extract_access_token -from models.model import App, InstalledApp, RecommendedApp +from models.model import App, ExporleBanner, InstalledApp, RecommendedApp, TrialApp def admin_required(view: Callable[P, R]): @@ -52,6 +52,8 @@ class InsertExploreAppListApi(Resource): "language": fields.String(required=True, description="Language code"), "category": fields.String(required=True, description="App category"), "position": fields.Integer(required=True, description="Display position"), + "can_trial": fields.Boolean(required=True, description="Can trial"), + "trial_limit": fields.Integer(required=True, description="Trial limit"), }, ) ) @@ -71,6 +73,8 @@ class InsertExploreAppListApi(Resource): .add_argument("language", type=supported_language, required=True, nullable=False, location="json") .add_argument("category", type=str, required=True, nullable=False, location="json") .add_argument("position", type=int, required=True, nullable=False, location="json") + .add_argument("can_trial", type=bool, required=True, nullable=False, location="json") + .add_argument("trial_limit", type=int, required=True, nullable=False, location="json") ) args = parser.parse_args() @@ -108,6 +112,20 @@ class InsertExploreAppListApi(Resource): ) db.session.add(recommended_app) + if args["can_trial"]: + trial_app = db.session.execute( + select(TrialApp).where(TrialApp.app_id == args["app_id"]) + ).scalar_one_or_none() + if not trial_app: + db.session.add( + TrialApp( + app_id=args["app_id"], + tenant_id=app.tenant_id, + trial_limit=args["trial_limit"], + ) + ) + else: + trial_app.trial_limit = args["trial_limit"] app.is_public = True db.session.commit() @@ -122,6 +140,20 @@ class InsertExploreAppListApi(Resource): recommended_app.category = args["category"] recommended_app.position = args["position"] + if args["can_trial"]: + trial_app = db.session.execute( + select(TrialApp).where(TrialApp.app_id == args["app_id"]) + ).scalar_one_or_none() + if not trial_app: + db.session.add( + TrialApp( + app_id=args["app_id"], + tenant_id=app.tenant_id, + trial_limit=args["trial_limit"], + ) + ) + else: + trial_app.trial_limit = args["trial_limit"] app.is_public = True db.session.commit() @@ -167,7 +199,67 @@ class InsertExploreAppApi(Resource): for installed_app in installed_apps: session.delete(installed_app) + trial_app = session.execute( + select(TrialApp).where(TrialApp.app_id == recommended_app.app_id) + ).scalar_one_or_none() + if trial_app: + session.delete(trial_app) + db.session.delete(recommended_app) db.session.commit() return {"result": "success"}, 204 + + +@console_ns.route("/admin/insert-explore-banner") +class InsertExploreBanner(Resource): + @api.doc("insert_explore_banner") + @api.doc(description="Insert an explore banner") + @api.expect( + api.model( + "InsertExploreBannerRequest", + { + "content": fields.String(required=True, description="Banner content"), + "link": fields.String(required=True, description="Banner link"), + "sort": fields.Integer(required=True, description="Banner sort"), + }, + ) + ) + @api.response(200, "Banner inserted successfully") + @admin_required + @only_edition_cloud + def post(self): + parser = reqparse.RequestParser() + parser.add_argument("content", type=str, required=True, nullable=False, location="json") + parser.add_argument("link", type=str, required=True, nullable=False, location="json") + parser.add_argument("sort", type=int, required=True, nullable=False, location="json") + + args = parser.parse_args() + + banner = ExporleBanner( + content=args["content"], + link=args["link"], + sort=args["sort"], + ) + db.session.add(banner) + db.session.commit() + + return {"result": "success"}, 200 + + +@console_ns.route("/admin/delete-explore-banner/") +class DeleteExploreBanner(Resource): + @api.doc("delete_explore_banner") + @api.doc(description="Delete an explore banner") + @api.response(204, "Banner deleted successfully") + @admin_required + @only_edition_cloud + def delete(self, banner_id): + banner = db.session.execute(select(ExporleBanner).where(ExporleBanner.id == banner_id)).scalar_one_or_none() + if not banner: + raise NotFound(f"Banner '{banner_id}' is not found") + + db.session.delete(banner) + db.session.commit() + + return {"result": "success"}, 204 diff --git a/api/controllers/console/explore/banner.py b/api/controllers/console/explore/banner.py new file mode 100644 index 0000000000..5e7aa1ec81 --- /dev/null +++ b/api/controllers/console/explore/banner.py @@ -0,0 +1,34 @@ +from flask_restx import Resource + +from controllers.console import api +from controllers.console.explore.wraps import explore_banner_enabled +from extensions.ext_database import db +from models.model import ExporleBanner + + +class BannerApi(Resource): + """Resource for banner list.""" + + @explore_banner_enabled + def get(self): + """Get banner list.""" + banners = ( + db.session.query(ExporleBanner).filter(ExporleBanner.status == "enabled").order_by(ExporleBanner.sort).all() + ) + + # Convert banners to serializable format + result = [] + for banner in banners: + banner_data = { + "content": banner.content, # Already parsed as JSON by SQLAlchemy + "link": banner.link, + "sort": banner.sort, + "status": banner.status, + "created_at": banner.created_at.isoformat() if banner.created_at else None, + } + result.append(banner_data) + + return result + + +api.add_resource(BannerApi, "/explore/banners") diff --git a/api/controllers/console/explore/error.py b/api/controllers/console/explore/error.py index 1e05ff4206..e96fa64f84 100644 --- a/api/controllers/console/explore/error.py +++ b/api/controllers/console/explore/error.py @@ -29,3 +29,25 @@ class AppAccessDeniedError(BaseHTTPException): error_code = "access_denied" description = "App access denied." code = 403 + + +class TrialAppNotAllowed(BaseHTTPException): + """*403* `Trial App Not Allowed` + + Raise if the user has reached the trial app limit. + """ + + error_code = "trial_app_not_allowed" + code = 403 + description = "the app is not allowed to be trial." + + +class TrialAppLimitExceeded(BaseHTTPException): + """*403* `Trial App Limit Exceeded` + + Raise if the user has exceeded the trial app limit. + """ + + error_code = "trial_app_limit_exceeded" + code = 403 + description = "The user has exceeded the trial app limit." diff --git a/api/controllers/console/explore/recommended_app.py b/api/controllers/console/explore/recommended_app.py index 751012757a..3518107c6c 100644 --- a/api/controllers/console/explore/recommended_app.py +++ b/api/controllers/console/explore/recommended_app.py @@ -27,6 +27,7 @@ recommended_app_fields = { "category": fields.String, "position": fields.Integer, "is_listed": fields.Boolean, + "can_trial": fields.Boolean, } recommended_app_list_fields = { diff --git a/api/controllers/console/explore/trial.py b/api/controllers/console/explore/trial.py new file mode 100644 index 0000000000..c4976c0577 --- /dev/null +++ b/api/controllers/console/explore/trial.py @@ -0,0 +1,349 @@ +import logging + +from flask import request +from flask_restx import Resource, marshal_with, reqparse +from werkzeug.exceptions import Forbidden, InternalServerError, NotFound + +import services +from controllers.common import fields +from controllers.console.app.error import ( + AppUnavailableError, + AudioTooLargeError, + CompletionRequestError, + ConversationCompletedError, + NoAudioUploadedError, + ProviderModelCurrentlyNotSupportError, + ProviderNotInitializeError, + ProviderNotSupportSpeechToTextError, + ProviderQuotaExceededError, + UnsupportedAudioTypeError, +) +from controllers.console.app.wraps import get_app_model +from controllers.console.explore.error import ( + AppSuggestedQuestionsAfterAnswerDisabledError, + NotChatAppError, + NotCompletionAppError, +) +from controllers.console.explore.wraps import TrialAppResource, trial_feature_enable +from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError +from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict +from core.app.entities.app_invoke_entities import InvokeFrom +from core.errors.error import ( + ModelCurrentlyNotSupportError, + ProviderTokenNotInitError, + QuotaExceededError, +) +from core.model_runtime.errors.invoke import InvokeError +from extensions.ext_database import db +from libs import helper +from libs.helper import uuid_value +from libs.login import current_user +from models import Account +from models.account import TenantStatus +from models.model import AppMode, Site +from services.app_generate_service import AppGenerateService +from services.app_service import AppService +from services.audio_service import AudioService +from services.errors.audio import ( + AudioTooLargeServiceError, + NoAudioUploadedServiceError, + ProviderNotSupportSpeechToTextServiceError, + UnsupportedAudioTypeServiceError, +) +from services.errors.conversation import ConversationNotExistsError +from services.errors.llm import InvokeRateLimitError +from services.errors.message import ( + MessageNotExistsError, + SuggestedQuestionsAfterAnswerDisabledError, +) +from services.message_service import MessageService +from services.recommended_app_service import RecommendedAppService + +logger = logging.getLogger(__name__) + + +class TrialChatApi(TrialAppResource): + @trial_feature_enable + def post(self, trial_app): + app_model = trial_app + app_mode = AppMode.value_of(app_model.mode) + if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: + raise NotChatAppError() + + parser = reqparse.RequestParser() + parser.add_argument("inputs", type=dict, required=True, location="json") + parser.add_argument("query", type=str, required=True, location="json") + parser.add_argument("files", type=list, required=False, location="json") + parser.add_argument("conversation_id", type=uuid_value, location="json") + parser.add_argument("parent_message_id", type=uuid_value, required=False, location="json") + parser.add_argument("retriever_from", type=str, required=False, default="explore_app", location="json") + args = parser.parse_args() + + args["auto_generate_name"] = False + + try: + if not isinstance(current_user, Account): + raise ValueError("current_user must be an Account instance") + response = AppGenerateService.generate( + app_model=app_model, user=current_user, args=args, invoke_from=InvokeFrom.EXPLORE, streaming=True + ) + RecommendedAppService.add_trial_app_record(app_model.id, current_user.id) + return helper.compact_generate_response(response) + except services.errors.conversation.ConversationNotExistsError: + raise NotFound("Conversation Not Exists.") + except services.errors.conversation.ConversationCompletedError: + raise ConversationCompletedError() + except services.errors.app_model_config.AppModelConfigBrokenError: + logger.exception("App model config broken.") + raise AppUnavailableError() + except ProviderTokenNotInitError as ex: + raise ProviderNotInitializeError(ex.description) + except QuotaExceededError: + raise ProviderQuotaExceededError() + except ModelCurrentlyNotSupportError: + raise ProviderModelCurrentlyNotSupportError() + except InvokeError as e: + raise CompletionRequestError(e.description) + except InvokeRateLimitError as ex: + raise InvokeRateLimitHttpError(ex.description) + except ValueError as e: + raise e + except Exception: + logger.exception("internal server error.") + raise InternalServerError() + + +class TrialMessageSuggestedQuestionApi(TrialAppResource): + @trial_feature_enable + def get(self, trial_app, message_id): + app_model = trial_app + app_mode = AppMode.value_of(app_model.mode) + if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: + raise NotChatAppError() + + message_id = str(message_id) + + try: + if not isinstance(current_user, Account): + raise ValueError("current_user must be an Account instance") + questions = MessageService.get_suggested_questions_after_answer( + app_model=app_model, user=current_user, message_id=message_id, invoke_from=InvokeFrom.EXPLORE + ) + except MessageNotExistsError: + raise NotFound("Message not found") + except ConversationNotExistsError: + raise NotFound("Conversation not found") + except SuggestedQuestionsAfterAnswerDisabledError: + raise AppSuggestedQuestionsAfterAnswerDisabledError() + except ProviderTokenNotInitError as ex: + raise ProviderNotInitializeError(ex.description) + except QuotaExceededError: + raise ProviderQuotaExceededError() + except ModelCurrentlyNotSupportError: + raise ProviderModelCurrentlyNotSupportError() + except InvokeError as e: + raise CompletionRequestError(e.description) + except Exception: + logger.exception("internal server error.") + raise InternalServerError() + + return {"data": questions} + + +class TrialChatAudioApi(TrialAppResource): + @trial_feature_enable + def post(self, trial_app): + app_model = trial_app + + file = request.files["file"] + + try: + if not isinstance(current_user, Account): + raise ValueError("current_user must be an Account instance") + response = AudioService.transcript_asr(app_model=app_model, file=file, end_user=None) + RecommendedAppService.add_trial_app_record(app_model.id, current_user.id) + return response + except services.errors.app_model_config.AppModelConfigBrokenError: + logger.exception("App model config broken.") + raise AppUnavailableError() + except NoAudioUploadedServiceError: + raise NoAudioUploadedError() + except AudioTooLargeServiceError as e: + raise AudioTooLargeError(str(e)) + except UnsupportedAudioTypeServiceError: + raise UnsupportedAudioTypeError() + except ProviderNotSupportSpeechToTextServiceError: + raise ProviderNotSupportSpeechToTextError() + except ProviderTokenNotInitError as ex: + raise ProviderNotInitializeError(ex.description) + except QuotaExceededError: + raise ProviderQuotaExceededError() + except ModelCurrentlyNotSupportError: + raise ProviderModelCurrentlyNotSupportError() + except InvokeError as e: + raise CompletionRequestError(e.description) + except ValueError as e: + raise e + except Exception as e: + logger.exception("internal server error.") + raise InternalServerError() + + +class TrialChatTextApi(TrialAppResource): + @trial_feature_enable + def post(self, trial_app): + app_model = trial_app + try: + parser = reqparse.RequestParser() + parser.add_argument("message_id", type=str, required=False, location="json") + parser.add_argument("voice", type=str, location="json") + parser.add_argument("text", type=str, location="json") + parser.add_argument("streaming", type=bool, location="json") + args = parser.parse_args() + + message_id = args.get("message_id", None) + text = args.get("text", None) + voice = args.get("voice", None) + if not isinstance(current_user, Account): + raise ValueError("current_user must be an Account instance") + response = AudioService.transcript_tts(app_model=app_model, text=text, voice=voice, message_id=message_id) + RecommendedAppService.add_trial_app_record(app_model.id, current_user.id) + return response + except services.errors.app_model_config.AppModelConfigBrokenError: + logger.exception("App model config broken.") + raise AppUnavailableError() + except NoAudioUploadedServiceError: + raise NoAudioUploadedError() + except AudioTooLargeServiceError as e: + raise AudioTooLargeError(str(e)) + except UnsupportedAudioTypeServiceError: + raise UnsupportedAudioTypeError() + except ProviderNotSupportSpeechToTextServiceError: + raise ProviderNotSupportSpeechToTextError() + except ProviderTokenNotInitError as ex: + raise ProviderNotInitializeError(ex.description) + except QuotaExceededError: + raise ProviderQuotaExceededError() + except ModelCurrentlyNotSupportError: + raise ProviderModelCurrentlyNotSupportError() + except InvokeError as e: + raise CompletionRequestError(e.description) + except ValueError as e: + raise e + except Exception as e: + logger.exception("internal server error.") + raise InternalServerError() + + +class TrialCompletionApi(TrialAppResource): + @trial_feature_enable + def post(self, trial_app): + app_model = trial_app + if app_model.mode != "completion": + raise NotCompletionAppError() + + parser = reqparse.RequestParser() + parser.add_argument("inputs", type=dict, required=True, location="json") + parser.add_argument("query", type=str, location="json", default="") + parser.add_argument("files", type=list, required=False, location="json") + parser.add_argument("response_mode", type=str, choices=["blocking", "streaming"], location="json") + parser.add_argument("retriever_from", type=str, required=False, default="explore_app", location="json") + args = parser.parse_args() + + streaming = args["response_mode"] == "streaming" + args["auto_generate_name"] = False + + try: + if not isinstance(current_user, Account): + raise ValueError("current_user must be an Account instance") + response = AppGenerateService.generate( + app_model=app_model, user=current_user, args=args, invoke_from=InvokeFrom.EXPLORE, streaming=streaming + ) + + RecommendedAppService.add_trial_app_record(app_model.id, current_user.id) + return helper.compact_generate_response(response) + except services.errors.conversation.ConversationNotExistsError: + raise NotFound("Conversation Not Exists.") + except services.errors.conversation.ConversationCompletedError: + raise ConversationCompletedError() + except services.errors.app_model_config.AppModelConfigBrokenError: + logger.exception("App model config broken.") + raise AppUnavailableError() + except ProviderTokenNotInitError as ex: + raise ProviderNotInitializeError(ex.description) + except QuotaExceededError: + raise ProviderQuotaExceededError() + except ModelCurrentlyNotSupportError: + raise ProviderModelCurrentlyNotSupportError() + except InvokeError as e: + raise CompletionRequestError(e.description) + except ValueError as e: + raise e + except Exception: + logger.exception("internal server error.") + raise InternalServerError() + + +class TrialSitApi(Resource): + """Resource for trial app sites.""" + + @trial_feature_enable + @get_app_model + def get(self, app_model): + """Retrieve app site info. + + Returns the site configuration for the application including theme, icons, and text. + """ + site = db.session.query(Site).where(Site.app_id == app_model.id).first() + + if not site: + raise Forbidden() + + assert app_model.tenant + if app_model.tenant.status == TenantStatus.ARCHIVE: + raise Forbidden() + + return site + + +class TrialAppParameterApi(Resource): + """Resource for app variables.""" + + @trial_feature_enable + @get_app_model + @marshal_with(fields.parameters_fields) + def get(self, app_model): + """Retrieve app parameters.""" + + if app_model is None: + raise AppUnavailableError() + + if app_model.mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}: + workflow = app_model.workflow + if workflow is None: + raise AppUnavailableError() + + features_dict = workflow.features_dict + user_input_form = workflow.user_input_form(to_old_structure=True) + else: + app_model_config = app_model.app_model_config + if app_model_config is None: + raise AppUnavailableError() + + features_dict = app_model_config.to_dict() + + user_input_form = features_dict.get("user_input_form", []) + + return get_parameters_from_feature_dict(features_dict=features_dict, user_input_form=user_input_form) + + +class AppApi(Resource): + @trial_feature_enable + @get_app_model + def get(self, app_model): + """Get app detail""" + + app_service = AppService() + app_model = app_service.get_app(app_model) + + return app_model diff --git a/api/controllers/console/explore/wraps.py b/api/controllers/console/explore/wraps.py index 2a97d312aa..150dd40b80 100644 --- a/api/controllers/console/explore/wraps.py +++ b/api/controllers/console/explore/wraps.py @@ -2,14 +2,17 @@ from collections.abc import Callable from functools import wraps from typing import Concatenate, ParamSpec, TypeVar +from flask import abort from flask_restx import Resource from werkzeug.exceptions import NotFound -from controllers.console.explore.error import AppAccessDeniedError +from controllers.console.explore.error import AppAccessDeniedError, TrialAppLimitExceeded, TrialAppNotAllowed from controllers.console.wraps import account_initialization_required from extensions.ext_database import db from libs.login import current_account_with_tenant, login_required from models import InstalledApp +from models import AccountTrialAppRecord, App, InstalledApp, TrialApp +from libs.login import current_user from services.enterprise.enterprise_service import EnterpriseService from services.feature_service import FeatureService @@ -71,6 +74,59 @@ def user_allowed_to_access_app(view: Callable[Concatenate[InstalledApp, P], R] | return decorator +def trial_app_required(view: Callable[Concatenate[App, P], R] | None = None): + def decorator(view: Callable[Concatenate[App, P], R]): + @wraps(view) + def decorated(app_id: str, *args: P.args, **kwargs: P.kwargs): + trial_app = db.session.query(TrialApp).where(TrialApp.app_id == str(app_id)).first() + + if trial_app is None: + raise TrialAppNotAllowed() + app = trial_app.app + + if app is None: + raise TrialAppNotAllowed() + + account_trial_app_record = ( + db.session.query(AccountTrialAppRecord) + .where(AccountTrialAppRecord.account_id == current_user.id, AccountTrialAppRecord.app_id == app_id) + .first() + ) + if account_trial_app_record: + if account_trial_app_record.count >= trial_app.trial_limit: + raise TrialAppLimitExceeded() + + return view(app, *args, **kwargs) + + return decorated + + if view: + return decorator(view) + return decorator + + +def trial_feature_enable(view: Callable[..., R]) -> Callable[..., R]: + @wraps(view) + def decorated(*args, **kwargs): + features = FeatureService.get_system_features() + if not features.enable_trial_app: + abort(403, "Trial app feature is not enabled.") + return view(*args, **kwargs) + + return decorated + + +def explore_banner_enabled(view: Callable[..., R]) -> Callable[..., R]: + @wraps(view) + def decorated(*args, **kwargs): + features = FeatureService.get_system_features() + if not features.enable_explore_banner: + abort(403, "Explore banner feature is not enabled.") + return view(*args, **kwargs) + + return decorated + + class InstalledAppResource(Resource): # must be reversed if there are multiple decorators @@ -80,3 +136,13 @@ class InstalledAppResource(Resource): account_initialization_required, login_required, ] + + +class TrialAppResource(Resource): + # must be reversed if there are multiple decorators + + method_decorators = [ + trial_app_required, + account_initialization_required, + login_required, + ] diff --git a/api/migrations/versions/2025_09_19_1442-1b435d90db42_add_table_explore_banner_and_trial.py b/api/migrations/versions/2025_09_19_1442-1b435d90db42_add_table_explore_banner_and_trial.py new file mode 100644 index 0000000000..6d20273c5d --- /dev/null +++ b/api/migrations/versions/2025_09_19_1442-1b435d90db42_add_table_explore_banner_and_trial.py @@ -0,0 +1,79 @@ +"""add table explore banner and trial + +Revision ID: 1b435d90db42 +Revises: cf7c38a32b2d +Create Date: 2025-09-19 14:42:58.416649 + +""" +from alembic import op +import models as models +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '1b435d90db42' +down_revision = 'cf7c38a32b2d' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('account_trial_app_records', + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('account_id', models.types.StringUUID(), nullable=False), + sa.Column('app_id', models.types.StringUUID(), nullable=False), + sa.Column('count', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.PrimaryKeyConstraint('id', name='user_trial_app_pkey'), + sa.UniqueConstraint('account_id', 'app_id', name='unique_account_trial_app_record') + ) + with op.batch_alter_table('account_trial_app_records', schema=None) as batch_op: + batch_op.create_index('account_trial_app_record_account_id_idx', ['account_id'], unique=False) + batch_op.create_index('account_trial_app_record_app_id_idx', ['app_id'], unique=False) + + op.create_table('exporle_banners', + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('content', sa.JSON(), nullable=False), + sa.Column('link', sa.String(length=255), nullable=False), + sa.Column('sort', sa.Integer(), nullable=False), + sa.Column('status', sa.String(length=255), server_default=sa.text("'enabled'::character varying"), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.PrimaryKeyConstraint('id', name='exporler_banner_pkey') + ) + op.create_table('trial_apps', + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('app_id', models.types.StringUUID(), nullable=False), + sa.Column('tenant_id', models.types.StringUUID(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('trial_limit', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('id', name='trial_app_pkey'), + sa.UniqueConstraint('app_id', name='unique_trail_app_id') + ) + with op.batch_alter_table('trial_apps', schema=None) as batch_op: + batch_op.create_index('trial_app_app_id_idx', ['app_id'], unique=False) + batch_op.create_index('trial_app_tenant_id_idx', ['tenant_id'], unique=False) + + with op.batch_alter_table('providers', schema=None) as batch_op: + batch_op.drop_column('credential_status') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('providers', schema=None) as batch_op: + batch_op.add_column(sa.Column('credential_status', sa.VARCHAR(length=20), server_default=sa.text("'active'::character varying"), autoincrement=False, nullable=True)) + + with op.batch_alter_table('trial_apps', schema=None) as batch_op: + batch_op.drop_index('trial_app_tenant_id_idx') + batch_op.drop_index('trial_app_app_id_idx') + + op.drop_table('trial_apps') + op.drop_table('exporle_banners') + with op.batch_alter_table('account_trial_app_records', schema=None) as batch_op: + batch_op.drop_index('account_trial_app_record_app_id_idx') + batch_op.drop_index('account_trial_app_record_account_id_idx') + + op.drop_table('account_trial_app_records') + # ### end Alembic commands ### diff --git a/api/models/__init__.py b/api/models/__init__.py index 779484283f..6adc8e56b6 100644 --- a/api/models/__init__.py +++ b/api/models/__init__.py @@ -28,6 +28,7 @@ from .dataset import ( ) from .enums import CreatorUserRole, UserFrom, WorkflowRunTriggeredFrom from .model import ( + AccountTrialAppRecord, ApiRequest, ApiToken, App, @@ -40,6 +41,7 @@ from .model import ( DatasetRetrieverResource, DifySetup, EndUser, + ExporleBanner, IconType, InstalledApp, Message, @@ -54,6 +56,7 @@ from .model import ( Tag, TagBinding, TraceAppConfig, + TrialApp, UploadFile, ) from .oauth import DatasourceOauthParamConfig, DatasourceProvider @@ -98,6 +101,7 @@ __all__ = [ "Account", "AccountIntegrate", "AccountStatus", + "AccountTrialAppRecord", "ApiRequest", "ApiToken", "ApiToolProvider", @@ -131,6 +135,7 @@ __all__ = [ "DocumentSegment", "Embedding", "EndUser", + "ExporleBanner", "ExternalKnowledgeApis", "ExternalKnowledgeBindings", "IconType", @@ -168,6 +173,7 @@ __all__ = [ "ToolLabelBinding", "ToolModelInvoke", "TraceAppConfig", + "TrialApp", "UploadFile", "UserFrom", "Whitelist", diff --git a/api/models/model.py b/api/models/model.py index 8a8574e2fe..c15c86c569 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -581,6 +581,63 @@ class InstalledApp(Base): return tenant +class TrialApp(Base): + __tablename__ = "trial_apps" + __table_args__ = ( + sa.PrimaryKeyConstraint("id", name="trial_app_pkey"), + sa.Index("trial_app_app_id_idx", "app_id"), + sa.Index("trial_app_tenant_id_idx", "tenant_id"), + sa.UniqueConstraint("app_id", name="unique_trail_app_id"), + ) + + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) + app_id = mapped_column(StringUUID, nullable=False) + tenant_id = mapped_column(StringUUID, nullable=False) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) + trial_limit = mapped_column(sa.Integer, nullable=False, default=3) + + @property + def app(self) -> App | None: + app = db.session.query(App).where(App.id == self.app_id).first() + return app + + +class AccountTrialAppRecord(Base): + __tablename__ = "account_trial_app_records" + __table_args__ = ( + sa.PrimaryKeyConstraint("id", name="user_trial_app_pkey"), + sa.Index("account_trial_app_record_account_id_idx", "account_id"), + sa.Index("account_trial_app_record_app_id_idx", "app_id"), + sa.UniqueConstraint("account_id", "app_id", name="unique_account_trial_app_record"), + ) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) + account_id = mapped_column(StringUUID, nullable=False) + app_id = mapped_column(StringUUID, nullable=False) + count = mapped_column(sa.Integer, nullable=False, default=0) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) + + @property + def app(self) -> App | None: + app = db.session.query(App).where(App.id == self.app_id).first() + return app + + @property + def user(self) -> Account | None: + user = db.session.query(Account).where(Account.id == self.account_id).first() + return user + + +class ExporleBanner(Base): + __tablename__ = "exporle_banners" + __table_args__ = (sa.PrimaryKeyConstraint("id", name="exporler_banner_pkey"),) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) + content = mapped_column(sa.JSON, nullable=False) + link = mapped_column(String(255), nullable=False) + sort = mapped_column(sa.Integer, nullable=False) + status = mapped_column(sa.String(255), nullable=False, server_default=sa.text("'enabled'::character varying")) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) + + class OAuthProviderApp(Base): """ Globally shared OAuth provider app information. diff --git a/api/services/feature_service.py b/api/services/feature_service.py index 148442f76e..57e7592b7a 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -160,6 +160,8 @@ class SystemFeatureModel(BaseModel): plugin_installation_permission: PluginInstallationPermissionModel = PluginInstallationPermissionModel() enable_change_email: bool = True plugin_manager: PluginManagerModel = PluginManagerModel() + enable_trial_app: bool = False + enable_explore_banner: bool = False class FeatureService: @@ -215,6 +217,8 @@ class FeatureService: system_features.is_allow_register = dify_config.ALLOW_REGISTER system_features.is_allow_create_workspace = dify_config.ALLOW_CREATE_WORKSPACE system_features.is_email_setup = dify_config.MAIL_TYPE is not None and dify_config.MAIL_TYPE != "" + system_features.enable_trial_app = dify_config.ENABLE_TRIAL_APP + system_features.enable_explore_banner = dify_config.ENABLE_EXPLORE_BANNER @classmethod def _fulfill_params_from_env(cls, features: FeatureModel): diff --git a/api/services/recommended_app_service.py b/api/services/recommended_app_service.py index 544383a106..b0c31e272b 100644 --- a/api/services/recommended_app_service.py +++ b/api/services/recommended_app_service.py @@ -1,4 +1,9 @@ +from sqlalchemy.orm import Session + from configs import dify_config +from extensions.ext_database import db +from models.model import AccountTrialAppRecord, TrialApp +from services.feature_service import FeatureService from services.recommend_app.recommend_app_factory import RecommendAppRetrievalFactory @@ -20,6 +25,15 @@ class RecommendedAppService: ) ) + if FeatureService.get_system_features().enable_trial_app: + apps = result["recommended_apps"] + for app in apps: + app_id = app["app_id"] + trial_app_model = db.session.query(TrialApp).where(TrialApp.app_id == app_id).first() + if trial_app_model: + app["can_trial"] = True + else: + app["can_trial"] = False return result @classmethod @@ -32,4 +46,27 @@ class RecommendedAppService: mode = dify_config.HOSTED_FETCH_APP_TEMPLATES_MODE retrieval_instance = RecommendAppRetrievalFactory.get_recommend_app_factory(mode)() result: dict = retrieval_instance.get_recommend_app_detail(app_id) + if FeatureService.get_system_features().enable_trial_app: + app_id = result["id"] + trial_app_model = db.session.query(TrialApp).where(TrialApp.app_id == app_id).first() + if trial_app_model: + result["can_trial"] = True + else: + result["can_trial"] = False return result + + @classmethod + def add_trial_app_record(cls, app_id: str, account_id: str): + """ + Add trial app record. + :param app_id: app id + :return: + """ + with Session(db.engine) as session: + account_trial_app_record = session.query(AccountTrialAppRecord).where(TrialApp.app_id == app_id).first() + if account_trial_app_record: + account_trial_app_record.count += 1 + session.commit() + else: + session.add(AccountTrialAppRecord(app_id=app_id, count=1, account_id=account_id)) + session.commit() From 3e448f01026a34333573a889d2f07ac3f7d6269d Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Mon, 22 Sep 2025 15:47:23 +0800 Subject: [PATCH 02/32] fix: add marshal site model to json --- api/controllers/console/explore/trial.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/controllers/console/explore/trial.py b/api/controllers/console/explore/trial.py index c4976c0577..4f27155cd2 100644 --- a/api/controllers/console/explore/trial.py +++ b/api/controllers/console/explore/trial.py @@ -6,6 +6,7 @@ from werkzeug.exceptions import Forbidden, InternalServerError, NotFound import services from controllers.common import fields +from controllers.common.fields import build_site_model from controllers.console.app.error import ( AppUnavailableError, AudioTooLargeError, @@ -25,6 +26,7 @@ from controllers.console.explore.error import ( NotCompletionAppError, ) from controllers.console.explore.wraps import TrialAppResource, trial_feature_enable +from controllers.service_api import service_api_ns from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict from core.app.entities.app_invoke_entities import InvokeFrom @@ -289,6 +291,7 @@ class TrialSitApi(Resource): @trial_feature_enable @get_app_model + @service_api_ns.marshal_with(build_site_model(service_api_ns)) def get(self, app_model): """Retrieve app site info. From 4dca9a12a82df10c979d06e52bc644dc89c75d33 Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Mon, 22 Sep 2025 16:14:28 +0800 Subject: [PATCH 03/32] fix: add marshal app model to json --- api/controllers/console/explore/trial.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/controllers/console/explore/trial.py b/api/controllers/console/explore/trial.py index 4f27155cd2..666dc52bad 100644 --- a/api/controllers/console/explore/trial.py +++ b/api/controllers/console/explore/trial.py @@ -6,7 +6,7 @@ from werkzeug.exceptions import Forbidden, InternalServerError, NotFound import services from controllers.common import fields -from controllers.common.fields import build_site_model +from controllers.common.fields import build_app_detail_fields_with_site, build_site_model from controllers.console.app.error import ( AppUnavailableError, AudioTooLargeError, @@ -343,6 +343,7 @@ class TrialAppParameterApi(Resource): class AppApi(Resource): @trial_feature_enable @get_app_model + @service_api_ns.marshal_with(build_app_detail_fields_with_site(service_api_ns)) def get(self, app_model): """Get app detail""" From 91110499dd778ba0c4be4ba74dd83d1b0a1c04cc Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Mon, 22 Sep 2025 16:23:42 +0800 Subject: [PATCH 04/32] fix: add marshal app model to json --- api/controllers/console/explore/trial.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/controllers/console/explore/trial.py b/api/controllers/console/explore/trial.py index 666dc52bad..d79b9deb86 100644 --- a/api/controllers/console/explore/trial.py +++ b/api/controllers/console/explore/trial.py @@ -6,7 +6,8 @@ from werkzeug.exceptions import Forbidden, InternalServerError, NotFound import services from controllers.common import fields -from controllers.common.fields import build_app_detail_fields_with_site, build_site_model +from controllers.common.fields import build_site_model +from fields.app_fields import app_detail_fields_with_site from controllers.console.app.error import ( AppUnavailableError, AudioTooLargeError, @@ -343,7 +344,7 @@ class TrialAppParameterApi(Resource): class AppApi(Resource): @trial_feature_enable @get_app_model - @service_api_ns.marshal_with(build_app_detail_fields_with_site(service_api_ns)) + @marshal_with(app_detail_fields_with_site) def get(self, app_model): """Get app detail""" From 38da19a729b205593830b3dc3494dfb4c21c89f5 Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Mon, 22 Sep 2025 16:23:55 +0800 Subject: [PATCH 05/32] fix: add marshal app model to json --- api/controllers/console/explore/trial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/console/explore/trial.py b/api/controllers/console/explore/trial.py index d79b9deb86..f743f62725 100644 --- a/api/controllers/console/explore/trial.py +++ b/api/controllers/console/explore/trial.py @@ -7,7 +7,6 @@ from werkzeug.exceptions import Forbidden, InternalServerError, NotFound import services from controllers.common import fields from controllers.common.fields import build_site_model -from fields.app_fields import app_detail_fields_with_site from controllers.console.app.error import ( AppUnavailableError, AudioTooLargeError, @@ -38,6 +37,7 @@ from core.errors.error import ( ) from core.model_runtime.errors.invoke import InvokeError from extensions.ext_database import db +from fields.app_fields import app_detail_fields_with_site from libs import helper from libs.helper import uuid_value from libs.login import current_user From e3c1310afa039c9139415979645ad3a2ec85dc74 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 06:44:39 +0000 Subject: [PATCH 06/32] [autofix.ci] apply automated fixes --- api/controllers/console/explore/banner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/console/explore/banner.py b/api/controllers/console/explore/banner.py index 5e7aa1ec81..813644d149 100644 --- a/api/controllers/console/explore/banner.py +++ b/api/controllers/console/explore/banner.py @@ -13,7 +13,7 @@ class BannerApi(Resource): def get(self): """Get banner list.""" banners = ( - db.session.query(ExporleBanner).filter(ExporleBanner.status == "enabled").order_by(ExporleBanner.sort).all() + db.session.query(ExporleBanner).where(ExporleBanner.status == "enabled").order_by(ExporleBanner.sort).all() ) # Convert banners to serializable format From 65d376bdaef74838a24e85fd2cb0c95cba4eda16 Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Sat, 11 Oct 2025 09:26:24 +0800 Subject: [PATCH 07/32] fix trial where condition --- api/services/recommended_app_service.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/services/recommended_app_service.py b/api/services/recommended_app_service.py index b0c31e272b..1bc24c5afe 100644 --- a/api/services/recommended_app_service.py +++ b/api/services/recommended_app_service.py @@ -63,7 +63,10 @@ class RecommendedAppService: :return: """ with Session(db.engine) as session: - account_trial_app_record = session.query(AccountTrialAppRecord).where(TrialApp.app_id == app_id).first() + account_trial_app_record = session.query(AccountTrialAppRecord).where( + AccountTrialAppRecord.app_id == app_id, + AccountTrialAppRecord.account_id == account_id + ).first() if account_trial_app_record: account_trial_app_record.count += 1 session.commit() From 0e1444d17ca77c96ea36200b227dca432292e325 Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Sat, 11 Oct 2025 09:42:41 +0800 Subject: [PATCH 08/32] fix: session of db --- api/services/recommended_app_service.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/api/services/recommended_app_service.py b/api/services/recommended_app_service.py index 1bc24c5afe..9d3a663553 100644 --- a/api/services/recommended_app_service.py +++ b/api/services/recommended_app_service.py @@ -1,5 +1,3 @@ -from sqlalchemy.orm import Session - from configs import dify_config from extensions.ext_database import db from models.model import AccountTrialAppRecord, TrialApp @@ -62,7 +60,7 @@ class RecommendedAppService: :param app_id: app id :return: """ - with Session(db.engine) as session: + with db.session as session: account_trial_app_record = session.query(AccountTrialAppRecord).where( AccountTrialAppRecord.app_id == app_id, AccountTrialAppRecord.account_id == account_id From 20109553b9fdaeefb35819559cd303481d6e6322 Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Sat, 11 Oct 2025 09:54:20 +0800 Subject: [PATCH 09/32] Separate object attributes before session --- api/controllers/console/explore/trial.py | 28 ++++++++++++++++++++---- api/services/recommended_app_service.py | 21 +++++++++--------- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/api/controllers/console/explore/trial.py b/api/controllers/console/explore/trial.py index f743f62725..eaa5c74739 100644 --- a/api/controllers/console/explore/trial.py +++ b/api/controllers/console/explore/trial.py @@ -87,10 +87,15 @@ class TrialChatApi(TrialAppResource): try: if not isinstance(current_user, Account): raise ValueError("current_user must be an Account instance") + + # Get IDs before they might be detached from session + app_id = app_model.id + user_id = current_user.id + response = AppGenerateService.generate( app_model=app_model, user=current_user, args=args, invoke_from=InvokeFrom.EXPLORE, streaming=True ) - RecommendedAppService.add_trial_app_record(app_model.id, current_user.id) + RecommendedAppService.add_trial_app_record(app_id, user_id) return helper.compact_generate_response(response) except services.errors.conversation.ConversationNotExistsError: raise NotFound("Conversation Not Exists.") @@ -163,8 +168,13 @@ class TrialChatAudioApi(TrialAppResource): try: if not isinstance(current_user, Account): raise ValueError("current_user must be an Account instance") + + # Get IDs before they might be detached from session + app_id = app_model.id + user_id = current_user.id + response = AudioService.transcript_asr(app_model=app_model, file=file, end_user=None) - RecommendedAppService.add_trial_app_record(app_model.id, current_user.id) + RecommendedAppService.add_trial_app_record(app_id, user_id) return response except services.errors.app_model_config.AppModelConfigBrokenError: logger.exception("App model config broken.") @@ -209,8 +219,13 @@ class TrialChatTextApi(TrialAppResource): voice = args.get("voice", None) if not isinstance(current_user, Account): raise ValueError("current_user must be an Account instance") + + # Get IDs before they might be detached from session + app_id = app_model.id + user_id = current_user.id + response = AudioService.transcript_tts(app_model=app_model, text=text, voice=voice, message_id=message_id) - RecommendedAppService.add_trial_app_record(app_model.id, current_user.id) + RecommendedAppService.add_trial_app_record(app_id, user_id) return response except services.errors.app_model_config.AppModelConfigBrokenError: logger.exception("App model config broken.") @@ -259,11 +274,16 @@ class TrialCompletionApi(TrialAppResource): try: if not isinstance(current_user, Account): raise ValueError("current_user must be an Account instance") + + # Get IDs before they might be detached from session + app_id = app_model.id + user_id = current_user.id + response = AppGenerateService.generate( app_model=app_model, user=current_user, args=args, invoke_from=InvokeFrom.EXPLORE, streaming=streaming ) - RecommendedAppService.add_trial_app_record(app_model.id, current_user.id) + RecommendedAppService.add_trial_app_record(app_id, user_id) return helper.compact_generate_response(response) except services.errors.conversation.ConversationNotExistsError: raise NotFound("Conversation Not Exists.") diff --git a/api/services/recommended_app_service.py b/api/services/recommended_app_service.py index 9d3a663553..be6e94a526 100644 --- a/api/services/recommended_app_service.py +++ b/api/services/recommended_app_service.py @@ -60,14 +60,13 @@ class RecommendedAppService: :param app_id: app id :return: """ - with db.session as session: - account_trial_app_record = session.query(AccountTrialAppRecord).where( - AccountTrialAppRecord.app_id == app_id, - AccountTrialAppRecord.account_id == account_id - ).first() - if account_trial_app_record: - account_trial_app_record.count += 1 - session.commit() - else: - session.add(AccountTrialAppRecord(app_id=app_id, count=1, account_id=account_id)) - session.commit() + account_trial_app_record = db.session.query(AccountTrialAppRecord).where( + AccountTrialAppRecord.app_id == app_id, + AccountTrialAppRecord.account_id == account_id + ).first() + if account_trial_app_record: + account_trial_app_record.count += 1 + db.session.commit() + else: + db.session.add(AccountTrialAppRecord(app_id=app_id, count=1, account_id=account_id)) + db.session.commit() From 26413264327b7b774256332ad6b7cae467693154 Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Sat, 11 Oct 2025 10:45:50 +0800 Subject: [PATCH 10/32] fix --- api/controllers/console/explore/trial.py | 93 +++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/api/controllers/console/explore/trial.py b/api/controllers/console/explore/trial.py index eaa5c74739..27b36d32ee 100644 --- a/api/controllers/console/explore/trial.py +++ b/api/controllers/console/explore/trial.py @@ -3,7 +3,7 @@ import logging from flask import request from flask_restx import Resource, marshal_with, reqparse from werkzeug.exceptions import Forbidden, InternalServerError, NotFound - +from controllers.console import api import services from controllers.common import fields from controllers.common.fields import build_site_model @@ -24,11 +24,13 @@ from controllers.console.explore.error import ( AppSuggestedQuestionsAfterAnswerDisabledError, NotChatAppError, NotCompletionAppError, + NotWorkflowAppError, ) from controllers.console.explore.wraps import TrialAppResource, trial_feature_enable from controllers.service_api import service_api_ns from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict +from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.entities.app_invoke_entities import InvokeFrom from core.errors.error import ( ModelCurrentlyNotSupportError, @@ -36,6 +38,7 @@ from core.errors.error import ( QuotaExceededError, ) from core.model_runtime.errors.invoke import InvokeError +from core.workflow.graph_engine.manager import GraphEngineManager from extensions.ext_database import db from fields.app_fields import app_detail_fields_with_site from libs import helper @@ -65,6 +68,71 @@ from services.recommended_app_service import RecommendedAppService logger = logging.getLogger(__name__) +class TrialAppWorkflowRunApi(TrialAppResource): + def post(self, trial_app): + """ + Run workflow + """ + app_model = trial_app + if not app_model: + raise NotWorkflowAppError() + app_mode = AppMode.value_of(app_model.mode) + if app_mode != AppMode.WORKFLOW: + raise NotWorkflowAppError() + + parser = reqparse.RequestParser() + parser.add_argument("inputs", type=dict, required=True, nullable=False, location="json") + parser.add_argument("files", type=list, required=False, location="json") + args = parser.parse_args() + assert current_user is not None + try: + app_id = app_model.id + user_id = current_user.id + response = AppGenerateService.generate( + app_model=app_model, user=current_user, args=args, invoke_from=InvokeFrom.EXPLORE, streaming=True + ) + RecommendedAppService.add_trial_app_record(app_id, user_id) + return helper.compact_generate_response(response) + except ProviderTokenNotInitError as ex: + raise ProviderNotInitializeError(ex.description) + except QuotaExceededError: + raise ProviderQuotaExceededError() + except ModelCurrentlyNotSupportError: + raise ProviderModelCurrentlyNotSupportError() + except InvokeError as e: + raise CompletionRequestError(e.description) + except InvokeRateLimitError as ex: + raise InvokeRateLimitHttpError(ex.description) + except ValueError as e: + raise e + except Exception: + logger.exception("internal server error.") + raise InternalServerError() + + +class TrialAppWorkflowTaskStopApi(TrialAppResource): + def post(self, trial_app, task_id: str): + """ + Stop workflow task + """ + app_model = trial_app + if not app_model: + raise NotWorkflowAppError() + app_mode = AppMode.value_of(app_model.mode) + if app_mode != AppMode.WORKFLOW: + raise NotWorkflowAppError() + assert current_user is not None + + # Stop using both mechanisms for backward compatibility + # Legacy stop flag mechanism (without user check) + AppQueueManager.set_stop_flag_no_user_check(task_id) + + # New graph engine command channel mechanism + GraphEngineManager.send_stop_command(task_id) + + return {"result": "success"} + + class TrialChatApi(TrialAppResource): @trial_feature_enable def post(self, trial_app): @@ -372,3 +440,26 @@ class AppApi(Resource): app_model = app_service.get_app(app_model) return app_model + + +api.add_resource(TrialChatApi, "/trial-apps//chat-messages", endpoint="trial_app_chat_completion") + +api.add_resource( + TrialMessageSuggestedQuestionApi, + "/trial-apps//messages//suggested-questions", + endpoint="trial_app_suggested_question", +) + +api.add_resource(TrialChatAudioApi, "/trial-apps//audio-to-text", endpoint="trial_app_audio") +api.add_resource(TrialChatTextApi, "/trial-apps//text-to-audio", endpoint="trial_app_text") + +api.add_resource(TrialCompletionApi, "/trial-apps//completion-messages", endpoint="trial_app_completion") + +api.add_resource(TrialSitApi, "/trial-apps//site") + +api.add_resource(TrialAppParameterApi, "/trial-apps//parameters", endpoint="trial_app_parameters") + +api.add_resource(AppApi, "/trial-apps/", endpoint="trial_app") + +api.add_resource(TrialAppWorkflowRunApi, "/trial-apps//workflows/run", endpoint="trial_app_workflow_run") +api.add_resource(TrialAppWorkflowTaskStopApi, "/trial-apps//workflows/tasks//stop") From d12015c722e73dbd2ec51bef07f42394ec7ea617 Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Sat, 11 Oct 2025 10:50:32 +0800 Subject: [PATCH 11/32] fix --- api/controllers/console/explore/trial.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/controllers/console/explore/trial.py b/api/controllers/console/explore/trial.py index 27b36d32ee..c3acd9db06 100644 --- a/api/controllers/console/explore/trial.py +++ b/api/controllers/console/explore/trial.py @@ -3,10 +3,11 @@ import logging from flask import request from flask_restx import Resource, marshal_with, reqparse from werkzeug.exceptions import Forbidden, InternalServerError, NotFound -from controllers.console import api + import services from controllers.common import fields from controllers.common.fields import build_site_model +from controllers.console import api from controllers.console.app.error import ( AppUnavailableError, AudioTooLargeError, From aadac22ce45c24877939ac21b0057d78a60bbc4c Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Sat, 11 Oct 2025 14:52:34 +0800 Subject: [PATCH 12/32] add: language for banner --- api/controllers/console/admin.py | 20 ++++++++++++++-- api/controllers/console/explore/banner.py | 6 ++++- ...c2f_add_table_explore_banner_and_trial.py} | 23 +++++++++++++++---- api/models/model.py | 1 + 4 files changed, 42 insertions(+), 8 deletions(-) rename api/migrations/versions/{2025_09_19_1442-1b435d90db42_add_table_explore_banner_and_trial.py => 2025_10_11_1442-3993fd9e9c2f_add_table_explore_banner_and_trial.py} (82%) diff --git a/api/controllers/console/admin.py b/api/controllers/console/admin.py index 8d021e0f9d..3119cb65e0 100644 --- a/api/controllers/console/admin.py +++ b/api/controllers/console/admin.py @@ -230,16 +230,32 @@ class InsertExploreBanner(Resource): @only_edition_cloud def post(self): parser = reqparse.RequestParser() - parser.add_argument("content", type=str, required=True, nullable=False, location="json") + parser.add_argument("category", type=str, required=True, nullable=False, location="json") + parser.add_argument("title", type=str, required=True, nullable=False, location="json") + parser.add_argument("description", type=str, required=True, nullable=False, location="json") + parser.add_argument("img-src", type=str, required=True, nullable=False, location="json") + + parser.add_argument("language", type=str, required=True, nullable=False, location="json") parser.add_argument("link", type=str, required=True, nullable=False, location="json") parser.add_argument("sort", type=int, required=True, nullable=False, location="json") args = parser.parse_args() + content = { + "category": args["category"], + "title": args["title"], + "description": args["description"], + "img-src": args["img-src"], + } + + if not args["language"]: + args["language"] = "en-US" + banner = ExporleBanner( - content=args["content"], + content=content, link=args["link"], sort=args["sort"], + language=args["language"], ) db.session.add(banner) db.session.commit() diff --git a/api/controllers/console/explore/banner.py b/api/controllers/console/explore/banner.py index 813644d149..0daf34d01d 100644 --- a/api/controllers/console/explore/banner.py +++ b/api/controllers/console/explore/banner.py @@ -1,3 +1,4 @@ +from flask import request from flask_restx import Resource from controllers.console import api @@ -12,8 +13,11 @@ class BannerApi(Resource): @explore_banner_enabled def get(self): """Get banner list.""" + language = request.args.get("language", "en-US") + banners = ( - db.session.query(ExporleBanner).where(ExporleBanner.status == "enabled").order_by(ExporleBanner.sort).all() + db.session.query(ExporleBanner).where(ExporleBanner.status == "enabled", + ExporleBanner.language == language).order_by(ExporleBanner.sort).all() ) # Convert banners to serializable format diff --git a/api/migrations/versions/2025_09_19_1442-1b435d90db42_add_table_explore_banner_and_trial.py b/api/migrations/versions/2025_10_11_1442-3993fd9e9c2f_add_table_explore_banner_and_trial.py similarity index 82% rename from api/migrations/versions/2025_09_19_1442-1b435d90db42_add_table_explore_banner_and_trial.py rename to api/migrations/versions/2025_10_11_1442-3993fd9e9c2f_add_table_explore_banner_and_trial.py index 6d20273c5d..46267992f4 100644 --- a/api/migrations/versions/2025_09_19_1442-1b435d90db42_add_table_explore_banner_and_trial.py +++ b/api/migrations/versions/2025_10_11_1442-3993fd9e9c2f_add_table_explore_banner_and_trial.py @@ -1,8 +1,8 @@ """add table explore banner and trial -Revision ID: 1b435d90db42 -Revises: cf7c38a32b2d -Create Date: 2025-09-19 14:42:58.416649 +Revision ID: 3993fd9e9c2f +Revises: 68519ad5cd18 +Create Date: 2025-10-11 14:42:01.954865 """ from alembic import op @@ -11,8 +11,8 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '1b435d90db42' -down_revision = 'cf7c38a32b2d' +revision = '3993fd9e9c2f' +down_revision = '68519ad5cd18' branch_labels = None depends_on = None @@ -39,6 +39,7 @@ def upgrade(): sa.Column('sort', sa.Integer(), nullable=False), sa.Column('status', sa.String(length=255), server_default=sa.text("'enabled'::character varying"), nullable=False), sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('language', sa.String(length=255), server_default=sa.text("'en-US'::character varying"), nullable=False), sa.PrimaryKeyConstraint('id', name='exporler_banner_pkey') ) op.create_table('trial_apps', @@ -54,6 +55,12 @@ def upgrade(): batch_op.create_index('trial_app_app_id_idx', ['app_id'], unique=False) batch_op.create_index('trial_app_tenant_id_idx', ['tenant_id'], unique=False) + with op.batch_alter_table('datasource_providers', schema=None) as batch_op: + batch_op.alter_column('avatar_url', + existing_type=sa.TEXT(), + type_=sa.String(length=255), + existing_nullable=True) + with op.batch_alter_table('providers', schema=None) as batch_op: batch_op.drop_column('credential_status') @@ -65,6 +72,12 @@ def downgrade(): with op.batch_alter_table('providers', schema=None) as batch_op: batch_op.add_column(sa.Column('credential_status', sa.VARCHAR(length=20), server_default=sa.text("'active'::character varying"), autoincrement=False, nullable=True)) + with op.batch_alter_table('datasource_providers', schema=None) as batch_op: + batch_op.alter_column('avatar_url', + existing_type=sa.String(length=255), + type_=sa.TEXT(), + existing_nullable=True) + with op.batch_alter_table('trial_apps', schema=None) as batch_op: batch_op.drop_index('trial_app_tenant_id_idx') batch_op.drop_index('trial_app_app_id_idx') diff --git a/api/models/model.py b/api/models/model.py index c15c86c569..9b2c40df46 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -636,6 +636,7 @@ class ExporleBanner(Base): sort = mapped_column(sa.Integer, nullable=False) status = mapped_column(sa.String(255), nullable=False, server_default=sa.text("'enabled'::character varying")) created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) + language = mapped_column(String(255), nullable=False, server_default=sa.text("'en-US'::character varying")) class OAuthProviderApp(Base): From e69b588bad282e0656a93f4f1308f8d7eab6d0d9 Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Sat, 11 Oct 2025 15:05:34 +0800 Subject: [PATCH 13/32] add: language for banner --- ..._1442-3993fd9e9c2f_add_table_explore_banner_and_trial.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/api/migrations/versions/2025_10_11_1442-3993fd9e9c2f_add_table_explore_banner_and_trial.py b/api/migrations/versions/2025_10_11_1442-3993fd9e9c2f_add_table_explore_banner_and_trial.py index 46267992f4..4d7e27acdc 100644 --- a/api/migrations/versions/2025_10_11_1442-3993fd9e9c2f_add_table_explore_banner_and_trial.py +++ b/api/migrations/versions/2025_10_11_1442-3993fd9e9c2f_add_table_explore_banner_and_trial.py @@ -61,17 +61,11 @@ def upgrade(): type_=sa.String(length=255), existing_nullable=True) - with op.batch_alter_table('providers', schema=None) as batch_op: - batch_op.drop_column('credential_status') - # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('providers', schema=None) as batch_op: - batch_op.add_column(sa.Column('credential_status', sa.VARCHAR(length=20), server_default=sa.text("'active'::character varying"), autoincrement=False, nullable=True)) - with op.batch_alter_table('datasource_providers', schema=None) as batch_op: batch_op.alter_column('avatar_url', existing_type=sa.String(length=255), From 7ba9d30775484f3334147cf9bbb783c850e59101 Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Sat, 11 Oct 2025 15:35:26 +0800 Subject: [PATCH 14/32] When there is no content in a certain language, it needs to fallback to English --- api/controllers/console/explore/banner.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/api/controllers/console/explore/banner.py b/api/controllers/console/explore/banner.py index 0daf34d01d..6a57e4d77b 100644 --- a/api/controllers/console/explore/banner.py +++ b/api/controllers/console/explore/banner.py @@ -15,11 +15,15 @@ class BannerApi(Resource): """Get banner list.""" language = request.args.get("language", "en-US") - banners = ( - db.session.query(ExporleBanner).where(ExporleBanner.status == "enabled", - ExporleBanner.language == language).order_by(ExporleBanner.sort).all() - ) - + # Build base query for enabled banners + base_query = db.session.query(ExporleBanner).where(ExporleBanner.status == "enabled") + + # Try to get banners in the requested language + banners = base_query.where(ExporleBanner.language == language).order_by(ExporleBanner.sort).all() + + # Fallback to en-US if no banners found and language is not en-US + if not banners and language != "en-US": + banners = base_query.where(ExporleBanner.language == "en-US").order_by(ExporleBanner.sort).all() # Convert banners to serializable format result = [] for banner in banners: From b5fb55069b7eb6302957124d7ff8f809f16a81f0 Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Mon, 13 Oct 2025 11:04:21 +0800 Subject: [PATCH 15/32] add: return id for banner list --- api/controllers/console/explore/banner.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/controllers/console/explore/banner.py b/api/controllers/console/explore/banner.py index 6a57e4d77b..e44f8f8930 100644 --- a/api/controllers/console/explore/banner.py +++ b/api/controllers/console/explore/banner.py @@ -28,6 +28,7 @@ class BannerApi(Resource): result = [] for banner in banners: banner_data = { + "id": banner.id, "content": banner.content, # Already parsed as JSON by SQLAlchemy "link": banner.link, "sort": banner.sort, From 2f456736948c7af0cbcacfc4d84fec10904be4be Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Mon, 13 Oct 2025 14:53:20 +0800 Subject: [PATCH 16/32] fix: linter --- api/controllers/console/explore/banner.py | 4 ++-- api/controllers/console/explore/trial.py | 16 ++++++++-------- api/controllers/console/explore/wraps.py | 2 ++ api/services/recommended_app_service.py | 9 +++++---- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/api/controllers/console/explore/banner.py b/api/controllers/console/explore/banner.py index e44f8f8930..da306fbc9d 100644 --- a/api/controllers/console/explore/banner.py +++ b/api/controllers/console/explore/banner.py @@ -17,10 +17,10 @@ class BannerApi(Resource): # Build base query for enabled banners base_query = db.session.query(ExporleBanner).where(ExporleBanner.status == "enabled") - + # Try to get banners in the requested language banners = base_query.where(ExporleBanner.language == language).order_by(ExporleBanner.sort).all() - + # Fallback to en-US if no banners found and language is not en-US if not banners and language != "en-US": banners = base_query.where(ExporleBanner.language == "en-US").order_by(ExporleBanner.sort).all() diff --git a/api/controllers/console/explore/trial.py b/api/controllers/console/explore/trial.py index c3acd9db06..afa3dedf43 100644 --- a/api/controllers/console/explore/trial.py +++ b/api/controllers/console/explore/trial.py @@ -156,11 +156,11 @@ class TrialChatApi(TrialAppResource): try: if not isinstance(current_user, Account): raise ValueError("current_user must be an Account instance") - + # Get IDs before they might be detached from session app_id = app_model.id user_id = current_user.id - + response = AppGenerateService.generate( app_model=app_model, user=current_user, args=args, invoke_from=InvokeFrom.EXPLORE, streaming=True ) @@ -237,11 +237,11 @@ class TrialChatAudioApi(TrialAppResource): try: if not isinstance(current_user, Account): raise ValueError("current_user must be an Account instance") - + # Get IDs before they might be detached from session app_id = app_model.id user_id = current_user.id - + response = AudioService.transcript_asr(app_model=app_model, file=file, end_user=None) RecommendedAppService.add_trial_app_record(app_id, user_id) return response @@ -288,11 +288,11 @@ class TrialChatTextApi(TrialAppResource): voice = args.get("voice", None) if not isinstance(current_user, Account): raise ValueError("current_user must be an Account instance") - + # Get IDs before they might be detached from session app_id = app_model.id user_id = current_user.id - + response = AudioService.transcript_tts(app_model=app_model, text=text, voice=voice, message_id=message_id) RecommendedAppService.add_trial_app_record(app_id, user_id) return response @@ -343,11 +343,11 @@ class TrialCompletionApi(TrialAppResource): try: if not isinstance(current_user, Account): raise ValueError("current_user must be an Account instance") - + # Get IDs before they might be detached from session app_id = app_model.id user_id = current_user.id - + response = AppGenerateService.generate( app_model=app_model, user=current_user, args=args, invoke_from=InvokeFrom.EXPLORE, streaming=streaming ) diff --git a/api/controllers/console/explore/wraps.py b/api/controllers/console/explore/wraps.py index 150dd40b80..008a47028f 100644 --- a/api/controllers/console/explore/wraps.py +++ b/api/controllers/console/explore/wraps.py @@ -87,6 +87,8 @@ def trial_app_required(view: Callable[Concatenate[App, P], R] | None = None): if app is None: raise TrialAppNotAllowed() + assert isinstance(current_user, Account) + account_trial_app_record = ( db.session.query(AccountTrialAppRecord) .where(AccountTrialAppRecord.account_id == current_user.id, AccountTrialAppRecord.app_id == app_id) diff --git a/api/services/recommended_app_service.py b/api/services/recommended_app_service.py index be6e94a526..6b211a5632 100644 --- a/api/services/recommended_app_service.py +++ b/api/services/recommended_app_service.py @@ -60,10 +60,11 @@ class RecommendedAppService: :param app_id: app id :return: """ - account_trial_app_record = db.session.query(AccountTrialAppRecord).where( - AccountTrialAppRecord.app_id == app_id, - AccountTrialAppRecord.account_id == account_id - ).first() + account_trial_app_record = ( + db.session.query(AccountTrialAppRecord) + .where(AccountTrialAppRecord.app_id == app_id, AccountTrialAppRecord.account_id == account_id) + .first() + ) if account_trial_app_record: account_trial_app_record.count += 1 db.session.commit() From 50bdbfae69be5d5b285b94036194d563aa0c24da Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Mon, 13 Oct 2025 15:12:44 +0800 Subject: [PATCH 17/32] fix: get app model without check tenant in trial --- api/controllers/console/app/wraps.py | 49 ++++++++++++++++++++++++ api/controllers/console/explore/trial.py | 8 ++-- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/api/controllers/console/app/wraps.py b/api/controllers/console/app/wraps.py index 9bb2718f89..b14280c844 100644 --- a/api/controllers/console/app/wraps.py +++ b/api/controllers/console/app/wraps.py @@ -22,6 +22,14 @@ def _load_app_model(app_id: str) -> App | None: ) return app_model +def _load_app_model_with_trial(app_id: str) -> App | None: + assert isinstance(current_user, Account) + app_model = ( + db.session.query(App) + .where(App.id == app_id, App.status == "normal") + .first() + ) + return app_model def get_app_model(view: Callable[P, R] | None = None, *, mode: Union[AppMode, list[AppMode], None] = None): def decorator(view_func: Callable[P1, R1]): @@ -62,3 +70,44 @@ def get_app_model(view: Callable[P, R] | None = None, *, mode: Union[AppMode, li return decorator else: return decorator(view) + + +def get_app_model_with_trial(view: Callable[P, R] | None = None, *, mode: Union[AppMode, list[AppMode], None] = None): + def decorator(view_func: Callable[P, R]): + @wraps(view_func) + def decorated_view(*args: P.args, **kwargs: P.kwargs): + if not kwargs.get("app_id"): + raise ValueError("missing app_id in path parameters") + + app_id = kwargs.get("app_id") + app_id = str(app_id) + + del kwargs["app_id"] + + app_model = _load_app_model_with_trial(app_id) + + if not app_model: + raise AppNotFoundError() + + app_mode = AppMode.value_of(app_model.mode) + + if mode is not None: + if isinstance(mode, list): + modes = mode + else: + modes = [mode] + + if app_mode not in modes: + mode_values = {m.value for m in modes} + raise AppNotFoundError(f"App mode is not in the supported list: {mode_values}") + + kwargs["app_model"] = app_model + + return view_func(*args, **kwargs) + + return decorated_view + + if view is None: + return decorator + else: + return decorator(view) \ No newline at end of file diff --git a/api/controllers/console/explore/trial.py b/api/controllers/console/explore/trial.py index afa3dedf43..cd45ae5645 100644 --- a/api/controllers/console/explore/trial.py +++ b/api/controllers/console/explore/trial.py @@ -20,7 +20,7 @@ from controllers.console.app.error import ( ProviderQuotaExceededError, UnsupportedAudioTypeError, ) -from controllers.console.app.wraps import get_app_model +from controllers.console.app.wraps import get_app_model_with_trial from controllers.console.explore.error import ( AppSuggestedQuestionsAfterAnswerDisabledError, NotChatAppError, @@ -380,7 +380,7 @@ class TrialSitApi(Resource): """Resource for trial app sites.""" @trial_feature_enable - @get_app_model + @get_app_model_with_trial @service_api_ns.marshal_with(build_site_model(service_api_ns)) def get(self, app_model): """Retrieve app site info. @@ -403,7 +403,7 @@ class TrialAppParameterApi(Resource): """Resource for app variables.""" @trial_feature_enable - @get_app_model + @get_app_model_with_trial @marshal_with(fields.parameters_fields) def get(self, app_model): """Retrieve app parameters.""" @@ -432,7 +432,7 @@ class TrialAppParameterApi(Resource): class AppApi(Resource): @trial_feature_enable - @get_app_model + @get_app_model_with_trial @marshal_with(app_detail_fields_with_site) def get(self, app_model): """Get app detail""" From cc349e70b16730ee7a5046ba930824ffe6fd253a Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Mon, 13 Oct 2025 15:13:07 +0800 Subject: [PATCH 18/32] fix: get app model without check tenant in trial --- api/controllers/console/app/wraps.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/controllers/console/app/wraps.py b/api/controllers/console/app/wraps.py index b14280c844..01f41d6222 100644 --- a/api/controllers/console/app/wraps.py +++ b/api/controllers/console/app/wraps.py @@ -22,6 +22,7 @@ def _load_app_model(app_id: str) -> App | None: ) return app_model + def _load_app_model_with_trial(app_id: str) -> App | None: assert isinstance(current_user, Account) app_model = ( @@ -31,6 +32,7 @@ def _load_app_model_with_trial(app_id: str) -> App | None: ) return app_model + def get_app_model(view: Callable[P, R] | None = None, *, mode: Union[AppMode, list[AppMode], None] = None): def decorator(view_func: Callable[P1, R1]): @wraps(view_func) From 04196288f82deb281baa981cb317981e08fab94d Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Tue, 14 Oct 2025 11:35:20 +0800 Subject: [PATCH 19/32] fix --- api/controllers/console/app/error.py | 5 +++ api/controllers/console/explore/trial.py | 45 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/api/controllers/console/app/error.py b/api/controllers/console/app/error.py index fbd7901646..5fde51cfb9 100644 --- a/api/controllers/console/app/error.py +++ b/api/controllers/console/app/error.py @@ -115,3 +115,8 @@ class InvokeRateLimitError(BaseHTTPException): error_code = "rate_limit_error" description = "Rate Limit Error" code = 429 + +class NeedAddIdsError(BaseHTTPException): + error_code = "need_add_ids" + description = "Need to add ids." + code = 400 \ No newline at end of file diff --git a/api/controllers/console/explore/trial.py b/api/controllers/console/explore/trial.py index cd45ae5645..e8f226f1d3 100644 --- a/api/controllers/console/explore/trial.py +++ b/api/controllers/console/explore/trial.py @@ -18,6 +18,7 @@ from controllers.console.app.error import ( ProviderNotInitializeError, ProviderNotSupportSpeechToTextError, ProviderQuotaExceededError, + NeedAddIdsError, UnsupportedAudioTypeError, ) from controllers.console.app.wraps import get_app_model_with_trial @@ -42,6 +43,7 @@ from core.model_runtime.errors.invoke import InvokeError from core.workflow.graph_engine.manager import GraphEngineManager from extensions.ext_database import db from fields.app_fields import app_detail_fields_with_site +from fields.workflow_fields import workflow_fields from libs import helper from libs.helper import uuid_value from libs.login import current_user @@ -65,6 +67,11 @@ from services.errors.message import ( ) from services.message_service import MessageService from services.recommended_app_service import RecommendedAppService +from models.workflow import Workflow +from services.dataset_service import DatasetService +from fields.dataset_fields import dataset_fields +from flask_restx import marshal +from typing import Any, cast logger = logging.getLogger(__name__) @@ -442,6 +449,41 @@ class AppApi(Resource): return app_model +class AppWorkflowApi(Resource): + @trial_feature_enable + @get_app_model_with_trial + @marshal_with(workflow_fields) + def get(self, app_model): + """Get workflow detail""" + if not app_model.workflow_id: + raise AppUnavailableError() + + workflow = ( + db.session.query(Workflow) + .where( + Workflow.id == app_model.workflow_id, + ) + .first() + ) + return workflow + +class DatasetListApi(Resource): + def get(self, app_model): + page = request.args.get("page", default=1, type=int) + limit = request.args.get("limit", default=20, type=int) + ids = request.args.getlist("ids") + + tenant_id = app_model.tenant_id + if ids: + datasets, total = DatasetService.get_datasets_by_ids(ids, tenant_id) + else: + raise NeedAddIdsError() + + + data = cast(list[dict[str, Any]], marshal(datasets, dataset_fields)) + + response = {"data": data, "has_more": len(datasets) == limit, "limit": limit, "total": total, "page": page} + return response api.add_resource(TrialChatApi, "/trial-apps//chat-messages", endpoint="trial_app_chat_completion") @@ -464,3 +506,6 @@ api.add_resource(AppApi, "/trial-apps/", endpoint="trial_app") api.add_resource(TrialAppWorkflowRunApi, "/trial-apps//workflows/run", endpoint="trial_app_workflow_run") api.add_resource(TrialAppWorkflowTaskStopApi, "/trial-apps//workflows/tasks//stop") + +api.add_resource(AppWorkflowApi, "/trial-apps//workflows", endpoint="trial_app_workflow") +api.add_resource(DatasetListApi, "/trial-apps//datasets", endpoint="trial_app_datasets") From b483d5fad52eb07b4f62278593b7143d05a761c9 Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Tue, 14 Oct 2025 11:37:27 +0800 Subject: [PATCH 20/32] fix --- api/controllers/console/app/error.py | 1 + api/controllers/console/explore/trial.py | 17 +++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/api/controllers/console/app/error.py b/api/controllers/console/app/error.py index 5fde51cfb9..46185c007b 100644 --- a/api/controllers/console/app/error.py +++ b/api/controllers/console/app/error.py @@ -116,6 +116,7 @@ class InvokeRateLimitError(BaseHTTPException): description = "Rate Limit Error" code = 429 + class NeedAddIdsError(BaseHTTPException): error_code = "need_add_ids" description = "Need to add ids." diff --git a/api/controllers/console/explore/trial.py b/api/controllers/console/explore/trial.py index e8f226f1d3..aa372619fb 100644 --- a/api/controllers/console/explore/trial.py +++ b/api/controllers/console/explore/trial.py @@ -1,7 +1,8 @@ import logging +from typing import Any, cast from flask import request -from flask_restx import Resource, marshal_with, reqparse +from flask_restx import Resource, marshal, marshal_with, reqparse from werkzeug.exceptions import Forbidden, InternalServerError, NotFound import services @@ -13,12 +14,12 @@ from controllers.console.app.error import ( AudioTooLargeError, CompletionRequestError, ConversationCompletedError, + NeedAddIdsError, NoAudioUploadedError, ProviderModelCurrentlyNotSupportError, ProviderNotInitializeError, ProviderNotSupportSpeechToTextError, ProviderQuotaExceededError, - NeedAddIdsError, UnsupportedAudioTypeError, ) from controllers.console.app.wraps import get_app_model_with_trial @@ -43,6 +44,7 @@ from core.model_runtime.errors.invoke import InvokeError from core.workflow.graph_engine.manager import GraphEngineManager from extensions.ext_database import db from fields.app_fields import app_detail_fields_with_site +from fields.dataset_fields import dataset_fields from fields.workflow_fields import workflow_fields from libs import helper from libs.helper import uuid_value @@ -50,9 +52,11 @@ from libs.login import current_user from models import Account from models.account import TenantStatus from models.model import AppMode, Site +from models.workflow import Workflow from services.app_generate_service import AppGenerateService from services.app_service import AppService from services.audio_service import AudioService +from services.dataset_service import DatasetService from services.errors.audio import ( AudioTooLargeServiceError, NoAudioUploadedServiceError, @@ -67,11 +71,6 @@ from services.errors.message import ( ) from services.message_service import MessageService from services.recommended_app_service import RecommendedAppService -from models.workflow import Workflow -from services.dataset_service import DatasetService -from fields.dataset_fields import dataset_fields -from flask_restx import marshal -from typing import Any, cast logger = logging.getLogger(__name__) @@ -449,6 +448,7 @@ class AppApi(Resource): return app_model + class AppWorkflowApi(Resource): @trial_feature_enable @get_app_model_with_trial @@ -467,6 +467,7 @@ class AppWorkflowApi(Resource): ) return workflow + class DatasetListApi(Resource): def get(self, app_model): page = request.args.get("page", default=1, type=int) @@ -479,12 +480,12 @@ class DatasetListApi(Resource): else: raise NeedAddIdsError() - data = cast(list[dict[str, Any]], marshal(datasets, dataset_fields)) response = {"data": data, "has_more": len(datasets) == limit, "limit": limit, "total": total, "page": page} return response + api.add_resource(TrialChatApi, "/trial-apps//chat-messages", endpoint="trial_app_chat_completion") api.add_resource( From 5e2b0d7b39fc78387482d58777d26131f13a9a8c Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Tue, 14 Oct 2025 11:35:59 +0800 Subject: [PATCH 21/32] add interface for review app --- api/controllers/console/explore/trial.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/controllers/console/explore/trial.py b/api/controllers/console/explore/trial.py index aa372619fb..592b770664 100644 --- a/api/controllers/console/explore/trial.py +++ b/api/controllers/console/explore/trial.py @@ -469,6 +469,8 @@ class AppWorkflowApi(Resource): class DatasetListApi(Resource): + @trial_feature_enable + @get_app_model_with_trial def get(self, app_model): page = request.args.get("page", default=1, type=int) limit = request.args.get("limit", default=20, type=int) From b8a29bfb358bdeefe2c7cd25622231a47a111c48 Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Tue, 14 Oct 2025 11:39:56 +0800 Subject: [PATCH 22/32] fix linter --- api/controllers/console/app/error.py | 2 +- api/controllers/console/app/wraps.py | 8 ++------ api/controllers/console/explore/trial.py | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/api/controllers/console/app/error.py b/api/controllers/console/app/error.py index 46185c007b..6b4bd6755a 100644 --- a/api/controllers/console/app/error.py +++ b/api/controllers/console/app/error.py @@ -120,4 +120,4 @@ class InvokeRateLimitError(BaseHTTPException): class NeedAddIdsError(BaseHTTPException): error_code = "need_add_ids" description = "Need to add ids." - code = 400 \ No newline at end of file + code = 400 diff --git a/api/controllers/console/app/wraps.py b/api/controllers/console/app/wraps.py index 01f41d6222..2f0e16ac79 100644 --- a/api/controllers/console/app/wraps.py +++ b/api/controllers/console/app/wraps.py @@ -25,11 +25,7 @@ def _load_app_model(app_id: str) -> App | None: def _load_app_model_with_trial(app_id: str) -> App | None: assert isinstance(current_user, Account) - app_model = ( - db.session.query(App) - .where(App.id == app_id, App.status == "normal") - .first() - ) + app_model = db.session.query(App).where(App.id == app_id, App.status == "normal").first() return app_model @@ -112,4 +108,4 @@ def get_app_model_with_trial(view: Callable[P, R] | None = None, *, mode: Union[ if view is None: return decorator else: - return decorator(view) \ No newline at end of file + return decorator(view) diff --git a/api/controllers/console/explore/trial.py b/api/controllers/console/explore/trial.py index 592b770664..eb3c22cd0c 100644 --- a/api/controllers/console/explore/trial.py +++ b/api/controllers/console/explore/trial.py @@ -475,7 +475,7 @@ class DatasetListApi(Resource): page = request.args.get("page", default=1, type=int) limit = request.args.get("limit", default=20, type=int) ids = request.args.getlist("ids") - + tenant_id = app_model.tenant_id if ids: datasets, total = DatasetService.get_datasets_by_ids(ids, tenant_id) From 04f9637b6fcfb8183708bb9c11fa3988550c103a Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Thu, 23 Oct 2025 11:11:35 +0800 Subject: [PATCH 23/32] mr main and rebuild migration --- ...7f9_add_table_explore_banner_and_trial.py} | 25 +++++-------------- 1 file changed, 6 insertions(+), 19 deletions(-) rename api/migrations/versions/{2025_10_11_1442-3993fd9e9c2f_add_table_explore_banner_and_trial.py => 2025_10_23_1110-f9f6d18a37f9_add_table_explore_banner_and_trial.py} (84%) diff --git a/api/migrations/versions/2025_10_11_1442-3993fd9e9c2f_add_table_explore_banner_and_trial.py b/api/migrations/versions/2025_10_23_1110-f9f6d18a37f9_add_table_explore_banner_and_trial.py similarity index 84% rename from api/migrations/versions/2025_10_11_1442-3993fd9e9c2f_add_table_explore_banner_and_trial.py rename to api/migrations/versions/2025_10_23_1110-f9f6d18a37f9_add_table_explore_banner_and_trial.py index 4d7e27acdc..ea2145c2d5 100644 --- a/api/migrations/versions/2025_10_11_1442-3993fd9e9c2f_add_table_explore_banner_and_trial.py +++ b/api/migrations/versions/2025_10_23_1110-f9f6d18a37f9_add_table_explore_banner_and_trial.py @@ -1,18 +1,18 @@ """add table explore banner and trial -Revision ID: 3993fd9e9c2f -Revises: 68519ad5cd18 -Create Date: 2025-10-11 14:42:01.954865 +Revision ID: f9f6d18a37f9 +Revises: ae662b25d9bc +Create Date: 2025-10-23 11:10:18.079355 """ from alembic import op import models as models import sqlalchemy as sa - +from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. -revision = '3993fd9e9c2f' -down_revision = '68519ad5cd18' +revision = 'f9f6d18a37f9' +down_revision = 'ae662b25d9bc' branch_labels = None depends_on = None @@ -54,24 +54,11 @@ def upgrade(): with op.batch_alter_table('trial_apps', schema=None) as batch_op: batch_op.create_index('trial_app_app_id_idx', ['app_id'], unique=False) batch_op.create_index('trial_app_tenant_id_idx', ['tenant_id'], unique=False) - - with op.batch_alter_table('datasource_providers', schema=None) as batch_op: - batch_op.alter_column('avatar_url', - existing_type=sa.TEXT(), - type_=sa.String(length=255), - existing_nullable=True) - # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('datasource_providers', schema=None) as batch_op: - batch_op.alter_column('avatar_url', - existing_type=sa.String(length=255), - type_=sa.TEXT(), - existing_nullable=True) - with op.batch_alter_table('trial_apps', schema=None) as batch_op: batch_op.drop_index('trial_app_tenant_id_idx') batch_op.drop_index('trial_app_app_id_idx') From cd9e28dbf461ff0d2b6da645541232e5cdae3e73 Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Thu, 23 Oct 2025 11:11:53 +0800 Subject: [PATCH 24/32] mr main and rebuild migration --- api/controllers/console/explore/wraps.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/api/controllers/console/explore/wraps.py b/api/controllers/console/explore/wraps.py index 008a47028f..475a617aae 100644 --- a/api/controllers/console/explore/wraps.py +++ b/api/controllers/console/explore/wraps.py @@ -9,10 +9,8 @@ from werkzeug.exceptions import NotFound from controllers.console.explore.error import AppAccessDeniedError, TrialAppLimitExceeded, TrialAppNotAllowed from controllers.console.wraps import account_initialization_required from extensions.ext_database import db -from libs.login import current_account_with_tenant, login_required -from models import InstalledApp +from libs.login import current_account_with_tenant, current_user, login_required from models import AccountTrialAppRecord, App, InstalledApp, TrialApp -from libs.login import current_user from services.enterprise.enterprise_service import EnterpriseService from services.feature_service import FeatureService From b22c28b0991a7eb8c91ac3c68c20bac8cad947a8 Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Thu, 23 Oct 2025 11:14:17 +0800 Subject: [PATCH 25/32] mr main and rebuild migration --- api/controllers/console/app/wraps.py | 1 - api/controllers/console/explore/wraps.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/api/controllers/console/app/wraps.py b/api/controllers/console/app/wraps.py index 2f0e16ac79..e687d980fa 100644 --- a/api/controllers/console/app/wraps.py +++ b/api/controllers/console/app/wraps.py @@ -24,7 +24,6 @@ def _load_app_model(app_id: str) -> App | None: def _load_app_model_with_trial(app_id: str) -> App | None: - assert isinstance(current_user, Account) app_model = db.session.query(App).where(App.id == app_id, App.status == "normal").first() return app_model diff --git a/api/controllers/console/explore/wraps.py b/api/controllers/console/explore/wraps.py index 475a617aae..0f2f8d6dc9 100644 --- a/api/controllers/console/explore/wraps.py +++ b/api/controllers/console/explore/wraps.py @@ -76,6 +76,8 @@ def trial_app_required(view: Callable[Concatenate[App, P], R] | None = None): def decorator(view: Callable[Concatenate[App, P], R]): @wraps(view) def decorated(app_id: str, *args: P.args, **kwargs: P.kwargs): + current_user, _ = current_account_with_tenant() + trial_app = db.session.query(TrialApp).where(TrialApp.app_id == str(app_id)).first() if trial_app is None: @@ -85,8 +87,6 @@ def trial_app_required(view: Callable[Concatenate[App, P], R] | None = None): if app is None: raise TrialAppNotAllowed() - assert isinstance(current_user, Account) - account_trial_app_record = ( db.session.query(AccountTrialAppRecord) .where(AccountTrialAppRecord.account_id == current_user.id, AccountTrialAppRecord.app_id == app_id) From 754f1a3cfad675798362e5a36ea6d5137decc091 Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Thu, 23 Oct 2025 11:14:24 +0800 Subject: [PATCH 26/32] mr main and rebuild migration --- api/controllers/console/explore/wraps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/console/explore/wraps.py b/api/controllers/console/explore/wraps.py index 0f2f8d6dc9..38f0a04904 100644 --- a/api/controllers/console/explore/wraps.py +++ b/api/controllers/console/explore/wraps.py @@ -9,7 +9,7 @@ from werkzeug.exceptions import NotFound from controllers.console.explore.error import AppAccessDeniedError, TrialAppLimitExceeded, TrialAppNotAllowed from controllers.console.wraps import account_initialization_required from extensions.ext_database import db -from libs.login import current_account_with_tenant, current_user, login_required +from libs.login import current_account_with_tenant, login_required from models import AccountTrialAppRecord, App, InstalledApp, TrialApp from services.enterprise.enterprise_service import EnterpriseService from services.feature_service import FeatureService From 3e082e697691cec47a8b20e55648bf7d5cb78f3b Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Fri, 9 Jan 2026 11:38:50 +0800 Subject: [PATCH 27/32] fix: migration --- ...1110-f9f6d18a37f9_add_table_explore_banner_and_trial.py} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename api/migrations/versions/{2025_10_23_1110-f9f6d18a37f9_add_table_explore_banner_and_trial.py => 2026_01_09_1110-f9f6d18a37f9_add_table_explore_banner_and_trial.py} (97%) diff --git a/api/migrations/versions/2025_10_23_1110-f9f6d18a37f9_add_table_explore_banner_and_trial.py b/api/migrations/versions/2026_01_09_1110-f9f6d18a37f9_add_table_explore_banner_and_trial.py similarity index 97% rename from api/migrations/versions/2025_10_23_1110-f9f6d18a37f9_add_table_explore_banner_and_trial.py rename to api/migrations/versions/2026_01_09_1110-f9f6d18a37f9_add_table_explore_banner_and_trial.py index ea2145c2d5..c34fa5f819 100644 --- a/api/migrations/versions/2025_10_23_1110-f9f6d18a37f9_add_table_explore_banner_and_trial.py +++ b/api/migrations/versions/2026_01_09_1110-f9f6d18a37f9_add_table_explore_banner_and_trial.py @@ -1,8 +1,8 @@ """add table explore banner and trial Revision ID: f9f6d18a37f9 -Revises: ae662b25d9bc -Create Date: 2025-10-23 11:10:18.079355 +Revises: 7df29de0f6be +Create Date: 2026-01-09 11:10:18.079355 """ from alembic import op @@ -12,7 +12,7 @@ from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. revision = 'f9f6d18a37f9' -down_revision = 'ae662b25d9bc' +down_revision = '7df29de0f6be' branch_labels = None depends_on = None From 905a5b348d3de2ad2ee53ed6667b370ddfee7073 Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Fri, 9 Jan 2026 12:01:39 +0800 Subject: [PATCH 28/32] fix trial get --- api/controllers/console/explore/trial.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/api/controllers/console/explore/trial.py b/api/controllers/console/explore/trial.py index eb3c22cd0c..7aa4ead946 100644 --- a/api/controllers/console/explore/trial.py +++ b/api/controllers/console/explore/trial.py @@ -7,7 +7,7 @@ from werkzeug.exceptions import Forbidden, InternalServerError, NotFound import services from controllers.common import fields -from controllers.common.fields import build_site_model +from controllers.common.fields import Site as SiteResponse from controllers.console import api from controllers.console.app.error import ( AppUnavailableError, @@ -387,7 +387,6 @@ class TrialSitApi(Resource): @trial_feature_enable @get_app_model_with_trial - @service_api_ns.marshal_with(build_site_model(service_api_ns)) def get(self, app_model): """Retrieve app site info. @@ -402,7 +401,7 @@ class TrialSitApi(Resource): if app_model.tenant.status == TenantStatus.ARCHIVE: raise Forbidden() - return site + return SiteResponse.model_validate(site).model_dump(mode="json") class TrialAppParameterApi(Resource): From 3d050f449cf890bfe4ad532b5402805b50708e0c Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Fri, 9 Jan 2026 12:02:00 +0800 Subject: [PATCH 29/32] fix trial get --- api/controllers/console/explore/trial.py | 1 - 1 file changed, 1 deletion(-) diff --git a/api/controllers/console/explore/trial.py b/api/controllers/console/explore/trial.py index 7aa4ead946..8fb4622d2f 100644 --- a/api/controllers/console/explore/trial.py +++ b/api/controllers/console/explore/trial.py @@ -30,7 +30,6 @@ from controllers.console.explore.error import ( NotWorkflowAppError, ) from controllers.console.explore.wraps import TrialAppResource, trial_feature_enable -from controllers.service_api import service_api_ns from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict from core.app.apps.base_app_queue_manager import AppQueueManager From 425a0f90953631c42e3a3484ef20eb79ec48193f Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Fri, 9 Jan 2026 12:15:40 +0800 Subject: [PATCH 30/32] fix trial get --- api/controllers/console/explore/trial.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/controllers/console/explore/trial.py b/api/controllers/console/explore/trial.py index 8fb4622d2f..97d856bebe 100644 --- a/api/controllers/console/explore/trial.py +++ b/api/controllers/console/explore/trial.py @@ -6,7 +6,7 @@ from flask_restx import Resource, marshal, marshal_with, reqparse from werkzeug.exceptions import Forbidden, InternalServerError, NotFound import services -from controllers.common import fields +from controllers.common.fields import Parameters as ParametersResponse from controllers.common.fields import Site as SiteResponse from controllers.console import api from controllers.console.app.error import ( @@ -408,7 +408,6 @@ class TrialAppParameterApi(Resource): @trial_feature_enable @get_app_model_with_trial - @marshal_with(fields.parameters_fields) def get(self, app_model): """Retrieve app parameters.""" @@ -431,7 +430,8 @@ class TrialAppParameterApi(Resource): user_input_form = features_dict.get("user_input_form", []) - return get_parameters_from_feature_dict(features_dict=features_dict, user_input_form=user_input_form) + parameters = get_parameters_from_feature_dict(features_dict=features_dict, user_input_form=user_input_form) + return ParametersResponse.model_validate(parameters).model_dump(mode="json") class AppApi(Resource): From 0421a6ac53914ab7faf2cce3d0a162b71e2773f6 Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Wed, 14 Jan 2026 11:35:01 +0800 Subject: [PATCH 31/32] change insert -> delete --- api/controllers/console/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/console/admin.py b/api/controllers/console/admin.py index 978df15cf1..e1ee2c24b8 100644 --- a/api/controllers/console/admin.py +++ b/api/controllers/console/admin.py @@ -262,7 +262,7 @@ class InsertExploreBannerApi(Resource): return {"result": "success"}, 201 -@console_ns.route("/admin/insert-explore-banner/") +@console_ns.route("/admin/delete-explore-banner/") class DeleteExploreBannerApi(Resource): @console_ns.doc("delete_explore_banner") @console_ns.doc(description="Delete an explore banner") From 67efac59941b141a6ffdb865574ad0c5a5e3e199 Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Mon, 19 Jan 2026 16:06:18 +0800 Subject: [PATCH 32/32] fix migration --- ...1110-f9f6d18a37f9_add_table_explore_banner_and_trial.py} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename api/migrations/versions/{2026_01_09_1110-f9f6d18a37f9_add_table_explore_banner_and_trial.py => 2026_01_17_1110-f9f6d18a37f9_add_table_explore_banner_and_trial.py} (97%) diff --git a/api/migrations/versions/2026_01_09_1110-f9f6d18a37f9_add_table_explore_banner_and_trial.py b/api/migrations/versions/2026_01_17_1110-f9f6d18a37f9_add_table_explore_banner_and_trial.py similarity index 97% rename from api/migrations/versions/2026_01_09_1110-f9f6d18a37f9_add_table_explore_banner_and_trial.py rename to api/migrations/versions/2026_01_17_1110-f9f6d18a37f9_add_table_explore_banner_and_trial.py index c34fa5f819..9c4e87bd3c 100644 --- a/api/migrations/versions/2026_01_09_1110-f9f6d18a37f9_add_table_explore_banner_and_trial.py +++ b/api/migrations/versions/2026_01_17_1110-f9f6d18a37f9_add_table_explore_banner_and_trial.py @@ -1,8 +1,8 @@ """add table explore banner and trial Revision ID: f9f6d18a37f9 -Revises: 7df29de0f6be -Create Date: 2026-01-09 11:10:18.079355 +Revises: 288345cd01d1 +Create Date: 2026-01-017 11:10:18.079355 """ from alembic import op @@ -12,7 +12,7 @@ from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. revision = 'f9f6d18a37f9' -down_revision = '7df29de0f6be' +down_revision = '288345cd01d1' branch_labels = None depends_on = None