Signed-off-by: -LAN- <laipz8200@outlook.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: -LAN- <laipz8200@outlook.com>tags/1.9.0
| from flask import Blueprint | from flask import Blueprint | ||||
| from flask_restx import Namespace | |||||
| from libs.external_api import ExternalApi | from libs.external_api import ExternalApi | ||||
| from .remote_files import RemoteFileInfoApi, RemoteFileUploadApi | from .remote_files import RemoteFileInfoApi, RemoteFileUploadApi | ||||
| bp = Blueprint("console", __name__, url_prefix="/console/api") | bp = Blueprint("console", __name__, url_prefix="/console/api") | ||||
| api = ExternalApi(bp) | |||||
| api = ExternalApi( | |||||
| bp, | |||||
| version="1.0", | |||||
| title="Console API", | |||||
| description="Console management APIs for app configuration, monitoring, and administration", | |||||
| ) | |||||
| # Create namespace | |||||
| console_ns = Namespace("console", description="Console management API operations", path="/") | |||||
| # File | # File | ||||
| api.add_resource(FileApi, "/files/upload") | api.add_resource(FileApi, "/files/upload") | ||||
| api.add_resource(AppImportCheckDependenciesApi, "/apps/imports/<string:app_id>/check-dependencies") | api.add_resource(AppImportCheckDependenciesApi, "/apps/imports/<string:app_id>/check-dependencies") | ||||
| # Import other controllers | # Import other controllers | ||||
| from . import admin, apikey, extension, feature, ping, setup, version # pyright: ignore[reportUnusedImport] | |||||
| from . import ( | |||||
| admin, # pyright: ignore[reportUnusedImport] | |||||
| apikey, # pyright: ignore[reportUnusedImport] | |||||
| extension, # pyright: ignore[reportUnusedImport] | |||||
| feature, # pyright: ignore[reportUnusedImport] | |||||
| init_validate, # pyright: ignore[reportUnusedImport] | |||||
| ping, # pyright: ignore[reportUnusedImport] | |||||
| setup, # pyright: ignore[reportUnusedImport] | |||||
| version, # pyright: ignore[reportUnusedImport] | |||||
| ) | |||||
| # Import app controllers | # Import app controllers | ||||
| from .app import ( | from .app import ( | ||||
| saved_message, # pyright: ignore[reportUnusedImport] | saved_message, # pyright: ignore[reportUnusedImport] | ||||
| ) | ) | ||||
| # Import tag controllers | |||||
| from .tag import tags # pyright: ignore[reportUnusedImport] | |||||
| # Import workspace controllers | |||||
| from .workspace import ( | |||||
| account, # pyright: ignore[reportUnusedImport] | |||||
| agent_providers, # pyright: ignore[reportUnusedImport] | |||||
| endpoint, # pyright: ignore[reportUnusedImport] | |||||
| load_balancing_config, # pyright: ignore[reportUnusedImport] | |||||
| members, # pyright: ignore[reportUnusedImport] | |||||
| model_providers, # pyright: ignore[reportUnusedImport] | |||||
| models, # pyright: ignore[reportUnusedImport] | |||||
| plugin, # pyright: ignore[reportUnusedImport] | |||||
| tool_providers, # pyright: ignore[reportUnusedImport] | |||||
| workspace, # pyright: ignore[reportUnusedImport] | |||||
| ) | |||||
| # Explore Audio | # Explore Audio | ||||
| api.add_resource(ChatAudioApi, "/installed-apps/<uuid:installed_app_id>/audio-to-text", endpoint="installed_app_audio") | api.add_resource(ChatAudioApi, "/installed-apps/<uuid:installed_app_id>/audio-to-text", endpoint="installed_app_audio") | ||||
| api.add_resource(ChatTextApi, "/installed-apps/<uuid:installed_app_id>/text-to-audio", endpoint="installed_app_text") | api.add_resource(ChatTextApi, "/installed-apps/<uuid:installed_app_id>/text-to-audio", endpoint="installed_app_text") | ||||
| InstalledAppWorkflowTaskStopApi, "/installed-apps/<uuid:installed_app_id>/workflows/tasks/<string:task_id>/stop" | InstalledAppWorkflowTaskStopApi, "/installed-apps/<uuid:installed_app_id>/workflows/tasks/<string:task_id>/stop" | ||||
| ) | ) | ||||
| # Import tag controllers | |||||
| from .tag import tags # pyright: ignore[reportUnusedImport] | |||||
| # Import workspace controllers | |||||
| from .workspace import ( | |||||
| account, # pyright: ignore[reportUnusedImport] | |||||
| agent_providers, # pyright: ignore[reportUnusedImport] | |||||
| endpoint, # pyright: ignore[reportUnusedImport] | |||||
| load_balancing_config, # pyright: ignore[reportUnusedImport] | |||||
| members, # pyright: ignore[reportUnusedImport] | |||||
| model_providers, # pyright: ignore[reportUnusedImport] | |||||
| models, # pyright: ignore[reportUnusedImport] | |||||
| plugin, # pyright: ignore[reportUnusedImport] | |||||
| tool_providers, # pyright: ignore[reportUnusedImport] | |||||
| workspace, # pyright: ignore[reportUnusedImport] | |||||
| ) | |||||
| api.add_namespace(console_ns) |
| from typing import ParamSpec, TypeVar | from typing import ParamSpec, TypeVar | ||||
| from flask import request | from flask import request | ||||
| from flask_restx import Resource, reqparse | |||||
| from flask_restx import Resource, fields, reqparse | |||||
| from sqlalchemy import select | from sqlalchemy import select | ||||
| from sqlalchemy.orm import Session | from sqlalchemy.orm import Session | ||||
| from werkzeug.exceptions import NotFound, Unauthorized | from werkzeug.exceptions import NotFound, Unauthorized | ||||
| R = TypeVar("R") | R = TypeVar("R") | ||||
| from configs import dify_config | from configs import dify_config | ||||
| from constants.languages import supported_language | from constants.languages import supported_language | ||||
| from controllers.console import api | |||||
| from controllers.console import api, console_ns | |||||
| from controllers.console.wraps import only_edition_cloud | from controllers.console.wraps import only_edition_cloud | ||||
| from extensions.ext_database import db | from extensions.ext_database import db | ||||
| from models.model import App, InstalledApp, RecommendedApp | from models.model import App, InstalledApp, RecommendedApp | ||||
| return decorated | return decorated | ||||
| @console_ns.route("/admin/insert-explore-apps") | |||||
| class InsertExploreAppListApi(Resource): | class InsertExploreAppListApi(Resource): | ||||
| @api.doc("insert_explore_app") | |||||
| @api.doc(description="Insert or update an app in the explore list") | |||||
| @api.expect( | |||||
| api.model( | |||||
| "InsertExploreAppRequest", | |||||
| { | |||||
| "app_id": fields.String(required=True, description="Application ID"), | |||||
| "desc": fields.String(description="App description"), | |||||
| "copyright": fields.String(description="Copyright information"), | |||||
| "privacy_policy": fields.String(description="Privacy policy"), | |||||
| "custom_disclaimer": fields.String(description="Custom disclaimer"), | |||||
| "language": fields.String(required=True, description="Language code"), | |||||
| "category": fields.String(required=True, description="App category"), | |||||
| "position": fields.Integer(required=True, description="Display position"), | |||||
| }, | |||||
| ) | |||||
| ) | |||||
| @api.response(200, "App updated successfully") | |||||
| @api.response(201, "App inserted successfully") | |||||
| @api.response(404, "App not found") | |||||
| @only_edition_cloud | @only_edition_cloud | ||||
| @admin_required | @admin_required | ||||
| def post(self): | def post(self): | ||||
| return {"result": "success"}, 200 | return {"result": "success"}, 200 | ||||
| @console_ns.route("/admin/insert-explore-apps/<uuid:app_id>") | |||||
| class InsertExploreAppApi(Resource): | class InsertExploreAppApi(Resource): | ||||
| @api.doc("delete_explore_app") | |||||
| @api.doc(description="Remove an app from the explore list") | |||||
| @api.doc(params={"app_id": "Application ID to remove"}) | |||||
| @api.response(204, "App removed successfully") | |||||
| @only_edition_cloud | @only_edition_cloud | ||||
| @admin_required | @admin_required | ||||
| def delete(self, app_id): | def delete(self, app_id): | ||||
| db.session.commit() | db.session.commit() | ||||
| return {"result": "success"}, 204 | return {"result": "success"}, 204 | ||||
| api.add_resource(InsertExploreAppListApi, "/admin/insert-explore-apps") | |||||
| api.add_resource(InsertExploreAppApi, "/admin/insert-explore-apps/<uuid:app_id>") |
| from models.dataset import Dataset | from models.dataset import Dataset | ||||
| from models.model import ApiToken, App | from models.model import ApiToken, App | ||||
| from . import api | |||||
| from . import api, console_ns | |||||
| from .wraps import account_initialization_required, setup_required | from .wraps import account_initialization_required, setup_required | ||||
| api_key_fields = { | api_key_fields = { | ||||
| return {"result": "success"}, 204 | return {"result": "success"}, 204 | ||||
| @console_ns.route("/apps/<uuid:resource_id>/api-keys") | |||||
| class AppApiKeyListResource(BaseApiKeyListResource): | class AppApiKeyListResource(BaseApiKeyListResource): | ||||
| @api.doc("get_app_api_keys") | |||||
| @api.doc(description="Get all API keys for an app") | |||||
| @api.doc(params={"resource_id": "App ID"}) | |||||
| @api.response(200, "Success", api_key_list) | |||||
| def get(self, resource_id): | |||||
| """Get all API keys for an app""" | |||||
| return super().get(resource_id) | |||||
| @api.doc("create_app_api_key") | |||||
| @api.doc(description="Create a new API key for an app") | |||||
| @api.doc(params={"resource_id": "App ID"}) | |||||
| @api.response(201, "API key created successfully", api_key_fields) | |||||
| @api.response(400, "Maximum keys exceeded") | |||||
| def post(self, resource_id): | |||||
| """Create a new API key for an app""" | |||||
| return super().post(resource_id) | |||||
| def after_request(self, resp): | def after_request(self, resp): | ||||
| resp.headers["Access-Control-Allow-Origin"] = "*" | resp.headers["Access-Control-Allow-Origin"] = "*" | ||||
| resp.headers["Access-Control-Allow-Credentials"] = "true" | resp.headers["Access-Control-Allow-Credentials"] = "true" | ||||
| token_prefix = "app-" | token_prefix = "app-" | ||||
| @console_ns.route("/apps/<uuid:resource_id>/api-keys/<uuid:api_key_id>") | |||||
| class AppApiKeyResource(BaseApiKeyResource): | class AppApiKeyResource(BaseApiKeyResource): | ||||
| @api.doc("delete_app_api_key") | |||||
| @api.doc(description="Delete an API key for an app") | |||||
| @api.doc(params={"resource_id": "App ID", "api_key_id": "API key ID"}) | |||||
| @api.response(204, "API key deleted successfully") | |||||
| def delete(self, resource_id, api_key_id): | |||||
| """Delete an API key for an app""" | |||||
| return super().delete(resource_id, api_key_id) | |||||
| def after_request(self, resp): | def after_request(self, resp): | ||||
| resp.headers["Access-Control-Allow-Origin"] = "*" | resp.headers["Access-Control-Allow-Origin"] = "*" | ||||
| resp.headers["Access-Control-Allow-Credentials"] = "true" | resp.headers["Access-Control-Allow-Credentials"] = "true" | ||||
| resource_id_field = "app_id" | resource_id_field = "app_id" | ||||
| @console_ns.route("/datasets/<uuid:resource_id>/api-keys") | |||||
| class DatasetApiKeyListResource(BaseApiKeyListResource): | class DatasetApiKeyListResource(BaseApiKeyListResource): | ||||
| @api.doc("get_dataset_api_keys") | |||||
| @api.doc(description="Get all API keys for a dataset") | |||||
| @api.doc(params={"resource_id": "Dataset ID"}) | |||||
| @api.response(200, "Success", api_key_list) | |||||
| def get(self, resource_id): | |||||
| """Get all API keys for a dataset""" | |||||
| return super().get(resource_id) | |||||
| @api.doc("create_dataset_api_key") | |||||
| @api.doc(description="Create a new API key for a dataset") | |||||
| @api.doc(params={"resource_id": "Dataset ID"}) | |||||
| @api.response(201, "API key created successfully", api_key_fields) | |||||
| @api.response(400, "Maximum keys exceeded") | |||||
| def post(self, resource_id): | |||||
| """Create a new API key for a dataset""" | |||||
| return super().post(resource_id) | |||||
| def after_request(self, resp): | def after_request(self, resp): | ||||
| resp.headers["Access-Control-Allow-Origin"] = "*" | resp.headers["Access-Control-Allow-Origin"] = "*" | ||||
| resp.headers["Access-Control-Allow-Credentials"] = "true" | resp.headers["Access-Control-Allow-Credentials"] = "true" | ||||
| token_prefix = "ds-" | token_prefix = "ds-" | ||||
| @console_ns.route("/datasets/<uuid:resource_id>/api-keys/<uuid:api_key_id>") | |||||
| class DatasetApiKeyResource(BaseApiKeyResource): | class DatasetApiKeyResource(BaseApiKeyResource): | ||||
| @api.doc("delete_dataset_api_key") | |||||
| @api.doc(description="Delete an API key for a dataset") | |||||
| @api.doc(params={"resource_id": "Dataset ID", "api_key_id": "API key ID"}) | |||||
| @api.response(204, "API key deleted successfully") | |||||
| def delete(self, resource_id, api_key_id): | |||||
| """Delete an API key for a dataset""" | |||||
| return super().delete(resource_id, api_key_id) | |||||
| def after_request(self, resp): | def after_request(self, resp): | ||||
| resp.headers["Access-Control-Allow-Origin"] = "*" | resp.headers["Access-Control-Allow-Origin"] = "*" | ||||
| resp.headers["Access-Control-Allow-Credentials"] = "true" | resp.headers["Access-Control-Allow-Credentials"] = "true" | ||||
| resource_type = "dataset" | resource_type = "dataset" | ||||
| resource_model = Dataset | resource_model = Dataset | ||||
| resource_id_field = "dataset_id" | resource_id_field = "dataset_id" | ||||
| api.add_resource(AppApiKeyListResource, "/apps/<uuid:resource_id>/api-keys") | |||||
| api.add_resource(AppApiKeyResource, "/apps/<uuid:resource_id>/api-keys/<uuid:api_key_id>") | |||||
| api.add_resource(DatasetApiKeyListResource, "/datasets/<uuid:resource_id>/api-keys") | |||||
| api.add_resource(DatasetApiKeyResource, "/datasets/<uuid:resource_id>/api-keys/<uuid:api_key_id>") |
| from flask import request | from flask import request | ||||
| from flask_restx import Resource, reqparse | |||||
| from flask_restx import Resource, fields, reqparse | |||||
| from constants.languages import supported_language | from constants.languages import supported_language | ||||
| from controllers.console import api | |||||
| from controllers.console import api, console_ns | |||||
| from controllers.console.error import AlreadyActivateError | from controllers.console.error import AlreadyActivateError | ||||
| from extensions.ext_database import db | from extensions.ext_database import db | ||||
| from libs.datetime_utils import naive_utc_now | from libs.datetime_utils import naive_utc_now | ||||
| from models.account import AccountStatus | from models.account import AccountStatus | ||||
| from services.account_service import AccountService, RegisterService | from services.account_service import AccountService, RegisterService | ||||
| active_check_parser = reqparse.RequestParser() | |||||
| active_check_parser.add_argument( | |||||
| "workspace_id", type=str, required=False, nullable=True, location="args", help="Workspace ID" | |||||
| ) | |||||
| active_check_parser.add_argument( | |||||
| "email", type=email, required=False, nullable=True, location="args", help="Email address" | |||||
| ) | |||||
| active_check_parser.add_argument( | |||||
| "token", type=str, required=True, nullable=False, location="args", help="Activation token" | |||||
| ) | |||||
| @console_ns.route("/activate/check") | |||||
| class ActivateCheckApi(Resource): | class ActivateCheckApi(Resource): | ||||
| @api.doc("check_activation_token") | |||||
| @api.doc(description="Check if activation token is valid") | |||||
| @api.expect(active_check_parser) | |||||
| @api.response( | |||||
| 200, | |||||
| "Success", | |||||
| api.model( | |||||
| "ActivationCheckResponse", | |||||
| { | |||||
| "is_valid": fields.Boolean(description="Whether token is valid"), | |||||
| "data": fields.Raw(description="Activation data if valid"), | |||||
| }, | |||||
| ), | |||||
| ) | |||||
| def get(self): | def get(self): | ||||
| parser = reqparse.RequestParser() | |||||
| parser.add_argument("workspace_id", type=str, required=False, nullable=True, location="args") | |||||
| parser.add_argument("email", type=email, required=False, nullable=True, location="args") | |||||
| parser.add_argument("token", type=str, required=True, nullable=False, location="args") | |||||
| args = parser.parse_args() | |||||
| args = active_check_parser.parse_args() | |||||
| workspaceId = args["workspace_id"] | workspaceId = args["workspace_id"] | ||||
| reg_email = args["email"] | reg_email = args["email"] | ||||
| return {"is_valid": False} | return {"is_valid": False} | ||||
| active_parser = reqparse.RequestParser() | |||||
| active_parser.add_argument("workspace_id", type=str, required=False, nullable=True, location="json") | |||||
| active_parser.add_argument("email", type=email, required=False, nullable=True, location="json") | |||||
| active_parser.add_argument("token", type=str, required=True, nullable=False, location="json") | |||||
| active_parser.add_argument("name", type=StrLen(30), required=True, nullable=False, location="json") | |||||
| active_parser.add_argument( | |||||
| "interface_language", type=supported_language, required=True, nullable=False, location="json" | |||||
| ) | |||||
| active_parser.add_argument("timezone", type=timezone, required=True, nullable=False, location="json") | |||||
| @console_ns.route("/activate") | |||||
| class ActivateApi(Resource): | class ActivateApi(Resource): | ||||
| @api.doc("activate_account") | |||||
| @api.doc(description="Activate account with invitation token") | |||||
| @api.expect(active_parser) | |||||
| @api.response( | |||||
| 200, | |||||
| "Account activated successfully", | |||||
| api.model( | |||||
| "ActivationResponse", | |||||
| { | |||||
| "result": fields.String(description="Operation result"), | |||||
| "data": fields.Raw(description="Login token data"), | |||||
| }, | |||||
| ), | |||||
| ) | |||||
| @api.response(400, "Already activated or invalid token") | |||||
| def post(self): | def post(self): | ||||
| parser = reqparse.RequestParser() | |||||
| parser.add_argument("workspace_id", type=str, required=False, nullable=True, location="json") | |||||
| parser.add_argument("email", type=email, required=False, nullable=True, location="json") | |||||
| parser.add_argument("token", type=str, required=True, nullable=False, location="json") | |||||
| parser.add_argument("name", type=StrLen(30), required=True, nullable=False, location="json") | |||||
| parser.add_argument( | |||||
| "interface_language", type=supported_language, required=True, nullable=False, location="json" | |||||
| ) | |||||
| parser.add_argument("timezone", type=timezone, required=True, nullable=False, location="json") | |||||
| args = parser.parse_args() | |||||
| args = active_parser.parse_args() | |||||
| invitation = RegisterService.get_invitation_if_token_valid(args["workspace_id"], args["email"], args["token"]) | invitation = RegisterService.get_invitation_if_token_valid(args["workspace_id"], args["email"], args["token"]) | ||||
| if invitation is None: | if invitation is None: | ||||
| token_pair = AccountService.login(account, ip_address=extract_remote_ip(request)) | token_pair = AccountService.login(account, ip_address=extract_remote_ip(request)) | ||||
| return {"result": "success", "data": token_pair.model_dump()} | return {"result": "success", "data": token_pair.model_dump()} | ||||
| api.add_resource(ActivateCheckApi, "/activate/check") | |||||
| api.add_resource(ActivateApi, "/activate") |
| import requests | import requests | ||||
| from flask import current_app, redirect, request | from flask import current_app, redirect, request | ||||
| from flask_login import current_user | from flask_login import current_user | ||||
| from flask_restx import Resource | |||||
| from flask_restx import Resource, fields | |||||
| from werkzeug.exceptions import Forbidden | from werkzeug.exceptions import Forbidden | ||||
| from configs import dify_config | from configs import dify_config | ||||
| from controllers.console import api | |||||
| from controllers.console import api, console_ns | |||||
| from libs.login import login_required | from libs.login import login_required | ||||
| from libs.oauth_data_source import NotionOAuth | from libs.oauth_data_source import NotionOAuth | ||||
| return OAUTH_PROVIDERS | return OAUTH_PROVIDERS | ||||
| @console_ns.route("/oauth/data-source/<string:provider>") | |||||
| class OAuthDataSource(Resource): | class OAuthDataSource(Resource): | ||||
| @api.doc("oauth_data_source") | |||||
| @api.doc(description="Get OAuth authorization URL for data source provider") | |||||
| @api.doc(params={"provider": "Data source provider name (notion)"}) | |||||
| @api.response( | |||||
| 200, | |||||
| "Authorization URL or internal setup success", | |||||
| api.model( | |||||
| "OAuthDataSourceResponse", | |||||
| {"data": fields.Raw(description="Authorization URL or 'internal' for internal setup")}, | |||||
| ), | |||||
| ) | |||||
| @api.response(400, "Invalid provider") | |||||
| @api.response(403, "Admin privileges required") | |||||
| def get(self, provider: str): | def get(self, provider: str): | ||||
| # The role of the current user in the table must be admin or owner | # The role of the current user in the table must be admin or owner | ||||
| if not current_user.is_admin_or_owner: | if not current_user.is_admin_or_owner: | ||||
| return {"data": auth_url}, 200 | return {"data": auth_url}, 200 | ||||
| @console_ns.route("/oauth/data-source/callback/<string:provider>") | |||||
| class OAuthDataSourceCallback(Resource): | class OAuthDataSourceCallback(Resource): | ||||
| @api.doc("oauth_data_source_callback") | |||||
| @api.doc(description="Handle OAuth callback from data source provider") | |||||
| @api.doc( | |||||
| params={ | |||||
| "provider": "Data source provider name (notion)", | |||||
| "code": "Authorization code from OAuth provider", | |||||
| "error": "Error message from OAuth provider", | |||||
| } | |||||
| ) | |||||
| @api.response(302, "Redirect to console with result") | |||||
| @api.response(400, "Invalid provider") | |||||
| def get(self, provider: str): | def get(self, provider: str): | ||||
| OAUTH_DATASOURCE_PROVIDERS = get_oauth_providers() | OAUTH_DATASOURCE_PROVIDERS = get_oauth_providers() | ||||
| with current_app.app_context(): | with current_app.app_context(): | ||||
| return redirect(f"{dify_config.CONSOLE_WEB_URL}?type=notion&error=Access denied") | return redirect(f"{dify_config.CONSOLE_WEB_URL}?type=notion&error=Access denied") | ||||
| @console_ns.route("/oauth/data-source/binding/<string:provider>") | |||||
| class OAuthDataSourceBinding(Resource): | class OAuthDataSourceBinding(Resource): | ||||
| @api.doc("oauth_data_source_binding") | |||||
| @api.doc(description="Bind OAuth data source with authorization code") | |||||
| @api.doc( | |||||
| params={"provider": "Data source provider name (notion)", "code": "Authorization code from OAuth provider"} | |||||
| ) | |||||
| @api.response( | |||||
| 200, | |||||
| "Data source binding success", | |||||
| api.model("OAuthDataSourceBindingResponse", {"result": fields.String(description="Operation result")}), | |||||
| ) | |||||
| @api.response(400, "Invalid provider or code") | |||||
| def get(self, provider: str): | def get(self, provider: str): | ||||
| OAUTH_DATASOURCE_PROVIDERS = get_oauth_providers() | OAUTH_DATASOURCE_PROVIDERS = get_oauth_providers() | ||||
| with current_app.app_context(): | with current_app.app_context(): | ||||
| return {"result": "success"}, 200 | return {"result": "success"}, 200 | ||||
| @console_ns.route("/oauth/data-source/<string:provider>/<uuid:binding_id>/sync") | |||||
| class OAuthDataSourceSync(Resource): | class OAuthDataSourceSync(Resource): | ||||
| @api.doc("oauth_data_source_sync") | |||||
| @api.doc(description="Sync data from OAuth data source") | |||||
| @api.doc(params={"provider": "Data source provider name (notion)", "binding_id": "Data source binding ID"}) | |||||
| @api.response( | |||||
| 200, | |||||
| "Data source sync success", | |||||
| api.model("OAuthDataSourceSyncResponse", {"result": fields.String(description="Operation result")}), | |||||
| ) | |||||
| @api.response(400, "Invalid provider or sync failed") | |||||
| @setup_required | @setup_required | ||||
| @login_required | @login_required | ||||
| @account_initialization_required | @account_initialization_required | ||||
| return {"error": "OAuth data source process failed"}, 400 | return {"error": "OAuth data source process failed"}, 400 | ||||
| return {"result": "success"}, 200 | return {"result": "success"}, 200 | ||||
| api.add_resource(OAuthDataSource, "/oauth/data-source/<string:provider>") | |||||
| api.add_resource(OAuthDataSourceCallback, "/oauth/data-source/callback/<string:provider>") | |||||
| api.add_resource(OAuthDataSourceBinding, "/oauth/data-source/binding/<string:provider>") | |||||
| api.add_resource(OAuthDataSourceSync, "/oauth/data-source/<string:provider>/<uuid:binding_id>/sync") |
| import secrets | import secrets | ||||
| from flask import request | from flask import request | ||||
| from flask_restx import Resource, reqparse | |||||
| from flask_restx import Resource, fields, reqparse | |||||
| from sqlalchemy import select | from sqlalchemy import select | ||||
| from sqlalchemy.orm import Session | from sqlalchemy.orm import Session | ||||
| from constants.languages import languages | from constants.languages import languages | ||||
| from controllers.console import api | |||||
| from controllers.console import api, console_ns | |||||
| from controllers.console.auth.error import ( | from controllers.console.auth.error import ( | ||||
| EmailCodeError, | EmailCodeError, | ||||
| EmailPasswordResetLimitError, | EmailPasswordResetLimitError, | ||||
| from services.feature_service import FeatureService | from services.feature_service import FeatureService | ||||
| @console_ns.route("/forgot-password") | |||||
| class ForgotPasswordSendEmailApi(Resource): | class ForgotPasswordSendEmailApi(Resource): | ||||
| @api.doc("send_forgot_password_email") | |||||
| @api.doc(description="Send password reset email") | |||||
| @api.expect( | |||||
| api.model( | |||||
| "ForgotPasswordEmailRequest", | |||||
| { | |||||
| "email": fields.String(required=True, description="Email address"), | |||||
| "language": fields.String(description="Language for email (zh-Hans/en-US)"), | |||||
| }, | |||||
| ) | |||||
| ) | |||||
| @api.response( | |||||
| 200, | |||||
| "Email sent successfully", | |||||
| api.model( | |||||
| "ForgotPasswordEmailResponse", | |||||
| { | |||||
| "result": fields.String(description="Operation result"), | |||||
| "data": fields.String(description="Reset token"), | |||||
| "code": fields.String(description="Error code if account not found"), | |||||
| }, | |||||
| ), | |||||
| ) | |||||
| @api.response(400, "Invalid email or rate limit exceeded") | |||||
| @setup_required | @setup_required | ||||
| @email_password_login_enabled | @email_password_login_enabled | ||||
| def post(self): | def post(self): | ||||
| return {"result": "success", "data": token} | return {"result": "success", "data": token} | ||||
| @console_ns.route("/forgot-password/validity") | |||||
| class ForgotPasswordCheckApi(Resource): | class ForgotPasswordCheckApi(Resource): | ||||
| @api.doc("check_forgot_password_code") | |||||
| @api.doc(description="Verify password reset code") | |||||
| @api.expect( | |||||
| api.model( | |||||
| "ForgotPasswordCheckRequest", | |||||
| { | |||||
| "email": fields.String(required=True, description="Email address"), | |||||
| "code": fields.String(required=True, description="Verification code"), | |||||
| "token": fields.String(required=True, description="Reset token"), | |||||
| }, | |||||
| ) | |||||
| ) | |||||
| @api.response( | |||||
| 200, | |||||
| "Code verified successfully", | |||||
| api.model( | |||||
| "ForgotPasswordCheckResponse", | |||||
| { | |||||
| "is_valid": fields.Boolean(description="Whether code is valid"), | |||||
| "email": fields.String(description="Email address"), | |||||
| "token": fields.String(description="New reset token"), | |||||
| }, | |||||
| ), | |||||
| ) | |||||
| @api.response(400, "Invalid code or token") | |||||
| @setup_required | @setup_required | ||||
| @email_password_login_enabled | @email_password_login_enabled | ||||
| def post(self): | def post(self): | ||||
| return {"is_valid": True, "email": token_data.get("email"), "token": new_token} | return {"is_valid": True, "email": token_data.get("email"), "token": new_token} | ||||
| @console_ns.route("/forgot-password/resets") | |||||
| class ForgotPasswordResetApi(Resource): | class ForgotPasswordResetApi(Resource): | ||||
| @api.doc("reset_password") | |||||
| @api.doc(description="Reset password with verification token") | |||||
| @api.expect( | |||||
| api.model( | |||||
| "ForgotPasswordResetRequest", | |||||
| { | |||||
| "token": fields.String(required=True, description="Verification token"), | |||||
| "new_password": fields.String(required=True, description="New password"), | |||||
| "password_confirm": fields.String(required=True, description="Password confirmation"), | |||||
| }, | |||||
| ) | |||||
| ) | |||||
| @api.response( | |||||
| 200, | |||||
| "Password reset successfully", | |||||
| api.model("ForgotPasswordResetResponse", {"result": fields.String(description="Operation result")}), | |||||
| ) | |||||
| @api.response(400, "Invalid token or password mismatch") | |||||
| @setup_required | @setup_required | ||||
| @email_password_login_enabled | @email_password_login_enabled | ||||
| def post(self): | def post(self): | ||||
| pass | pass | ||||
| except AccountRegisterError: | except AccountRegisterError: | ||||
| raise AccountInFreezeError() | raise AccountInFreezeError() | ||||
| api.add_resource(ForgotPasswordSendEmailApi, "/forgot-password") | |||||
| api.add_resource(ForgotPasswordCheckApi, "/forgot-password/validity") | |||||
| api.add_resource(ForgotPasswordResetApi, "/forgot-password/resets") |
| from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkSpaceNotFoundError | from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkSpaceNotFoundError | ||||
| from services.feature_service import FeatureService | from services.feature_service import FeatureService | ||||
| from .. import api | |||||
| from .. import api, console_ns | |||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||
| return OAUTH_PROVIDERS | return OAUTH_PROVIDERS | ||||
| @console_ns.route("/oauth/login/<provider>") | |||||
| class OAuthLogin(Resource): | class OAuthLogin(Resource): | ||||
| @api.doc("oauth_login") | |||||
| @api.doc(description="Initiate OAuth login process") | |||||
| @api.doc(params={"provider": "OAuth provider name (github/google)", "invite_token": "Optional invitation token"}) | |||||
| @api.response(302, "Redirect to OAuth authorization URL") | |||||
| @api.response(400, "Invalid provider") | |||||
| def get(self, provider: str): | def get(self, provider: str): | ||||
| invite_token = request.args.get("invite_token") or None | invite_token = request.args.get("invite_token") or None | ||||
| OAUTH_PROVIDERS = get_oauth_providers() | OAUTH_PROVIDERS = get_oauth_providers() | ||||
| return redirect(auth_url) | return redirect(auth_url) | ||||
| @console_ns.route("/oauth/authorize/<provider>") | |||||
| class OAuthCallback(Resource): | class OAuthCallback(Resource): | ||||
| @api.doc("oauth_callback") | |||||
| @api.doc(description="Handle OAuth callback and complete login process") | |||||
| @api.doc( | |||||
| params={ | |||||
| "provider": "OAuth provider name (github/google)", | |||||
| "code": "Authorization code from OAuth provider", | |||||
| "state": "Optional state parameter (used for invite token)", | |||||
| } | |||||
| ) | |||||
| @api.response(302, "Redirect to console with access token") | |||||
| @api.response(400, "OAuth process failed") | |||||
| def get(self, provider: str): | def get(self, provider: str): | ||||
| OAUTH_PROVIDERS = get_oauth_providers() | OAUTH_PROVIDERS = get_oauth_providers() | ||||
| with current_app.app_context(): | with current_app.app_context(): | ||||
| AccountService.link_account_integrate(provider, user_info.id, account) | AccountService.link_account_integrate(provider, user_info.id, account) | ||||
| return account | return account | ||||
| api.add_resource(OAuthLogin, "/oauth/login/<provider>") | |||||
| api.add_resource(OAuthCallback, "/oauth/authorize/<provider>") |
| from flask_login import current_user | from flask_login import current_user | ||||
| from flask_restx import Resource, marshal_with, reqparse | |||||
| from flask_restx import Resource, fields, marshal_with, reqparse | |||||
| from constants import HIDDEN_VALUE | from constants import HIDDEN_VALUE | ||||
| from controllers.console import api | |||||
| from controllers.console import api, console_ns | |||||
| from controllers.console.wraps import account_initialization_required, setup_required | from controllers.console.wraps import account_initialization_required, setup_required | ||||
| from fields.api_based_extension_fields import api_based_extension_fields | from fields.api_based_extension_fields import api_based_extension_fields | ||||
| from libs.login import login_required | from libs.login import login_required | ||||
| from services.code_based_extension_service import CodeBasedExtensionService | from services.code_based_extension_service import CodeBasedExtensionService | ||||
| @console_ns.route("/code-based-extension") | |||||
| class CodeBasedExtensionAPI(Resource): | class CodeBasedExtensionAPI(Resource): | ||||
| @api.doc("get_code_based_extension") | |||||
| @api.doc(description="Get code-based extension data by module name") | |||||
| @api.expect( | |||||
| api.parser().add_argument("module", type=str, required=True, location="args", help="Extension module name") | |||||
| ) | |||||
| @api.response( | |||||
| 200, | |||||
| "Success", | |||||
| api.model( | |||||
| "CodeBasedExtensionResponse", | |||||
| {"module": fields.String(description="Module name"), "data": fields.Raw(description="Extension data")}, | |||||
| ), | |||||
| ) | |||||
| @setup_required | @setup_required | ||||
| @login_required | @login_required | ||||
| @account_initialization_required | @account_initialization_required | ||||
| return {"module": args["module"], "data": CodeBasedExtensionService.get_code_based_extension(args["module"])} | return {"module": args["module"], "data": CodeBasedExtensionService.get_code_based_extension(args["module"])} | ||||
| @console_ns.route("/api-based-extension") | |||||
| class APIBasedExtensionAPI(Resource): | class APIBasedExtensionAPI(Resource): | ||||
| @api.doc("get_api_based_extensions") | |||||
| @api.doc(description="Get all API-based extensions for current tenant") | |||||
| @api.response(200, "Success", fields.List(fields.Nested(api_based_extension_fields))) | |||||
| @setup_required | @setup_required | ||||
| @login_required | @login_required | ||||
| @account_initialization_required | @account_initialization_required | ||||
| tenant_id = current_user.current_tenant_id | tenant_id = current_user.current_tenant_id | ||||
| return APIBasedExtensionService.get_all_by_tenant_id(tenant_id) | return APIBasedExtensionService.get_all_by_tenant_id(tenant_id) | ||||
| @api.doc("create_api_based_extension") | |||||
| @api.doc(description="Create a new API-based extension") | |||||
| @api.expect( | |||||
| api.model( | |||||
| "CreateAPIBasedExtensionRequest", | |||||
| { | |||||
| "name": fields.String(required=True, description="Extension name"), | |||||
| "api_endpoint": fields.String(required=True, description="API endpoint URL"), | |||||
| "api_key": fields.String(required=True, description="API key for authentication"), | |||||
| }, | |||||
| ) | |||||
| ) | |||||
| @api.response(201, "Extension created successfully", api_based_extension_fields) | |||||
| @setup_required | @setup_required | ||||
| @login_required | @login_required | ||||
| @account_initialization_required | @account_initialization_required | ||||
| return APIBasedExtensionService.save(extension_data) | return APIBasedExtensionService.save(extension_data) | ||||
| @console_ns.route("/api-based-extension/<uuid:id>") | |||||
| class APIBasedExtensionDetailAPI(Resource): | class APIBasedExtensionDetailAPI(Resource): | ||||
| @api.doc("get_api_based_extension") | |||||
| @api.doc(description="Get API-based extension by ID") | |||||
| @api.doc(params={"id": "Extension ID"}) | |||||
| @api.response(200, "Success", api_based_extension_fields) | |||||
| @setup_required | @setup_required | ||||
| @login_required | @login_required | ||||
| @account_initialization_required | @account_initialization_required | ||||
| return APIBasedExtensionService.get_with_tenant_id(tenant_id, api_based_extension_id) | return APIBasedExtensionService.get_with_tenant_id(tenant_id, api_based_extension_id) | ||||
| @api.doc("update_api_based_extension") | |||||
| @api.doc(description="Update API-based extension") | |||||
| @api.doc(params={"id": "Extension ID"}) | |||||
| @api.expect( | |||||
| api.model( | |||||
| "UpdateAPIBasedExtensionRequest", | |||||
| { | |||||
| "name": fields.String(required=True, description="Extension name"), | |||||
| "api_endpoint": fields.String(required=True, description="API endpoint URL"), | |||||
| "api_key": fields.String(required=True, description="API key for authentication"), | |||||
| }, | |||||
| ) | |||||
| ) | |||||
| @api.response(200, "Extension updated successfully", api_based_extension_fields) | |||||
| @setup_required | @setup_required | ||||
| @login_required | @login_required | ||||
| @account_initialization_required | @account_initialization_required | ||||
| return APIBasedExtensionService.save(extension_data_from_db) | return APIBasedExtensionService.save(extension_data_from_db) | ||||
| @api.doc("delete_api_based_extension") | |||||
| @api.doc(description="Delete API-based extension") | |||||
| @api.doc(params={"id": "Extension ID"}) | |||||
| @api.response(204, "Extension deleted successfully") | |||||
| @setup_required | @setup_required | ||||
| @login_required | @login_required | ||||
| @account_initialization_required | @account_initialization_required | ||||
| APIBasedExtensionService.delete(extension_data_from_db) | APIBasedExtensionService.delete(extension_data_from_db) | ||||
| return {"result": "success"}, 204 | return {"result": "success"}, 204 | ||||
| api.add_resource(CodeBasedExtensionAPI, "/code-based-extension") | |||||
| api.add_resource(APIBasedExtensionAPI, "/api-based-extension") | |||||
| api.add_resource(APIBasedExtensionDetailAPI, "/api-based-extension/<uuid:id>") |
| from flask_login import current_user | from flask_login import current_user | ||||
| from flask_restx import Resource | |||||
| from flask_restx import Resource, fields | |||||
| from libs.login import login_required | from libs.login import login_required | ||||
| from services.feature_service import FeatureService | from services.feature_service import FeatureService | ||||
| from . import api | |||||
| from . import api, console_ns | |||||
| from .wraps import account_initialization_required, cloud_utm_record, setup_required | from .wraps import account_initialization_required, cloud_utm_record, setup_required | ||||
| @console_ns.route("/features") | |||||
| class FeatureApi(Resource): | class FeatureApi(Resource): | ||||
| @api.doc("get_tenant_features") | |||||
| @api.doc(description="Get feature configuration for current tenant") | |||||
| @api.response( | |||||
| 200, | |||||
| "Success", | |||||
| api.model("FeatureResponse", {"features": fields.Raw(description="Feature configuration object")}), | |||||
| ) | |||||
| @setup_required | @setup_required | ||||
| @login_required | @login_required | ||||
| @account_initialization_required | @account_initialization_required | ||||
| @cloud_utm_record | @cloud_utm_record | ||||
| def get(self): | def get(self): | ||||
| """Get feature configuration for current tenant""" | |||||
| return FeatureService.get_features(current_user.current_tenant_id).model_dump() | return FeatureService.get_features(current_user.current_tenant_id).model_dump() | ||||
| @console_ns.route("/system-features") | |||||
| class SystemFeatureApi(Resource): | class SystemFeatureApi(Resource): | ||||
| @api.doc("get_system_features") | |||||
| @api.doc(description="Get system-wide feature configuration") | |||||
| @api.response( | |||||
| 200, | |||||
| "Success", | |||||
| api.model("SystemFeatureResponse", {"features": fields.Raw(description="System feature configuration object")}), | |||||
| ) | |||||
| def get(self): | def get(self): | ||||
| """Get system-wide feature configuration""" | |||||
| return FeatureService.get_system_features().model_dump() | return FeatureService.get_system_features().model_dump() | ||||
| api.add_resource(FeatureApi, "/features") | |||||
| api.add_resource(SystemFeatureApi, "/system-features") |
| import os | import os | ||||
| from flask import session | from flask import session | ||||
| from flask_restx import Resource, reqparse | |||||
| from flask_restx import Resource, fields, reqparse | |||||
| from sqlalchemy import select | from sqlalchemy import select | ||||
| from sqlalchemy.orm import Session | from sqlalchemy.orm import Session | ||||
| from models.model import DifySetup | from models.model import DifySetup | ||||
| from services.account_service import TenantService | from services.account_service import TenantService | ||||
| from . import api | |||||
| from . import api, console_ns | |||||
| from .error import AlreadySetupError, InitValidateFailedError | from .error import AlreadySetupError, InitValidateFailedError | ||||
| from .wraps import only_edition_self_hosted | from .wraps import only_edition_self_hosted | ||||
| @console_ns.route("/init") | |||||
| class InitValidateAPI(Resource): | class InitValidateAPI(Resource): | ||||
| @api.doc("get_init_status") | |||||
| @api.doc(description="Get initialization validation status") | |||||
| @api.response( | |||||
| 200, | |||||
| "Success", | |||||
| model=api.model( | |||||
| "InitStatusResponse", | |||||
| {"status": fields.String(description="Initialization status", enum=["finished", "not_started"])}, | |||||
| ), | |||||
| ) | |||||
| def get(self): | def get(self): | ||||
| """Get initialization validation status""" | |||||
| init_status = get_init_validate_status() | init_status = get_init_validate_status() | ||||
| if init_status: | if init_status: | ||||
| return {"status": "finished"} | return {"status": "finished"} | ||||
| return {"status": "not_started"} | return {"status": "not_started"} | ||||
| @api.doc("validate_init_password") | |||||
| @api.doc(description="Validate initialization password for self-hosted edition") | |||||
| @api.expect( | |||||
| api.model( | |||||
| "InitValidateRequest", | |||||
| {"password": fields.String(required=True, description="Initialization password", max_length=30)}, | |||||
| ) | |||||
| ) | |||||
| @api.response( | |||||
| 201, | |||||
| "Success", | |||||
| model=api.model("InitValidateResponse", {"result": fields.String(description="Operation result")}), | |||||
| ) | |||||
| @api.response(400, "Already setup or validation failed") | |||||
| @only_edition_self_hosted | @only_edition_self_hosted | ||||
| def post(self): | def post(self): | ||||
| """Validate initialization password""" | |||||
| # is tenant created | # is tenant created | ||||
| tenant_count = TenantService.get_tenant_count() | tenant_count = TenantService.get_tenant_count() | ||||
| if tenant_count > 0: | if tenant_count > 0: | ||||
| return db_session.execute(select(DifySetup)).scalar_one_or_none() | return db_session.execute(select(DifySetup)).scalar_one_or_none() | ||||
| return True | return True | ||||
| api.add_resource(InitValidateAPI, "/init") |
| from flask_restx import Resource | |||||
| from flask_restx import Resource, fields | |||||
| from controllers.console import api | |||||
| from . import api, console_ns | |||||
| @console_ns.route("/ping") | |||||
| class PingApi(Resource): | class PingApi(Resource): | ||||
| @api.doc("health_check") | |||||
| @api.doc(description="Health check endpoint for connection testing") | |||||
| @api.response( | |||||
| 200, | |||||
| "Success", | |||||
| api.model("PingResponse", {"result": fields.String(description="Health check result", example="pong")}), | |||||
| ) | |||||
| def get(self): | def get(self): | ||||
| """ | |||||
| For connection health check | |||||
| """ | |||||
| """Health check endpoint for connection testing""" | |||||
| return {"result": "pong"} | return {"result": "pong"} | ||||
| api.add_resource(PingApi, "/ping") |
| from flask import request | from flask import request | ||||
| from flask_restx import Resource, reqparse | |||||
| from flask_restx import Resource, fields, reqparse | |||||
| from configs import dify_config | from configs import dify_config | ||||
| from libs.helper import StrLen, email, extract_remote_ip | from libs.helper import StrLen, email, extract_remote_ip | ||||
| from models.model import DifySetup, db | from models.model import DifySetup, db | ||||
| from services.account_service import RegisterService, TenantService | from services.account_service import RegisterService, TenantService | ||||
| from . import api | |||||
| from . import api, console_ns | |||||
| from .error import AlreadySetupError, NotInitValidateError | from .error import AlreadySetupError, NotInitValidateError | ||||
| from .init_validate import get_init_validate_status | from .init_validate import get_init_validate_status | ||||
| from .wraps import only_edition_self_hosted | from .wraps import only_edition_self_hosted | ||||
| @console_ns.route("/setup") | |||||
| class SetupApi(Resource): | class SetupApi(Resource): | ||||
| @api.doc("get_setup_status") | |||||
| @api.doc(description="Get system setup status") | |||||
| @api.response( | |||||
| 200, | |||||
| "Success", | |||||
| api.model( | |||||
| "SetupStatusResponse", | |||||
| { | |||||
| "step": fields.String(description="Setup step status", enum=["not_started", "finished"]), | |||||
| "setup_at": fields.String(description="Setup completion time (ISO format)", required=False), | |||||
| }, | |||||
| ), | |||||
| ) | |||||
| def get(self): | def get(self): | ||||
| """Get system setup status""" | |||||
| if dify_config.EDITION == "SELF_HOSTED": | if dify_config.EDITION == "SELF_HOSTED": | ||||
| setup_status = get_setup_status() | setup_status = get_setup_status() | ||||
| if setup_status: | |||||
| # Check if setup_status is a DifySetup object rather than a bool | |||||
| if setup_status and not isinstance(setup_status, bool): | |||||
| return {"step": "finished", "setup_at": setup_status.setup_at.isoformat()} | return {"step": "finished", "setup_at": setup_status.setup_at.isoformat()} | ||||
| elif setup_status: | |||||
| return {"step": "finished"} | |||||
| return {"step": "not_started"} | return {"step": "not_started"} | ||||
| return {"step": "finished"} | return {"step": "finished"} | ||||
| @api.doc("setup_system") | |||||
| @api.doc(description="Initialize system setup with admin account") | |||||
| @api.expect( | |||||
| api.model( | |||||
| "SetupRequest", | |||||
| { | |||||
| "email": fields.String(required=True, description="Admin email address"), | |||||
| "name": fields.String(required=True, description="Admin name (max 30 characters)"), | |||||
| "password": fields.String(required=True, description="Admin password"), | |||||
| }, | |||||
| ) | |||||
| ) | |||||
| @api.response(201, "Success", api.model("SetupResponse", {"result": fields.String(description="Setup result")})) | |||||
| @api.response(400, "Already setup or validation failed") | |||||
| @only_edition_self_hosted | @only_edition_self_hosted | ||||
| def post(self): | def post(self): | ||||
| """Initialize system setup with admin account""" | |||||
| # is set up | # is set up | ||||
| if get_setup_status(): | if get_setup_status(): | ||||
| raise AlreadySetupError() | raise AlreadySetupError() | ||||
| return db.session.query(DifySetup).first() | return db.session.query(DifySetup).first() | ||||
| else: | else: | ||||
| return True | return True | ||||
| api.add_resource(SetupApi, "/setup") |
| import logging | import logging | ||||
| import requests | import requests | ||||
| from flask_restx import Resource, reqparse | |||||
| from flask_restx import Resource, fields, reqparse | |||||
| from packaging import version | from packaging import version | ||||
| from configs import dify_config | from configs import dify_config | ||||
| from . import api | |||||
| from . import api, console_ns | |||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||
| @console_ns.route("/version") | |||||
| class VersionApi(Resource): | class VersionApi(Resource): | ||||
| @api.doc("check_version_update") | |||||
| @api.doc(description="Check for application version updates") | |||||
| @api.expect( | |||||
| api.parser().add_argument( | |||||
| "current_version", type=str, required=True, location="args", help="Current application version" | |||||
| ) | |||||
| ) | |||||
| @api.response( | |||||
| 200, | |||||
| "Success", | |||||
| api.model( | |||||
| "VersionResponse", | |||||
| { | |||||
| "version": fields.String(description="Latest version number"), | |||||
| "release_date": fields.String(description="Release date of latest version"), | |||||
| "release_notes": fields.String(description="Release notes for latest version"), | |||||
| "can_auto_update": fields.Boolean(description="Whether auto-update is supported"), | |||||
| "features": fields.Raw(description="Feature flags and capabilities"), | |||||
| }, | |||||
| ), | |||||
| ) | |||||
| def get(self): | def get(self): | ||||
| """Check for application version updates""" | |||||
| parser = reqparse.RequestParser() | parser = reqparse.RequestParser() | ||||
| parser.add_argument("current_version", type=str, required=True, location="args") | parser.add_argument("current_version", type=str, required=True, location="args") | ||||
| args = parser.parse_args() | args = parser.parse_args() | ||||
| except version.InvalidVersion: | except version.InvalidVersion: | ||||
| logger.warning("Invalid version format: latest=%s, current=%s", latest_version, current_version) | logger.warning("Invalid version format: latest=%s, current=%s", latest_version, current_version) | ||||
| return False | return False | ||||
| api.add_resource(VersionApi, "/version") |
| from flask_login import current_user | from flask_login import current_user | ||||
| from flask_restx import Resource | |||||
| from flask_restx import Resource, fields | |||||
| from controllers.console import api | |||||
| from controllers.console import api, console_ns | |||||
| from controllers.console.wraps import account_initialization_required, setup_required | from controllers.console.wraps import account_initialization_required, setup_required | ||||
| from core.model_runtime.utils.encoders import jsonable_encoder | from core.model_runtime.utils.encoders import jsonable_encoder | ||||
| from libs.login import login_required | from libs.login import login_required | ||||
| from services.agent_service import AgentService | from services.agent_service import AgentService | ||||
| @console_ns.route("/workspaces/current/agent-providers") | |||||
| class AgentProviderListApi(Resource): | class AgentProviderListApi(Resource): | ||||
| @api.doc("list_agent_providers") | |||||
| @api.doc(description="Get list of available agent providers") | |||||
| @api.response( | |||||
| 200, | |||||
| "Success", | |||||
| fields.List(fields.Raw(description="Agent provider information")), | |||||
| ) | |||||
| @setup_required | @setup_required | ||||
| @login_required | @login_required | ||||
| @account_initialization_required | @account_initialization_required | ||||
| return jsonable_encoder(AgentService.list_agent_providers(user_id, tenant_id)) | return jsonable_encoder(AgentService.list_agent_providers(user_id, tenant_id)) | ||||
| @console_ns.route("/workspaces/current/agent-provider/<path:provider_name>") | |||||
| class AgentProviderApi(Resource): | class AgentProviderApi(Resource): | ||||
| @api.doc("get_agent_provider") | |||||
| @api.doc(description="Get specific agent provider details") | |||||
| @api.doc(params={"provider_name": "Agent provider name"}) | |||||
| @api.response( | |||||
| 200, | |||||
| "Success", | |||||
| fields.Raw(description="Agent provider details"), | |||||
| ) | |||||
| @setup_required | @setup_required | ||||
| @login_required | @login_required | ||||
| @account_initialization_required | @account_initialization_required | ||||
| user_id = user.id | user_id = user.id | ||||
| tenant_id = user.current_tenant_id | tenant_id = user.current_tenant_id | ||||
| return jsonable_encoder(AgentService.get_agent_provider(user_id, tenant_id, provider_name)) | return jsonable_encoder(AgentService.get_agent_provider(user_id, tenant_id, provider_name)) | ||||
| api.add_resource(AgentProviderListApi, "/workspaces/current/agent-providers") | |||||
| api.add_resource(AgentProviderApi, "/workspaces/current/agent-provider/<path:provider_name>") |
| from flask_login import current_user | from flask_login import current_user | ||||
| from flask_restx import Resource, reqparse | |||||
| from flask_restx import Resource, fields, reqparse | |||||
| from werkzeug.exceptions import Forbidden | from werkzeug.exceptions import Forbidden | ||||
| from controllers.console import api | |||||
| from controllers.console import api, console_ns | |||||
| from controllers.console.wraps import account_initialization_required, setup_required | from controllers.console.wraps import account_initialization_required, setup_required | ||||
| from core.model_runtime.utils.encoders import jsonable_encoder | from core.model_runtime.utils.encoders import jsonable_encoder | ||||
| from core.plugin.impl.exc import PluginPermissionDeniedError | from core.plugin.impl.exc import PluginPermissionDeniedError | ||||
| from services.plugin.endpoint_service import EndpointService | from services.plugin.endpoint_service import EndpointService | ||||
| @console_ns.route("/workspaces/current/endpoints/create") | |||||
| class EndpointCreateApi(Resource): | class EndpointCreateApi(Resource): | ||||
| @api.doc("create_endpoint") | |||||
| @api.doc(description="Create a new plugin endpoint") | |||||
| @api.expect( | |||||
| api.model( | |||||
| "EndpointCreateRequest", | |||||
| { | |||||
| "plugin_unique_identifier": fields.String(required=True, description="Plugin unique identifier"), | |||||
| "settings": fields.Raw(required=True, description="Endpoint settings"), | |||||
| "name": fields.String(required=True, description="Endpoint name"), | |||||
| }, | |||||
| ) | |||||
| ) | |||||
| @api.response( | |||||
| 200, | |||||
| "Endpoint created successfully", | |||||
| api.model("EndpointCreateResponse", {"success": fields.Boolean(description="Operation success")}), | |||||
| ) | |||||
| @api.response(403, "Admin privileges required") | |||||
| @setup_required | @setup_required | ||||
| @login_required | @login_required | ||||
| @account_initialization_required | @account_initialization_required | ||||
| raise ValueError(e.description) from e | raise ValueError(e.description) from e | ||||
| @console_ns.route("/workspaces/current/endpoints/list") | |||||
| class EndpointListApi(Resource): | class EndpointListApi(Resource): | ||||
| @api.doc("list_endpoints") | |||||
| @api.doc(description="List plugin endpoints with pagination") | |||||
| @api.expect( | |||||
| api.parser() | |||||
| .add_argument("page", type=int, required=True, location="args", help="Page number") | |||||
| .add_argument("page_size", type=int, required=True, location="args", help="Page size") | |||||
| ) | |||||
| @api.response( | |||||
| 200, | |||||
| "Success", | |||||
| api.model("EndpointListResponse", {"endpoints": fields.List(fields.Raw(description="Endpoint information"))}), | |||||
| ) | |||||
| @setup_required | @setup_required | ||||
| @login_required | @login_required | ||||
| @account_initialization_required | @account_initialization_required | ||||
| ) | ) | ||||
| @console_ns.route("/workspaces/current/endpoints/list/plugin") | |||||
| class EndpointListForSinglePluginApi(Resource): | class EndpointListForSinglePluginApi(Resource): | ||||
| @api.doc("list_plugin_endpoints") | |||||
| @api.doc(description="List endpoints for a specific plugin") | |||||
| @api.expect( | |||||
| api.parser() | |||||
| .add_argument("page", type=int, required=True, location="args", help="Page number") | |||||
| .add_argument("page_size", type=int, required=True, location="args", help="Page size") | |||||
| .add_argument("plugin_id", type=str, required=True, location="args", help="Plugin ID") | |||||
| ) | |||||
| @api.response( | |||||
| 200, | |||||
| "Success", | |||||
| api.model( | |||||
| "PluginEndpointListResponse", {"endpoints": fields.List(fields.Raw(description="Endpoint information"))} | |||||
| ), | |||||
| ) | |||||
| @setup_required | @setup_required | ||||
| @login_required | @login_required | ||||
| @account_initialization_required | @account_initialization_required | ||||
| ) | ) | ||||
| @console_ns.route("/workspaces/current/endpoints/delete") | |||||
| class EndpointDeleteApi(Resource): | class EndpointDeleteApi(Resource): | ||||
| @api.doc("delete_endpoint") | |||||
| @api.doc(description="Delete a plugin endpoint") | |||||
| @api.expect( | |||||
| api.model("EndpointDeleteRequest", {"endpoint_id": fields.String(required=True, description="Endpoint ID")}) | |||||
| ) | |||||
| @api.response( | |||||
| 200, | |||||
| "Endpoint deleted successfully", | |||||
| api.model("EndpointDeleteResponse", {"success": fields.Boolean(description="Operation success")}), | |||||
| ) | |||||
| @api.response(403, "Admin privileges required") | |||||
| @setup_required | @setup_required | ||||
| @login_required | @login_required | ||||
| @account_initialization_required | @account_initialization_required | ||||
| } | } | ||||
| @console_ns.route("/workspaces/current/endpoints/update") | |||||
| class EndpointUpdateApi(Resource): | class EndpointUpdateApi(Resource): | ||||
| @api.doc("update_endpoint") | |||||
| @api.doc(description="Update a plugin endpoint") | |||||
| @api.expect( | |||||
| api.model( | |||||
| "EndpointUpdateRequest", | |||||
| { | |||||
| "endpoint_id": fields.String(required=True, description="Endpoint ID"), | |||||
| "settings": fields.Raw(required=True, description="Updated settings"), | |||||
| "name": fields.String(required=True, description="Updated name"), | |||||
| }, | |||||
| ) | |||||
| ) | |||||
| @api.response( | |||||
| 200, | |||||
| "Endpoint updated successfully", | |||||
| api.model("EndpointUpdateResponse", {"success": fields.Boolean(description="Operation success")}), | |||||
| ) | |||||
| @api.response(403, "Admin privileges required") | |||||
| @setup_required | @setup_required | ||||
| @login_required | @login_required | ||||
| @account_initialization_required | @account_initialization_required | ||||
| } | } | ||||
| @console_ns.route("/workspaces/current/endpoints/enable") | |||||
| class EndpointEnableApi(Resource): | class EndpointEnableApi(Resource): | ||||
| @api.doc("enable_endpoint") | |||||
| @api.doc(description="Enable a plugin endpoint") | |||||
| @api.expect( | |||||
| api.model("EndpointEnableRequest", {"endpoint_id": fields.String(required=True, description="Endpoint ID")}) | |||||
| ) | |||||
| @api.response( | |||||
| 200, | |||||
| "Endpoint enabled successfully", | |||||
| api.model("EndpointEnableResponse", {"success": fields.Boolean(description="Operation success")}), | |||||
| ) | |||||
| @api.response(403, "Admin privileges required") | |||||
| @setup_required | @setup_required | ||||
| @login_required | @login_required | ||||
| @account_initialization_required | @account_initialization_required | ||||
| } | } | ||||
| @console_ns.route("/workspaces/current/endpoints/disable") | |||||
| class EndpointDisableApi(Resource): | class EndpointDisableApi(Resource): | ||||
| @api.doc("disable_endpoint") | |||||
| @api.doc(description="Disable a plugin endpoint") | |||||
| @api.expect( | |||||
| api.model("EndpointDisableRequest", {"endpoint_id": fields.String(required=True, description="Endpoint ID")}) | |||||
| ) | |||||
| @api.response( | |||||
| 200, | |||||
| "Endpoint disabled successfully", | |||||
| api.model("EndpointDisableResponse", {"success": fields.Boolean(description="Operation success")}), | |||||
| ) | |||||
| @api.response(403, "Admin privileges required") | |||||
| @setup_required | @setup_required | ||||
| @login_required | @login_required | ||||
| @account_initialization_required | @account_initialization_required | ||||
| tenant_id=user.current_tenant_id, user_id=user.id, endpoint_id=endpoint_id | tenant_id=user.current_tenant_id, user_id=user.id, endpoint_id=endpoint_id | ||||
| ) | ) | ||||
| } | } | ||||
| api.add_resource(EndpointCreateApi, "/workspaces/current/endpoints/create") | |||||
| api.add_resource(EndpointListApi, "/workspaces/current/endpoints/list") | |||||
| api.add_resource(EndpointListForSinglePluginApi, "/workspaces/current/endpoints/list/plugin") | |||||
| api.add_resource(EndpointDeleteApi, "/workspaces/current/endpoints/delete") | |||||
| api.add_resource(EndpointUpdateApi, "/workspaces/current/endpoints/update") | |||||
| api.add_resource(EndpointEnableApi, "/workspaces/current/endpoints/enable") | |||||
| api.add_resource(EndpointDisableApi, "/workspaces/current/endpoints/disable") |
| version="1.0", | version="1.0", | ||||
| title="Files API", | title="Files API", | ||||
| description="API for file operations including upload and preview", | description="API for file operations including upload and preview", | ||||
| doc="/docs", # Enable Swagger UI at /files/docs | |||||
| ) | ) | ||||
| files_ns = Namespace("files", description="File operations", path="/") | files_ns = Namespace("files", description="File operations", path="/") |
| version="1.0", | version="1.0", | ||||
| title="Inner API", | title="Inner API", | ||||
| description="Internal APIs for enterprise features, billing, and plugin communication", | description="Internal APIs for enterprise features, billing, and plugin communication", | ||||
| doc="/docs", # Enable Swagger UI at /inner/api/docs | |||||
| ) | ) | ||||
| # Create namespace | # Create namespace |
| version="1.0", | version="1.0", | ||||
| title="MCP API", | title="MCP API", | ||||
| description="API for Model Context Protocol operations", | description="API for Model Context Protocol operations", | ||||
| doc="/docs", # Enable Swagger UI at /mcp/docs | |||||
| ) | ) | ||||
| mcp_ns = Namespace("mcp", description="MCP operations", path="/") | mcp_ns = Namespace("mcp", description="MCP operations", path="/") |
| version="1.0", | version="1.0", | ||||
| title="Service API", | title="Service API", | ||||
| description="API for application services", | description="API for application services", | ||||
| doc="/docs", # Enable Swagger UI at /v1/docs | |||||
| ) | ) | ||||
| service_api_ns = Namespace("service_api", description="Service operations", path="/") | service_api_ns = Namespace("service_api", description="Service operations", path="/") |
| version="1.0", | version="1.0", | ||||
| title="Web API", | title="Web API", | ||||
| description="Public APIs for web applications including file uploads, chat interactions, and app management", | description="Public APIs for web applications including file uploads, chat interactions, and app management", | ||||
| doc="/docs", # Enable Swagger UI at /api/docs | |||||
| ) | ) | ||||
| # Create namespace | # Create namespace |
| from werkzeug.exceptions import InternalServerError | from werkzeug.exceptions import InternalServerError | ||||
| import services | import services | ||||
| from controllers.web import api | |||||
| from controllers.web import web_ns | |||||
| from controllers.web.error import ( | from controllers.web.error import ( | ||||
| AppUnavailableError, | AppUnavailableError, | ||||
| AudioTooLargeError, | AudioTooLargeError, | ||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||
| @web_ns.route("/audio-to-text") | |||||
| class AudioApi(WebApiResource): | class AudioApi(WebApiResource): | ||||
| audio_to_text_response_fields = { | audio_to_text_response_fields = { | ||||
| "text": fields.String, | "text": fields.String, | ||||
| } | } | ||||
| @marshal_with(audio_to_text_response_fields) | @marshal_with(audio_to_text_response_fields) | ||||
| @api.doc("Audio to Text") | |||||
| @api.doc(description="Convert audio file to text using speech-to-text service.") | |||||
| @api.doc( | |||||
| @web_ns.doc("Audio to Text") | |||||
| @web_ns.doc(description="Convert audio file to text using speech-to-text service.") | |||||
| @web_ns.doc( | |||||
| responses={ | responses={ | ||||
| 200: "Success", | 200: "Success", | ||||
| 400: "Bad Request", | 400: "Bad Request", | ||||
| raise InternalServerError() | raise InternalServerError() | ||||
| @web_ns.route("/text-to-audio") | |||||
| class TextApi(WebApiResource): | class TextApi(WebApiResource): | ||||
| text_to_audio_response_fields = { | text_to_audio_response_fields = { | ||||
| "audio_url": fields.String, | "audio_url": fields.String, | ||||
| } | } | ||||
| @marshal_with(text_to_audio_response_fields) | @marshal_with(text_to_audio_response_fields) | ||||
| @api.doc("Text to Audio") | |||||
| @api.doc(description="Convert text to audio using text-to-speech service.") | |||||
| @api.doc( | |||||
| @web_ns.doc("Text to Audio") | |||||
| @web_ns.doc(description="Convert text to audio using text-to-speech service.") | |||||
| @web_ns.doc( | |||||
| responses={ | responses={ | ||||
| 200: "Success", | 200: "Success", | ||||
| 400: "Bad Request", | 400: "Bad Request", | ||||
| except Exception as e: | except Exception as e: | ||||
| logger.exception("Failed to handle post request to TextApi") | logger.exception("Failed to handle post request to TextApi") | ||||
| raise InternalServerError() | raise InternalServerError() | ||||
| api.add_resource(AudioApi, "/audio-to-text") | |||||
| api.add_resource(TextApi, "/text-to-audio") |
| from werkzeug.exceptions import InternalServerError, NotFound | from werkzeug.exceptions import InternalServerError, NotFound | ||||
| import services | import services | ||||
| from controllers.web import api | |||||
| from controllers.web import web_ns | |||||
| from controllers.web.error import ( | from controllers.web.error import ( | ||||
| AppUnavailableError, | AppUnavailableError, | ||||
| CompletionRequestError, | CompletionRequestError, | ||||
| # define completion api for user | # define completion api for user | ||||
| @web_ns.route("/completion-messages") | |||||
| class CompletionApi(WebApiResource): | class CompletionApi(WebApiResource): | ||||
| @api.doc("Create Completion Message") | |||||
| @api.doc(description="Create a completion message for text generation applications.") | |||||
| @api.doc( | |||||
| @web_ns.doc("Create Completion Message") | |||||
| @web_ns.doc(description="Create a completion message for text generation applications.") | |||||
| @web_ns.doc( | |||||
| params={ | params={ | ||||
| "inputs": {"description": "Input variables for the completion", "type": "object", "required": True}, | "inputs": {"description": "Input variables for the completion", "type": "object", "required": True}, | ||||
| "query": {"description": "Query text for completion", "type": "string", "required": False}, | "query": {"description": "Query text for completion", "type": "string", "required": False}, | ||||
| "retriever_from": {"description": "Source of retriever", "type": "string", "required": False}, | "retriever_from": {"description": "Source of retriever", "type": "string", "required": False}, | ||||
| } | } | ||||
| ) | ) | ||||
| @api.doc( | |||||
| @web_ns.doc( | |||||
| responses={ | responses={ | ||||
| 200: "Success", | 200: "Success", | ||||
| 400: "Bad Request", | 400: "Bad Request", | ||||
| raise InternalServerError() | raise InternalServerError() | ||||
| @web_ns.route("/completion-messages/<string:task_id>/stop") | |||||
| class CompletionStopApi(WebApiResource): | class CompletionStopApi(WebApiResource): | ||||
| @api.doc("Stop Completion Message") | |||||
| @api.doc(description="Stop a running completion message task.") | |||||
| @api.doc(params={"task_id": {"description": "Task ID to stop", "type": "string", "required": True}}) | |||||
| @api.doc( | |||||
| @web_ns.doc("Stop Completion Message") | |||||
| @web_ns.doc(description="Stop a running completion message task.") | |||||
| @web_ns.doc(params={"task_id": {"description": "Task ID to stop", "type": "string", "required": True}}) | |||||
| @web_ns.doc( | |||||
| responses={ | responses={ | ||||
| 200: "Success", | 200: "Success", | ||||
| 400: "Bad Request", | 400: "Bad Request", | ||||
| return {"result": "success"}, 200 | return {"result": "success"}, 200 | ||||
| @web_ns.route("/chat-messages") | |||||
| class ChatApi(WebApiResource): | class ChatApi(WebApiResource): | ||||
| @api.doc("Create Chat Message") | |||||
| @api.doc(description="Create a chat message for conversational applications.") | |||||
| @api.doc( | |||||
| @web_ns.doc("Create Chat Message") | |||||
| @web_ns.doc(description="Create a chat message for conversational applications.") | |||||
| @web_ns.doc( | |||||
| params={ | params={ | ||||
| "inputs": {"description": "Input variables for the chat", "type": "object", "required": True}, | "inputs": {"description": "Input variables for the chat", "type": "object", "required": True}, | ||||
| "query": {"description": "User query/message", "type": "string", "required": True}, | "query": {"description": "User query/message", "type": "string", "required": True}, | ||||
| "retriever_from": {"description": "Source of retriever", "type": "string", "required": False}, | "retriever_from": {"description": "Source of retriever", "type": "string", "required": False}, | ||||
| } | } | ||||
| ) | ) | ||||
| @api.doc( | |||||
| @web_ns.doc( | |||||
| responses={ | responses={ | ||||
| 200: "Success", | 200: "Success", | ||||
| 400: "Bad Request", | 400: "Bad Request", | ||||
| raise InternalServerError() | raise InternalServerError() | ||||
| @web_ns.route("/chat-messages/<string:task_id>/stop") | |||||
| class ChatStopApi(WebApiResource): | class ChatStopApi(WebApiResource): | ||||
| @api.doc("Stop Chat Message") | |||||
| @api.doc(description="Stop a running chat message task.") | |||||
| @api.doc(params={"task_id": {"description": "Task ID to stop", "type": "string", "required": True}}) | |||||
| @api.doc( | |||||
| @web_ns.doc("Stop Chat Message") | |||||
| @web_ns.doc(description="Stop a running chat message task.") | |||||
| @web_ns.doc(params={"task_id": {"description": "Task ID to stop", "type": "string", "required": True}}) | |||||
| @web_ns.doc( | |||||
| responses={ | responses={ | ||||
| 200: "Success", | 200: "Success", | ||||
| 400: "Bad Request", | 400: "Bad Request", | ||||
| AppQueueManager.set_stop_flag(task_id, InvokeFrom.WEB_APP, end_user.id) | AppQueueManager.set_stop_flag(task_id, InvokeFrom.WEB_APP, end_user.id) | ||||
| return {"result": "success"}, 200 | return {"result": "success"}, 200 | ||||
| api.add_resource(CompletionApi, "/completion-messages") | |||||
| api.add_resource(CompletionStopApi, "/completion-messages/<string:task_id>/stop") | |||||
| api.add_resource(ChatApi, "/chat-messages") | |||||
| api.add_resource(ChatStopApi, "/chat-messages/<string:task_id>/stop") |
| from sqlalchemy.orm import Session | from sqlalchemy.orm import Session | ||||
| from werkzeug.exceptions import NotFound | from werkzeug.exceptions import NotFound | ||||
| from controllers.web import api | |||||
| from controllers.web import web_ns | |||||
| from controllers.web.error import NotChatAppError | from controllers.web.error import NotChatAppError | ||||
| from controllers.web.wraps import WebApiResource | from controllers.web.wraps import WebApiResource | ||||
| from core.app.entities.app_invoke_entities import InvokeFrom | from core.app.entities.app_invoke_entities import InvokeFrom | ||||
| from services.web_conversation_service import WebConversationService | from services.web_conversation_service import WebConversationService | ||||
| @web_ns.route("/conversations") | |||||
| class ConversationListApi(WebApiResource): | class ConversationListApi(WebApiResource): | ||||
| @web_ns.doc("Get Conversation List") | |||||
| @web_ns.doc(description="Retrieve paginated list of conversations for a chat application.") | |||||
| @web_ns.doc( | |||||
| params={ | |||||
| "last_id": {"description": "Last conversation ID for pagination", "type": "string", "required": False}, | |||||
| "limit": { | |||||
| "description": "Number of conversations to return (1-100)", | |||||
| "type": "integer", | |||||
| "required": False, | |||||
| "default": 20, | |||||
| }, | |||||
| "pinned": { | |||||
| "description": "Filter by pinned status", | |||||
| "type": "string", | |||||
| "enum": ["true", "false"], | |||||
| "required": False, | |||||
| }, | |||||
| "sort_by": { | |||||
| "description": "Sort order", | |||||
| "type": "string", | |||||
| "enum": ["created_at", "-created_at", "updated_at", "-updated_at"], | |||||
| "required": False, | |||||
| "default": "-updated_at", | |||||
| }, | |||||
| } | |||||
| ) | |||||
| @web_ns.doc( | |||||
| responses={ | |||||
| 200: "Success", | |||||
| 400: "Bad Request", | |||||
| 401: "Unauthorized", | |||||
| 403: "Forbidden", | |||||
| 404: "App Not Found or Not a Chat App", | |||||
| 500: "Internal Server Error", | |||||
| } | |||||
| ) | |||||
| @marshal_with(conversation_infinite_scroll_pagination_fields) | @marshal_with(conversation_infinite_scroll_pagination_fields) | ||||
| def get(self, app_model, end_user): | def get(self, app_model, end_user): | ||||
| app_mode = AppMode.value_of(app_model.mode) | app_mode = AppMode.value_of(app_model.mode) | ||||
| raise NotFound("Last Conversation Not Exists.") | raise NotFound("Last Conversation Not Exists.") | ||||
| @web_ns.route("/conversations/<uuid:c_id>") | |||||
| class ConversationApi(WebApiResource): | class ConversationApi(WebApiResource): | ||||
| delete_response_fields = { | delete_response_fields = { | ||||
| "result": fields.String, | "result": fields.String, | ||||
| } | } | ||||
| @web_ns.doc("Delete Conversation") | |||||
| @web_ns.doc(description="Delete a specific conversation.") | |||||
| @web_ns.doc(params={"c_id": {"description": "Conversation UUID", "type": "string", "required": True}}) | |||||
| @web_ns.doc( | |||||
| responses={ | |||||
| 204: "Conversation deleted successfully", | |||||
| 400: "Bad Request", | |||||
| 401: "Unauthorized", | |||||
| 403: "Forbidden", | |||||
| 404: "Conversation Not Found or Not a Chat App", | |||||
| 500: "Internal Server Error", | |||||
| } | |||||
| ) | |||||
| @marshal_with(delete_response_fields) | @marshal_with(delete_response_fields) | ||||
| def delete(self, app_model, end_user, c_id): | def delete(self, app_model, end_user, c_id): | ||||
| app_mode = AppMode.value_of(app_model.mode) | app_mode = AppMode.value_of(app_model.mode) | ||||
| return {"result": "success"}, 204 | return {"result": "success"}, 204 | ||||
| @web_ns.route("/conversations/<uuid:c_id>/name") | |||||
| class ConversationRenameApi(WebApiResource): | class ConversationRenameApi(WebApiResource): | ||||
| @web_ns.doc("Rename Conversation") | |||||
| @web_ns.doc(description="Rename a specific conversation with a custom name or auto-generate one.") | |||||
| @web_ns.doc(params={"c_id": {"description": "Conversation UUID", "type": "string", "required": True}}) | |||||
| @web_ns.doc( | |||||
| params={ | |||||
| "name": {"description": "New conversation name", "type": "string", "required": False}, | |||||
| "auto_generate": { | |||||
| "description": "Auto-generate conversation name", | |||||
| "type": "boolean", | |||||
| "required": False, | |||||
| "default": False, | |||||
| }, | |||||
| } | |||||
| ) | |||||
| @web_ns.doc( | |||||
| responses={ | |||||
| 200: "Conversation renamed successfully", | |||||
| 400: "Bad Request", | |||||
| 401: "Unauthorized", | |||||
| 403: "Forbidden", | |||||
| 404: "Conversation Not Found or Not a Chat App", | |||||
| 500: "Internal Server Error", | |||||
| } | |||||
| ) | |||||
| @marshal_with(simple_conversation_fields) | @marshal_with(simple_conversation_fields) | ||||
| def post(self, app_model, end_user, c_id): | def post(self, app_model, end_user, c_id): | ||||
| app_mode = AppMode.value_of(app_model.mode) | app_mode = AppMode.value_of(app_model.mode) | ||||
| raise NotFound("Conversation Not Exists.") | raise NotFound("Conversation Not Exists.") | ||||
| @web_ns.route("/conversations/<uuid:c_id>/pin") | |||||
| class ConversationPinApi(WebApiResource): | class ConversationPinApi(WebApiResource): | ||||
| pin_response_fields = { | pin_response_fields = { | ||||
| "result": fields.String, | "result": fields.String, | ||||
| } | } | ||||
| @web_ns.doc("Pin Conversation") | |||||
| @web_ns.doc(description="Pin a specific conversation to keep it at the top of the list.") | |||||
| @web_ns.doc(params={"c_id": {"description": "Conversation UUID", "type": "string", "required": True}}) | |||||
| @web_ns.doc( | |||||
| responses={ | |||||
| 200: "Conversation pinned successfully", | |||||
| 400: "Bad Request", | |||||
| 401: "Unauthorized", | |||||
| 403: "Forbidden", | |||||
| 404: "Conversation Not Found or Not a Chat App", | |||||
| 500: "Internal Server Error", | |||||
| } | |||||
| ) | |||||
| @marshal_with(pin_response_fields) | @marshal_with(pin_response_fields) | ||||
| def patch(self, app_model, end_user, c_id): | def patch(self, app_model, end_user, c_id): | ||||
| app_mode = AppMode.value_of(app_model.mode) | app_mode = AppMode.value_of(app_model.mode) | ||||
| return {"result": "success"} | return {"result": "success"} | ||||
| @web_ns.route("/conversations/<uuid:c_id>/unpin") | |||||
| class ConversationUnPinApi(WebApiResource): | class ConversationUnPinApi(WebApiResource): | ||||
| unpin_response_fields = { | unpin_response_fields = { | ||||
| "result": fields.String, | "result": fields.String, | ||||
| } | } | ||||
| @web_ns.doc("Unpin Conversation") | |||||
| @web_ns.doc(description="Unpin a specific conversation to remove it from the top of the list.") | |||||
| @web_ns.doc(params={"c_id": {"description": "Conversation UUID", "type": "string", "required": True}}) | |||||
| @web_ns.doc( | |||||
| responses={ | |||||
| 200: "Conversation unpinned successfully", | |||||
| 400: "Bad Request", | |||||
| 401: "Unauthorized", | |||||
| 403: "Forbidden", | |||||
| 404: "Conversation Not Found or Not a Chat App", | |||||
| 500: "Internal Server Error", | |||||
| } | |||||
| ) | |||||
| @marshal_with(unpin_response_fields) | @marshal_with(unpin_response_fields) | ||||
| def patch(self, app_model, end_user, c_id): | def patch(self, app_model, end_user, c_id): | ||||
| app_mode = AppMode.value_of(app_model.mode) | app_mode = AppMode.value_of(app_model.mode) | ||||
| WebConversationService.unpin(app_model, conversation_id, end_user) | WebConversationService.unpin(app_model, conversation_id, end_user) | ||||
| return {"result": "success"} | return {"result": "success"} | ||||
| api.add_resource(ConversationRenameApi, "/conversations/<uuid:c_id>/name", endpoint="web_conversation_name") | |||||
| api.add_resource(ConversationListApi, "/conversations") | |||||
| api.add_resource(ConversationApi, "/conversations/<uuid:c_id>") | |||||
| api.add_resource(ConversationPinApi, "/conversations/<uuid:c_id>/pin") | |||||
| api.add_resource(ConversationUnPinApi, "/conversations/<uuid:c_id>/unpin") |
| from flask_restx.inputs import int_range | from flask_restx.inputs import int_range | ||||
| from werkzeug.exceptions import InternalServerError, NotFound | from werkzeug.exceptions import InternalServerError, NotFound | ||||
| from controllers.web import api | |||||
| from controllers.web import web_ns | |||||
| from controllers.web.error import ( | from controllers.web.error import ( | ||||
| AppMoreLikeThisDisabledError, | AppMoreLikeThisDisabledError, | ||||
| AppSuggestedQuestionsAfterAnswerDisabledError, | AppSuggestedQuestionsAfterAnswerDisabledError, | ||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||
| @web_ns.route("/messages") | |||||
| class MessageListApi(WebApiResource): | class MessageListApi(WebApiResource): | ||||
| message_fields = { | message_fields = { | ||||
| "id": fields.String, | "id": fields.String, | ||||
| "data": fields.List(fields.Nested(message_fields)), | "data": fields.List(fields.Nested(message_fields)), | ||||
| } | } | ||||
| @web_ns.doc("Get Message List") | |||||
| @web_ns.doc(description="Retrieve paginated list of messages from a conversation in a chat application.") | |||||
| @web_ns.doc( | |||||
| params={ | |||||
| "conversation_id": {"description": "Conversation UUID", "type": "string", "required": True}, | |||||
| "first_id": {"description": "First message ID for pagination", "type": "string", "required": False}, | |||||
| "limit": { | |||||
| "description": "Number of messages to return (1-100)", | |||||
| "type": "integer", | |||||
| "required": False, | |||||
| "default": 20, | |||||
| }, | |||||
| } | |||||
| ) | |||||
| @web_ns.doc( | |||||
| responses={ | |||||
| 200: "Success", | |||||
| 400: "Bad Request", | |||||
| 401: "Unauthorized", | |||||
| 403: "Forbidden", | |||||
| 404: "Conversation Not Found or Not a Chat App", | |||||
| 500: "Internal Server Error", | |||||
| } | |||||
| ) | |||||
| @marshal_with(message_infinite_scroll_pagination_fields) | @marshal_with(message_infinite_scroll_pagination_fields) | ||||
| def get(self, app_model, end_user): | def get(self, app_model, end_user): | ||||
| app_mode = AppMode.value_of(app_model.mode) | app_mode = AppMode.value_of(app_model.mode) | ||||
| raise NotFound("First Message Not Exists.") | raise NotFound("First Message Not Exists.") | ||||
| @web_ns.route("/messages/<uuid:message_id>/feedbacks") | |||||
| class MessageFeedbackApi(WebApiResource): | class MessageFeedbackApi(WebApiResource): | ||||
| feedback_response_fields = { | feedback_response_fields = { | ||||
| "result": fields.String, | "result": fields.String, | ||||
| } | } | ||||
| @web_ns.doc("Create Message Feedback") | |||||
| @web_ns.doc(description="Submit feedback (like/dislike) for a specific message.") | |||||
| @web_ns.doc(params={"message_id": {"description": "Message UUID", "type": "string", "required": True}}) | |||||
| @web_ns.doc( | |||||
| params={ | |||||
| "rating": { | |||||
| "description": "Feedback rating", | |||||
| "type": "string", | |||||
| "enum": ["like", "dislike"], | |||||
| "required": False, | |||||
| }, | |||||
| "content": {"description": "Feedback content/comment", "type": "string", "required": False}, | |||||
| } | |||||
| ) | |||||
| @web_ns.doc( | |||||
| responses={ | |||||
| 200: "Feedback submitted successfully", | |||||
| 400: "Bad Request", | |||||
| 401: "Unauthorized", | |||||
| 403: "Forbidden", | |||||
| 404: "Message Not Found", | |||||
| 500: "Internal Server Error", | |||||
| } | |||||
| ) | |||||
| @marshal_with(feedback_response_fields) | @marshal_with(feedback_response_fields) | ||||
| def post(self, app_model, end_user, message_id): | def post(self, app_model, end_user, message_id): | ||||
| message_id = str(message_id) | message_id = str(message_id) | ||||
| return {"result": "success"} | return {"result": "success"} | ||||
| @web_ns.route("/messages/<uuid:message_id>/more-like-this") | |||||
| class MessageMoreLikeThisApi(WebApiResource): | class MessageMoreLikeThisApi(WebApiResource): | ||||
| @web_ns.doc("Generate More Like This") | |||||
| @web_ns.doc(description="Generate a new completion similar to an existing message (completion apps only).") | |||||
| @web_ns.doc( | |||||
| params={ | |||||
| "message_id": {"description": "Message UUID", "type": "string", "required": True}, | |||||
| "response_mode": { | |||||
| "description": "Response mode", | |||||
| "type": "string", | |||||
| "enum": ["blocking", "streaming"], | |||||
| "required": True, | |||||
| }, | |||||
| } | |||||
| ) | |||||
| @web_ns.doc( | |||||
| responses={ | |||||
| 200: "Success", | |||||
| 400: "Bad Request - Not a completion app or feature disabled", | |||||
| 401: "Unauthorized", | |||||
| 403: "Forbidden", | |||||
| 404: "Message Not Found", | |||||
| 500: "Internal Server Error", | |||||
| } | |||||
| ) | |||||
| def get(self, app_model, end_user, message_id): | def get(self, app_model, end_user, message_id): | ||||
| if app_model.mode != "completion": | if app_model.mode != "completion": | ||||
| raise NotCompletionAppError() | raise NotCompletionAppError() | ||||
| raise InternalServerError() | raise InternalServerError() | ||||
| @web_ns.route("/messages/<uuid:message_id>/suggested-questions") | |||||
| class MessageSuggestedQuestionApi(WebApiResource): | class MessageSuggestedQuestionApi(WebApiResource): | ||||
| suggested_questions_response_fields = { | suggested_questions_response_fields = { | ||||
| "data": fields.List(fields.String), | "data": fields.List(fields.String), | ||||
| } | } | ||||
| @web_ns.doc("Get Suggested Questions") | |||||
| @web_ns.doc(description="Get suggested follow-up questions after a message (chat apps only).") | |||||
| @web_ns.doc(params={"message_id": {"description": "Message UUID", "type": "string", "required": True}}) | |||||
| @web_ns.doc( | |||||
| responses={ | |||||
| 200: "Success", | |||||
| 400: "Bad Request - Not a chat app or feature disabled", | |||||
| 401: "Unauthorized", | |||||
| 403: "Forbidden", | |||||
| 404: "Message Not Found or Conversation Not Found", | |||||
| 500: "Internal Server Error", | |||||
| } | |||||
| ) | |||||
| @marshal_with(suggested_questions_response_fields) | @marshal_with(suggested_questions_response_fields) | ||||
| def get(self, app_model, end_user, message_id): | def get(self, app_model, end_user, message_id): | ||||
| app_mode = AppMode.value_of(app_model.mode) | app_mode = AppMode.value_of(app_model.mode) | ||||
| raise InternalServerError() | raise InternalServerError() | ||||
| return {"data": questions} | return {"data": questions} | ||||
| api.add_resource(MessageListApi, "/messages") | |||||
| api.add_resource(MessageFeedbackApi, "/messages/<uuid:message_id>/feedbacks") | |||||
| api.add_resource(MessageMoreLikeThisApi, "/messages/<uuid:message_id>/more-like-this") | |||||
| api.add_resource(MessageSuggestedQuestionApi, "/messages/<uuid:message_id>/suggested-questions") |
| from flask_restx.inputs import int_range | from flask_restx.inputs import int_range | ||||
| from werkzeug.exceptions import NotFound | from werkzeug.exceptions import NotFound | ||||
| from controllers.web import api | |||||
| from controllers.web import web_ns | |||||
| from controllers.web.error import NotCompletionAppError | from controllers.web.error import NotCompletionAppError | ||||
| from controllers.web.wraps import WebApiResource | from controllers.web.wraps import WebApiResource | ||||
| from fields.conversation_fields import message_file_fields | from fields.conversation_fields import message_file_fields | ||||
| } | } | ||||
| @web_ns.route("/saved-messages") | |||||
| class SavedMessageListApi(WebApiResource): | class SavedMessageListApi(WebApiResource): | ||||
| saved_message_infinite_scroll_pagination_fields = { | saved_message_infinite_scroll_pagination_fields = { | ||||
| "limit": fields.Integer, | "limit": fields.Integer, | ||||
| "result": fields.String, | "result": fields.String, | ||||
| } | } | ||||
| @web_ns.doc("Get Saved Messages") | |||||
| @web_ns.doc(description="Retrieve paginated list of saved messages for a completion application.") | |||||
| @web_ns.doc( | |||||
| params={ | |||||
| "last_id": {"description": "Last message ID for pagination", "type": "string", "required": False}, | |||||
| "limit": { | |||||
| "description": "Number of messages to return (1-100)", | |||||
| "type": "integer", | |||||
| "required": False, | |||||
| "default": 20, | |||||
| }, | |||||
| } | |||||
| ) | |||||
| @web_ns.doc( | |||||
| responses={ | |||||
| 200: "Success", | |||||
| 400: "Bad Request - Not a completion app", | |||||
| 401: "Unauthorized", | |||||
| 403: "Forbidden", | |||||
| 404: "App Not Found", | |||||
| 500: "Internal Server Error", | |||||
| } | |||||
| ) | |||||
| @marshal_with(saved_message_infinite_scroll_pagination_fields) | @marshal_with(saved_message_infinite_scroll_pagination_fields) | ||||
| def get(self, app_model, end_user): | def get(self, app_model, end_user): | ||||
| if app_model.mode != "completion": | if app_model.mode != "completion": | ||||
| return SavedMessageService.pagination_by_last_id(app_model, end_user, args["last_id"], args["limit"]) | return SavedMessageService.pagination_by_last_id(app_model, end_user, args["last_id"], args["limit"]) | ||||
| @web_ns.doc("Save Message") | |||||
| @web_ns.doc(description="Save a specific message for later reference.") | |||||
| @web_ns.doc( | |||||
| params={ | |||||
| "message_id": {"description": "Message UUID to save", "type": "string", "required": True}, | |||||
| } | |||||
| ) | |||||
| @web_ns.doc( | |||||
| responses={ | |||||
| 200: "Message saved successfully", | |||||
| 400: "Bad Request - Not a completion app", | |||||
| 401: "Unauthorized", | |||||
| 403: "Forbidden", | |||||
| 404: "Message Not Found", | |||||
| 500: "Internal Server Error", | |||||
| } | |||||
| ) | |||||
| @marshal_with(post_response_fields) | @marshal_with(post_response_fields) | ||||
| def post(self, app_model, end_user): | def post(self, app_model, end_user): | ||||
| if app_model.mode != "completion": | if app_model.mode != "completion": | ||||
| return {"result": "success"} | return {"result": "success"} | ||||
| @web_ns.route("/saved-messages/<uuid:message_id>") | |||||
| class SavedMessageApi(WebApiResource): | class SavedMessageApi(WebApiResource): | ||||
| delete_response_fields = { | delete_response_fields = { | ||||
| "result": fields.String, | "result": fields.String, | ||||
| } | } | ||||
| @web_ns.doc("Delete Saved Message") | |||||
| @web_ns.doc(description="Remove a message from saved messages.") | |||||
| @web_ns.doc(params={"message_id": {"description": "Message UUID to delete", "type": "string", "required": True}}) | |||||
| @web_ns.doc( | |||||
| responses={ | |||||
| 204: "Message removed successfully", | |||||
| 400: "Bad Request - Not a completion app", | |||||
| 401: "Unauthorized", | |||||
| 403: "Forbidden", | |||||
| 404: "Message Not Found", | |||||
| 500: "Internal Server Error", | |||||
| } | |||||
| ) | |||||
| @marshal_with(delete_response_fields) | @marshal_with(delete_response_fields) | ||||
| def delete(self, app_model, end_user, message_id): | def delete(self, app_model, end_user, message_id): | ||||
| message_id = str(message_id) | message_id = str(message_id) | ||||
| SavedMessageService.delete(app_model, end_user, message_id) | SavedMessageService.delete(app_model, end_user, message_id) | ||||
| return {"result": "success"}, 204 | return {"result": "success"}, 204 | ||||
| api.add_resource(SavedMessageListApi, "/saved-messages") | |||||
| api.add_resource(SavedMessageApi, "/saved-messages/<uuid:message_id>") |
| from werkzeug.exceptions import Forbidden | from werkzeug.exceptions import Forbidden | ||||
| from configs import dify_config | from configs import dify_config | ||||
| from controllers.web import api | |||||
| from controllers.web import web_ns | |||||
| from controllers.web.wraps import WebApiResource | from controllers.web.wraps import WebApiResource | ||||
| from extensions.ext_database import db | from extensions.ext_database import db | ||||
| from libs.helper import AppIconUrlField | from libs.helper import AppIconUrlField | ||||
| from services.feature_service import FeatureService | from services.feature_service import FeatureService | ||||
| @web_ns.route("/site") | |||||
| class AppSiteApi(WebApiResource): | class AppSiteApi(WebApiResource): | ||||
| """Resource for app sites.""" | """Resource for app sites.""" | ||||
| "custom_config": fields.Raw(attribute="custom_config"), | "custom_config": fields.Raw(attribute="custom_config"), | ||||
| } | } | ||||
| @api.doc("Get App Site Info") | |||||
| @api.doc(description="Retrieve app site information and configuration.") | |||||
| @api.doc( | |||||
| @web_ns.doc("Get App Site Info") | |||||
| @web_ns.doc(description="Retrieve app site information and configuration.") | |||||
| @web_ns.doc( | |||||
| responses={ | responses={ | ||||
| 200: "Success", | 200: "Success", | ||||
| 400: "Bad Request", | 400: "Bad Request", | ||||
| return AppSiteInfo(app_model.tenant, app_model, site, end_user.id, can_replace_logo) | return AppSiteInfo(app_model.tenant, app_model, site, end_user.id, can_replace_logo) | ||||
| api.add_resource(AppSiteApi, "/site") | |||||
| class AppSiteInfo: | class AppSiteInfo: | ||||
| """Class to store site information.""" | """Class to store site information.""" | ||||
| from flask_restx import reqparse | from flask_restx import reqparse | ||||
| from werkzeug.exceptions import InternalServerError | from werkzeug.exceptions import InternalServerError | ||||
| from controllers.web import api | |||||
| from controllers.web import web_ns | |||||
| from controllers.web.error import ( | from controllers.web.error import ( | ||||
| CompletionRequestError, | CompletionRequestError, | ||||
| NotWorkflowAppError, | NotWorkflowAppError, | ||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||
| @web_ns.route("/workflows/run") | |||||
| class WorkflowRunApi(WebApiResource): | class WorkflowRunApi(WebApiResource): | ||||
| @api.doc("Run Workflow") | |||||
| @api.doc(description="Execute a workflow with provided inputs and files.") | |||||
| @api.doc( | |||||
| @web_ns.doc("Run Workflow") | |||||
| @web_ns.doc(description="Execute a workflow with provided inputs and files.") | |||||
| @web_ns.doc( | |||||
| params={ | params={ | ||||
| "inputs": {"description": "Input variables for the workflow", "type": "object", "required": True}, | "inputs": {"description": "Input variables for the workflow", "type": "object", "required": True}, | ||||
| "files": {"description": "Files to be processed by the workflow", "type": "array", "required": False}, | "files": {"description": "Files to be processed by the workflow", "type": "array", "required": False}, | ||||
| } | } | ||||
| ) | ) | ||||
| @api.doc( | |||||
| @web_ns.doc( | |||||
| responses={ | responses={ | ||||
| 200: "Success", | 200: "Success", | ||||
| 400: "Bad Request", | 400: "Bad Request", | ||||
| raise InternalServerError() | raise InternalServerError() | ||||
| @web_ns.route("/workflows/tasks/<string:task_id>/stop") | |||||
| class WorkflowTaskStopApi(WebApiResource): | class WorkflowTaskStopApi(WebApiResource): | ||||
| @api.doc("Stop Workflow Task") | |||||
| @api.doc(description="Stop a running workflow task.") | |||||
| @api.doc( | |||||
| @web_ns.doc("Stop Workflow Task") | |||||
| @web_ns.doc(description="Stop a running workflow task.") | |||||
| @web_ns.doc( | |||||
| params={ | params={ | ||||
| "task_id": {"description": "Task ID to stop", "type": "string", "required": True}, | "task_id": {"description": "Task ID to stop", "type": "string", "required": True}, | ||||
| } | } | ||||
| ) | ) | ||||
| @api.doc( | |||||
| @web_ns.doc( | |||||
| responses={ | responses={ | ||||
| 200: "Success", | 200: "Success", | ||||
| 400: "Bad Request", | 400: "Bad Request", | ||||
| AppQueueManager.set_stop_flag(task_id, InvokeFrom.WEB_APP, end_user.id) | AppQueueManager.set_stop_flag(task_id, InvokeFrom.WEB_APP, end_user.id) | ||||
| return {"result": "success"} | return {"result": "success"} | ||||
| api.add_resource(WorkflowRunApi, "/workflows/run") | |||||
| api.add_resource(WorkflowTaskStopApi, "/workflows/tasks/<string:task_id>/stop") |