mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-14 02:35:55 +08:00
feat: Check and compare the DSL version before import an app (#10969)
Co-authored-by: Yi <yxiaoisme@gmail.com>
This commit is contained in:
parent
d9579f418d
commit
5172f0bf39
@ -2,6 +2,7 @@ from flask import Blueprint
|
|||||||
|
|
||||||
from libs.external_api import ExternalApi
|
from libs.external_api import ExternalApi
|
||||||
|
|
||||||
|
from .app.app_import import AppImportApi, AppImportConfirmApi
|
||||||
from .files import FileApi, FilePreviewApi, FileSupportTypeApi
|
from .files import FileApi, FilePreviewApi, FileSupportTypeApi
|
||||||
from .remote_files import RemoteFileInfoApi, RemoteFileUploadApi
|
from .remote_files import RemoteFileInfoApi, RemoteFileUploadApi
|
||||||
|
|
||||||
@ -17,6 +18,10 @@ api.add_resource(FileSupportTypeApi, "/files/support-type")
|
|||||||
api.add_resource(RemoteFileInfoApi, "/remote-files/<path:url>")
|
api.add_resource(RemoteFileInfoApi, "/remote-files/<path:url>")
|
||||||
api.add_resource(RemoteFileUploadApi, "/remote-files/upload")
|
api.add_resource(RemoteFileUploadApi, "/remote-files/upload")
|
||||||
|
|
||||||
|
# Import App
|
||||||
|
api.add_resource(AppImportApi, "/apps/imports")
|
||||||
|
api.add_resource(AppImportConfirmApi, "/apps/imports/<string:import_id>/confirm")
|
||||||
|
|
||||||
# Import other controllers
|
# Import other controllers
|
||||||
from . import admin, apikey, extension, feature, ping, setup, version
|
from . import admin, apikey, extension, feature, ping, setup, version
|
||||||
|
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_restful import Resource, inputs, marshal, marshal_with, reqparse
|
from flask_restful import Resource, inputs, marshal, marshal_with, reqparse
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
from werkzeug.exceptions import BadRequest, Forbidden, abort
|
from werkzeug.exceptions import BadRequest, Forbidden, abort
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
@ -13,13 +16,15 @@ from controllers.console.wraps import (
|
|||||||
setup_required,
|
setup_required,
|
||||||
)
|
)
|
||||||
from core.ops.ops_trace_manager import OpsTraceManager
|
from core.ops.ops_trace_manager import OpsTraceManager
|
||||||
|
from extensions.ext_database import db
|
||||||
from fields.app_fields import (
|
from fields.app_fields import (
|
||||||
app_detail_fields,
|
app_detail_fields,
|
||||||
app_detail_fields_with_site,
|
app_detail_fields_with_site,
|
||||||
app_pagination_fields,
|
app_pagination_fields,
|
||||||
)
|
)
|
||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
from services.app_dsl_service import AppDslService
|
from models import Account, App
|
||||||
|
from services.app_dsl_service import AppDslService, ImportMode
|
||||||
from services.app_service import AppService
|
from services.app_service import AppService
|
||||||
|
|
||||||
ALLOW_CREATE_APP_MODES = ["chat", "agent-chat", "advanced-chat", "workflow", "completion"]
|
ALLOW_CREATE_APP_MODES = ["chat", "agent-chat", "advanced-chat", "workflow", "completion"]
|
||||||
@ -92,61 +97,6 @@ class AppListApi(Resource):
|
|||||||
return app, 201
|
return app, 201
|
||||||
|
|
||||||
|
|
||||||
class AppImportApi(Resource):
|
|
||||||
@setup_required
|
|
||||||
@login_required
|
|
||||||
@account_initialization_required
|
|
||||||
@marshal_with(app_detail_fields_with_site)
|
|
||||||
@cloud_edition_billing_resource_check("apps")
|
|
||||||
def post(self):
|
|
||||||
"""Import app"""
|
|
||||||
# The role of the current user in the ta table must be admin, owner, or editor
|
|
||||||
if not current_user.is_editor:
|
|
||||||
raise Forbidden()
|
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
|
||||||
parser.add_argument("data", type=str, required=True, nullable=False, location="json")
|
|
||||||
parser.add_argument("name", type=str, location="json")
|
|
||||||
parser.add_argument("description", type=str, location="json")
|
|
||||||
parser.add_argument("icon_type", type=str, location="json")
|
|
||||||
parser.add_argument("icon", type=str, location="json")
|
|
||||||
parser.add_argument("icon_background", type=str, location="json")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
app = AppDslService.import_and_create_new_app(
|
|
||||||
tenant_id=current_user.current_tenant_id, data=args["data"], args=args, account=current_user
|
|
||||||
)
|
|
||||||
|
|
||||||
return app, 201
|
|
||||||
|
|
||||||
|
|
||||||
class AppImportFromUrlApi(Resource):
|
|
||||||
@setup_required
|
|
||||||
@login_required
|
|
||||||
@account_initialization_required
|
|
||||||
@marshal_with(app_detail_fields_with_site)
|
|
||||||
@cloud_edition_billing_resource_check("apps")
|
|
||||||
def post(self):
|
|
||||||
"""Import app from url"""
|
|
||||||
# The role of the current user in the ta table must be admin, owner, or editor
|
|
||||||
if not current_user.is_editor:
|
|
||||||
raise Forbidden()
|
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
|
||||||
parser.add_argument("url", type=str, required=True, nullable=False, location="json")
|
|
||||||
parser.add_argument("name", type=str, location="json")
|
|
||||||
parser.add_argument("description", type=str, location="json")
|
|
||||||
parser.add_argument("icon", type=str, location="json")
|
|
||||||
parser.add_argument("icon_background", type=str, location="json")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
app = AppDslService.import_and_create_new_app_from_url(
|
|
||||||
tenant_id=current_user.current_tenant_id, url=args["url"], args=args, account=current_user
|
|
||||||
)
|
|
||||||
|
|
||||||
return app, 201
|
|
||||||
|
|
||||||
|
|
||||||
class AppApi(Resource):
|
class AppApi(Resource):
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@ -224,10 +174,24 @@ class AppCopyApi(Resource):
|
|||||||
parser.add_argument("icon_background", type=str, location="json")
|
parser.add_argument("icon_background", type=str, location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
data = AppDslService.export_dsl(app_model=app_model, include_secret=True)
|
with Session(db.engine) as session:
|
||||||
app = AppDslService.import_and_create_new_app(
|
import_service = AppDslService(session)
|
||||||
tenant_id=current_user.current_tenant_id, data=data, args=args, account=current_user
|
yaml_content = import_service.export_dsl(app_model=app_model, include_secret=True)
|
||||||
)
|
account = cast(Account, current_user)
|
||||||
|
result = import_service.import_app(
|
||||||
|
account=account,
|
||||||
|
import_mode=ImportMode.YAML_CONTENT.value,
|
||||||
|
yaml_content=yaml_content,
|
||||||
|
name=args.get("name"),
|
||||||
|
description=args.get("description"),
|
||||||
|
icon_type=args.get("icon_type"),
|
||||||
|
icon=args.get("icon"),
|
||||||
|
icon_background=args.get("icon_background"),
|
||||||
|
)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
stmt = select(App).where(App.id == result.app.id)
|
||||||
|
app = session.scalar(stmt)
|
||||||
|
|
||||||
return app, 201
|
return app, 201
|
||||||
|
|
||||||
@ -368,8 +332,6 @@ class AppTraceApi(Resource):
|
|||||||
|
|
||||||
|
|
||||||
api.add_resource(AppListApi, "/apps")
|
api.add_resource(AppListApi, "/apps")
|
||||||
api.add_resource(AppImportApi, "/apps/import")
|
|
||||||
api.add_resource(AppImportFromUrlApi, "/apps/import/url")
|
|
||||||
api.add_resource(AppApi, "/apps/<uuid:app_id>")
|
api.add_resource(AppApi, "/apps/<uuid:app_id>")
|
||||||
api.add_resource(AppCopyApi, "/apps/<uuid:app_id>/copy")
|
api.add_resource(AppCopyApi, "/apps/<uuid:app_id>/copy")
|
||||||
api.add_resource(AppExportApi, "/apps/<uuid:app_id>/export")
|
api.add_resource(AppExportApi, "/apps/<uuid:app_id>/export")
|
||||||
|
90
api/controllers/console/app/app_import.py
Normal file
90
api/controllers/console/app/app_import.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
from typing import cast
|
||||||
|
|
||||||
|
from flask_login import current_user
|
||||||
|
from flask_restful import Resource, marshal_with, reqparse
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from werkzeug.exceptions import Forbidden
|
||||||
|
|
||||||
|
from controllers.console.wraps import (
|
||||||
|
account_initialization_required,
|
||||||
|
setup_required,
|
||||||
|
)
|
||||||
|
from extensions.ext_database import db
|
||||||
|
from fields.app_fields import app_import_fields
|
||||||
|
from libs.login import login_required
|
||||||
|
from models import Account
|
||||||
|
from services.app_dsl_service import AppDslService, ImportStatus
|
||||||
|
|
||||||
|
|
||||||
|
class AppImportApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@marshal_with(app_import_fields)
|
||||||
|
def post(self):
|
||||||
|
# Check user role first
|
||||||
|
if not current_user.is_editor:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("mode", type=str, required=True, location="json")
|
||||||
|
parser.add_argument("yaml_content", type=str, location="json")
|
||||||
|
parser.add_argument("yaml_url", type=str, location="json")
|
||||||
|
parser.add_argument("name", type=str, location="json")
|
||||||
|
parser.add_argument("description", type=str, location="json")
|
||||||
|
parser.add_argument("icon_type", type=str, location="json")
|
||||||
|
parser.add_argument("icon", type=str, location="json")
|
||||||
|
parser.add_argument("icon_background", type=str, location="json")
|
||||||
|
parser.add_argument("app_id", type=str, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Create service with session
|
||||||
|
with Session(db.engine) as session:
|
||||||
|
import_service = AppDslService(session)
|
||||||
|
# Import app
|
||||||
|
account = cast(Account, current_user)
|
||||||
|
result = import_service.import_app(
|
||||||
|
account=account,
|
||||||
|
import_mode=args["mode"],
|
||||||
|
yaml_content=args.get("yaml_content"),
|
||||||
|
yaml_url=args.get("yaml_url"),
|
||||||
|
name=args.get("name"),
|
||||||
|
description=args.get("description"),
|
||||||
|
icon_type=args.get("icon_type"),
|
||||||
|
icon=args.get("icon"),
|
||||||
|
icon_background=args.get("icon_background"),
|
||||||
|
app_id=args.get("app_id"),
|
||||||
|
)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Return appropriate status code based on result
|
||||||
|
status = result.status
|
||||||
|
if status == ImportStatus.FAILED.value:
|
||||||
|
return result.model_dump(mode="json"), 400
|
||||||
|
elif status == ImportStatus.PENDING.value:
|
||||||
|
return result.model_dump(mode="json"), 202
|
||||||
|
return result.model_dump(mode="json"), 200
|
||||||
|
|
||||||
|
|
||||||
|
class AppImportConfirmApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@marshal_with(app_import_fields)
|
||||||
|
def post(self, import_id):
|
||||||
|
# Check user role first
|
||||||
|
if not current_user.is_editor:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
# Create service with session
|
||||||
|
with Session(db.engine) as session:
|
||||||
|
import_service = AppDslService(session)
|
||||||
|
# Confirm import
|
||||||
|
account = cast(Account, current_user)
|
||||||
|
result = import_service.confirm_import(import_id=import_id, account=account)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Return appropriate status code based on result
|
||||||
|
if result.status == ImportStatus.FAILED.value:
|
||||||
|
return result.model_dump(mode="json"), 400
|
||||||
|
return result.model_dump(mode="json"), 200
|
@ -20,7 +20,6 @@ from libs.helper import TimestampField, uuid_value
|
|||||||
from libs.login import current_user, login_required
|
from libs.login import current_user, login_required
|
||||||
from models import App
|
from models import App
|
||||||
from models.model import AppMode
|
from models.model import AppMode
|
||||||
from services.app_dsl_service import AppDslService
|
|
||||||
from services.app_generate_service import AppGenerateService
|
from services.app_generate_service import AppGenerateService
|
||||||
from services.errors.app import WorkflowHashNotEqualError
|
from services.errors.app import WorkflowHashNotEqualError
|
||||||
from services.workflow_service import WorkflowService
|
from services.workflow_service import WorkflowService
|
||||||
@ -126,31 +125,6 @@ class DraftWorkflowApi(Resource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DraftWorkflowImportApi(Resource):
|
|
||||||
@setup_required
|
|
||||||
@login_required
|
|
||||||
@account_initialization_required
|
|
||||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
|
||||||
@marshal_with(workflow_fields)
|
|
||||||
def post(self, app_model: App):
|
|
||||||
"""
|
|
||||||
Import draft workflow
|
|
||||||
"""
|
|
||||||
# The role of the current user in the ta table must be admin, owner, or editor
|
|
||||||
if not current_user.is_editor:
|
|
||||||
raise Forbidden()
|
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
|
||||||
parser.add_argument("data", type=str, required=True, nullable=False, location="json")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
workflow = AppDslService.import_and_overwrite_workflow(
|
|
||||||
app_model=app_model, data=args["data"], account=current_user
|
|
||||||
)
|
|
||||||
|
|
||||||
return workflow
|
|
||||||
|
|
||||||
|
|
||||||
class AdvancedChatDraftWorkflowRunApi(Resource):
|
class AdvancedChatDraftWorkflowRunApi(Resource):
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@ -453,7 +427,6 @@ class ConvertToWorkflowApi(Resource):
|
|||||||
|
|
||||||
|
|
||||||
api.add_resource(DraftWorkflowApi, "/apps/<uuid:app_id>/workflows/draft")
|
api.add_resource(DraftWorkflowApi, "/apps/<uuid:app_id>/workflows/draft")
|
||||||
api.add_resource(DraftWorkflowImportApi, "/apps/<uuid:app_id>/workflows/draft/import")
|
|
||||||
api.add_resource(AdvancedChatDraftWorkflowRunApi, "/apps/<uuid:app_id>/advanced-chat/workflows/draft/run")
|
api.add_resource(AdvancedChatDraftWorkflowRunApi, "/apps/<uuid:app_id>/advanced-chat/workflows/draft/run")
|
||||||
api.add_resource(DraftWorkflowRunApi, "/apps/<uuid:app_id>/workflows/draft/run")
|
api.add_resource(DraftWorkflowRunApi, "/apps/<uuid:app_id>/workflows/draft/run")
|
||||||
api.add_resource(WorkflowTaskStopApi, "/apps/<uuid:app_id>/workflow-runs/tasks/<string:task_id>/stop")
|
api.add_resource(WorkflowTaskStopApi, "/apps/<uuid:app_id>/workflow-runs/tasks/<string:task_id>/stop")
|
||||||
|
@ -190,3 +190,12 @@ app_site_fields = {
|
|||||||
"show_workflow_steps": fields.Boolean,
|
"show_workflow_steps": fields.Boolean,
|
||||||
"use_icon_as_answer_icon": fields.Boolean,
|
"use_icon_as_answer_icon": fields.Boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app_import_fields = {
|
||||||
|
"id": fields.String,
|
||||||
|
"status": fields.String,
|
||||||
|
"app_id": fields.String,
|
||||||
|
"current_dsl_version": fields.String,
|
||||||
|
"imported_dsl_version": fields.String,
|
||||||
|
"error": fields.String,
|
||||||
|
}
|
||||||
|
@ -31,9 +31,12 @@ class AppIconUrlField(fields.Raw):
|
|||||||
if obj is None:
|
if obj is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
from models.model import IconType
|
from models.model import App, IconType
|
||||||
|
|
||||||
if obj.icon_type == IconType.IMAGE.value:
|
if isinstance(obj, dict) and "app" in obj:
|
||||||
|
obj = obj["app"]
|
||||||
|
|
||||||
|
if isinstance(obj, App) and obj.icon_type == IconType.IMAGE.value:
|
||||||
return file_helpers.get_signed_file_url(obj.icon)
|
return file_helpers.get_signed_file_url(obj.icon)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ class App(db.Model):
|
|||||||
name = db.Column(db.String(255), nullable=False)
|
name = db.Column(db.String(255), nullable=False)
|
||||||
description = db.Column(db.Text, nullable=False, server_default=db.text("''::character varying"))
|
description = db.Column(db.Text, nullable=False, server_default=db.text("''::character varying"))
|
||||||
mode = db.Column(db.String(255), nullable=False)
|
mode = db.Column(db.String(255), nullable=False)
|
||||||
icon_type = db.Column(db.String(255), nullable=True)
|
icon_type = db.Column(db.String(255), nullable=True) # image, emoji
|
||||||
icon = db.Column(db.String(255))
|
icon = db.Column(db.String(255))
|
||||||
icon_background = db.Column(db.String(255))
|
icon_background = db.Column(db.String(255))
|
||||||
app_model_config_id = db.Column(StringUUID, nullable=True)
|
app_model_config_id = db.Column(StringUUID, nullable=True)
|
||||||
|
485
api/services/app_dsl_service.py
Normal file
485
api/services/app_dsl_service.py
Normal file
@ -0,0 +1,485 @@
|
|||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from packaging import version
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from core.helper import ssrf_proxy
|
||||||
|
from events.app_event import app_model_config_was_updated, app_was_created
|
||||||
|
from extensions.ext_redis import redis_client
|
||||||
|
from factories import variable_factory
|
||||||
|
from models import Account, App, AppMode
|
||||||
|
from models.model import AppModelConfig
|
||||||
|
from services.workflow_service import WorkflowService
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
IMPORT_INFO_REDIS_KEY_PREFIX = "app_import_info:"
|
||||||
|
IMPORT_INFO_REDIS_EXPIRY = 180 # 3 minutes
|
||||||
|
CURRENT_DSL_VERSION = "0.2.0"
|
||||||
|
|
||||||
|
|
||||||
|
class ImportMode(str, Enum):
|
||||||
|
YAML_CONTENT = "yaml-content"
|
||||||
|
YAML_URL = "yaml-url"
|
||||||
|
|
||||||
|
|
||||||
|
class ImportStatus(str, Enum):
|
||||||
|
COMPLETED = "completed"
|
||||||
|
COMPLETED_WITH_WARNINGS = "completed-with-warnings"
|
||||||
|
PENDING = "pending"
|
||||||
|
FAILED = "failed"
|
||||||
|
|
||||||
|
|
||||||
|
class Import(BaseModel):
|
||||||
|
id: str
|
||||||
|
status: ImportStatus
|
||||||
|
app_id: Optional[str] = None
|
||||||
|
current_dsl_version: str = CURRENT_DSL_VERSION
|
||||||
|
imported_dsl_version: str = ""
|
||||||
|
error: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
def _check_version_compatibility(imported_version: str) -> ImportStatus:
|
||||||
|
"""Determine import status based on version comparison"""
|
||||||
|
try:
|
||||||
|
current_ver = version.parse(CURRENT_DSL_VERSION)
|
||||||
|
imported_ver = version.parse(imported_version)
|
||||||
|
except version.InvalidVersion:
|
||||||
|
return ImportStatus.FAILED
|
||||||
|
|
||||||
|
# Compare major version and minor version
|
||||||
|
if current_ver.major != imported_ver.major or current_ver.minor != imported_ver.minor:
|
||||||
|
return ImportStatus.PENDING
|
||||||
|
|
||||||
|
if current_ver.micro != imported_ver.micro:
|
||||||
|
return ImportStatus.COMPLETED_WITH_WARNINGS
|
||||||
|
|
||||||
|
return ImportStatus.COMPLETED
|
||||||
|
|
||||||
|
|
||||||
|
class PendingData(BaseModel):
|
||||||
|
import_mode: str
|
||||||
|
yaml_content: str
|
||||||
|
name: str | None
|
||||||
|
description: str | None
|
||||||
|
icon_type: str | None
|
||||||
|
icon: str | None
|
||||||
|
icon_background: str | None
|
||||||
|
app_id: str | None
|
||||||
|
|
||||||
|
|
||||||
|
class AppDslService:
|
||||||
|
def __init__(self, session: Session):
|
||||||
|
self._session = session
|
||||||
|
|
||||||
|
def import_app(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
account: Account,
|
||||||
|
import_mode: str,
|
||||||
|
yaml_content: Optional[str] = None,
|
||||||
|
yaml_url: Optional[str] = None,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
description: Optional[str] = None,
|
||||||
|
icon_type: Optional[str] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_background: Optional[str] = None,
|
||||||
|
app_id: Optional[str] = None,
|
||||||
|
) -> Import:
|
||||||
|
"""Import an app from YAML content or URL."""
|
||||||
|
import_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Validate import mode
|
||||||
|
try:
|
||||||
|
mode = ImportMode(import_mode)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(f"Invalid import_mode: {import_mode}")
|
||||||
|
|
||||||
|
# Get YAML content
|
||||||
|
content = ""
|
||||||
|
if mode == ImportMode.YAML_URL:
|
||||||
|
if not yaml_url:
|
||||||
|
return Import(
|
||||||
|
id=import_id,
|
||||||
|
status=ImportStatus.FAILED,
|
||||||
|
error="yaml_url is required when import_mode is yaml-url",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
max_size = 10 * 1024 * 1024 # 10MB
|
||||||
|
response = ssrf_proxy.get(yaml_url.strip(), follow_redirects=True, timeout=(10, 10))
|
||||||
|
response.raise_for_status()
|
||||||
|
content = response.content
|
||||||
|
|
||||||
|
if len(content) > max_size:
|
||||||
|
return Import(
|
||||||
|
id=import_id,
|
||||||
|
status=ImportStatus.FAILED,
|
||||||
|
error="File size exceeds the limit of 10MB",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not content:
|
||||||
|
return Import(
|
||||||
|
id=import_id,
|
||||||
|
status=ImportStatus.FAILED,
|
||||||
|
error="Empty content from url",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
content = content.decode("utf-8")
|
||||||
|
except UnicodeDecodeError as e:
|
||||||
|
return Import(
|
||||||
|
id=import_id,
|
||||||
|
status=ImportStatus.FAILED,
|
||||||
|
error=f"Error decoding content: {e}",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return Import(
|
||||||
|
id=import_id,
|
||||||
|
status=ImportStatus.FAILED,
|
||||||
|
error=f"Error fetching YAML from URL: {str(e)}",
|
||||||
|
)
|
||||||
|
elif mode == ImportMode.YAML_CONTENT:
|
||||||
|
if not yaml_content:
|
||||||
|
return Import(
|
||||||
|
id=import_id,
|
||||||
|
status=ImportStatus.FAILED,
|
||||||
|
error="yaml_content is required when import_mode is yaml-content",
|
||||||
|
)
|
||||||
|
content = yaml_content
|
||||||
|
|
||||||
|
# Process YAML content
|
||||||
|
try:
|
||||||
|
# Parse YAML to validate format
|
||||||
|
data = yaml.safe_load(content)
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return Import(
|
||||||
|
id=import_id,
|
||||||
|
status=ImportStatus.FAILED,
|
||||||
|
error="Invalid YAML format: content must be a mapping",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate and fix DSL version
|
||||||
|
if not data.get("version"):
|
||||||
|
data["version"] = "0.1.0"
|
||||||
|
if not data.get("kind") or data.get("kind") != "app":
|
||||||
|
data["kind"] = "app"
|
||||||
|
|
||||||
|
imported_version = data.get("version", "0.1.0")
|
||||||
|
status = _check_version_compatibility(imported_version)
|
||||||
|
|
||||||
|
# Extract app data
|
||||||
|
app_data = data.get("app")
|
||||||
|
if not app_data:
|
||||||
|
return Import(
|
||||||
|
id=import_id,
|
||||||
|
status=ImportStatus.FAILED,
|
||||||
|
error="Missing app data in YAML content",
|
||||||
|
)
|
||||||
|
|
||||||
|
# If app_id is provided, check if it exists
|
||||||
|
app = None
|
||||||
|
if app_id:
|
||||||
|
stmt = select(App).where(App.id == app_id, App.tenant_id == account.current_tenant_id)
|
||||||
|
app = self._session.scalar(stmt)
|
||||||
|
|
||||||
|
if not app:
|
||||||
|
return Import(
|
||||||
|
id=import_id,
|
||||||
|
status=ImportStatus.FAILED,
|
||||||
|
error="App not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
if app.mode not in [AppMode.WORKFLOW.value, AppMode.ADVANCED_CHAT.value]:
|
||||||
|
return Import(
|
||||||
|
id=import_id,
|
||||||
|
status=ImportStatus.FAILED,
|
||||||
|
error="Only workflow or advanced chat apps can be overwritten",
|
||||||
|
)
|
||||||
|
|
||||||
|
# If major version mismatch, store import info in Redis
|
||||||
|
if status == ImportStatus.PENDING:
|
||||||
|
panding_data = PendingData(
|
||||||
|
import_mode=import_mode,
|
||||||
|
yaml_content=content,
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
icon_type=icon_type,
|
||||||
|
icon=icon,
|
||||||
|
icon_background=icon_background,
|
||||||
|
app_id=app_id,
|
||||||
|
)
|
||||||
|
redis_client.setex(
|
||||||
|
f"{IMPORT_INFO_REDIS_KEY_PREFIX}{import_id}",
|
||||||
|
IMPORT_INFO_REDIS_EXPIRY,
|
||||||
|
panding_data.model_dump_json(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return Import(
|
||||||
|
id=import_id,
|
||||||
|
status=status,
|
||||||
|
app_id=app_id,
|
||||||
|
imported_dsl_version=imported_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create or update app
|
||||||
|
app = self._create_or_update_app(
|
||||||
|
app=app,
|
||||||
|
data=data,
|
||||||
|
account=account,
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
icon_type=icon_type,
|
||||||
|
icon=icon,
|
||||||
|
icon_background=icon_background,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Import(
|
||||||
|
id=import_id,
|
||||||
|
status=status,
|
||||||
|
app_id=app.id,
|
||||||
|
imported_dsl_version=imported_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
return Import(
|
||||||
|
id=import_id,
|
||||||
|
status=ImportStatus.FAILED,
|
||||||
|
error=f"Invalid YAML format: {str(e)}",
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Failed to import app")
|
||||||
|
return Import(
|
||||||
|
id=import_id,
|
||||||
|
status=ImportStatus.FAILED,
|
||||||
|
error=str(e),
|
||||||
|
)
|
||||||
|
|
||||||
|
def confirm_import(self, *, import_id: str, account: Account) -> Import:
|
||||||
|
"""
|
||||||
|
Confirm an import that requires confirmation
|
||||||
|
"""
|
||||||
|
redis_key = f"{IMPORT_INFO_REDIS_KEY_PREFIX}{import_id}"
|
||||||
|
pending_data = redis_client.get(redis_key)
|
||||||
|
|
||||||
|
if not pending_data:
|
||||||
|
return Import(
|
||||||
|
id=import_id,
|
||||||
|
status=ImportStatus.FAILED,
|
||||||
|
error="Import information expired or does not exist",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not isinstance(pending_data, str | bytes):
|
||||||
|
return Import(
|
||||||
|
id=import_id,
|
||||||
|
status=ImportStatus.FAILED,
|
||||||
|
error="Invalid import information",
|
||||||
|
)
|
||||||
|
pending_data = PendingData.model_validate_json(pending_data)
|
||||||
|
data = yaml.safe_load(pending_data.yaml_content)
|
||||||
|
|
||||||
|
app = None
|
||||||
|
if pending_data.app_id:
|
||||||
|
stmt = select(App).where(App.id == pending_data.app_id, App.tenant_id == account.current_tenant_id)
|
||||||
|
app = self._session.scalar(stmt)
|
||||||
|
|
||||||
|
# Create or update app
|
||||||
|
app = self._create_or_update_app(
|
||||||
|
app=app,
|
||||||
|
data=data,
|
||||||
|
account=account,
|
||||||
|
name=pending_data.name,
|
||||||
|
description=pending_data.description,
|
||||||
|
icon_type=pending_data.icon_type,
|
||||||
|
icon=pending_data.icon,
|
||||||
|
icon_background=pending_data.icon_background,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delete import info from Redis
|
||||||
|
redis_client.delete(redis_key)
|
||||||
|
|
||||||
|
return Import(
|
||||||
|
id=import_id,
|
||||||
|
status=ImportStatus.COMPLETED,
|
||||||
|
app_id=app.id,
|
||||||
|
current_dsl_version=CURRENT_DSL_VERSION,
|
||||||
|
imported_dsl_version=data.get("version", "0.1.0"),
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Error confirming import")
|
||||||
|
return Import(
|
||||||
|
id=import_id,
|
||||||
|
status=ImportStatus.FAILED,
|
||||||
|
error=str(e),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_or_update_app(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
app: Optional[App],
|
||||||
|
data: dict,
|
||||||
|
account: Account,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
description: Optional[str] = None,
|
||||||
|
icon_type: Optional[str] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_background: Optional[str] = None,
|
||||||
|
) -> App:
|
||||||
|
"""Create a new app or update an existing one."""
|
||||||
|
app_data = data.get("app", {})
|
||||||
|
app_mode = AppMode(app_data["mode"])
|
||||||
|
|
||||||
|
# Set icon type
|
||||||
|
icon_type_value = icon_type or app_data.get("icon_type")
|
||||||
|
if icon_type_value in ["emoji", "link"]:
|
||||||
|
icon_type = icon_type_value
|
||||||
|
else:
|
||||||
|
icon_type = "emoji"
|
||||||
|
icon = icon or str(app_data.get("icon", ""))
|
||||||
|
|
||||||
|
if app:
|
||||||
|
# Update existing app
|
||||||
|
app.name = name or app_data.get("name", app.name)
|
||||||
|
app.description = description or app_data.get("description", app.description)
|
||||||
|
app.icon_type = icon_type
|
||||||
|
app.icon = icon
|
||||||
|
app.icon_background = icon_background or app_data.get("icon_background", app.icon_background)
|
||||||
|
app.updated_by = account.id
|
||||||
|
else:
|
||||||
|
# Create new app
|
||||||
|
app = App()
|
||||||
|
app.id = str(uuid4())
|
||||||
|
app.tenant_id = account.current_tenant_id
|
||||||
|
app.mode = app_mode.value
|
||||||
|
app.name = name or app_data.get("name", "")
|
||||||
|
app.description = description or app_data.get("description", "")
|
||||||
|
app.icon_type = icon_type
|
||||||
|
app.icon = icon
|
||||||
|
app.icon_background = icon_background or app_data.get("icon_background", "#FFFFFF")
|
||||||
|
app.enable_site = True
|
||||||
|
app.enable_api = True
|
||||||
|
app.use_icon_as_answer_icon = app_data.get("use_icon_as_answer_icon", False)
|
||||||
|
app.created_by = account.id
|
||||||
|
app.updated_by = account.id
|
||||||
|
|
||||||
|
self._session.add(app)
|
||||||
|
self._session.commit()
|
||||||
|
app_was_created.send(app, account=account)
|
||||||
|
|
||||||
|
# Initialize app based on mode
|
||||||
|
if app_mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
|
||||||
|
workflow_data = data.get("workflow")
|
||||||
|
if not workflow_data or not isinstance(workflow_data, dict):
|
||||||
|
raise ValueError("Missing workflow data for workflow/advanced chat app")
|
||||||
|
|
||||||
|
environment_variables_list = workflow_data.get("environment_variables", [])
|
||||||
|
environment_variables = [
|
||||||
|
variable_factory.build_variable_from_mapping(obj) for obj in environment_variables_list
|
||||||
|
]
|
||||||
|
conversation_variables_list = workflow_data.get("conversation_variables", [])
|
||||||
|
conversation_variables = [
|
||||||
|
variable_factory.build_variable_from_mapping(obj) for obj in conversation_variables_list
|
||||||
|
]
|
||||||
|
|
||||||
|
workflow_service = WorkflowService()
|
||||||
|
current_draft_workflow = workflow_service.get_draft_workflow(app_model=app)
|
||||||
|
if current_draft_workflow:
|
||||||
|
unique_hash = current_draft_workflow.unique_hash
|
||||||
|
else:
|
||||||
|
unique_hash = None
|
||||||
|
workflow_service.sync_draft_workflow(
|
||||||
|
app_model=app,
|
||||||
|
graph=workflow_data.get("graph", {}),
|
||||||
|
features=workflow_data.get("features", {}),
|
||||||
|
unique_hash=unique_hash,
|
||||||
|
account=account,
|
||||||
|
environment_variables=environment_variables,
|
||||||
|
conversation_variables=conversation_variables,
|
||||||
|
)
|
||||||
|
elif app_mode in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.COMPLETION}:
|
||||||
|
# Initialize model config
|
||||||
|
model_config = data.get("model_config")
|
||||||
|
if not model_config or not isinstance(model_config, dict):
|
||||||
|
raise ValueError("Missing model_config for chat/agent-chat/completion app")
|
||||||
|
# Initialize or update model config
|
||||||
|
if not app.app_model_config:
|
||||||
|
app_model_config = AppModelConfig().from_model_config_dict(model_config)
|
||||||
|
app_model_config.id = str(uuid4())
|
||||||
|
app_model_config.app_id = app.id
|
||||||
|
app_model_config.created_by = account.id
|
||||||
|
app_model_config.updated_by = account.id
|
||||||
|
|
||||||
|
app.app_model_config_id = app_model_config.id
|
||||||
|
|
||||||
|
self._session.add(app_model_config)
|
||||||
|
app_model_config_was_updated.send(app, app_model_config=app_model_config)
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid app mode")
|
||||||
|
return app
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_dsl(cls, app_model: App, include_secret: bool = False) -> str:
|
||||||
|
"""
|
||||||
|
Export app
|
||||||
|
:param app_model: App instance
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
app_mode = AppMode.value_of(app_model.mode)
|
||||||
|
|
||||||
|
export_data = {
|
||||||
|
"version": CURRENT_DSL_VERSION,
|
||||||
|
"kind": "app",
|
||||||
|
"app": {
|
||||||
|
"name": app_model.name,
|
||||||
|
"mode": app_model.mode,
|
||||||
|
"icon": "🤖" if app_model.icon_type == "image" else app_model.icon,
|
||||||
|
"icon_background": "#FFEAD5" if app_model.icon_type == "image" else app_model.icon_background,
|
||||||
|
"description": app_model.description,
|
||||||
|
"use_icon_as_answer_icon": app_model.use_icon_as_answer_icon,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if app_mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
|
||||||
|
cls._append_workflow_export_data(
|
||||||
|
export_data=export_data, app_model=app_model, include_secret=include_secret
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cls._append_model_config_export_data(export_data, app_model)
|
||||||
|
|
||||||
|
return yaml.dump(export_data, allow_unicode=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _append_workflow_export_data(cls, *, export_data: dict, app_model: App, include_secret: bool) -> None:
|
||||||
|
"""
|
||||||
|
Append workflow export data
|
||||||
|
:param export_data: export data
|
||||||
|
:param app_model: App instance
|
||||||
|
"""
|
||||||
|
workflow_service = WorkflowService()
|
||||||
|
workflow = workflow_service.get_draft_workflow(app_model)
|
||||||
|
if not workflow:
|
||||||
|
raise ValueError("Missing draft workflow configuration, please check.")
|
||||||
|
|
||||||
|
export_data["workflow"] = workflow.to_dict(include_secret=include_secret)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _append_model_config_export_data(cls, export_data: dict, app_model: App) -> None:
|
||||||
|
"""
|
||||||
|
Append model config export data
|
||||||
|
:param export_data: export data
|
||||||
|
:param app_model: App instance
|
||||||
|
"""
|
||||||
|
app_model_config = app_model.app_model_config
|
||||||
|
if not app_model_config:
|
||||||
|
raise ValueError("Missing app configuration, please check.")
|
||||||
|
|
||||||
|
export_data["model_config"] = app_model_config.to_dict()
|
@ -1,3 +0,0 @@
|
|||||||
from .service import AppDslService
|
|
||||||
|
|
||||||
__all__ = ["AppDslService"]
|
|
@ -1,34 +0,0 @@
|
|||||||
class DSLVersionNotSupportedError(ValueError):
|
|
||||||
"""Raised when the imported DSL version is not supported by the current Dify version."""
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidYAMLFormatError(ValueError):
|
|
||||||
"""Raised when the provided YAML format is invalid."""
|
|
||||||
|
|
||||||
|
|
||||||
class MissingAppDataError(ValueError):
|
|
||||||
"""Raised when the app data is missing in the provided DSL."""
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidAppModeError(ValueError):
|
|
||||||
"""Raised when the app mode is invalid."""
|
|
||||||
|
|
||||||
|
|
||||||
class MissingWorkflowDataError(ValueError):
|
|
||||||
"""Raised when the workflow data is missing in the provided DSL."""
|
|
||||||
|
|
||||||
|
|
||||||
class MissingModelConfigError(ValueError):
|
|
||||||
"""Raised when the model config data is missing in the provided DSL."""
|
|
||||||
|
|
||||||
|
|
||||||
class FileSizeLimitExceededError(ValueError):
|
|
||||||
"""Raised when the file size exceeds the allowed limit."""
|
|
||||||
|
|
||||||
|
|
||||||
class EmptyContentError(ValueError):
|
|
||||||
"""Raised when the content fetched from the URL is empty."""
|
|
||||||
|
|
||||||
|
|
||||||
class ContentDecodingError(ValueError):
|
|
||||||
"""Raised when there is an error decoding the content."""
|
|
@ -1,484 +0,0 @@
|
|||||||
import logging
|
|
||||||
from collections.abc import Mapping
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
from packaging import version
|
|
||||||
|
|
||||||
from core.helper import ssrf_proxy
|
|
||||||
from events.app_event import app_model_config_was_updated, app_was_created
|
|
||||||
from extensions.ext_database import db
|
|
||||||
from factories import variable_factory
|
|
||||||
from models.account import Account
|
|
||||||
from models.model import App, AppMode, AppModelConfig
|
|
||||||
from models.workflow import Workflow
|
|
||||||
from services.workflow_service import WorkflowService
|
|
||||||
|
|
||||||
from .exc import (
|
|
||||||
ContentDecodingError,
|
|
||||||
EmptyContentError,
|
|
||||||
FileSizeLimitExceededError,
|
|
||||||
InvalidAppModeError,
|
|
||||||
InvalidYAMLFormatError,
|
|
||||||
MissingAppDataError,
|
|
||||||
MissingModelConfigError,
|
|
||||||
MissingWorkflowDataError,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
current_dsl_version = "0.1.3"
|
|
||||||
|
|
||||||
|
|
||||||
class AppDslService:
|
|
||||||
@classmethod
|
|
||||||
def import_and_create_new_app_from_url(cls, tenant_id: str, url: str, args: dict, account: Account) -> App:
|
|
||||||
"""
|
|
||||||
Import app dsl from url and create new app
|
|
||||||
:param tenant_id: tenant id
|
|
||||||
:param url: import url
|
|
||||||
:param args: request args
|
|
||||||
:param account: Account instance
|
|
||||||
"""
|
|
||||||
max_size = 10 * 1024 * 1024 # 10MB
|
|
||||||
response = ssrf_proxy.get(url.strip(), follow_redirects=True, timeout=(10, 10))
|
|
||||||
response.raise_for_status()
|
|
||||||
content = response.content
|
|
||||||
|
|
||||||
if len(content) > max_size:
|
|
||||||
raise FileSizeLimitExceededError("File size exceeds the limit of 10MB")
|
|
||||||
|
|
||||||
if not content:
|
|
||||||
raise EmptyContentError("Empty content from url")
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = content.decode("utf-8")
|
|
||||||
except UnicodeDecodeError as e:
|
|
||||||
raise ContentDecodingError(f"Error decoding content: {e}")
|
|
||||||
|
|
||||||
return cls.import_and_create_new_app(tenant_id, data, args, account)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def import_and_create_new_app(cls, tenant_id: str, data: str, args: dict, account: Account) -> App:
|
|
||||||
"""
|
|
||||||
Import app dsl and create new app
|
|
||||||
:param tenant_id: tenant id
|
|
||||||
:param data: import data
|
|
||||||
:param args: request args
|
|
||||||
:param account: Account instance
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
import_data = yaml.safe_load(data)
|
|
||||||
except yaml.YAMLError:
|
|
||||||
raise InvalidYAMLFormatError("Invalid YAML format in data argument.")
|
|
||||||
|
|
||||||
# check or repair dsl version
|
|
||||||
import_data = _check_or_fix_dsl(import_data)
|
|
||||||
|
|
||||||
app_data = import_data.get("app")
|
|
||||||
if not app_data:
|
|
||||||
raise MissingAppDataError("Missing app in data argument")
|
|
||||||
|
|
||||||
# get app basic info
|
|
||||||
name = args.get("name") or app_data.get("name")
|
|
||||||
description = args.get("description") or app_data.get("description", "")
|
|
||||||
icon_type = args.get("icon_type") or app_data.get("icon_type")
|
|
||||||
icon = args.get("icon") or app_data.get("icon")
|
|
||||||
icon_background = args.get("icon_background") or app_data.get("icon_background")
|
|
||||||
use_icon_as_answer_icon = app_data.get("use_icon_as_answer_icon", False)
|
|
||||||
|
|
||||||
# import dsl and create app
|
|
||||||
app_mode = AppMode.value_of(app_data.get("mode"))
|
|
||||||
|
|
||||||
if app_mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
|
|
||||||
workflow_data = import_data.get("workflow")
|
|
||||||
if not workflow_data or not isinstance(workflow_data, dict):
|
|
||||||
raise MissingWorkflowDataError(
|
|
||||||
"Missing workflow in data argument when app mode is advanced-chat or workflow"
|
|
||||||
)
|
|
||||||
|
|
||||||
app = cls._import_and_create_new_workflow_based_app(
|
|
||||||
tenant_id=tenant_id,
|
|
||||||
app_mode=app_mode,
|
|
||||||
workflow_data=workflow_data,
|
|
||||||
account=account,
|
|
||||||
name=name,
|
|
||||||
description=description,
|
|
||||||
icon_type=icon_type,
|
|
||||||
icon=icon,
|
|
||||||
icon_background=icon_background,
|
|
||||||
use_icon_as_answer_icon=use_icon_as_answer_icon,
|
|
||||||
)
|
|
||||||
elif app_mode in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.COMPLETION}:
|
|
||||||
model_config = import_data.get("model_config")
|
|
||||||
if not model_config or not isinstance(model_config, dict):
|
|
||||||
raise MissingModelConfigError(
|
|
||||||
"Missing model_config in data argument when app mode is chat, agent-chat or completion"
|
|
||||||
)
|
|
||||||
|
|
||||||
app = cls._import_and_create_new_model_config_based_app(
|
|
||||||
tenant_id=tenant_id,
|
|
||||||
app_mode=app_mode,
|
|
||||||
model_config_data=model_config,
|
|
||||||
account=account,
|
|
||||||
name=name,
|
|
||||||
description=description,
|
|
||||||
icon_type=icon_type,
|
|
||||||
icon=icon,
|
|
||||||
icon_background=icon_background,
|
|
||||||
use_icon_as_answer_icon=use_icon_as_answer_icon,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise InvalidAppModeError("Invalid app mode")
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def import_and_overwrite_workflow(cls, app_model: App, data: str, account: Account) -> Workflow:
|
|
||||||
"""
|
|
||||||
Import app dsl and overwrite workflow
|
|
||||||
:param app_model: App instance
|
|
||||||
:param data: import data
|
|
||||||
:param account: Account instance
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
import_data = yaml.safe_load(data)
|
|
||||||
except yaml.YAMLError:
|
|
||||||
raise InvalidYAMLFormatError("Invalid YAML format in data argument.")
|
|
||||||
|
|
||||||
# check or repair dsl version
|
|
||||||
import_data = _check_or_fix_dsl(import_data)
|
|
||||||
|
|
||||||
app_data = import_data.get("app")
|
|
||||||
if not app_data:
|
|
||||||
raise MissingAppDataError("Missing app in data argument")
|
|
||||||
|
|
||||||
# import dsl and overwrite app
|
|
||||||
app_mode = AppMode.value_of(app_data.get("mode"))
|
|
||||||
if app_mode not in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
|
|
||||||
raise InvalidAppModeError("Only support import workflow in advanced-chat or workflow app.")
|
|
||||||
|
|
||||||
if app_data.get("mode") != app_model.mode:
|
|
||||||
raise ValueError(f"App mode {app_data.get('mode')} is not matched with current app mode {app_mode.value}")
|
|
||||||
|
|
||||||
workflow_data = import_data.get("workflow")
|
|
||||||
if not workflow_data or not isinstance(workflow_data, dict):
|
|
||||||
raise MissingWorkflowDataError(
|
|
||||||
"Missing workflow in data argument when app mode is advanced-chat or workflow"
|
|
||||||
)
|
|
||||||
|
|
||||||
return cls._import_and_overwrite_workflow_based_app(
|
|
||||||
app_model=app_model,
|
|
||||||
workflow_data=workflow_data,
|
|
||||||
account=account,
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def export_dsl(cls, app_model: App, include_secret: bool = False) -> str:
|
|
||||||
"""
|
|
||||||
Export app
|
|
||||||
:param app_model: App instance
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
app_mode = AppMode.value_of(app_model.mode)
|
|
||||||
|
|
||||||
export_data = {
|
|
||||||
"version": current_dsl_version,
|
|
||||||
"kind": "app",
|
|
||||||
"app": {
|
|
||||||
"name": app_model.name,
|
|
||||||
"mode": app_model.mode,
|
|
||||||
"icon": "🤖" if app_model.icon_type == "image" else app_model.icon,
|
|
||||||
"icon_background": "#FFEAD5" if app_model.icon_type == "image" else app_model.icon_background,
|
|
||||||
"description": app_model.description,
|
|
||||||
"use_icon_as_answer_icon": app_model.use_icon_as_answer_icon,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if app_mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
|
|
||||||
cls._append_workflow_export_data(
|
|
||||||
export_data=export_data, app_model=app_model, include_secret=include_secret
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
cls._append_model_config_export_data(export_data, app_model)
|
|
||||||
|
|
||||||
return yaml.dump(export_data, allow_unicode=True)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _import_and_create_new_workflow_based_app(
|
|
||||||
cls,
|
|
||||||
tenant_id: str,
|
|
||||||
app_mode: AppMode,
|
|
||||||
workflow_data: Mapping[str, Any],
|
|
||||||
account: Account,
|
|
||||||
name: str,
|
|
||||||
description: str,
|
|
||||||
icon_type: str,
|
|
||||||
icon: str,
|
|
||||||
icon_background: str,
|
|
||||||
use_icon_as_answer_icon: bool,
|
|
||||||
) -> App:
|
|
||||||
"""
|
|
||||||
Import app dsl and create new workflow based app
|
|
||||||
|
|
||||||
:param tenant_id: tenant id
|
|
||||||
:param app_mode: app mode
|
|
||||||
:param workflow_data: workflow data
|
|
||||||
:param account: Account instance
|
|
||||||
:param name: app name
|
|
||||||
:param description: app description
|
|
||||||
:param icon_type: app icon type, "emoji" or "image"
|
|
||||||
:param icon: app icon
|
|
||||||
:param icon_background: app icon background
|
|
||||||
:param use_icon_as_answer_icon: use app icon as answer icon
|
|
||||||
"""
|
|
||||||
if not workflow_data:
|
|
||||||
raise MissingWorkflowDataError(
|
|
||||||
"Missing workflow in data argument when app mode is advanced-chat or workflow"
|
|
||||||
)
|
|
||||||
|
|
||||||
app = cls._create_app(
|
|
||||||
tenant_id=tenant_id,
|
|
||||||
app_mode=app_mode,
|
|
||||||
account=account,
|
|
||||||
name=name,
|
|
||||||
description=description,
|
|
||||||
icon_type=icon_type,
|
|
||||||
icon=icon,
|
|
||||||
icon_background=icon_background,
|
|
||||||
use_icon_as_answer_icon=use_icon_as_answer_icon,
|
|
||||||
)
|
|
||||||
|
|
||||||
# init draft workflow
|
|
||||||
environment_variables_list = workflow_data.get("environment_variables") or []
|
|
||||||
environment_variables = [
|
|
||||||
variable_factory.build_variable_from_mapping(obj) for obj in environment_variables_list
|
|
||||||
]
|
|
||||||
conversation_variables_list = workflow_data.get("conversation_variables") or []
|
|
||||||
conversation_variables = [
|
|
||||||
variable_factory.build_variable_from_mapping(obj) for obj in conversation_variables_list
|
|
||||||
]
|
|
||||||
workflow_service = WorkflowService()
|
|
||||||
draft_workflow = workflow_service.sync_draft_workflow(
|
|
||||||
app_model=app,
|
|
||||||
graph=workflow_data.get("graph", {}),
|
|
||||||
features=workflow_data.get("features", {}),
|
|
||||||
unique_hash=None,
|
|
||||||
account=account,
|
|
||||||
environment_variables=environment_variables,
|
|
||||||
conversation_variables=conversation_variables,
|
|
||||||
)
|
|
||||||
workflow_service.publish_workflow(app_model=app, account=account, draft_workflow=draft_workflow)
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _import_and_overwrite_workflow_based_app(
|
|
||||||
cls, app_model: App, workflow_data: Mapping[str, Any], account: Account
|
|
||||||
) -> Workflow:
|
|
||||||
"""
|
|
||||||
Import app dsl and overwrite workflow based app
|
|
||||||
|
|
||||||
:param app_model: App instance
|
|
||||||
:param workflow_data: workflow data
|
|
||||||
:param account: Account instance
|
|
||||||
"""
|
|
||||||
if not workflow_data:
|
|
||||||
raise MissingWorkflowDataError(
|
|
||||||
"Missing workflow in data argument when app mode is advanced-chat or workflow"
|
|
||||||
)
|
|
||||||
|
|
||||||
# fetch draft workflow by app_model
|
|
||||||
workflow_service = WorkflowService()
|
|
||||||
current_draft_workflow = workflow_service.get_draft_workflow(app_model=app_model)
|
|
||||||
if current_draft_workflow:
|
|
||||||
unique_hash = current_draft_workflow.unique_hash
|
|
||||||
else:
|
|
||||||
unique_hash = None
|
|
||||||
|
|
||||||
# sync draft workflow
|
|
||||||
environment_variables_list = workflow_data.get("environment_variables") or []
|
|
||||||
environment_variables = [
|
|
||||||
variable_factory.build_variable_from_mapping(obj) for obj in environment_variables_list
|
|
||||||
]
|
|
||||||
conversation_variables_list = workflow_data.get("conversation_variables") or []
|
|
||||||
conversation_variables = [
|
|
||||||
variable_factory.build_variable_from_mapping(obj) for obj in conversation_variables_list
|
|
||||||
]
|
|
||||||
draft_workflow = workflow_service.sync_draft_workflow(
|
|
||||||
app_model=app_model,
|
|
||||||
graph=workflow_data.get("graph", {}),
|
|
||||||
features=workflow_data.get("features", {}),
|
|
||||||
unique_hash=unique_hash,
|
|
||||||
account=account,
|
|
||||||
environment_variables=environment_variables,
|
|
||||||
conversation_variables=conversation_variables,
|
|
||||||
)
|
|
||||||
|
|
||||||
return draft_workflow
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _import_and_create_new_model_config_based_app(
|
|
||||||
cls,
|
|
||||||
tenant_id: str,
|
|
||||||
app_mode: AppMode,
|
|
||||||
model_config_data: Mapping[str, Any],
|
|
||||||
account: Account,
|
|
||||||
name: str,
|
|
||||||
description: str,
|
|
||||||
icon_type: str,
|
|
||||||
icon: str,
|
|
||||||
icon_background: str,
|
|
||||||
use_icon_as_answer_icon: bool,
|
|
||||||
) -> App:
|
|
||||||
"""
|
|
||||||
Import app dsl and create new model config based app
|
|
||||||
|
|
||||||
:param tenant_id: tenant id
|
|
||||||
:param app_mode: app mode
|
|
||||||
:param model_config_data: model config data
|
|
||||||
:param account: Account instance
|
|
||||||
:param name: app name
|
|
||||||
:param description: app description
|
|
||||||
:param icon: app icon
|
|
||||||
:param icon_background: app icon background
|
|
||||||
"""
|
|
||||||
if not model_config_data:
|
|
||||||
raise MissingModelConfigError(
|
|
||||||
"Missing model_config in data argument when app mode is chat, agent-chat or completion"
|
|
||||||
)
|
|
||||||
|
|
||||||
app = cls._create_app(
|
|
||||||
tenant_id=tenant_id,
|
|
||||||
app_mode=app_mode,
|
|
||||||
account=account,
|
|
||||||
name=name,
|
|
||||||
description=description,
|
|
||||||
icon_type=icon_type,
|
|
||||||
icon=icon,
|
|
||||||
icon_background=icon_background,
|
|
||||||
use_icon_as_answer_icon=use_icon_as_answer_icon,
|
|
||||||
)
|
|
||||||
|
|
||||||
app_model_config = AppModelConfig()
|
|
||||||
app_model_config = app_model_config.from_model_config_dict(model_config_data)
|
|
||||||
app_model_config.app_id = app.id
|
|
||||||
app_model_config.created_by = account.id
|
|
||||||
app_model_config.updated_by = account.id
|
|
||||||
|
|
||||||
db.session.add(app_model_config)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
app.app_model_config_id = app_model_config.id
|
|
||||||
|
|
||||||
app_model_config_was_updated.send(app, app_model_config=app_model_config)
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _create_app(
|
|
||||||
cls,
|
|
||||||
tenant_id: str,
|
|
||||||
app_mode: AppMode,
|
|
||||||
account: Account,
|
|
||||||
name: str,
|
|
||||||
description: str,
|
|
||||||
icon_type: str,
|
|
||||||
icon: str,
|
|
||||||
icon_background: str,
|
|
||||||
use_icon_as_answer_icon: bool,
|
|
||||||
) -> App:
|
|
||||||
"""
|
|
||||||
Create new app
|
|
||||||
|
|
||||||
:param tenant_id: tenant id
|
|
||||||
:param app_mode: app mode
|
|
||||||
:param account: Account instance
|
|
||||||
:param name: app name
|
|
||||||
:param description: app description
|
|
||||||
:param icon_type: app icon type, "emoji" or "image"
|
|
||||||
:param icon: app icon
|
|
||||||
:param icon_background: app icon background
|
|
||||||
:param use_icon_as_answer_icon: use app icon as answer icon
|
|
||||||
"""
|
|
||||||
app = App(
|
|
||||||
tenant_id=tenant_id,
|
|
||||||
mode=app_mode.value,
|
|
||||||
name=name,
|
|
||||||
description=description,
|
|
||||||
icon_type=icon_type,
|
|
||||||
icon=icon,
|
|
||||||
icon_background=icon_background,
|
|
||||||
enable_site=True,
|
|
||||||
enable_api=True,
|
|
||||||
use_icon_as_answer_icon=use_icon_as_answer_icon,
|
|
||||||
created_by=account.id,
|
|
||||||
updated_by=account.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
db.session.add(app)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
app_was_created.send(app, account=account)
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _append_workflow_export_data(cls, *, export_data: dict, app_model: App, include_secret: bool) -> None:
|
|
||||||
"""
|
|
||||||
Append workflow export data
|
|
||||||
:param export_data: export data
|
|
||||||
:param app_model: App instance
|
|
||||||
"""
|
|
||||||
workflow_service = WorkflowService()
|
|
||||||
workflow = workflow_service.get_draft_workflow(app_model)
|
|
||||||
if not workflow:
|
|
||||||
raise ValueError("Missing draft workflow configuration, please check.")
|
|
||||||
|
|
||||||
export_data["workflow"] = workflow.to_dict(include_secret=include_secret)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _append_model_config_export_data(cls, export_data: dict, app_model: App) -> None:
|
|
||||||
"""
|
|
||||||
Append model config export data
|
|
||||||
:param export_data: export data
|
|
||||||
:param app_model: App instance
|
|
||||||
"""
|
|
||||||
app_model_config = app_model.app_model_config
|
|
||||||
if not app_model_config:
|
|
||||||
raise ValueError("Missing app configuration, please check.")
|
|
||||||
|
|
||||||
export_data["model_config"] = app_model_config.to_dict()
|
|
||||||
|
|
||||||
|
|
||||||
def _check_or_fix_dsl(import_data: dict[str, Any]) -> Mapping[str, Any]:
|
|
||||||
"""
|
|
||||||
Check or fix dsl
|
|
||||||
|
|
||||||
:param import_data: import data
|
|
||||||
:raises DSLVersionNotSupportedError: if the imported DSL version is newer than the current version
|
|
||||||
"""
|
|
||||||
if not import_data.get("version"):
|
|
||||||
import_data["version"] = "0.1.0"
|
|
||||||
|
|
||||||
if not import_data.get("kind") or import_data.get("kind") != "app":
|
|
||||||
import_data["kind"] = "app"
|
|
||||||
|
|
||||||
imported_version = import_data.get("version")
|
|
||||||
if imported_version != current_dsl_version:
|
|
||||||
if imported_version and version.parse(imported_version) > version.parse(current_dsl_version):
|
|
||||||
errmsg = (
|
|
||||||
f"The imported DSL version {imported_version} is newer than "
|
|
||||||
f"the current supported version {current_dsl_version}. "
|
|
||||||
f"Please upgrade your Dify instance to import this configuration."
|
|
||||||
)
|
|
||||||
logger.warning(errmsg)
|
|
||||||
# raise DSLVersionNotSupportedError(errmsg)
|
|
||||||
else:
|
|
||||||
logger.warning(
|
|
||||||
f"DSL version {imported_version} is older than "
|
|
||||||
f"the current version {current_dsl_version}. "
|
|
||||||
f"This may cause compatibility issues."
|
|
||||||
)
|
|
||||||
|
|
||||||
return import_data
|
|
@ -155,7 +155,7 @@ class AppService:
|
|||||||
"""
|
"""
|
||||||
# get original app model config
|
# get original app model config
|
||||||
if app.mode == AppMode.AGENT_CHAT.value or app.is_agent:
|
if app.mode == AppMode.AGENT_CHAT.value or app.is_agent:
|
||||||
model_config: AppModelConfig = app.app_model_config
|
model_config = app.app_model_config
|
||||||
agent_mode = model_config.agent_mode_dict
|
agent_mode = model_config.agent_mode_dict
|
||||||
# decrypt agent tool parameters if it's secret-input
|
# decrypt agent tool parameters if it's secret-input
|
||||||
for tool in agent_mode.get("tools") or []:
|
for tool in agent_mode.get("tools") or []:
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
import pytest
|
|
||||||
from packaging import version
|
|
||||||
|
|
||||||
from services.app_dsl_service import AppDslService
|
|
||||||
from services.app_dsl_service.exc import DSLVersionNotSupportedError
|
|
||||||
from services.app_dsl_service.service import _check_or_fix_dsl, current_dsl_version
|
|
||||||
|
|
||||||
|
|
||||||
class TestAppDSLService:
|
|
||||||
@pytest.mark.skip(reason="Test skipped")
|
|
||||||
def test_check_or_fix_dsl_missing_version(self):
|
|
||||||
import_data = {}
|
|
||||||
result = _check_or_fix_dsl(import_data)
|
|
||||||
assert result["version"] == "0.1.0"
|
|
||||||
assert result["kind"] == "app"
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Test skipped")
|
|
||||||
def test_check_or_fix_dsl_missing_kind(self):
|
|
||||||
import_data = {"version": "0.1.0"}
|
|
||||||
result = _check_or_fix_dsl(import_data)
|
|
||||||
assert result["kind"] == "app"
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Test skipped")
|
|
||||||
def test_check_or_fix_dsl_older_version(self):
|
|
||||||
import_data = {"version": "0.0.9", "kind": "app"}
|
|
||||||
result = _check_or_fix_dsl(import_data)
|
|
||||||
assert result["version"] == "0.0.9"
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Test skipped")
|
|
||||||
def test_check_or_fix_dsl_current_version(self):
|
|
||||||
import_data = {"version": current_dsl_version, "kind": "app"}
|
|
||||||
result = _check_or_fix_dsl(import_data)
|
|
||||||
assert result["version"] == current_dsl_version
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Test skipped")
|
|
||||||
def test_check_or_fix_dsl_newer_version(self):
|
|
||||||
current_version = version.parse(current_dsl_version)
|
|
||||||
newer_version = f"{current_version.major}.{current_version.minor + 1}.0"
|
|
||||||
import_data = {"version": newer_version, "kind": "app"}
|
|
||||||
with pytest.raises(DSLVersionNotSupportedError):
|
|
||||||
_check_or_fix_dsl(import_data)
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Test skipped")
|
|
||||||
def test_check_or_fix_dsl_invalid_kind(self):
|
|
||||||
import_data = {"version": current_dsl_version, "kind": "invalid"}
|
|
||||||
result = _check_or_fix_dsl(import_data)
|
|
||||||
assert result["kind"] == "app"
|
|
@ -12,9 +12,13 @@ import Input from '@/app/components/base/input'
|
|||||||
import Modal from '@/app/components/base/modal'
|
import Modal from '@/app/components/base/modal'
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
import {
|
import {
|
||||||
importApp,
|
importDSL,
|
||||||
importAppFromUrl,
|
importDSLConfirm,
|
||||||
} from '@/service/apps'
|
} from '@/service/apps'
|
||||||
|
import {
|
||||||
|
DSLImportMode,
|
||||||
|
DSLImportStatus,
|
||||||
|
} from '@/models/app'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
||||||
@ -43,6 +47,9 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
|||||||
const [fileContent, setFileContent] = useState<string>()
|
const [fileContent, setFileContent] = useState<string>()
|
||||||
const [currentTab, setCurrentTab] = useState(activeTab)
|
const [currentTab, setCurrentTab] = useState(activeTab)
|
||||||
const [dslUrlValue, setDslUrlValue] = useState(dslUrl)
|
const [dslUrlValue, setDslUrlValue] = useState(dslUrl)
|
||||||
|
const [showErrorModal, setShowErrorModal] = useState(false)
|
||||||
|
const [versions, setVersions] = useState<{ importedVersion: string; systemVersion: string }>()
|
||||||
|
const [importId, setImportId] = useState<string>()
|
||||||
|
|
||||||
const readFile = (file: File) => {
|
const readFile = (file: File) => {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
@ -66,6 +73,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
|||||||
const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
|
const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
|
||||||
|
|
||||||
const isCreatingRef = useRef(false)
|
const isCreatingRef = useRef(false)
|
||||||
|
|
||||||
const onCreate: MouseEventHandler = async () => {
|
const onCreate: MouseEventHandler = async () => {
|
||||||
if (currentTab === CreateFromDSLModalTab.FROM_FILE && !currentFile)
|
if (currentTab === CreateFromDSLModalTab.FROM_FILE && !currentFile)
|
||||||
return
|
return
|
||||||
@ -75,25 +83,54 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
|||||||
return
|
return
|
||||||
isCreatingRef.current = true
|
isCreatingRef.current = true
|
||||||
try {
|
try {
|
||||||
let app
|
let response
|
||||||
|
|
||||||
if (currentTab === CreateFromDSLModalTab.FROM_FILE) {
|
if (currentTab === CreateFromDSLModalTab.FROM_FILE) {
|
||||||
app = await importApp({
|
response = await importDSL({
|
||||||
data: fileContent || '',
|
mode: DSLImportMode.YAML_CONTENT,
|
||||||
|
yaml_content: fileContent || '',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (currentTab === CreateFromDSLModalTab.FROM_URL) {
|
if (currentTab === CreateFromDSLModalTab.FROM_URL) {
|
||||||
app = await importAppFromUrl({
|
response = await importDSL({
|
||||||
url: dslUrlValue || '',
|
mode: DSLImportMode.YAML_URL,
|
||||||
|
yaml_url: dslUrlValue || '',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (onSuccess)
|
|
||||||
onSuccess()
|
if (!response)
|
||||||
if (onClose)
|
return
|
||||||
onClose()
|
|
||||||
notify({ type: 'success', message: t('app.newApp.appCreated') })
|
const { id, status, app_id, imported_dsl_version, current_dsl_version } = response
|
||||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
if (status === DSLImportStatus.COMPLETED || status === DSLImportStatus.COMPLETED_WITH_WARNINGS) {
|
||||||
getRedirection(isCurrentWorkspaceEditor, app, push)
|
if (onSuccess)
|
||||||
|
onSuccess()
|
||||||
|
if (onClose)
|
||||||
|
onClose()
|
||||||
|
|
||||||
|
notify({
|
||||||
|
type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning',
|
||||||
|
message: t(status === DSLImportStatus.COMPLETED ? 'app.newApp.appCreated' : 'app.newApp.caution'),
|
||||||
|
children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('app.newApp.appCreateDSLWarning'),
|
||||||
|
})
|
||||||
|
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||||
|
getRedirection(isCurrentWorkspaceEditor, { id: app_id }, push)
|
||||||
|
}
|
||||||
|
else if (status === DSLImportStatus.PENDING) {
|
||||||
|
setVersions({
|
||||||
|
importedVersion: imported_dsl_version ?? '',
|
||||||
|
systemVersion: current_dsl_version ?? '',
|
||||||
|
})
|
||||||
|
if (onClose)
|
||||||
|
onClose()
|
||||||
|
setTimeout(() => {
|
||||||
|
setShowErrorModal(true)
|
||||||
|
}, 300)
|
||||||
|
setImportId(id)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||||
@ -101,6 +138,38 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
|||||||
isCreatingRef.current = false
|
isCreatingRef.current = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onDSLConfirm: MouseEventHandler = async () => {
|
||||||
|
try {
|
||||||
|
if (!importId)
|
||||||
|
return
|
||||||
|
const response = await importDSLConfirm({
|
||||||
|
import_id: importId,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { status, app_id } = response
|
||||||
|
|
||||||
|
if (status === DSLImportStatus.COMPLETED) {
|
||||||
|
if (onSuccess)
|
||||||
|
onSuccess()
|
||||||
|
if (onClose)
|
||||||
|
onClose()
|
||||||
|
|
||||||
|
notify({
|
||||||
|
type: 'success',
|
||||||
|
message: t('app.newApp.appCreated'),
|
||||||
|
})
|
||||||
|
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||||
|
getRedirection(isCurrentWorkspaceEditor, { id: app_id }, push)
|
||||||
|
}
|
||||||
|
else if (status === DSLImportStatus.FAILED) {
|
||||||
|
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
key: CreateFromDSLModalTab.FROM_FILE,
|
key: CreateFromDSLModalTab.FROM_FILE,
|
||||||
@ -123,74 +192,96 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
|||||||
}, [isAppsFull, currentTab, currentFile, dslUrlValue])
|
}, [isAppsFull, currentTab, currentFile, dslUrlValue])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<>
|
||||||
className='p-0 w-[520px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl'
|
<Modal
|
||||||
isShow={show}
|
className='p-0 w-[520px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl'
|
||||||
onClose={() => { }}
|
isShow={show}
|
||||||
>
|
onClose={() => { }}
|
||||||
<div className='flex items-center justify-between pt-6 pl-6 pr-5 pb-3 text-text-primary title-2xl-semi-bold'>
|
>
|
||||||
{t('app.importFromDSL')}
|
<div className='flex items-center justify-between pt-6 pl-6 pr-5 pb-3 text-text-primary title-2xl-semi-bold'>
|
||||||
<div
|
{t('app.importFromDSL')}
|
||||||
className='flex items-center w-8 h-8 cursor-pointer'
|
<div
|
||||||
onClick={() => onClose()}
|
className='flex items-center w-8 h-8 cursor-pointer'
|
||||||
>
|
onClick={() => onClose()}
|
||||||
<RiCloseLine className='w-5 h-5 text-text-tertiary' />
|
>
|
||||||
|
<RiCloseLine className='w-5 h-5 text-text-tertiary' />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className='flex items-center px-6 h-9 space-x-6 system-md-semibold text-text-tertiary border-b border-divider-subtle'>
|
||||||
<div className='flex items-center px-6 h-9 space-x-6 system-md-semibold text-text-tertiary border-b border-divider-subtle'>
|
{
|
||||||
{
|
tabs.map(tab => (
|
||||||
tabs.map(tab => (
|
<div
|
||||||
<div
|
key={tab.key}
|
||||||
key={tab.key}
|
className={cn(
|
||||||
className={cn(
|
'relative flex items-center h-full cursor-pointer',
|
||||||
'relative flex items-center h-full cursor-pointer',
|
currentTab === tab.key && 'text-text-primary',
|
||||||
currentTab === tab.key && 'text-text-primary',
|
)}
|
||||||
)}
|
onClick={() => setCurrentTab(tab.key)}
|
||||||
onClick={() => setCurrentTab(tab.key)}
|
>
|
||||||
>
|
{tab.label}
|
||||||
{tab.label}
|
{
|
||||||
{
|
currentTab === tab.key && (
|
||||||
currentTab === tab.key && (
|
<div className='absolute bottom-0 w-full h-[2px] bg-util-colors-blue-brand-blue-brand-600'></div>
|
||||||
<div className='absolute bottom-0 w-full h-[2px] bg-util-colors-blue-brand-blue-brand-600'></div>
|
)
|
||||||
)
|
}
|
||||||
}
|
</div>
|
||||||
</div>
|
))
|
||||||
))
|
}
|
||||||
}
|
</div>
|
||||||
</div>
|
<div className='px-6 py-4'>
|
||||||
<div className='px-6 py-4'>
|
{
|
||||||
{
|
currentTab === CreateFromDSLModalTab.FROM_FILE && (
|
||||||
currentTab === CreateFromDSLModalTab.FROM_FILE && (
|
<Uploader
|
||||||
<Uploader
|
className='mt-0'
|
||||||
className='mt-0'
|
file={currentFile}
|
||||||
file={currentFile}
|
updateFile={handleFile}
|
||||||
updateFile={handleFile}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
currentTab === CreateFromDSLModalTab.FROM_URL && (
|
|
||||||
<div>
|
|
||||||
<div className='mb-1 system-md-semibold leading6'>DSL URL</div>
|
|
||||||
<Input
|
|
||||||
placeholder={t('app.importFromDSLUrlPlaceholder') || ''}
|
|
||||||
value={dslUrlValue}
|
|
||||||
onChange={e => setDslUrlValue(e.target.value)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
)
|
||||||
)
|
}
|
||||||
}
|
{
|
||||||
</div>
|
currentTab === CreateFromDSLModalTab.FROM_URL && (
|
||||||
{isAppsFull && (
|
<div>
|
||||||
<div className='px-6'>
|
<div className='mb-1 system-md-semibold leading6'>DSL URL</div>
|
||||||
<AppsFull className='mt-0' loc='app-create-dsl' />
|
<Input
|
||||||
|
placeholder={t('app.importFromDSLUrlPlaceholder') || ''}
|
||||||
|
value={dslUrlValue}
|
||||||
|
onChange={e => setDslUrlValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
)}
|
{isAppsFull && (
|
||||||
<div className='flex justify-end px-6 py-5'>
|
<div className='px-6'>
|
||||||
<Button className='mr-2' onClick={onClose}>{t('app.newApp.Cancel')}</Button>
|
<AppsFull className='mt-0' loc='app-create-dsl' />
|
||||||
<Button disabled={buttonDisabled} variant="primary" onClick={onCreate}>{t('app.newApp.Create')}</Button>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</Modal>
|
<div className='flex justify-end px-6 py-5'>
|
||||||
|
<Button className='mr-2' onClick={onClose}>{t('app.newApp.Cancel')}</Button>
|
||||||
|
<Button disabled={buttonDisabled} variant="primary" onClick={onCreate}>{t('app.newApp.Create')}</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
isShow={showErrorModal}
|
||||||
|
onClose={() => setShowErrorModal(false)}
|
||||||
|
className='w-[480px]'
|
||||||
|
>
|
||||||
|
<div className='flex pb-4 flex-col items-start gap-2 self-stretch'>
|
||||||
|
<div className='text-text-primary title-2xl-semi-bold'>{t('app.newApp.appCreateDSLErrorTitle')}</div>
|
||||||
|
<div className='flex flex-grow flex-col text-text-secondary system-md-regular'>
|
||||||
|
<div>{t('app.newApp.appCreateDSLErrorPart1')}</div>
|
||||||
|
<div>{t('app.newApp.appCreateDSLErrorPart2')}</div>
|
||||||
|
<br />
|
||||||
|
<div>{t('app.newApp.appCreateDSLErrorPart3')}<span className='system-md-medium'>{versions?.importedVersion}</span></div>
|
||||||
|
<div>{t('app.newApp.appCreateDSLErrorPart4')}<span className='system-md-medium'>{versions?.systemVersion}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex pt-6 justify-end items-start gap-2 self-stretch'>
|
||||||
|
<Button variant='secondary' onClick={() => setShowErrorModal(false)}>{t('app.newApp.Cancel')}</Button>
|
||||||
|
<Button variant='primary' destructive onClick={onDSLConfirm}>{t('app.newApp.Confirm')}</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useContext } from 'use-context-selector'
|
import { useContext } from 'use-context-selector'
|
||||||
|
import { formatFileSize } from '@/utils/format'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import { Yaml as YamlIcon } from '@/app/components/base/icons/src/public/files'
|
import { Yaml as YamlIcon } from '@/app/components/base/icons/src/public/files'
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
@ -58,8 +59,13 @@ const Uploader: FC<Props> = ({
|
|||||||
updateFile(files[0])
|
updateFile(files[0])
|
||||||
}
|
}
|
||||||
const selectHandle = () => {
|
const selectHandle = () => {
|
||||||
if (fileUploader.current)
|
const originalFile = file
|
||||||
|
if (fileUploader.current) {
|
||||||
|
fileUploader.current.value = ''
|
||||||
fileUploader.current.click()
|
fileUploader.current.click()
|
||||||
|
// If no file is selected, restore the original file
|
||||||
|
fileUploader.current.oncancel = () => updateFile(originalFile)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const removeFile = () => {
|
const removeFile = () => {
|
||||||
if (fileUploader.current)
|
if (fileUploader.current)
|
||||||
@ -96,7 +102,7 @@ const Uploader: FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
<div ref={dropRef}>
|
<div ref={dropRef}>
|
||||||
{!file && (
|
{!file && (
|
||||||
<div className={cn('flex items-center h-20 rounded-xl bg-gray-50 border border-dashed border-gray-200 text-sm font-normal', dragging && 'bg-[#F5F8FF] border border-[#B2CCFF]')}>
|
<div className={cn('flex items-center h-12 rounded-xl bg-gray-50 border border-dashed border-gray-200 text-sm font-normal', dragging && 'bg-[#F5F8FF] border border-[#B2CCFF]')}>
|
||||||
<div className='w-full flex items-center justify-center space-x-2'>
|
<div className='w-full flex items-center justify-center space-x-2'>
|
||||||
<UploadCloud01 className='w-6 h-6 mr-2' />
|
<UploadCloud01 className='w-6 h-6 mr-2' />
|
||||||
<div className='text-gray-500'>
|
<div className='text-gray-500'>
|
||||||
@ -108,17 +114,23 @@ const Uploader: FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{file && (
|
{file && (
|
||||||
<div className={cn('flex items-center h-20 px-6 rounded-xl bg-gray-50 border border-gray-200 text-sm font-normal group', 'hover:bg-[#F5F8FF] hover:border-[#B2CCFF]')}>
|
<div className={cn('flex items-center rounded-lg bg-components-panel-on-panel-item-bg border-[0.5px] border-components-panel-border shadow-xs group', 'hover:bg-[#F5F8FF] hover:border-[#B2CCFF]')}>
|
||||||
<YamlIcon className="shrink-0" />
|
<div className='flex p-3 justify-center items-center'>
|
||||||
<div className='flex ml-2 w-0 grow'>
|
<YamlIcon className="w-6 h-6 shrink-0" />
|
||||||
<span className='max-w-[calc(100%_-_30px)] text-ellipsis whitespace-nowrap overflow-hidden text-gray-800'>{file.name.replace(/(.yaml|.yml)$/, '')}</span>
|
</div>
|
||||||
<span className='shrink-0 text-gray-500'>.yml</span>
|
<div className='flex py-1 pr-2 grow flex-col items-start gap-0.5'>
|
||||||
|
<span className='max-w-[calc(100%_-_30px)] text-ellipsis whitespace-nowrap overflow-hidden text-text-secondary font-inter text-[12px] font-medium leading-4'>{file.name}</span>
|
||||||
|
<div className='flex h-3 items-center gap-1 self-stretch text-text-tertiary font-inter text-[10px] font-medium leading-3 uppercase'>
|
||||||
|
<span>YAML</span>
|
||||||
|
<span className='text-text-quaternary'>·</span>
|
||||||
|
<span>{formatFileSize(file.size)}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='hidden group-hover:flex items-center'>
|
<div className='hidden group-hover:flex items-center'>
|
||||||
<Button onClick={selectHandle}>{t('datasetCreation.stepOne.uploader.change')}</Button>
|
<Button onClick={selectHandle}>{t('datasetCreation.stepOne.uploader.change')}</Button>
|
||||||
<div className='mx-2 w-px h-4 bg-gray-200' />
|
<div className='mx-2 w-px h-4 bg-gray-200' />
|
||||||
<div className='p-2 cursor-pointer' onClick={removeFile}>
|
<div className='p-2 cursor-pointer' onClick={removeFile}>
|
||||||
<RiDeleteBinLine className='w-4 h-4 text-gray-500' />
|
<RiDeleteBinLine className='w-4 h-4 text-text-tertiary' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,16 +3,19 @@ import type { ReactNode } from 'react'
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import {
|
import {
|
||||||
CheckCircleIcon,
|
RiAlertFill,
|
||||||
ExclamationTriangleIcon,
|
RiCheckboxCircleFill,
|
||||||
InformationCircleIcon,
|
RiCloseLine,
|
||||||
XCircleIcon,
|
RiErrorWarningFill,
|
||||||
} from '@heroicons/react/20/solid'
|
RiInformation2Fill,
|
||||||
|
} from '@remixicon/react'
|
||||||
import { createContext, useContext } from 'use-context-selector'
|
import { createContext, useContext } from 'use-context-selector'
|
||||||
|
import ActionButton from '@/app/components/base/action-button'
|
||||||
import classNames from '@/utils/classnames'
|
import classNames from '@/utils/classnames'
|
||||||
|
|
||||||
export type IToastProps = {
|
export type IToastProps = {
|
||||||
type?: 'success' | 'error' | 'warning' | 'info'
|
type?: 'success' | 'error' | 'warning' | 'info'
|
||||||
|
size?: 'md' | 'sm'
|
||||||
duration?: number
|
duration?: number
|
||||||
message: string
|
message: string
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
@ -21,60 +24,55 @@ export type IToastProps = {
|
|||||||
}
|
}
|
||||||
type IToastContext = {
|
type IToastContext = {
|
||||||
notify: (props: IToastProps) => void
|
notify: (props: IToastProps) => void
|
||||||
|
close: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ToastContext = createContext<IToastContext>({} as IToastContext)
|
export const ToastContext = createContext<IToastContext>({} as IToastContext)
|
||||||
export const useToastContext = () => useContext(ToastContext)
|
export const useToastContext = () => useContext(ToastContext)
|
||||||
const Toast = ({
|
const Toast = ({
|
||||||
type = 'info',
|
type = 'info',
|
||||||
|
size = 'md',
|
||||||
message,
|
message,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
}: IToastProps) => {
|
}: IToastProps) => {
|
||||||
|
const { close } = useToastContext()
|
||||||
// sometimes message is react node array. Not handle it.
|
// sometimes message is react node array. Not handle it.
|
||||||
if (typeof message !== 'string')
|
if (typeof message !== 'string')
|
||||||
return null
|
return null
|
||||||
|
|
||||||
return <div className={classNames(
|
return <div className={classNames(
|
||||||
className,
|
className,
|
||||||
'fixed rounded-md p-4 my-4 mx-8 z-[9999]',
|
'fixed w-[360px] rounded-xl my-4 mx-8 flex-grow z-[9999] overflow-hidden',
|
||||||
|
size === 'md' ? 'p-3' : 'p-2',
|
||||||
|
'border border-components-panel-border-subtle bg-components-panel-bg-blur shadow-sm',
|
||||||
'top-0',
|
'top-0',
|
||||||
'right-0',
|
'right-0',
|
||||||
type === 'success' ? 'bg-green-50' : '',
|
|
||||||
type === 'error' ? 'bg-red-50' : '',
|
|
||||||
type === 'warning' ? 'bg-yellow-50' : '',
|
|
||||||
type === 'info' ? 'bg-blue-50' : '',
|
|
||||||
)}>
|
)}>
|
||||||
<div className="flex">
|
<div className={`absolute inset-0 opacity-40 ${
|
||||||
<div className="flex-shrink-0">
|
(type === 'success' && 'bg-[linear-gradient(92deg,rgba(23,178,106,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|
||||||
{type === 'success' && <CheckCircleIcon className="w-5 h-5 text-green-400" aria-hidden="true" />}
|
|| (type === 'warning' && 'bg-[linear-gradient(92deg,rgba(247,144,9,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|
||||||
{type === 'error' && <XCircleIcon className="w-5 h-5 text-red-400" aria-hidden="true" />}
|
|| (type === 'error' && 'bg-[linear-gradient(92deg,rgba(240,68,56,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|
||||||
{type === 'warning' && <ExclamationTriangleIcon className="w-5 h-5 text-yellow-400" aria-hidden="true" />}
|
|| (type === 'info' && 'bg-[linear-gradient(92deg,rgba(11,165,236,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|
||||||
{type === 'info' && <InformationCircleIcon className="w-5 h-5 text-blue-400" aria-hidden="true" />}
|
}`}
|
||||||
|
/>
|
||||||
|
<div className={`flex ${size === 'md' ? 'gap-1' : 'gap-0.5'}`}>
|
||||||
|
<div className={`flex justify-center items-center ${size === 'md' ? 'p-0.5' : 'p-1'}`}>
|
||||||
|
{type === 'success' && <RiCheckboxCircleFill className={`${size === 'md' ? 'w-5 h-5' : 'w-4 h-4'} text-text-success`} aria-hidden="true" />}
|
||||||
|
{type === 'error' && <RiErrorWarningFill className={`${size === 'md' ? 'w-5 h-5' : 'w-4 h-4'} text-text-destructive`} aria-hidden="true" />}
|
||||||
|
{type === 'warning' && <RiAlertFill className={`${size === 'md' ? 'w-5 h-5' : 'w-4 h-4'} text-text-warning-secondary`} aria-hidden="true" />}
|
||||||
|
{type === 'info' && <RiInformation2Fill className={`${size === 'md' ? 'w-5 h-5' : 'w-4 h-4'} text-text-accent`} aria-hidden="true" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3">
|
<div className={`flex py-1 ${size === 'md' ? 'px-1' : 'px-0.5'} flex-col items-start gap-1 flex-grow`}>
|
||||||
<h3 className={
|
<div className='text-text-primary system-sm-semibold'>{message}</div>
|
||||||
classNames(
|
{children && <div className='text-text-secondary system-xs-regular'>
|
||||||
'text-sm font-medium',
|
|
||||||
type === 'success' ? 'text-green-800' : '',
|
|
||||||
type === 'error' ? 'text-red-800' : '',
|
|
||||||
type === 'warning' ? 'text-yellow-800' : '',
|
|
||||||
type === 'info' ? 'text-blue-800' : '',
|
|
||||||
)
|
|
||||||
}>{message}</h3>
|
|
||||||
{children && <div className={
|
|
||||||
classNames(
|
|
||||||
'mt-2 text-sm',
|
|
||||||
type === 'success' ? 'text-green-700' : '',
|
|
||||||
type === 'error' ? 'text-red-700' : '',
|
|
||||||
type === 'warning' ? 'text-yellow-700' : '',
|
|
||||||
type === 'info' ? 'text-blue-700' : '',
|
|
||||||
)
|
|
||||||
}>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<ActionButton className='z-[1000]' onClick={close}>
|
||||||
|
<RiCloseLine className='w-4 h-4 flex-shrink-0 text-text-tertiary' />
|
||||||
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -106,6 +104,7 @@ export const ToastProvider = ({
|
|||||||
setMounted(true)
|
setMounted(true)
|
||||||
setParams(props)
|
setParams(props)
|
||||||
},
|
},
|
||||||
|
close: () => setMounted(false),
|
||||||
}}>
|
}}>
|
||||||
{mounted && <Toast {...params} />}
|
{mounted && <Toast {...params} />}
|
||||||
{children}
|
{children}
|
||||||
@ -114,16 +113,17 @@ export const ToastProvider = ({
|
|||||||
|
|
||||||
Toast.notify = ({
|
Toast.notify = ({
|
||||||
type,
|
type,
|
||||||
|
size = 'md',
|
||||||
message,
|
message,
|
||||||
duration,
|
duration,
|
||||||
className,
|
className,
|
||||||
}: Pick<IToastProps, 'type' | 'message' | 'duration' | 'className'>) => {
|
}: Pick<IToastProps, 'type' | 'size' | 'message' | 'duration' | 'className'>) => {
|
||||||
const defaultDuring = (type === 'success' || type === 'info') ? 3000 : 6000
|
const defaultDuring = (type === 'success' || type === 'info') ? 3000 : 6000
|
||||||
if (typeof window === 'object') {
|
if (typeof window === 'object') {
|
||||||
const holder = document.createElement('div')
|
const holder = document.createElement('div')
|
||||||
const root = createRoot(holder)
|
const root = createRoot(holder)
|
||||||
|
|
||||||
root.render(<Toast type={type} message={message} duration={duration} className={className} />)
|
root.render(<Toast type={type} size={size} message={message} duration={duration} className={className} />)
|
||||||
document.body.appendChild(holder)
|
document.body.appendChild(holder)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (holder)
|
if (holder)
|
||||||
|
@ -10,8 +10,9 @@ import {
|
|||||||
import { useContext } from 'use-context-selector'
|
import { useContext } from 'use-context-selector'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
RiAlertLine,
|
RiAlertFill,
|
||||||
RiCloseLine,
|
RiCloseLine,
|
||||||
|
RiFileDownloadLine,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import { WORKFLOW_DATA_UPDATE } from './constants'
|
import { WORKFLOW_DATA_UPDATE } from './constants'
|
||||||
import {
|
import {
|
||||||
@ -21,11 +22,19 @@ import {
|
|||||||
initialEdges,
|
initialEdges,
|
||||||
initialNodes,
|
initialNodes,
|
||||||
} from './utils'
|
} from './utils'
|
||||||
|
import {
|
||||||
|
importDSL,
|
||||||
|
importDSLConfirm,
|
||||||
|
} from '@/service/apps'
|
||||||
|
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||||
|
import {
|
||||||
|
DSLImportMode,
|
||||||
|
DSLImportStatus,
|
||||||
|
} from '@/models/app'
|
||||||
import Uploader from '@/app/components/app/create-from-dsl-modal/uploader'
|
import Uploader from '@/app/components/app/create-from-dsl-modal/uploader'
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import Modal from '@/app/components/base/modal'
|
import Modal from '@/app/components/base/modal'
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
import { updateWorkflowDraftFromDSL } from '@/service/workflow'
|
|
||||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||||
@ -48,6 +57,10 @@ const UpdateDSLModal = ({
|
|||||||
const [fileContent, setFileContent] = useState<string>()
|
const [fileContent, setFileContent] = useState<string>()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const { eventEmitter } = useEventEmitterContextContext()
|
const { eventEmitter } = useEventEmitterContextContext()
|
||||||
|
const [show, setShow] = useState(true)
|
||||||
|
const [showErrorModal, setShowErrorModal] = useState(false)
|
||||||
|
const [versions, setVersions] = useState<{ importedVersion: string; systemVersion: string }>()
|
||||||
|
const [importId, setImportId] = useState<string>()
|
||||||
|
|
||||||
const readFile = (file: File) => {
|
const readFile = (file: File) => {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
@ -66,6 +79,51 @@ const UpdateDSLModal = ({
|
|||||||
setFileContent('')
|
setFileContent('')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleWorkflowUpdate = async (app_id: string) => {
|
||||||
|
const {
|
||||||
|
graph,
|
||||||
|
features,
|
||||||
|
hash,
|
||||||
|
} = await fetchWorkflowDraft(`/apps/${app_id}/workflows/draft`)
|
||||||
|
|
||||||
|
const { nodes, edges, viewport } = graph
|
||||||
|
const newFeatures = {
|
||||||
|
file: {
|
||||||
|
image: {
|
||||||
|
enabled: !!features.file_upload?.image?.enabled,
|
||||||
|
number_limits: features.file_upload?.image?.number_limits || 3,
|
||||||
|
transfer_methods: features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
||||||
|
},
|
||||||
|
enabled: !!(features.file_upload?.enabled || features.file_upload?.image?.enabled),
|
||||||
|
allowed_file_types: features.file_upload?.allowed_file_types || [SupportUploadFileTypes.image],
|
||||||
|
allowed_file_extensions: features.file_upload?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image].map(ext => `.${ext}`),
|
||||||
|
allowed_file_upload_methods: features.file_upload?.allowed_file_upload_methods || features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
||||||
|
number_limits: features.file_upload?.number_limits || features.file_upload?.image?.number_limits || 3,
|
||||||
|
},
|
||||||
|
opening: {
|
||||||
|
enabled: !!features.opening_statement,
|
||||||
|
opening_statement: features.opening_statement,
|
||||||
|
suggested_questions: features.suggested_questions,
|
||||||
|
},
|
||||||
|
suggested: features.suggested_questions_after_answer || { enabled: false },
|
||||||
|
speech2text: features.speech_to_text || { enabled: false },
|
||||||
|
text2speech: features.text_to_speech || { enabled: false },
|
||||||
|
citation: features.retriever_resource || { enabled: false },
|
||||||
|
moderation: features.sensitive_word_avoidance || { enabled: false },
|
||||||
|
}
|
||||||
|
|
||||||
|
eventEmitter?.emit({
|
||||||
|
type: WORKFLOW_DATA_UPDATE,
|
||||||
|
payload: {
|
||||||
|
nodes: initialNodes(nodes, edges),
|
||||||
|
edges: initialEdges(edges, nodes),
|
||||||
|
viewport,
|
||||||
|
features: newFeatures,
|
||||||
|
hash,
|
||||||
|
},
|
||||||
|
} as any)
|
||||||
|
}
|
||||||
|
|
||||||
const isCreatingRef = useRef(false)
|
const isCreatingRef = useRef(false)
|
||||||
const handleImport: MouseEventHandler = useCallback(async () => {
|
const handleImport: MouseEventHandler = useCallback(async () => {
|
||||||
if (isCreatingRef.current)
|
if (isCreatingRef.current)
|
||||||
@ -76,51 +134,39 @@ const UpdateDSLModal = ({
|
|||||||
try {
|
try {
|
||||||
if (appDetail && fileContent) {
|
if (appDetail && fileContent) {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const {
|
const response = await importDSL({ mode: DSLImportMode.YAML_CONTENT, yaml_content: fileContent, app_id: appDetail.id })
|
||||||
graph,
|
const { id, status, app_id, imported_dsl_version, current_dsl_version } = response
|
||||||
features,
|
if (status === DSLImportStatus.COMPLETED || status === DSLImportStatus.COMPLETED_WITH_WARNINGS) {
|
||||||
hash,
|
if (!app_id) {
|
||||||
} = await updateWorkflowDraftFromDSL(appDetail.id, fileContent)
|
notify({ type: 'error', message: t('workflow.common.importFailure') })
|
||||||
const { nodes, edges, viewport } = graph
|
return
|
||||||
const newFeatures = {
|
}
|
||||||
file: {
|
handleWorkflowUpdate(app_id)
|
||||||
image: {
|
if (onImport)
|
||||||
enabled: !!features.file_upload?.image?.enabled,
|
onImport()
|
||||||
number_limits: features.file_upload?.image?.number_limits || 3,
|
notify({
|
||||||
transfer_methods: features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning',
|
||||||
},
|
message: t(status === DSLImportStatus.COMPLETED ? 'workflow.common.importSuccess' : 'workflow.common.importWarning'),
|
||||||
enabled: !!(features.file_upload?.enabled || features.file_upload?.image?.enabled),
|
children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('workflow.common.importWarningDetails'),
|
||||||
allowed_file_types: features.file_upload?.allowed_file_types || [SupportUploadFileTypes.image],
|
})
|
||||||
allowed_file_extensions: features.file_upload?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image].map(ext => `.${ext}`),
|
setLoading(false)
|
||||||
allowed_file_upload_methods: features.file_upload?.allowed_file_upload_methods || features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
onCancel()
|
||||||
number_limits: features.file_upload?.number_limits || features.file_upload?.image?.number_limits || 3,
|
}
|
||||||
},
|
else if (status === DSLImportStatus.PENDING) {
|
||||||
opening: {
|
setShow(false)
|
||||||
enabled: !!features.opening_statement,
|
setTimeout(() => {
|
||||||
opening_statement: features.opening_statement,
|
setShowErrorModal(true)
|
||||||
suggested_questions: features.suggested_questions,
|
}, 300)
|
||||||
},
|
setVersions({
|
||||||
suggested: features.suggested_questions_after_answer || { enabled: false },
|
importedVersion: imported_dsl_version ?? '',
|
||||||
speech2text: features.speech_to_text || { enabled: false },
|
systemVersion: current_dsl_version ?? '',
|
||||||
text2speech: features.text_to_speech || { enabled: false },
|
})
|
||||||
citation: features.retriever_resource || { enabled: false },
|
setImportId(id)
|
||||||
moderation: features.sensitive_word_avoidance || { enabled: false },
|
}
|
||||||
|
else {
|
||||||
|
setLoading(false)
|
||||||
|
notify({ type: 'error', message: t('workflow.common.importFailure') })
|
||||||
}
|
}
|
||||||
eventEmitter?.emit({
|
|
||||||
type: WORKFLOW_DATA_UPDATE,
|
|
||||||
payload: {
|
|
||||||
nodes: initialNodes(nodes, edges),
|
|
||||||
edges: initialEdges(edges, nodes),
|
|
||||||
viewport,
|
|
||||||
features: newFeatures,
|
|
||||||
hash,
|
|
||||||
},
|
|
||||||
} as any)
|
|
||||||
if (onImport)
|
|
||||||
onImport()
|
|
||||||
notify({ type: 'success', message: t('workflow.common.importSuccess') })
|
|
||||||
setLoading(false)
|
|
||||||
onCancel()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
@ -130,52 +176,119 @@ const UpdateDSLModal = ({
|
|||||||
isCreatingRef.current = false
|
isCreatingRef.current = false
|
||||||
}, [currentFile, fileContent, onCancel, notify, t, eventEmitter, appDetail, onImport])
|
}, [currentFile, fileContent, onCancel, notify, t, eventEmitter, appDetail, onImport])
|
||||||
|
|
||||||
|
const onUpdateDSLConfirm: MouseEventHandler = async () => {
|
||||||
|
try {
|
||||||
|
if (!importId)
|
||||||
|
return
|
||||||
|
const response = await importDSLConfirm({
|
||||||
|
import_id: importId,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { status, app_id } = response
|
||||||
|
|
||||||
|
if (status === DSLImportStatus.COMPLETED) {
|
||||||
|
if (!app_id) {
|
||||||
|
notify({ type: 'error', message: t('workflow.common.importFailure') })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleWorkflowUpdate(app_id)
|
||||||
|
if (onImport)
|
||||||
|
onImport()
|
||||||
|
notify({ type: 'success', message: t('workflow.common.importSuccess') })
|
||||||
|
setLoading(false)
|
||||||
|
onCancel()
|
||||||
|
}
|
||||||
|
else if (status === DSLImportStatus.FAILED) {
|
||||||
|
setLoading(false)
|
||||||
|
notify({ type: 'error', message: t('workflow.common.importFailure') })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
setLoading(false)
|
||||||
|
notify({ type: 'error', message: t('workflow.common.importFailure') })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<>
|
||||||
className='p-6 w-[520px] rounded-2xl'
|
<Modal
|
||||||
isShow={true}
|
className='p-6 w-[520px] rounded-2xl'
|
||||||
onClose={() => {}}
|
isShow={show}
|
||||||
>
|
onClose={onCancel}
|
||||||
<div className='flex items-center justify-between mb-6'>
|
>
|
||||||
<div className='text-2xl font-semibold text-[#101828]'>{t('workflow.common.importDSL')}</div>
|
<div className='flex items-center justify-between mb-3'>
|
||||||
<div className='flex items-center justify-center w-[22px] h-[22px] cursor-pointer' onClick={onCancel}>
|
<div className='title-2xl-semi-bold text-text-primary'>{t('workflow.common.importDSL')}</div>
|
||||||
<RiCloseLine className='w-5 h-5 text-gray-500' />
|
<div className='flex items-center justify-center w-[22px] h-[22px] cursor-pointer' onClick={onCancel}>
|
||||||
|
<RiCloseLine className='w-[18px] h-[18px] text-text-tertiary' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex relative p-2 mb-2 gap-0.5 flex-grow rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-xs overflow-hidden'>
|
||||||
|
<div className='absolute top-0 left-0 w-full h-full opacity-40 bg-[linear-gradient(92deg,rgba(247,144,9,0.25)_0%,rgba(255,255,255,0.00)_100%)]' />
|
||||||
|
<div className='flex p-1 justify-center items-start'>
|
||||||
|
<RiAlertFill className='w-4 h-4 flex-shrink-0 text-text-warning-secondary' />
|
||||||
|
</div>
|
||||||
|
<div className='flex py-1 flex-col items-start gap-0.5 flex-grow'>
|
||||||
|
<div className='text-text-primary system-xs-medium whitespace-pre-line'>{t('workflow.common.importDSLTip')}</div>
|
||||||
|
<div className='flex pt-1 pb-0.5 items-start gap-1 self-stretch'>
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
variant='secondary'
|
||||||
|
className='z-[1000]'
|
||||||
|
onClick={onBackup}
|
||||||
|
>
|
||||||
|
<RiFileDownloadLine className='w-3.5 h-3.5 text-components-button-secondary-text' />
|
||||||
|
<div className='flex px-[3px] justify-center items-center gap-1'>
|
||||||
|
{t('workflow.common.backupCurrentDraft')}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className='flex mb-4 px-4 py-3 bg-[#FFFAEB] rounded-xl border border-[#FEDF89]'>
|
|
||||||
<RiAlertLine className='shrink-0 mt-0.5 mr-2 w-4 h-4 text-[#F79009]' />
|
|
||||||
<div>
|
<div>
|
||||||
<div className='mb-2 text-sm font-medium text-[#354052]'>{t('workflow.common.importDSLTip')}</div>
|
<div className='pt-2 text-text-primary system-md-semibold'>
|
||||||
|
{t('workflow.common.chooseDSL')}
|
||||||
|
</div>
|
||||||
|
<div className='flex w-full py-4 flex-col justify-center items-start gap-4 self-stretch'>
|
||||||
|
<Uploader
|
||||||
|
file={currentFile}
|
||||||
|
updateFile={handleFile}
|
||||||
|
className='!mt-0 w-full'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex pt-5 gap-2 items-center justify-end self-stretch'>
|
||||||
|
<Button onClick={onCancel}>{t('app.newApp.Cancel')}</Button>
|
||||||
<Button
|
<Button
|
||||||
variant='secondary-accent'
|
disabled={!currentFile || loading}
|
||||||
onClick={onBackup}
|
variant='warning'
|
||||||
|
onClick={handleImport}
|
||||||
|
loading={loading}
|
||||||
>
|
>
|
||||||
{t('workflow.common.backupCurrentDraft')}
|
{t('workflow.common.overwriteAndImport')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
<div className='mb-8'>
|
<Modal
|
||||||
<div className='mb-1 text-[13px] font-semibold text-[#354052]'>
|
isShow={showErrorModal}
|
||||||
{t('workflow.common.chooseDSL')}
|
onClose={() => setShowErrorModal(false)}
|
||||||
|
className='w-[480px]'
|
||||||
|
>
|
||||||
|
<div className='flex pb-4 flex-col items-start gap-2 self-stretch'>
|
||||||
|
<div className='text-text-primary title-2xl-semi-bold'>{t('app.newApp.appCreateDSLErrorTitle')}</div>
|
||||||
|
<div className='flex flex-grow flex-col text-text-secondary system-md-regular'>
|
||||||
|
<div>{t('app.newApp.appCreateDSLErrorPart1')}</div>
|
||||||
|
<div>{t('app.newApp.appCreateDSLErrorPart2')}</div>
|
||||||
|
<br />
|
||||||
|
<div>{t('app.newApp.appCreateDSLErrorPart3')}<span className='system-md-medium'>{versions?.importedVersion}</span></div>
|
||||||
|
<div>{t('app.newApp.appCreateDSLErrorPart4')}<span className='system-md-medium'>{versions?.systemVersion}</span></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Uploader
|
<div className='flex pt-6 justify-end items-start gap-2 self-stretch'>
|
||||||
file={currentFile}
|
<Button variant='secondary' onClick={() => setShowErrorModal(false)}>{t('app.newApp.Cancel')}</Button>
|
||||||
updateFile={handleFile}
|
<Button variant='primary' destructive onClick={onUpdateDSLConfirm}>{t('app.newApp.Confirm')}</Button>
|
||||||
className='!mt-0'
|
</div>
|
||||||
/>
|
</Modal>
|
||||||
</div>
|
</>
|
||||||
<div className='flex justify-end'>
|
|
||||||
<Button className='mr-2' onClick={onCancel}>{t('app.newApp.Cancel')}</Button>
|
|
||||||
<Button
|
|
||||||
disabled={!currentFile || loading}
|
|
||||||
variant='warning'
|
|
||||||
onClick={handleImport}
|
|
||||||
loading={loading}
|
|
||||||
>
|
|
||||||
{t('workflow.common.overwriteAndImport')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,10 +61,18 @@ const translation = {
|
|||||||
hideTemplates: 'Go back to mode selection',
|
hideTemplates: 'Go back to mode selection',
|
||||||
Create: 'Create',
|
Create: 'Create',
|
||||||
Cancel: 'Cancel',
|
Cancel: 'Cancel',
|
||||||
|
Confirm: 'Confirm',
|
||||||
nameNotEmpty: 'Name cannot be empty',
|
nameNotEmpty: 'Name cannot be empty',
|
||||||
appTemplateNotSelected: 'Please select a template',
|
appTemplateNotSelected: 'Please select a template',
|
||||||
appTypeRequired: 'Please select an app type',
|
appTypeRequired: 'Please select an app type',
|
||||||
appCreated: 'App created',
|
appCreated: 'App created',
|
||||||
|
caution: 'Caution',
|
||||||
|
appCreateDSLWarning: 'Caution: DSL version difference may affect certain features',
|
||||||
|
appCreateDSLErrorTitle: 'Version Incompatibility',
|
||||||
|
appCreateDSLErrorPart1: 'A significant difference in DSL versions has been detected. Forcing the import may cause the application to malfunction.',
|
||||||
|
appCreateDSLErrorPart2: 'Do you want to continue?',
|
||||||
|
appCreateDSLErrorPart3: 'Current application DSL version: ',
|
||||||
|
appCreateDSLErrorPart4: 'System-supported DSL version: ',
|
||||||
appCreateFailed: 'Failed to create app',
|
appCreateFailed: 'Failed to create app',
|
||||||
},
|
},
|
||||||
editApp: 'Edit Info',
|
editApp: 'Edit Info',
|
||||||
|
@ -75,12 +75,14 @@ const translation = {
|
|||||||
viewDetailInTracingPanel: 'View details',
|
viewDetailInTracingPanel: 'View details',
|
||||||
syncingData: 'Syncing data, just a few seconds.',
|
syncingData: 'Syncing data, just a few seconds.',
|
||||||
importDSL: 'Import DSL',
|
importDSL: 'Import DSL',
|
||||||
importDSLTip: 'Current draft will be overwritten. Export workflow as backup before importing.',
|
importDSLTip: 'Current draft will be overwritten.\nExport workflow as backup before importing.',
|
||||||
backupCurrentDraft: 'Backup Current Draft',
|
backupCurrentDraft: 'Backup Current Draft',
|
||||||
chooseDSL: 'Choose DSL(yml) file',
|
chooseDSL: 'Choose DSL file',
|
||||||
overwriteAndImport: 'Overwrite and Import',
|
overwriteAndImport: 'Overwrite and Import',
|
||||||
importFailure: 'Import failure',
|
importFailure: 'Import Failed',
|
||||||
importSuccess: 'Import success',
|
importWarning: 'Caution',
|
||||||
|
importWarningDetails: 'DSL version difference may affect certain features',
|
||||||
|
importSuccess: 'Import Successfully',
|
||||||
parallelRun: 'Parallel Run',
|
parallelRun: 'Parallel Run',
|
||||||
parallelTip: {
|
parallelTip: {
|
||||||
click: {
|
click: {
|
||||||
|
@ -64,6 +64,13 @@ const translation = {
|
|||||||
appTemplateNotSelected: '请选择应用模版',
|
appTemplateNotSelected: '请选择应用模版',
|
||||||
appTypeRequired: '请选择应用类型',
|
appTypeRequired: '请选择应用类型',
|
||||||
appCreated: '应用已创建',
|
appCreated: '应用已创建',
|
||||||
|
caution: '注意',
|
||||||
|
appCreateDSLWarning: '注意:DSL 版本差异可能影响部分功能表现',
|
||||||
|
appCreateDSLErrorTitle: '版本不兼容',
|
||||||
|
appCreateDSLErrorPart1: '检测到 DSL 版本差异较大,强制导入应用可能无法正常运行。',
|
||||||
|
appCreateDSLErrorPart2: '是否继续?',
|
||||||
|
appCreateDSLErrorPart3: '当前应用 DSL 版本:',
|
||||||
|
appCreateDSLErrorPart4: '系统支持 DSL 版本:',
|
||||||
appCreateFailed: '应用创建失败',
|
appCreateFailed: '应用创建失败',
|
||||||
},
|
},
|
||||||
editApp: '编辑信息',
|
editApp: '编辑信息',
|
||||||
|
@ -80,6 +80,8 @@ const translation = {
|
|||||||
chooseDSL: '选择 DSL(yml) 文件',
|
chooseDSL: '选择 DSL(yml) 文件',
|
||||||
overwriteAndImport: '覆盖并导入',
|
overwriteAndImport: '覆盖并导入',
|
||||||
importFailure: '导入失败',
|
importFailure: '导入失败',
|
||||||
|
importWarning: '注意',
|
||||||
|
importWarningDetails: 'DSL 版本差异可能影响部分功能表现',
|
||||||
importSuccess: '导入成功',
|
importSuccess: '导入成功',
|
||||||
parallelRun: '并行运行',
|
parallelRun: '并行运行',
|
||||||
parallelTip: {
|
parallelTip: {
|
||||||
|
@ -58,6 +58,18 @@ export type SiteConfig = {
|
|||||||
prompt_public: boolean
|
prompt_public: boolean
|
||||||
} */
|
} */
|
||||||
|
|
||||||
|
export enum DSLImportMode {
|
||||||
|
YAML_CONTENT = 'yaml-content',
|
||||||
|
YAML_URL = 'yaml-url',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DSLImportStatus {
|
||||||
|
COMPLETED = 'completed',
|
||||||
|
COMPLETED_WITH_WARNINGS = 'completed-with-warnings',
|
||||||
|
PENDING = 'pending',
|
||||||
|
FAILED = 'failed',
|
||||||
|
}
|
||||||
|
|
||||||
export type AppListResponse = {
|
export type AppListResponse = {
|
||||||
data: App[]
|
data: App[]
|
||||||
has_more: boolean
|
has_more: boolean
|
||||||
@ -67,6 +79,16 @@ export type AppListResponse = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type AppDetailResponse = App
|
export type AppDetailResponse = App
|
||||||
|
|
||||||
|
export type DSLImportResponse = {
|
||||||
|
id: string
|
||||||
|
status: DSLImportStatus
|
||||||
|
app_id?: string
|
||||||
|
current_dsl_version?: string
|
||||||
|
imported_dsl_version?: string
|
||||||
|
error: string
|
||||||
|
}
|
||||||
|
|
||||||
export type AppSSOResponse = { enabled: AppSSO['enable_sso'] }
|
export type AppSSOResponse = { enabled: AppSSO['enable_sso'] }
|
||||||
|
|
||||||
export type AppTemplatesResponse = {
|
export type AppTemplatesResponse = {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { Fetcher } from 'swr'
|
import type { Fetcher } from 'swr'
|
||||||
import { del, get, patch, post, put } from './base'
|
import { del, get, patch, post, put } from './base'
|
||||||
import type { ApiKeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyMessagesResponse, AppDetailResponse, AppListResponse, AppSSOResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WorkflowDailyConversationsResponse } from '@/models/app'
|
import type { ApiKeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyMessagesResponse, AppDetailResponse, AppListResponse, AppSSOResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, DSLImportMode, DSLImportResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WorkflowDailyConversationsResponse } from '@/models/app'
|
||||||
import type { CommonResponse } from '@/models/common'
|
import type { CommonResponse } from '@/models/common'
|
||||||
import type { AppIconType, AppMode, ModelConfig } from '@/types/app'
|
import type { AppIconType, AppMode, ModelConfig } from '@/types/app'
|
||||||
import type { TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type'
|
import type { TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type'
|
||||||
@ -40,14 +40,24 @@ export const exportAppConfig: Fetcher<{ data: string }, { appID: string; include
|
|||||||
return get<{ data: string }>(`apps/${appID}/export?include_secret=${include}`)
|
return get<{ data: string }>(`apps/${appID}/export?include_secret=${include}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: delete
|
||||||
export const importApp: Fetcher<AppDetailResponse, { data: string; name?: string; description?: string; icon_type?: AppIconType; icon?: string; icon_background?: string }> = ({ data, name, description, icon_type, icon, icon_background }) => {
|
export const importApp: Fetcher<AppDetailResponse, { data: string; name?: string; description?: string; icon_type?: AppIconType; icon?: string; icon_background?: string }> = ({ data, name, description, icon_type, icon, icon_background }) => {
|
||||||
return post<AppDetailResponse>('apps/import', { body: { data, name, description, icon_type, icon, icon_background } })
|
return post<AppDetailResponse>('apps/import', { body: { data, name, description, icon_type, icon, icon_background } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: delete
|
||||||
export const importAppFromUrl: Fetcher<AppDetailResponse, { url: string; name?: string; description?: string; icon?: string; icon_background?: string }> = ({ url, name, description, icon, icon_background }) => {
|
export const importAppFromUrl: Fetcher<AppDetailResponse, { url: string; name?: string; description?: string; icon?: string; icon_background?: string }> = ({ url, name, description, icon, icon_background }) => {
|
||||||
return post<AppDetailResponse>('apps/import/url', { body: { url, name, description, icon, icon_background } })
|
return post<AppDetailResponse>('apps/import/url', { body: { url, name, description, icon, icon_background } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const importDSL: Fetcher<DSLImportResponse, { mode: DSLImportMode; yaml_content?: string; yaml_url?: string; app_id?: string; name?: string; description?: string; icon_type?: AppIconType; icon?: string; icon_background?: string }> = ({ mode, yaml_content, yaml_url, app_id, name, description, icon_type, icon, icon_background }) => {
|
||||||
|
return post<DSLImportResponse>('apps/imports', { body: { mode, yaml_content, yaml_url, app_id, name, description, icon, icon_type, icon_background } })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const importDSLConfirm: Fetcher<DSLImportResponse, { import_id: string }> = ({ import_id }) => {
|
||||||
|
return post<DSLImportResponse>(`apps/imports/${import_id}/confirm`, { body: {} })
|
||||||
|
}
|
||||||
|
|
||||||
export const switchApp: Fetcher<{ new_app_id: string }, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string | null }> = ({ appID, name, icon_type, icon, icon_background }) => {
|
export const switchApp: Fetcher<{ new_app_id: string }, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string | null }> = ({ appID, name, icon_type, icon, icon_background }) => {
|
||||||
return post<{ new_app_id: string }>(`apps/${appID}/convert-to-workflow`, { body: { name, icon_type, icon, icon_background } })
|
return post<{ new_app_id: string }>(`apps/${appID}/convert-to-workflow`, { body: { name, icon_type, icon, icon_background } })
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,7 @@ export const fetchNodeDefault = (appId: string, blockType: BlockEnum, query = {}
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: archived
|
||||||
export const updateWorkflowDraftFromDSL = (appId: string, data: string) => {
|
export const updateWorkflowDraftFromDSL = (appId: string, data: string) => {
|
||||||
return post<FetchWorkflowDraftResponse>(`apps/${appId}/workflows/draft/import`, { body: { data } })
|
return post<FetchWorkflowDraftResponse>(`apps/${appId}/workflows/draft/import`, { body: { data } })
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user