refactor(api): migrate workspace account endpoints to BaseModel (#37954)

This commit is contained in:
chariri 2026-07-06 15:57:40 +09:00 committed by GitHub
parent 8208b786ee
commit c3b1508712
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
58 changed files with 1436 additions and 1127 deletions

View File

@ -16,7 +16,7 @@ from core.plugin.plugin_service import PluginService
from core.tools.utils.system_encryption import encrypt_system_params
from extensions.ext_database import db
from models import Tenant
from models.account import TenantPluginAutoUpgradeStrategy
from models.account import TenantPluginAutoUpgradeCategory, TenantPluginAutoUpgradeStrategy
from models.oauth import DatasourceOauthParamConfig, DatasourceProvider
from models.provider_ids import DatasourceProviderID, ToolProviderID
from models.source import DataSourceApiKeyAuthBinding, DataSourceOauthBinding
@ -406,7 +406,7 @@ def migrate_data_for_plugin():
def _candidate_auto_upgrade_strategy_tenant_ids_stmt(limit: int | None = None):
category_count = len(TenantPluginAutoUpgradeStrategy.PluginCategory)
category_count = len(TenantPluginAutoUpgradeCategory)
stmt = (
select(TenantPluginAutoUpgradeStrategy.tenant_id)
.group_by(TenantPluginAutoUpgradeStrategy.tenant_id)

View File

@ -8,7 +8,7 @@ from werkzeug.exceptions import Forbidden
from configs import dify_config
from extensions.ext_database import db
from libs.login import current_account_with_tenant
from models.account import TenantPluginPermission
from models.account import TenantPluginDebugPermission, TenantPluginInstallPermission, TenantPluginPermission
def plugin_permission_required(
@ -40,22 +40,22 @@ def plugin_permission_required(
if install_required:
match permission.install_permission:
case TenantPluginPermission.InstallPermission.NOBODY:
case TenantPluginInstallPermission.NOBODY:
raise Forbidden()
case TenantPluginPermission.InstallPermission.ADMINS:
case TenantPluginInstallPermission.ADMINS:
if not user.is_admin_or_owner:
raise Forbidden()
case TenantPluginPermission.InstallPermission.EVERYONE:
case TenantPluginInstallPermission.EVERYONE:
pass
if debug_required:
match permission.debug_permission:
case TenantPluginPermission.DebugPermission.NOBODY:
case TenantPluginDebugPermission.NOBODY:
raise Forbidden()
case TenantPluginPermission.DebugPermission.ADMINS:
case TenantPluginDebugPermission.ADMINS:
if not user.is_admin_or_owner:
raise Forbidden()
case TenantPluginPermission.DebugPermission.EVERYONE:
case TenantPluginDebugPermission.EVERYONE:
pass
return view(*args, **kwargs)

View File

@ -1,12 +1,13 @@
from __future__ import annotations
from datetime import datetime
from typing import Any, Literal
from http import HTTPStatus
from typing import Literal
import pytz
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, RootModel, field_validator, model_validator
from pydantic import BaseModel, Field, field_validator, model_validator
from sqlalchemy import select
from werkzeug.exceptions import NotFound
@ -47,7 +48,7 @@ from controllers.console.wraps import (
)
from extensions.ext_database import db
from fields.base import ResponseModel
from fields.member_fields import Account as AccountResponse
from fields.member_fields import AccountResponse
from graphon.file import helpers as file_helpers
from libs.datetime_utils import naive_utc_now
from libs.helper import EmailStr, dump_response, extract_remote_ip, timezone, to_timestamp
@ -194,10 +195,6 @@ register_schema_models(
)
def _serialize_account(account) -> dict[str, Any]:
return AccountResponse.model_validate(account, from_attributes=True).model_dump(mode="json")
class AccountIntegrateResponse(ResponseModel):
provider: str
created_at: int | None = None
@ -236,23 +233,15 @@ class EducationAutocompleteResponse(ResponseModel):
has_next: bool | None = None
class EducationActivateResponse(RootModel[dict[str, Any]]):
root: dict[str, Any]
register_schema_models(
console_ns,
AccountIntegrateResponse,
AccountIntegrateListResponse,
EducationVerifyResponse,
EducationStatusResponse,
EducationAutocompleteResponse,
)
register_response_schema_models(
console_ns,
AccountResponse,
AccountIntegrateResponse,
AccountIntegrateListResponse,
AvatarUrlResponse,
EducationActivateResponse,
EducationVerifyResponse,
EducationStatusResponse,
EducationAutocompleteResponse,
SimpleResultDataResponse,
SimpleResultResponse,
VerificationTokenResponse,
@ -262,7 +251,7 @@ register_response_schema_models(
@console_ns.route("/account/init")
class AccountInitApi(Resource):
@console_ns.expect(console_ns.models[AccountInitPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultResponse.__name__])
@setup_required
@login_required
@with_current_user
@ -302,7 +291,7 @@ class AccountInitApi(Resource):
account.initialized_at = naive_utc_now()
db.session.commit()
return {"result": "success"}
return SimpleResultResponse(result="success").model_dump(mode="json")
@console_ns.route("/account/profile")
@ -310,11 +299,11 @@ class AccountProfileApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
@enterprise_license_required
@with_current_user
def get(self, current_user: Account):
return _serialize_account(current_user)
return dump_response(AccountResponse, current_user)
@console_ns.route("/account/name")
@ -323,14 +312,14 @@ class AccountNameApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
@with_current_user
def post(self, current_user: Account):
payload = console_ns.payload or {}
args = AccountNamePayload.model_validate(payload)
updated_account = AccountService.update_account(current_user, session=db.session, name=args.name)
return _serialize_account(updated_account)
return dump_response(AccountResponse, updated_account)
@console_ns.route("/account/avatar")
@ -338,7 +327,7 @@ class AccountAvatarApi(Resource):
@console_ns.doc("get_account_avatar")
@console_ns.doc(description="Get account avatar url")
@console_ns.doc(params=query_params_from_model(AccountAvatarQuery))
@console_ns.response(200, "Success", console_ns.models[AvatarUrlResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AvatarUrlResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -349,7 +338,7 @@ class AccountAvatarApi(Resource):
avatar = args.avatar
if avatar.startswith(("http://", "https://")):
return dump_response(AvatarUrlResponse, {"avatar_url": avatar})
return AvatarUrlResponse(avatar_url=avatar).model_dump(mode="json")
upload_file = db.session.scalar(select(UploadFile).where(UploadFile.id == avatar).limit(1))
if upload_file is None:
@ -362,13 +351,13 @@ class AccountAvatarApi(Resource):
raise NotFound("Avatar file not found")
avatar_url = file_helpers.get_signed_file_url(upload_file_id=upload_file.id)
return dump_response(AvatarUrlResponse, {"avatar_url": avatar_url})
return AvatarUrlResponse(avatar_url=avatar_url).model_dump(mode="json")
@console_ns.expect(console_ns.models[AccountAvatarPayload.__name__])
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
@with_current_user
def post(self, current_user: Account):
payload = console_ns.payload or {}
@ -376,7 +365,7 @@ class AccountAvatarApi(Resource):
updated_account = AccountService.update_account(current_user, session=db.session, avatar=args.avatar)
return _serialize_account(updated_account)
return dump_response(AccountResponse, updated_account)
@console_ns.route("/account/interface-language")
@ -385,7 +374,7 @@ class AccountInterfaceLanguageApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
@with_current_user
def post(self, current_user: Account):
payload = console_ns.payload or {}
@ -395,7 +384,7 @@ class AccountInterfaceLanguageApi(Resource):
current_user, session=db.session, interface_language=args.interface_language
)
return _serialize_account(updated_account)
return dump_response(AccountResponse, updated_account)
@console_ns.route("/account/interface-theme")
@ -404,7 +393,7 @@ class AccountInterfaceThemeApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
@with_current_user
def post(self, current_user: Account):
payload = console_ns.payload or {}
@ -414,7 +403,7 @@ class AccountInterfaceThemeApi(Resource):
current_user, session=db.session, interface_theme=args.interface_theme
)
return _serialize_account(updated_account)
return dump_response(AccountResponse, updated_account)
@console_ns.route("/account/timezone")
@ -423,7 +412,7 @@ class AccountTimezoneApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
@with_current_user
def post(self, current_user: Account):
payload = console_ns.payload or {}
@ -431,7 +420,7 @@ class AccountTimezoneApi(Resource):
updated_account = AccountService.update_account(current_user, session=db.session, timezone=args.timezone)
return _serialize_account(updated_account)
return dump_response(AccountResponse, updated_account)
@console_ns.route("/account/password")
@ -440,7 +429,7 @@ class AccountPasswordApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
@with_current_user
def post(self, current_user: Account):
payload = console_ns.payload or {}
@ -452,7 +441,7 @@ class AccountPasswordApi(Resource):
except ServiceCurrentPasswordIncorrectError:
raise CurrentPasswordIncorrectError()
return _serialize_account(current_user)
return dump_response(AccountResponse, current_user)
@console_ns.route("/account/integrates")
@ -460,7 +449,7 @@ class AccountIntegrateApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[AccountIntegrateListResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountIntegrateListResponse.__name__])
@with_current_user
def get(self, account: Account):
account_integrates = db.session.scalars(
@ -471,33 +460,29 @@ class AccountIntegrateApi(Resource):
oauth_base_path = "/console/api/oauth/login"
providers = ["github", "google"]
integrate_data = []
integrate_data: list[AccountIntegrateResponse] = []
for provider in providers:
existing_integrate = next((ai for ai in account_integrates if ai.provider == provider), None)
if existing_integrate:
integrate_data.append(
{
"id": existing_integrate.id,
"provider": provider,
"created_at": existing_integrate.created_at,
"is_bound": True,
"link": None,
}
AccountIntegrateResponse(
provider=provider,
created_at=to_timestamp(existing_integrate.created_at),
is_bound=True,
link=None,
)
)
else:
integrate_data.append(
{
"id": None,
"provider": provider,
"created_at": None,
"is_bound": False,
"link": f"{base_url}{oauth_base_path}/{provider}",
}
AccountIntegrateResponse(
provider=provider,
created_at=None,
is_bound=False,
link=f"{base_url}{oauth_base_path}/{provider}",
)
)
return AccountIntegrateListResponse(
data=[AccountIntegrateResponse.model_validate(item) for item in integrate_data]
).model_dump(mode="json")
return AccountIntegrateListResponse(data=integrate_data).model_dump(mode="json")
@console_ns.route("/account/delete/verify")
@ -505,19 +490,19 @@ class AccountDeleteVerifyApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[SimpleResultDataResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultDataResponse.__name__])
@with_current_user
def get(self, account: Account):
token, code = AccountService.generate_account_deletion_verification_code(account)
AccountService.send_account_deletion_verification_email(account, code)
return {"result": "success", "data": token}
return SimpleResultDataResponse(result="success", data=token).model_dump(mode="json")
@console_ns.route("/account/delete")
class AccountDeleteApi(Resource):
@console_ns.expect(console_ns.models[AccountDeletePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -531,13 +516,13 @@ class AccountDeleteApi(Resource):
AccountService.delete_account(account)
return {"result": "success"}
return SimpleResultResponse(result="success").model_dump(mode="json")
@console_ns.route("/account/delete/feedback")
class AccountDeleteUpdateFeedbackApi(Resource):
@console_ns.expect(console_ns.models[AccountDeletionFeedbackPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultResponse.__name__])
@setup_required
def post(self):
payload = console_ns.payload or {}
@ -545,7 +530,7 @@ class AccountDeleteUpdateFeedbackApi(Resource):
BillingService.update_account_deletion_feedback(args.email, args.feedback)
return {"result": "success"}
return SimpleResultResponse(result="success").model_dump(mode="json")
@console_ns.route("/account/education/verify")
@ -555,18 +540,19 @@ class EducationVerifyApi(Resource):
@account_initialization_required
@only_edition_cloud
@cloud_edition_billing_enabled
@console_ns.response(200, "Success", console_ns.models[EducationVerifyResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[EducationVerifyResponse.__name__])
@with_current_user
def get(self, account: Account):
return EducationVerifyResponse.model_validate(
BillingService.EducationIdentity.verify(account.id, account.email) or {}
).model_dump(mode="json")
return dump_response(
EducationVerifyResponse, BillingService.EducationIdentity.verify(account.id, account.email) or {}
)
@console_ns.route("/account/education")
class EducationApi(Resource):
@console_ns.expect(console_ns.models[EducationActivatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[EducationActivateResponse.__name__])
# response-contract:ignore billing-service activation payload; TODO: model education activation result.
@console_ns.response(HTTPStatus.OK, "Success")
@setup_required
@login_required
@account_initialization_required
@ -577,21 +563,22 @@ class EducationApi(Resource):
payload = console_ns.payload or {}
args = EducationActivatePayload.model_validate(payload)
return BillingService.EducationIdentity.activate(account, args.token, args.institution, args.role)
result = BillingService.EducationIdentity.activate(account, args.token, args.institution, args.role)
return result
@setup_required
@login_required
@account_initialization_required
@only_edition_cloud
@cloud_edition_billing_enabled
@console_ns.response(200, "Success", console_ns.models[EducationStatusResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[EducationStatusResponse.__name__])
@with_current_user
def get(self, account: Account):
res = BillingService.EducationIdentity.status(account.id) or {}
# convert expire_at to UTC timestamp from isoformat
if res and "expire_at" in res:
res["expire_at"] = datetime.fromisoformat(res["expire_at"]).astimezone(pytz.utc)
return EducationStatusResponse.model_validate(res).model_dump(mode="json")
return dump_response(EducationStatusResponse, res)
@console_ns.route("/account/education/autocomplete")
@ -602,20 +589,21 @@ class EducationAutoCompleteApi(Resource):
@account_initialization_required
@only_edition_cloud
@cloud_edition_billing_enabled
@console_ns.response(200, "Success", console_ns.models[EducationAutocompleteResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[EducationAutocompleteResponse.__name__])
def get(self):
payload = request.args.to_dict(flat=True)
args = EducationAutocompleteQuery.model_validate(payload)
return EducationAutocompleteResponse.model_validate(
BillingService.EducationIdentity.autocomplete(args.keywords, args.page, args.limit) or {}
).model_dump(mode="json")
return dump_response(
EducationAutocompleteResponse,
BillingService.EducationIdentity.autocomplete(args.keywords, args.page, args.limit) or {},
)
@console_ns.route("/account/change-email")
class ChangeEmailSendEmailApi(Resource):
@console_ns.expect(console_ns.models[ChangeEmailSendPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultDataResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultDataResponse.__name__])
@enable_change_email
@setup_required
@login_required
@ -669,13 +657,13 @@ class ChangeEmailSendEmailApi(Resource):
language=language,
phase=send_phase,
)
return {"result": "success", "data": token}
return SimpleResultDataResponse(result="success", data=token).model_dump(mode="json")
@console_ns.route("/account/change-email/validity")
class ChangeEmailCheckApi(Resource):
@console_ns.expect(console_ns.models[ChangeEmailValidityPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[VerificationTokenResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[VerificationTokenResponse.__name__])
@enable_change_email
@setup_required
@login_required
@ -716,7 +704,9 @@ class ChangeEmailCheckApi(Resource):
new_token = AccountService.generate_change_email_token(refreshed_token_data, current_user)
AccountService.reset_change_email_error_rate_limit(user_email)
return {"is_valid": True, "email": normalized_token_email, "token": new_token}
return VerificationTokenResponse(is_valid=True, email=normalized_token_email, token=new_token).model_dump(
mode="json"
)
@console_ns.route("/account/change-email/reset")
@ -726,7 +716,7 @@ class ChangeEmailResetApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[AccountResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountResponse.__name__])
@with_current_user
def post(self, current_user: Account):
payload = console_ns.payload or {}
@ -768,13 +758,13 @@ class ChangeEmailResetApi(Resource):
email=normalized_new_email,
)
return _serialize_account(updated_account)
return dump_response(AccountResponse, updated_account)
@console_ns.route("/account/change-email/check-email-unique")
class CheckEmailUnique(Resource):
@console_ns.expect(console_ns.models[CheckEmailUniquePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultResponse.__name__])
@setup_required
def post(self):
payload = console_ns.payload or {}
@ -784,4 +774,4 @@ class CheckEmailUnique(Resource):
raise AccountInFreezeError()
if not AccountService.check_email_unique(normalized_email, session=db.session):
raise EmailAlreadyInUseError()
return {"result": "success"}
return SimpleResultResponse(result="success").model_dump(mode="json")

View File

@ -6,13 +6,17 @@ verb-based aliases stay available as deprecated resources so OpenAPI metadata
marks only the legacy paths as deprecated.
"""
from datetime import datetime
from enum import StrEnum
from http import HTTPStatus
from typing import Any
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field
from controllers.common.schema import query_params_from_model, register_schema_models
from controllers.common.fields import SuccessResponse
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console import console_ns
from controllers.console.wraps import (
RBACPermission,
@ -24,8 +28,14 @@ from controllers.console.wraps import (
with_current_tenant_id,
with_current_user_id,
)
from core.entities.parameter_entities import (
AppSelectorScope,
ModelSelectorScope,
ToolSelectorScope,
)
from core.entities.provider_entities import ProviderConfigType
from core.plugin.impl.exc import PluginPermissionDeniedError
from graphon.model_runtime.utils.encoders import jsonable_encoder
from fields.base import ResponseModel
from libs.login import login_required
from services.plugin.endpoint_service import EndpointService
@ -40,14 +50,17 @@ class EndpointIdPayload(BaseModel):
endpoint_id: str
class EndpointUpdatePayload(BaseModel):
class EndpointSettingsPayload(BaseModel):
settings: dict[str, Any]
name: str = Field(min_length=1)
class LegacyEndpointUpdatePayload(EndpointIdPayload):
settings: dict[str, Any]
name: str = Field(min_length=1)
class EndpointUpdatePayload(EndpointSettingsPayload):
pass
class LegacyEndpointUpdatePayload(EndpointIdPayload, EndpointSettingsPayload):
pass
class EndpointListQuery(BaseModel):
@ -59,98 +72,158 @@ class EndpointListForPluginQuery(EndpointListQuery):
plugin_id: str
class EndpointCreateResponse(BaseModel):
success: bool = Field(description="Operation success")
class EndpointProviderConfigScope(StrEnum):
ALL = AppSelectorScope.ALL.value
CHAT = AppSelectorScope.CHAT.value
WORKFLOW = AppSelectorScope.WORKFLOW.value
COMPLETION = AppSelectorScope.COMPLETION.value
LLM = ModelSelectorScope.LLM.value
TEXT_EMBEDDING = ModelSelectorScope.TEXT_EMBEDDING.value
RERANK = ModelSelectorScope.RERANK.value
TTS = ModelSelectorScope.TTS.value
SPEECH2TEXT = ModelSelectorScope.SPEECH2TEXT.value
MODERATION = ModelSelectorScope.MODERATION.value
VISION = ModelSelectorScope.VISION.value
CUSTOM = ToolSelectorScope.CUSTOM.value
BUILTIN = ToolSelectorScope.BUILTIN.value
class EndpointListResponse(BaseModel):
endpoints: list[dict[str, Any]] = Field(
description="Endpoint information",
)
class EndpointProviderConfigI18nResponse(ResponseModel):
en_US: str
zh_Hans: str | None = None
pt_BR: str | None = None
ja_JP: str | None = None
class PluginEndpointListResponse(BaseModel):
endpoints: list[dict[str, Any]] = Field(
description="Endpoint information",
)
class EndpointProviderConfigOptionResponse(ResponseModel):
value: str
label: EndpointProviderConfigI18nResponse
class EndpointDeleteResponse(BaseModel):
success: bool = Field(description="Operation success")
class EndpointProviderConfigResponse(ResponseModel):
type: ProviderConfigType
name: str
scope: EndpointProviderConfigScope | None = None
required: bool = False
default: int | str | float | bool | None = None
options: list[EndpointProviderConfigOptionResponse] | None = None
multiple: bool = False
label: EndpointProviderConfigI18nResponse | None = None
help: EndpointProviderConfigI18nResponse | None = None
url: str | None = None
placeholder: EndpointProviderConfigI18nResponse | None = None
class EndpointUpdateResponse(BaseModel):
success: bool = Field(description="Operation success")
class EndpointDeclarationResponse(ResponseModel):
path: str
method: str
hidden: bool = False
class EndpointEnableResponse(BaseModel):
success: bool = Field(description="Operation success")
class EndpointProviderDeclarationResponse(ResponseModel):
settings: list[EndpointProviderConfigResponse] = Field(default_factory=list)
endpoints: list[EndpointDeclarationResponse] | None = Field(default_factory=list)
class EndpointDisableResponse(BaseModel):
success: bool = Field(description="Operation success")
class EndpointListItemResponse(ResponseModel):
id: str
created_at: datetime
updated_at: datetime
tenant_id: str
plugin_id: str
settings: dict[str, Any]
expired_at: datetime
declaration: EndpointProviderDeclarationResponse = Field(default_factory=EndpointProviderDeclarationResponse)
name: str
enabled: bool
url: str
hook_id: str
class EndpointListResponse(ResponseModel):
endpoints: list[EndpointListItemResponse] = Field(description="Endpoint information")
register_schema_models(
console_ns,
EndpointCreatePayload,
EndpointIdPayload,
EndpointSettingsPayload,
EndpointUpdatePayload,
LegacyEndpointUpdatePayload,
EndpointListQuery,
EndpointListForPluginQuery,
EndpointCreateResponse,
)
register_response_schema_models(
console_ns,
SuccessResponse,
EndpointProviderConfigOptionResponse,
EndpointProviderConfigResponse,
EndpointDeclarationResponse,
EndpointProviderDeclarationResponse,
EndpointListItemResponse,
EndpointListResponse,
PluginEndpointListResponse,
EndpointDeleteResponse,
EndpointUpdateResponse,
EndpointEnableResponse,
EndpointDisableResponse,
)
def _create_endpoint(tenant_id: str, user_id: str) -> dict[str, bool]:
def _create_endpoint(tenant_id: str, user_id: str) -> bool:
"""Create a plugin endpoint for the injected workspace and user."""
args = EndpointCreatePayload.model_validate(console_ns.payload)
try:
return {
"success": EndpointService.create_endpoint(
tenant_id=tenant_id,
user_id=user_id,
plugin_unique_identifier=args.plugin_unique_identifier,
name=args.name,
settings=args.settings,
)
}
return EndpointService.create_endpoint(
tenant_id=tenant_id,
user_id=user_id,
plugin_unique_identifier=args.plugin_unique_identifier,
name=args.name,
settings=args.settings,
)
except PluginPermissionDeniedError as e:
raise ValueError(e.description) from e
def _update_endpoint(tenant_id: str, user_id: str, endpoint_id: str) -> dict[str, bool]:
def _update_endpoint(tenant_id: str, user_id: str, endpoint_id: str) -> bool:
"""Update a plugin endpoint identified by the canonical path parameter."""
args = EndpointUpdatePayload.model_validate(console_ns.payload)
return {
"success": EndpointService.update_endpoint(
tenant_id=tenant_id,
user_id=user_id,
endpoint_id=endpoint_id,
name=args.name,
settings=args.settings,
)
}
return EndpointService.update_endpoint(
tenant_id=tenant_id,
user_id=user_id,
endpoint_id=endpoint_id,
name=args.name,
settings=args.settings,
)
def _delete_endpoint(tenant_id: str, user_id: str, endpoint_id: str) -> dict[str, bool]:
def _legacy_update_endpoint(tenant_id: str, user_id: str) -> bool:
args = LegacyEndpointUpdatePayload.model_validate(console_ns.payload)
return EndpointService.update_endpoint(
tenant_id=tenant_id,
user_id=user_id,
endpoint_id=args.endpoint_id,
name=args.name,
settings=args.settings,
)
def _delete_endpoint(tenant_id: str, user_id: str, endpoint_id: str) -> bool:
"""Delete a plugin endpoint identified by the canonical path parameter."""
return {
"success": EndpointService.delete_endpoint(
tenant_id=tenant_id,
user_id=user_id,
endpoint_id=endpoint_id,
)
}
return EndpointService.delete_endpoint(
tenant_id=tenant_id,
user_id=user_id,
endpoint_id=endpoint_id,
)
def _delete_endpoint_from_payload(tenant_id: str, user_id: str) -> bool:
args = EndpointIdPayload.model_validate(console_ns.payload)
return _delete_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=args.endpoint_id)
def _set_endpoint_enabled(tenant_id: str, user_id: str, *, enabled: bool) -> bool:
args = EndpointIdPayload.model_validate(console_ns.payload)
action = EndpointService.enable_endpoint if enabled else EndpointService.disable_endpoint
return action(tenant_id=tenant_id, user_id=user_id, endpoint_id=args.endpoint_id)
@console_ns.route("/workspaces/current/endpoints")
@ -161,11 +234,11 @@ class EndpointCollectionApi(Resource):
@console_ns.doc(description="Create a new plugin endpoint")
@console_ns.expect(console_ns.models[EndpointCreatePayload.__name__])
@console_ns.response(
200,
HTTPStatus.OK,
"Endpoint created successfully",
console_ns.models[EndpointCreateResponse.__name__],
console_ns.models[SuccessResponse.__name__],
)
@console_ns.response(403, "Admin privileges required")
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
@setup_required
@login_required
@is_admin_or_owner_required
@ -174,7 +247,7 @@ class EndpointCollectionApi(Resource):
@with_current_user_id
@with_current_tenant_id
def post(self, tenant_id: str, user_id: str):
return _create_endpoint(tenant_id=tenant_id, user_id=user_id)
return SuccessResponse(success=_create_endpoint(tenant_id=tenant_id, user_id=user_id)).model_dump(mode="json")
@console_ns.route("/workspaces/current/endpoints/create")
@ -190,11 +263,11 @@ class DeprecatedEndpointCreateApi(Resource):
)
@console_ns.expect(console_ns.models[EndpointCreatePayload.__name__])
@console_ns.response(
200,
HTTPStatus.OK,
"Endpoint created successfully",
console_ns.models[EndpointCreateResponse.__name__],
console_ns.models[SuccessResponse.__name__],
)
@console_ns.response(403, "Admin privileges required")
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
@setup_required
@login_required
@is_admin_or_owner_required
@ -203,7 +276,7 @@ class DeprecatedEndpointCreateApi(Resource):
@with_current_user_id
@with_current_tenant_id
def post(self, tenant_id: str, user_id: str):
return _create_endpoint(tenant_id=tenant_id, user_id=user_id)
return SuccessResponse(success=_create_endpoint(tenant_id=tenant_id, user_id=user_id)).model_dump(mode="json")
@console_ns.route("/workspaces/current/endpoints/list")
@ -212,7 +285,7 @@ class EndpointListApi(Resource):
@console_ns.doc(description="List plugin endpoints with pagination")
@console_ns.doc(params=query_params_from_model(EndpointListQuery))
@console_ns.response(
200,
HTTPStatus.OK,
"Success",
console_ns.models[EndpointListResponse.__name__],
)
@ -224,20 +297,15 @@ class EndpointListApi(Resource):
def get(self, tenant_id: str, user_id: str):
args = EndpointListQuery.model_validate(request.args.to_dict(flat=True))
page = args.page
page_size = args.page_size
return jsonable_encoder(
{
"endpoints": EndpointService.list_endpoints(
tenant_id=tenant_id,
user_id=user_id,
page=page,
page_size=page_size,
)
}
endpoints = EndpointService.list_endpoints(
tenant_id=tenant_id,
user_id=user_id,
page=args.page,
page_size=args.page_size,
)
return EndpointListResponse(endpoints=endpoints).model_dump(mode="json")
@console_ns.route("/workspaces/current/endpoints/list/plugin")
class EndpointListForSinglePluginApi(Resource):
@ -245,9 +313,9 @@ class EndpointListForSinglePluginApi(Resource):
@console_ns.doc(description="List endpoints for a specific plugin")
@console_ns.doc(params=query_params_from_model(EndpointListForPluginQuery))
@console_ns.response(
200,
HTTPStatus.OK,
"Success",
console_ns.models[PluginEndpointListResponse.__name__],
console_ns.models[EndpointListResponse.__name__],
)
@setup_required
@login_required
@ -257,22 +325,16 @@ class EndpointListForSinglePluginApi(Resource):
def get(self, tenant_id: str, user_id: str):
args = EndpointListForPluginQuery.model_validate(request.args.to_dict(flat=True))
page = args.page
page_size = args.page_size
plugin_id = args.plugin_id
return jsonable_encoder(
{
"endpoints": EndpointService.list_endpoints_for_single_plugin(
tenant_id=tenant_id,
user_id=user_id,
plugin_id=plugin_id,
page=page,
page_size=page_size,
)
}
endpoints = EndpointService.list_endpoints_for_single_plugin(
tenant_id=tenant_id,
user_id=user_id,
plugin_id=args.plugin_id,
page=args.page,
page_size=args.page_size,
)
return EndpointListResponse(endpoints=endpoints).model_dump(mode="json")
@console_ns.route("/workspaces/current/endpoints/<string:id>")
class EndpointItemApi(Resource):
@ -282,11 +344,11 @@ class EndpointItemApi(Resource):
@console_ns.doc(description="Delete a plugin endpoint")
@console_ns.doc(params={"id": {"description": "Endpoint ID", "type": "string", "required": True}})
@console_ns.response(
200,
HTTPStatus.OK,
"Endpoint deleted successfully",
console_ns.models[EndpointDeleteResponse.__name__],
console_ns.models[SuccessResponse.__name__],
)
@console_ns.response(403, "Admin privileges required")
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
@setup_required
@login_required
@is_admin_or_owner_required
@ -295,18 +357,20 @@ class EndpointItemApi(Resource):
@with_current_user_id
@with_current_tenant_id
def delete(self, tenant_id: str, user_id: str, id: str):
return _delete_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=id)
return SuccessResponse(
success=_delete_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=id)
).model_dump(mode="json")
@console_ns.doc("update_endpoint")
@console_ns.doc(description="Update a plugin endpoint")
@console_ns.expect(console_ns.models[EndpointUpdatePayload.__name__])
@console_ns.doc(params={"id": {"description": "Endpoint ID", "type": "string", "required": True}})
@console_ns.response(
200,
HTTPStatus.OK,
"Endpoint updated successfully",
console_ns.models[EndpointUpdateResponse.__name__],
console_ns.models[SuccessResponse.__name__],
)
@console_ns.response(403, "Admin privileges required")
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
@setup_required
@login_required
@is_admin_or_owner_required
@ -315,7 +379,9 @@ class EndpointItemApi(Resource):
@with_current_user_id
@with_current_tenant_id
def patch(self, tenant_id: str, user_id: str, id: str):
return _update_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=id)
return SuccessResponse(
success=_update_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=id)
).model_dump(mode="json")
@console_ns.route("/workspaces/current/endpoints/delete")
@ -332,11 +398,11 @@ class DeprecatedEndpointDeleteApi(Resource):
)
@console_ns.expect(console_ns.models[EndpointIdPayload.__name__])
@console_ns.response(
200,
HTTPStatus.OK,
"Endpoint deleted successfully",
console_ns.models[EndpointDeleteResponse.__name__],
console_ns.models[SuccessResponse.__name__],
)
@console_ns.response(403, "Admin privileges required")
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
@setup_required
@login_required
@is_admin_or_owner_required
@ -345,8 +411,9 @@ class DeprecatedEndpointDeleteApi(Resource):
@with_current_user_id
@with_current_tenant_id
def post(self, tenant_id: str, user_id: str):
args = EndpointIdPayload.model_validate(console_ns.payload)
return _delete_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=args.endpoint_id)
return SuccessResponse(success=_delete_endpoint_from_payload(tenant_id=tenant_id, user_id=user_id)).model_dump(
mode="json"
)
@console_ns.route("/workspaces/current/endpoints/update")
@ -363,11 +430,11 @@ class DeprecatedEndpointUpdateApi(Resource):
)
@console_ns.expect(console_ns.models[LegacyEndpointUpdatePayload.__name__])
@console_ns.response(
200,
HTTPStatus.OK,
"Endpoint updated successfully",
console_ns.models[EndpointUpdateResponse.__name__],
console_ns.models[SuccessResponse.__name__],
)
@console_ns.response(403, "Admin privileges required")
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
@setup_required
@login_required
@is_admin_or_owner_required
@ -376,8 +443,9 @@ class DeprecatedEndpointUpdateApi(Resource):
@with_current_user_id
@with_current_tenant_id
def post(self, tenant_id: str, user_id: str):
args = LegacyEndpointUpdatePayload.model_validate(console_ns.payload)
return _update_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=args.endpoint_id)
return SuccessResponse(success=_legacy_update_endpoint(tenant_id=tenant_id, user_id=user_id)).model_dump(
mode="json"
)
@console_ns.route("/workspaces/current/endpoints/enable")
@ -386,11 +454,11 @@ class EndpointEnableApi(Resource):
@console_ns.doc(description="Enable a plugin endpoint")
@console_ns.expect(console_ns.models[EndpointIdPayload.__name__])
@console_ns.response(
200,
HTTPStatus.OK,
"Endpoint enabled successfully",
console_ns.models[EndpointEnableResponse.__name__],
console_ns.models[SuccessResponse.__name__],
)
@console_ns.response(403, "Admin privileges required")
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
@setup_required
@login_required
@is_admin_or_owner_required
@ -399,13 +467,9 @@ class EndpointEnableApi(Resource):
@with_current_user_id
@with_current_tenant_id
def post(self, tenant_id: str, user_id: str):
args = EndpointIdPayload.model_validate(console_ns.payload)
return {
"success": EndpointService.enable_endpoint(
tenant_id=tenant_id, user_id=user_id, endpoint_id=args.endpoint_id
)
}
return SuccessResponse(
success=_set_endpoint_enabled(tenant_id=tenant_id, user_id=user_id, enabled=True)
).model_dump(mode="json")
@console_ns.route("/workspaces/current/endpoints/disable")
@ -414,11 +478,11 @@ class EndpointDisableApi(Resource):
@console_ns.doc(description="Disable a plugin endpoint")
@console_ns.expect(console_ns.models[EndpointIdPayload.__name__])
@console_ns.response(
200,
HTTPStatus.OK,
"Endpoint disabled successfully",
console_ns.models[EndpointDisableResponse.__name__],
console_ns.models[SuccessResponse.__name__],
)
@console_ns.response(403, "Admin privileges required")
@console_ns.response(HTTPStatus.FORBIDDEN, "Admin privileges required")
@setup_required
@login_required
@is_admin_or_owner_required
@ -427,10 +491,6 @@ class EndpointDisableApi(Resource):
@with_current_user_id
@with_current_tenant_id
def post(self, tenant_id: str, user_id: str):
args = EndpointIdPayload.model_validate(console_ns.payload)
return {
"success": EndpointService.disable_endpoint(
tenant_id=tenant_id, user_id=user_id, endpoint_id=args.endpoint_id
)
}
return SuccessResponse(
success=_set_endpoint_enabled(tenant_id=tenant_id, user_id=user_id, enabled=False)
).model_dump(mode="json")

View File

@ -1,10 +1,12 @@
from http import HTTPStatus
from urllib import parse
from uuid import UUID
from flask import abort, request
from flask_restx import Resource
from pydantic import BaseModel, Field, TypeAdapter
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import func, select
from werkzeug.exceptions import NotFound
import services
from configs import dify_config
@ -30,8 +32,8 @@ from controllers.console.wraps import (
from extensions.ext_database import db
from extensions.ext_redis import redis_client
from fields.base import ResponseModel
from fields.member_fields import AccountWithRole, AccountWithRoleList
from libs.helper import extract_remote_ip
from fields.member_fields import AccountWithRoleListResponse, AccountWithRoleResponse
from libs.helper import dump_response, extract_remote_ip
from libs.login import current_account_with_tenant, login_required
from models.account import Account, TenantAccountJoin, TenantAccountRole
from services.account_service import AccountService, RegisterService, TenantService
@ -45,6 +47,11 @@ class MemberInvitePayload(BaseModel):
role: str
language: str | None = None
@field_validator("emails")
@classmethod
def normalize_emails(cls, emails: list[str]) -> list[str]:
return list(dict.fromkeys(email.lower() for email in emails))
class MemberRoleUpdatePayload(BaseModel):
role: str
@ -70,14 +77,14 @@ class MemberInviteResultResponse(ResponseModel):
message: str | None = None
class MemberInviteResponse(ResponseModel):
class MemberActionResponse(ResponseModel):
result: str
invitation_results: list[MemberInviteResultResponse]
tenant_id: str
class MemberActionTenantResponse(ResponseModel):
class MemberInviteResponse(ResponseModel):
result: str
invitation_results: list[MemberInviteResultResponse]
tenant_id: str
@ -92,13 +99,14 @@ register_schema_models(
)
register_response_schema_models(
console_ns,
AccountWithRole,
AccountWithRoleList,
AccountWithRoleResponse,
AccountWithRoleListResponse,
MemberActionResponse,
MemberInviteResponse,
MemberInviteResultResponse,
SimpleResultDataResponse,
SimpleResultResponse,
VerificationTokenResponse,
MemberInviteResponse,
MemberActionTenantResponse,
)
@ -124,10 +132,6 @@ def _normalize_enum_value(value: object) -> str:
return str(normalized) if normalized is not None else ""
def _normalize_invitee_emails(emails: list[str]) -> list[str]:
return list(dict.fromkeys(email.lower() for email in emails))
def _count_new_member_invites(tenant_id: str, emails: list[str]) -> int:
new_member_count = 0
for email in emails:
@ -179,7 +183,7 @@ class MemberListApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[AccountWithRoleList.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountWithRoleListResponse.__name__])
@with_current_user
def get(self, current_user: Account | None = None):
if current_user is None:
@ -216,9 +220,7 @@ class MemberListApi(Resource):
}
)
member_models = TypeAdapter(list[AccountWithRole]).validate_python(serialized_members)
response = AccountWithRoleList(accounts=member_models)
return response.model_dump(mode="json"), 200
return dump_response(AccountWithRoleListResponse, {"accounts": serialized_members}), HTTPStatus.OK
@console_ns.route("/workspaces/current/members/invite-email")
@ -226,7 +228,7 @@ class MemberInviteEmailApi(Resource):
"""Invite a new member by email."""
@console_ns.expect(console_ns.models[MemberInvitePayload.__name__])
@console_ns.response(201, "Success", console_ns.models[MemberInviteResponse.__name__])
@console_ns.response(HTTPStatus.CREATED, "Success", console_ns.models[MemberInviteResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -235,26 +237,26 @@ class MemberInviteEmailApi(Resource):
payload = console_ns.payload or {}
args = MemberInvitePayload.model_validate(payload)
invitee_emails = _normalize_invitee_emails(args.emails)
invitee_emails = args.emails
invitee_role = args.role
interface_language = args.language
if not dify_config.RBAC_ENABLED:
if not TenantAccountRole.is_valid_role(invitee_role):
return {"code": "invalid-role", "message": "Invalid role"}, 400
return {"code": "invalid-role", "message": "Invalid role"}, HTTPStatus.BAD_REQUEST
if not TenantAccountRole.is_non_owner_role(TenantAccountRole(invitee_role)):
return {"code": "invalid-role", "message": "Invalid role"}, 400
return {"code": "invalid-role", "message": "Invalid role"}, HTTPStatus.BAD_REQUEST
inviter = current_user
if not inviter.current_tenant:
raise ValueError("No current tenant")
if not _is_role_enabled(invitee_role, inviter.current_tenant.id):
return {"code": "invalid-role", "message": "Invalid role"}, 400
return {"code": "invalid-role", "message": "Invalid role"}, HTTPStatus.BAD_REQUEST
# Check workspace permission for member invitations
from libs.workspace_permission import check_workspace_member_invite_permission
check_workspace_member_invite_permission(inviter.current_tenant.id)
invitation_results = []
invitation_results: list[MemberInviteResultResponse] = []
console_web_url = dify_config.CONSOLE_WEB_URL
tenant_id = inviter.current_tenant.id
@ -277,63 +279,65 @@ class MemberInviteEmailApi(Resource):
)
encoded_invitee_email = parse.quote(invitee_email)
invitation_results.append(
{
"status": "success",
"email": invitee_email,
"url": f"{console_web_url}/activate?email={encoded_invitee_email}&token={token}",
}
MemberInviteResultResponse(
status="success",
email=invitee_email,
url=f"{console_web_url}/activate?email={encoded_invitee_email}&token={token}",
)
)
except AccountAlreadyInTenantError:
invitation_results.append(
{
"status": "already_member",
"email": invitee_email,
"message": "Account already in workspace.",
}
MemberInviteResultResponse(
status="already_member",
email=invitee_email,
message="Account already in workspace.",
)
)
except Exception as e:
invitation_results.append({"status": "failed", "email": invitee_email, "message": str(e)})
invitation_results.append(
MemberInviteResultResponse(status="failed", email=invitee_email, message=str(e))
)
return {
"result": "success",
"invitation_results": invitation_results,
"tenant_id": str(inviter.current_tenant.id) if inviter.current_tenant else "",
}, 201
return MemberInviteResponse(
result="success",
invitation_results=invitation_results,
tenant_id=inviter.current_tenant.id if inviter.current_tenant else "",
).model_dump(mode="json"), HTTPStatus.CREATED
@console_ns.route("/workspaces/current/members/<uuid:member_id>")
class MemberCancelInviteApi(Resource):
"""Cancel an invitation by member id."""
@console_ns.response(200, "Success", console_ns.models[MemberActionTenantResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[MemberActionResponse.__name__])
@with_current_user
def delete(self, current_user: Account, member_id: UUID):
if not current_user.current_tenant:
raise ValueError("No current tenant")
member = db.session.get(Account, str(member_id))
if member is None:
abort(404)
abort(HTTPStatus.NOT_FOUND)
else:
try:
TenantService.remove_member_from_tenant(
current_user.current_tenant, member, current_user, session=db.session
)
except services.errors.account.CannotOperateSelfError as e:
return {"code": "cannot-operate-self", "message": str(e)}, 400
return {"code": "cannot-operate-self", "message": str(e)}, HTTPStatus.BAD_REQUEST
except services.errors.account.NoPermissionError as e:
return {"code": "forbidden", "message": str(e)}, 403
return {"code": "forbidden", "message": str(e)}, HTTPStatus.FORBIDDEN
except services.errors.account.MemberNotInTenantError as e:
return {"code": "member-not-found", "message": str(e)}, 404
return {"code": "member-not-found", "message": str(e)}, HTTPStatus.NOT_FOUND
except Exception as e:
raise ValueError(str(e))
return {
"result": "success",
"tenant_id": str(current_user.current_tenant.id) if current_user.current_tenant else "",
}, 200
return MemberActionResponse(
result="success",
tenant_id=current_user.current_tenant.id if current_user.current_tenant else "",
).model_dump(mode="json"), HTTPStatus.OK
@console_ns.route("/workspaces/current/members/<uuid:member_id>/update-role")
@ -341,7 +345,7 @@ class MemberUpdateRoleApi(Resource):
"""Update member role."""
@console_ns.expect(console_ns.models[MemberRoleUpdatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -352,14 +356,14 @@ class MemberUpdateRoleApi(Resource):
new_role = args.role
if not TenantAccountRole.is_valid_role(new_role):
return {"code": "invalid-role", "message": "Invalid role"}, 400
return {"code": "invalid-role", "message": "Invalid role"}, HTTPStatus.BAD_REQUEST
if not current_user.current_tenant:
raise ValueError("No current tenant")
if not _is_role_enabled(new_role, current_user.current_tenant.id):
return {"code": "invalid-role", "message": "Invalid role"}, 400
return {"code": "invalid-role", "message": "Invalid role"}, HTTPStatus.BAD_REQUEST
member = db.session.get(Account, str(member_id))
if not member:
abort(404)
abort(HTTPStatus.NOT_FOUND)
try:
assert member is not None, "Member not found"
@ -367,17 +371,17 @@ class MemberUpdateRoleApi(Resource):
current_user.current_tenant, member, new_role, current_user, session=db.session
)
except services.errors.account.CannotOperateSelfError as e:
return {"code": "cannot-operate-self", "message": str(e)}, 400
return {"code": "cannot-operate-self", "message": str(e)}, HTTPStatus.BAD_REQUEST
except services.errors.account.NoPermissionError as e:
return {"code": "forbidden", "message": str(e)}, 403
return {"code": "forbidden", "message": str(e)}, HTTPStatus.FORBIDDEN
except services.errors.account.MemberNotInTenantError as e:
return {"code": "member-not-found", "message": str(e)}, 404
return {"code": "member-not-found", "message": str(e)}, HTTPStatus.NOT_FOUND
except services.errors.account.RoleAlreadyAssignedError as e:
return {"code": "role-already-assigned", "message": str(e)}, 400
return {"code": "role-already-assigned", "message": str(e)}, HTTPStatus.BAD_REQUEST
except Exception as e:
raise ValueError(str(e))
return {"result": "success"}
return SimpleResultResponse(result="success").model_dump(mode="json")
@console_ns.route("/workspaces/current/dataset-operators")
@ -387,15 +391,13 @@ class DatasetOperatorMemberListApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[AccountWithRoleList.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountWithRoleListResponse.__name__])
@with_current_user
def get(self, current_user: Account):
if not current_user.current_tenant:
raise ValueError("No current tenant")
members = TenantService.get_dataset_operator_members(current_user.current_tenant, session=db.session)
member_models = TypeAdapter(list[AccountWithRole]).validate_python(members, from_attributes=True)
response = AccountWithRoleList(accounts=member_models)
return response.model_dump(mode="json"), 200
return dump_response(AccountWithRoleListResponse, {"accounts": members}), HTTPStatus.OK
@console_ns.route("/workspaces/current/members/send-owner-transfer-confirm-email")
@ -403,7 +405,7 @@ class SendOwnerTransferEmailApi(Resource):
"""Send owner transfer email."""
@console_ns.expect(console_ns.models[OwnerTransferEmailPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultDataResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultDataResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -435,13 +437,13 @@ class SendOwnerTransferEmailApi(Resource):
workspace_name=current_user.current_tenant.name if current_user.current_tenant else "",
)
return {"result": "success", "data": token}
return SimpleResultDataResponse(result="success", data=token).model_dump(mode="json")
@console_ns.route("/workspaces/current/members/owner-transfer-check")
class OwnerTransferCheckApi(Resource):
@console_ns.expect(console_ns.models[OwnerTransferCheckPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[VerificationTokenResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[VerificationTokenResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -480,13 +482,13 @@ class OwnerTransferCheckApi(Resource):
_, new_token = AccountService.generate_owner_transfer_token(user_email, code=args.code, additional_data={})
AccountService.reset_owner_transfer_error_rate_limit(user_email)
return {"is_valid": True, "email": token_data.get("email"), "token": new_token}
return VerificationTokenResponse(is_valid=True, email=user_email, token=new_token).model_dump(mode="json")
@console_ns.route("/workspaces/current/members/<uuid:member_id>/owner-transfer")
class OwnerTransfer(Resource):
@console_ns.expect(console_ns.models[OwnerTransferPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -516,8 +518,7 @@ class OwnerTransfer(Resource):
member = db.session.get(Account, str(member_id))
if not member:
abort(404)
return # Never reached, but helps type checker
raise NotFound()
if not current_user.current_tenant:
raise ValueError("No current tenant")
@ -546,4 +547,4 @@ class OwnerTransfer(Resource):
except Exception as e:
raise ValueError(str(e))
return {"result": "success"}
return SimpleResultResponse(result="success").model_dump(mode="json")

View File

@ -42,7 +42,15 @@ from fields.base import ResponseModel
from graphon.model_runtime.utils.encoders import jsonable_encoder
from libs.helper import dump_response
from libs.login import login_required
from models.account import Account, TenantPluginAutoUpgradeStrategy, TenantPluginPermission
from models.account import (
Account,
TenantPluginAutoUpgradeCategory,
TenantPluginAutoUpgradeMode,
TenantPluginAutoUpgradeStrategy,
TenantPluginAutoUpgradeStrategySetting,
TenantPluginDebugPermission,
TenantPluginInstallPermission,
)
from models.provider_ids import ToolProviderID
from services.entities.model_provider_entities import ProviderEntityResponse
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
@ -52,9 +60,9 @@ from services.tools.tools_transform_service import ToolTransformService
class AutoUpgradeSettingsResponse(TypedDict):
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting
strategy_setting: TenantPluginAutoUpgradeStrategySetting
upgrade_time_of_day: int
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode
upgrade_mode: TenantPluginAutoUpgradeMode
exclude_plugins: list[str]
include_plugins: list[str]
@ -127,8 +135,8 @@ class ParserUninstall(BaseModel):
class ParserPermissionChange(BaseModel):
install_permission: TenantPluginPermission.InstallPermission = TenantPluginPermission.InstallPermission.EVERYONE
debug_permission: TenantPluginPermission.DebugPermission = TenantPluginPermission.DebugPermission.EVERYONE
install_permission: TenantPluginInstallPermission = TenantPluginInstallPermission.EVERYONE
debug_permission: TenantPluginDebugPermission = TenantPluginDebugPermission.EVERYONE
class ParserDynamicOptions(BaseModel):
@ -150,16 +158,14 @@ class ParserDynamicOptionsWithCredentials(BaseModel):
class PluginPermissionSettingsPayload(BaseModel):
install_permission: TenantPluginPermission.InstallPermission = TenantPluginPermission.InstallPermission.EVERYONE
debug_permission: TenantPluginPermission.DebugPermission = TenantPluginPermission.DebugPermission.EVERYONE
install_permission: TenantPluginInstallPermission = TenantPluginInstallPermission.EVERYONE
debug_permission: TenantPluginDebugPermission = TenantPluginDebugPermission.EVERYONE
class PluginAutoUpgradeSettingsPayload(BaseModel):
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting = (
TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY
)
strategy_setting: TenantPluginAutoUpgradeStrategySetting = TenantPluginAutoUpgradeStrategySetting.FIX_ONLY
upgrade_time_of_day: int = 0
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode = TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
upgrade_mode: TenantPluginAutoUpgradeMode = TenantPluginAutoUpgradeMode.EXCLUDE
exclude_plugins: list[str] = Field(default_factory=list)
include_plugins: list[str] = Field(default_factory=list)
@ -170,15 +176,15 @@ class PluginAutoUpgradeChangeResponse(ResponseModel):
class PluginAutoUpgradeSettingsResponseModel(ResponseModel):
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting
strategy_setting: TenantPluginAutoUpgradeStrategySetting
upgrade_time_of_day: int
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode
upgrade_mode: TenantPluginAutoUpgradeMode
exclude_plugins: list[str]
include_plugins: list[str]
class PluginAutoUpgradeFetchResponse(ResponseModel):
category: TenantPluginAutoUpgradeStrategy.PluginCategory
category: TenantPluginAutoUpgradeCategory
auto_upgrade: PluginAutoUpgradeSettingsResponseModel
@ -209,19 +215,19 @@ class PluginDeclarationResponse(ResponseModel):
class ParserAutoUpgradeChange(BaseModel):
model_config = ConfigDict(extra="forbid")
category: TenantPluginAutoUpgradeStrategy.PluginCategory
category: TenantPluginAutoUpgradeCategory
auto_upgrade: PluginAutoUpgradeSettingsPayload
class ParserAutoUpgradeFetch(BaseModel):
category: TenantPluginAutoUpgradeStrategy.PluginCategory
category: TenantPluginAutoUpgradeCategory
class ParserExcludePlugin(BaseModel):
model_config = ConfigDict(extra="forbid")
plugin_id: str
category: TenantPluginAutoUpgradeStrategy.PluginCategory
category: TenantPluginAutoUpgradeCategory
class ParserReadme(BaseModel):
@ -339,8 +345,8 @@ class PluginTaskResponse(ResponseModel):
class PluginPermissionResponse(ResponseModel):
install_permission: TenantPluginPermission.InstallPermission
debug_permission: TenantPluginPermission.DebugPermission
install_permission: TenantPluginInstallPermission
debug_permission: TenantPluginDebugPermission
class PluginDynamicOptionsResponse(ResponseModel):
@ -408,22 +414,22 @@ register_response_schema_models(
register_enum_models(
console_ns,
TenantPluginPermission.DebugPermission,
TenantPluginAutoUpgradeStrategy.PluginCategory,
TenantPluginAutoUpgradeStrategy.UpgradeMode,
TenantPluginAutoUpgradeStrategy.StrategySetting,
TenantPluginPermission.InstallPermission,
TenantPluginDebugPermission,
TenantPluginAutoUpgradeCategory,
TenantPluginAutoUpgradeMode,
TenantPluginAutoUpgradeStrategySetting,
TenantPluginInstallPermission,
)
def _default_auto_upgrade_settings(
tenant_id: str,
category: TenantPluginAutoUpgradeStrategy.PluginCategory,
category: TenantPluginAutoUpgradeCategory,
) -> AutoUpgradeSettingsResponse:
return {
"strategy_setting": PluginAutoUpgradeService.default_strategy_setting_for_category(category),
"upgrade_time_of_day": PluginAutoUpgradeService.default_upgrade_time_of_day(tenant_id),
"upgrade_mode": TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
"upgrade_mode": TenantPluginAutoUpgradeMode.EXCLUDE,
"exclude_plugins": [],
"include_plugins": [],
}
@ -987,8 +993,8 @@ class PluginFetchPermissionApi(Resource):
if not permission:
return jsonable_encoder(
{
"install_permission": TenantPluginPermission.InstallPermission.EVERYONE,
"debug_permission": TenantPluginPermission.DebugPermission.EVERYONE,
"install_permission": TenantPluginInstallPermission.EVERYONE,
"debug_permission": TenantPluginDebugPermission.EVERYONE,
}
)

View File

@ -1,8 +1,9 @@
import logging
from datetime import datetime
from http import HTTPStatus
from flask import request
from flask_restx import Resource, fields, marshal
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import select
from werkzeug.exceptions import Unauthorized
@ -16,7 +17,12 @@ from controllers.common.errors import (
TooManyFilesError,
UnsupportedFileTypeError,
)
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.common.schema import (
query_params_from_model,
query_params_from_request,
register_response_schema_models,
register_schema_models,
)
from controllers.console import console_ns
from controllers.console.admin import admin_required
from controllers.console.error import AccountNotLinkTenantError
@ -31,7 +37,7 @@ from controllers.console.wraps import (
from enums.cloud_plan import CloudPlan
from extensions.ext_database import db
from fields.base import ResponseModel
from libs.helper import OptionalTimestampField, TimestampField, dump_response, to_timestamp
from libs.helper import dump_response, to_timestamp
from libs.login import login_required
from libs.pagination import paginate_query
from models.account import Account, Tenant, TenantAccountJoin, TenantCustomConfigDict, TenantStatus
@ -133,7 +139,7 @@ class WorkspaceListItemResponse(ResponseModel):
@field_validator("status", mode="before")
@classmethod
def _normalize_status(cls, value):
def _normalize_enum_like(cls, value):
if value is None:
return None
if isinstance(value, str):
@ -146,7 +152,7 @@ class WorkspaceListItemResponse(ResponseModel):
return to_timestamp(value)
class WorkspaceListResponse(ResponseModel):
class WorkspacePaginationResponse(ResponseModel):
data: list[WorkspaceListItemResponse]
has_more: bool
limit: int
@ -159,7 +165,7 @@ class SwitchWorkspaceResponse(ResponseModel):
new_tenant: TenantInfoResponse
class WorkspaceMutationResponse(ResponseModel):
class WorkspaceTenantResultResponse(ResponseModel):
result: str
tenant: TenantInfoResponse
@ -174,6 +180,16 @@ class WorkspacePermissionResponse(ResponseModel):
allow_owner_transfer: bool
WORKSPACE_LOGO_UPLOAD_PARAMS = {
"file": {
"in": "formData",
"type": "file",
"required": True,
"description": "Workspace web app logo file. Only SVG and PNG files are supported.",
}
}
register_schema_models(
console_ns,
WorkspaceListQuery,
@ -184,53 +200,21 @@ register_schema_models(
register_response_schema_models(
console_ns,
TenantInfoResponse,
TenantListItemResponse,
TenantListResponse,
WorkspaceListResponse,
SwitchWorkspaceResponse,
WorkspaceMutationResponse,
WorkspaceLogoUploadResponse,
WorkspaceCustomConfigResponse,
WorkspaceListItemResponse,
WorkspacePaginationResponse,
SwitchWorkspaceResponse,
WorkspaceTenantResultResponse,
WorkspaceLogoUploadResponse,
WorkspacePermissionResponse,
)
provider_fields = {
"provider_name": fields.String,
"provider_type": fields.String,
"is_valid": fields.Boolean,
"token_is_set": fields.Boolean,
}
tenant_fields = {
"id": fields.String,
"name": fields.String,
"plan": fields.String,
"status": fields.String,
"created_at": TimestampField,
"role": fields.String,
"in_trial": fields.Boolean,
"trial_end_reason": fields.String,
"custom_config": fields.Raw(attribute="custom_config"),
"trial_credits": fields.Integer,
"trial_credits_used": fields.Integer,
"next_credit_reset_date": fields.Integer,
}
tenants_fields = {
"id": fields.String,
"name": fields.String,
"plan": fields.String,
"status": fields.String,
"created_at": TimestampField,
"last_opened_at": OptionalTimestampField,
"current": fields.Boolean,
}
workspace_fields = {"id": fields.String, "name": fields.String, "status": fields.String, "created_at": TimestampField}
@console_ns.route("/workspaces")
class TenantListApi(Resource):
@console_ns.response(200, "Success", console_ns.models[TenantListResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[TenantListResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -281,18 +265,17 @@ class TenantListApi(Resource):
tenant_dicts.append(tenant_dict)
return {"workspaces": marshal(tenant_dicts, tenants_fields)}, 200
return dump_response(TenantListResponse, {"workspaces": tenant_dicts}), HTTPStatus.OK
@console_ns.route("/all-workspaces")
class WorkspaceListApi(Resource):
@console_ns.doc(params=query_params_from_model(WorkspaceListQuery))
@console_ns.response(200, "Success", console_ns.models[WorkspaceListResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[WorkspacePaginationResponse.__name__])
@setup_required
@admin_required
def get(self):
payload = request.args.to_dict(flat=True)
args = WorkspaceListQuery.model_validate(payload)
args = query_params_from_request(WorkspaceListQuery)
stmt = select(Tenant).order_by(Tenant.created_at.desc())
tenants = paginate_query(stmt, page=args.page, per_page=args.limit)
@ -301,13 +284,9 @@ class WorkspaceListApi(Resource):
if tenants.has_next:
has_more = True
return {
"data": marshal(tenants.items, workspace_fields),
"has_more": has_more,
"limit": args.limit,
"page": args.page,
"total": tenants.total,
}, 200
return WorkspacePaginationResponse(
data=tenants.items, has_more=has_more, limit=args.limit, page=args.page, total=tenants.total or 0
).model_dump(mode="json"), HTTPStatus.OK
@console_ns.route("/workspaces/current", endpoint="workspaces_current")
@ -316,7 +295,7 @@ class TenantApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[TenantInfoResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[TenantInfoResponse.__name__])
@with_current_user
def post(self, current_user: Account):
if request.path == "/info":
@ -336,13 +315,13 @@ class TenantApi(Resource):
else:
raise Unauthorized("workspace is archived")
return dump_response(TenantInfoResponse, WorkspaceService.get_tenant_info(tenant)), 200
return dump_response(TenantInfoResponse, WorkspaceService.get_tenant_info(tenant)), HTTPStatus.OK
@console_ns.route("/workspaces/switch")
class SwitchWorkspaceApi(Resource):
@console_ns.expect(console_ns.models[SwitchWorkspacePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SwitchWorkspaceResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SwitchWorkspaceResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -351,7 +330,7 @@ class SwitchWorkspaceApi(Resource):
payload = console_ns.payload or {}
args = SwitchWorkspacePayload.model_validate(payload)
# check if tenant_id is valid, 403 if not
# Check whether the tenant_id belongs to the current account.
try:
TenantService.switch_tenant(current_user, args.tenant_id, session=db.session)
except Exception:
@ -361,13 +340,15 @@ class SwitchWorkspaceApi(Resource):
if new_tenant is None:
raise ValueError("Tenant not found")
return {"result": "success", "new_tenant": marshal(WorkspaceService.get_tenant_info(new_tenant), tenant_fields)}
return SwitchWorkspaceResponse(
result="success", new_tenant=WorkspaceService.get_tenant_info(new_tenant)
).model_dump(mode="json")
@console_ns.route("/workspaces/custom-config")
class CustomConfigWorkspaceApi(Resource):
@console_ns.expect(console_ns.models[WorkspaceCustomConfigPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[WorkspaceMutationResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[WorkspaceTenantResultResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -390,12 +371,15 @@ class CustomConfigWorkspaceApi(Resource):
tenant.custom_config_dict = custom_config_dict
db.session.commit()
return {"result": "success", "tenant": marshal(WorkspaceService.get_tenant_info(tenant), tenant_fields)}
return WorkspaceTenantResultResponse(
result="success", tenant=WorkspaceService.get_tenant_info(tenant)
).model_dump(mode="json")
@console_ns.route("/workspaces/custom-config/webapp-logo/upload")
class WebappLogoWorkspaceApi(Resource):
@console_ns.response(201, "Logo uploaded", console_ns.models[WorkspaceLogoUploadResponse.__name__])
@console_ns.doc(consumes=["multipart/form-data"], params=WORKSPACE_LOGO_UPLOAD_PARAMS)
@console_ns.response(HTTPStatus.CREATED, "Logo uploaded", console_ns.models[WorkspaceLogoUploadResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -431,13 +415,13 @@ class WebappLogoWorkspaceApi(Resource):
except services.errors.file.UnsupportedFileTypeError:
raise UnsupportedFileTypeError()
return {"id": upload_file.id}, 201
return WorkspaceLogoUploadResponse(id=upload_file.id).model_dump(mode="json"), HTTPStatus.CREATED
@console_ns.route("/workspaces/info")
class WorkspaceInfoApi(Resource):
@console_ns.expect(console_ns.models[WorkspaceInfoPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[WorkspaceMutationResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[WorkspaceTenantResultResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -453,14 +437,16 @@ class WorkspaceInfoApi(Resource):
tenant.name = args.name
db.session.commit()
return {"result": "success", "tenant": marshal(WorkspaceService.get_tenant_info(tenant), tenant_fields)}
return WorkspaceTenantResultResponse(
result="success", tenant=WorkspaceService.get_tenant_info(tenant)
).model_dump(mode="json")
@console_ns.route("/workspaces/current/permission")
class WorkspacePermissionApi(Resource):
"""Get workspace permissions for the current workspace."""
@console_ns.response(200, "Success", console_ns.models[WorkspacePermissionResponse.__name__])
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[WorkspacePermissionResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -477,8 +463,8 @@ class WorkspacePermissionApi(Resource):
# Get workspace permissions from enterprise service
permission = EnterpriseService.WorkspacePermissionService.get_permission(current_tenant_id)
return {
"workspace_id": permission.workspace_id,
"allow_member_invite": permission.allow_member_invite,
"allow_owner_transfer": permission.allow_owner_transfer,
}, 200
return WorkspacePermissionResponse(
workspace_id=permission.workspace_id,
allow_member_invite=permission.allow_member_invite,
allow_owner_transfer=permission.allow_owner_transfer,
).model_dump(mode="json"), HTTPStatus.OK

View File

@ -3,7 +3,7 @@ from typing import Any
from core.datasource.__base.datasource_plugin import DatasourcePlugin
from core.datasource.entities.datasource_entities import DatasourceProviderEntityWithPlugin, DatasourceProviderType
from core.entities.provider_entities import ProviderConfig
from core.entities.provider_entities import ProviderConfig, ProviderConfigType
from core.plugin.impl.tool import PluginToolManager
from core.tools.errors import ToolProviderCredentialValidationError
@ -78,11 +78,11 @@ class DatasourcePluginProviderController(ABC):
if not credential_schema.required and credentials[credential_name] is None:
continue
if credential_schema.type in {ProviderConfig.Type.SECRET_INPUT, ProviderConfig.Type.TEXT_INPUT}:
if credential_schema.type in {ProviderConfigType.SECRET_INPUT, ProviderConfigType.TEXT_INPUT}:
if not isinstance(credentials[credential_name], str):
raise ToolProviderCredentialValidationError(f"credential {credential_name} should be string")
elif credential_schema.type == ProviderConfig.Type.SELECT:
elif credential_schema.type == ProviderConfigType.SELECT:
if not isinstance(credentials[credential_name], str):
raise ToolProviderCredentialValidationError(f"credential {credential_name} should be string")
@ -107,9 +107,9 @@ class DatasourcePluginProviderController(ABC):
default_value = credential_schema.default
# parse default value into the correct type
if credential_schema.type in {
ProviderConfig.Type.SECRET_INPUT,
ProviderConfig.Type.TEXT_INPUT,
ProviderConfig.Type.SELECT,
ProviderConfigType.SECRET_INPUT,
ProviderConfigType.TEXT_INPUT,
ProviderConfigType.SELECT,
}:
default_value = str(default_value)

View File

@ -9,7 +9,7 @@ from urllib.parse import urlparse
from pydantic import BaseModel
from configs import dify_config
from core.entities.provider_entities import BasicProviderConfig
from core.entities.provider_entities import BasicProviderConfig, ProviderConfigType
from core.helper import encrypter
from core.helper.provider_cache import NoOpProviderCredentialCache
from core.mcp.types import OAuthClientInformation, OAuthClientMetadata, OAuthTokens
@ -315,7 +315,7 @@ class MCPProviderEntity(BaseModel):
return data
# Create dynamic config only for encrypted fields
config = [BasicProviderConfig(type=BasicProviderConfig.Type.SECRET_INPUT, name=key) for key in encrypted_fields]
config = [BasicProviderConfig(type=ProviderConfigType.SECRET_INPUT, name=key) for key in encrypted_fields]
encrypter_instance, _ = create_provider_encrypter(
tenant_id=self.tenant_id,

View File

@ -165,34 +165,35 @@ class ModelSettings(BaseModel):
model_config = ConfigDict(protected_namespaces=())
class ProviderConfigType(StrEnum):
SECRET_INPUT = CommonParameterType.SECRET_INPUT
TEXT_INPUT = CommonParameterType.TEXT_INPUT
SELECT = CommonParameterType.SELECT
BOOLEAN = CommonParameterType.BOOLEAN
APP_SELECTOR = CommonParameterType.APP_SELECTOR
MODEL_SELECTOR = CommonParameterType.MODEL_SELECTOR
TOOLS_SELECTOR = CommonParameterType.TOOLS_SELECTOR
@classmethod
def value_of(cls, value: str) -> ProviderConfigType:
"""
Get value of given mode.
:param value: mode value
:return: mode
"""
for mode in cls:
if mode.value == value:
return mode
raise ValueError(f"invalid mode value {value}")
class BasicProviderConfig(BaseModel):
"""
Base model class for common provider settings like credentials
"""
class Type(StrEnum):
SECRET_INPUT = CommonParameterType.SECRET_INPUT
TEXT_INPUT = CommonParameterType.TEXT_INPUT
SELECT = CommonParameterType.SELECT
BOOLEAN = CommonParameterType.BOOLEAN
APP_SELECTOR = CommonParameterType.APP_SELECTOR
MODEL_SELECTOR = CommonParameterType.MODEL_SELECTOR
TOOLS_SELECTOR = CommonParameterType.TOOLS_SELECTOR
@classmethod
def value_of(cls, value: str) -> ProviderConfig.Type:
"""
Get value of given mode.
:param value: mode value
:return: mode
"""
for mode in cls:
if mode.value == value:
return mode
raise ValueError(f"invalid mode value {value}")
type: Type = Field(..., description="The type of the credentials")
type: ProviderConfigType = Field(..., description="The type of the credentials")
name: str = Field(..., description="The name of the credentials")

View File

@ -3,7 +3,7 @@ from collections.abc import Mapping
from copy import deepcopy
from typing import Any, Protocol
from core.entities.provider_entities import BasicProviderConfig
from core.entities.provider_entities import BasicProviderConfig, ProviderConfigType
from core.helper import encrypter
@ -60,7 +60,7 @@ class ProviderConfigEncrypter:
fields[credential.name] = credential
for field_name, field in fields.items():
if field.type == BasicProviderConfig.Type.SECRET_INPUT:
if field.type == ProviderConfigType.SECRET_INPUT:
if field_name in data:
encrypted = encrypter.encrypt_token(self.tenant_id, data[field_name] or "")
data[field_name] = encrypted
@ -81,7 +81,7 @@ class ProviderConfigEncrypter:
fields[credential.name] = credential
for field_name, field in fields.items():
if field.type == BasicProviderConfig.Type.SECRET_INPUT:
if field.type == ProviderConfigType.SECRET_INPUT:
if field_name in data:
if len(data[field_name]) > 6:
data[field_name] = (
@ -112,7 +112,7 @@ class ProviderConfigEncrypter:
fields[credential.name] = credential
for field_name, field in fields.items():
if field.type == BasicProviderConfig.Type.SECRET_INPUT:
if field.type == ProviderConfigType.SECRET_INPUT:
if field_name in data:
with contextlib.suppress(Exception):
# if the value is None or empty string, skip decrypt

View File

@ -5,12 +5,13 @@ from pydantic import BaseModel
from core.plugin.entities.plugin import PluginDeclaration, PluginInstallationSource
class PluginBundleDependency(BaseModel):
class Type(StrEnum):
Github = PluginInstallationSource.Github.value
Marketplace = PluginInstallationSource.Marketplace.value
Package = PluginInstallationSource.Package.value
class PluginBundleDependencyType(StrEnum):
Github = PluginInstallationSource.Github.value
Marketplace = PluginInstallationSource.Marketplace.value
Package = PluginInstallationSource.Package.value
class PluginBundleDependency(BaseModel):
class Github(BaseModel):
repo_address: str
repo: str
@ -26,5 +27,5 @@ class PluginBundleDependency(BaseModel):
unique_identifier: str
manifest: PluginDeclaration
type: Type
type: PluginBundleDependencyType
value: Github | Marketplace | Package

View File

@ -57,11 +57,12 @@ class MCPServerParameterType(StrEnum):
OBJECT = auto()
class PluginParameterAutoGenerate(BaseModel):
class Type(StrEnum):
PROMPT_INSTRUCTION = auto()
class PluginParameterAutoGenerateType(StrEnum):
PROMPT_INSTRUCTION = auto()
type: Type
class PluginParameterAutoGenerate(BaseModel):
type: PluginParameterAutoGenerateType
class PluginParameterTemplate(BaseModel):

View File

@ -166,12 +166,13 @@ class PluginEntity(PluginInstallation):
return self
class PluginDependency(BaseModel):
class Type(StrEnum):
Github = PluginInstallationSource.Github
Marketplace = PluginInstallationSource.Marketplace
Package = PluginInstallationSource.Package
class PluginDependencyType(StrEnum):
Github = PluginInstallationSource.Github
Marketplace = PluginInstallationSource.Marketplace
Package = PluginInstallationSource.Package
class PluginDependency(BaseModel):
class Github(BaseModel):
repo: str
version: str
@ -194,7 +195,7 @@ class PluginDependency(BaseModel):
plugin_unique_identifier: str
version: str | None = None
type: Type
type: PluginDependencyType
value: Github | Marketplace | Package
current_identifier: str | None = None

View File

@ -2,7 +2,7 @@ from abc import ABC, abstractmethod
from copy import deepcopy
from typing import Any
from core.entities.provider_entities import ProviderConfig
from core.entities.provider_entities import ProviderConfig, ProviderConfigType
from core.tools.__base.tool import Tool
from core.tools.entities.tool_entities import (
ToolProviderEntity,
@ -71,11 +71,11 @@ class ToolProviderController[ToolProviderEntityT: ToolProviderEntity, ToolProvid
if not credential_schema.required and credentials[credential_name] is None:
continue
if credential_schema.type in {ProviderConfig.Type.SECRET_INPUT, ProviderConfig.Type.TEXT_INPUT}:
if credential_schema.type in {ProviderConfigType.SECRET_INPUT, ProviderConfigType.TEXT_INPUT}:
if not isinstance(credentials[credential_name], str):
raise ToolProviderCredentialValidationError(f"credential {credential_name} should be string")
elif credential_schema.type == ProviderConfig.Type.SELECT:
elif credential_schema.type == ProviderConfigType.SELECT:
if not isinstance(credentials[credential_name], str):
raise ToolProviderCredentialValidationError(f"credential {credential_name} should be string")
@ -100,9 +100,9 @@ class ToolProviderController[ToolProviderEntityT: ToolProviderEntity, ToolProvid
default_value = credential_schema.default
# parse default value into the correct type
if credential_schema.type in {
ProviderConfig.Type.SECRET_INPUT,
ProviderConfig.Type.TEXT_INPUT,
ProviderConfig.Type.SELECT,
ProviderConfigType.SECRET_INPUT,
ProviderConfigType.TEXT_INPUT,
ProviderConfigType.SELECT,
}:
default_value = str(default_value)

View File

@ -5,7 +5,7 @@ from typing import override
from pydantic import Field
from sqlalchemy import select
from core.entities.provider_entities import ProviderConfig
from core.entities.provider_entities import ProviderConfig, ProviderConfigType
from core.tools.__base.tool_provider import ToolProviderController
from core.tools.__base.tool_runtime import ToolRuntime
from core.tools.custom_tool.tool import ApiTool
@ -41,7 +41,7 @@ class ApiToolProviderController(ToolProviderController[ToolProviderEntity, ApiTo
ProviderConfig(
name="auth_type",
required=True,
type=ProviderConfig.Type.SELECT,
type=ProviderConfigType.SELECT,
options=[
ProviderConfig.Option(value="none", label=I18nObject(en_US="None", zh_Hans="")),
ProviderConfig.Option(value="api_key_header", label=I18nObject(en_US="Header", zh_Hans="请求头")),
@ -60,20 +60,20 @@ class ApiToolProviderController(ToolProviderController[ToolProviderEntity, ApiTo
name="api_key_header",
required=False,
default="Authorization",
type=ProviderConfig.Type.TEXT_INPUT,
type=ProviderConfigType.TEXT_INPUT,
help=I18nObject(en_US="The header name of the api key", zh_Hans="携带 api key 的 header 名称"),
),
ProviderConfig(
name="api_key_value",
required=True,
type=ProviderConfig.Type.SECRET_INPUT,
type=ProviderConfigType.SECRET_INPUT,
help=I18nObject(en_US="The api key", zh_Hans="api key 的值"),
),
ProviderConfig(
name="api_key_header_prefix",
required=False,
default="basic",
type=ProviderConfig.Type.SELECT,
type=ProviderConfigType.SELECT,
help=I18nObject(en_US="The prefix of the api key header", zh_Hans="api key header 的前缀"),
options=[
ProviderConfig.Option(value="basic", label=I18nObject(en_US="Basic", zh_Hans="Basic")),
@ -89,7 +89,7 @@ class ApiToolProviderController(ToolProviderController[ToolProviderEntity, ApiTo
name="api_key_query_param",
required=False,
default="key",
type=ProviderConfig.Type.TEXT_INPUT,
type=ProviderConfigType.TEXT_INPUT,
help=I18nObject(
en_US="The query parameter name of the api key", zh_Hans="携带 api key 的查询参数名称"
),
@ -97,7 +97,7 @@ class ApiToolProviderController(ToolProviderController[ToolProviderEntity, ApiTo
ProviderConfig(
name="api_key_value",
required=True,
type=ProviderConfig.Type.SECRET_INPUT,
type=ProviderConfigType.SECRET_INPUT,
help=I18nObject(en_US="The api key", zh_Hans="api key 的值"),
),
]

View File

@ -1,7 +1,7 @@
from collections.abc import Mapping
from typing import Union, override
from core.entities.provider_entities import BasicProviderConfig, ProviderConfig
from core.entities.provider_entities import ProviderConfig, ProviderConfigType
from core.helper.provider_cache import ProviderCredentialsCache
from core.helper.provider_encryption import ProviderConfigCache, ProviderConfigEncrypter, create_provider_encrypter
from core.plugin.entities.plugin_daemon import CredentialType
@ -142,7 +142,7 @@ def masked_credentials(
if not config:
masked_credentials[key] = value
continue
if config.type == BasicProviderConfig.Type.SECRET_INPUT:
if config.type == ProviderConfigType.SECRET_INPUT:
if len(value) <= 4:
masked_credentials[key] = "*" * len(value)
else:

View File

@ -369,17 +369,19 @@ class InvitationCode(TypeBase):
)
class TenantPluginInstallPermission(enum.StrEnum):
EVERYONE = "everyone"
ADMINS = "admins"
NOBODY = "noone"
class TenantPluginDebugPermission(enum.StrEnum):
EVERYONE = "everyone"
ADMINS = "admins"
NOBODY = "noone"
class TenantPluginPermission(TypeBase):
class InstallPermission(enum.StrEnum):
EVERYONE = "everyone"
ADMINS = "admins"
NOBODY = "noone"
class DebugPermission(enum.StrEnum):
EVERYONE = "everyone"
ADMINS = "admins"
NOBODY = "noone"
__tablename__ = "account_plugin_permissions"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="account_plugin_permission_pkey"),
@ -390,36 +392,42 @@ class TenantPluginPermission(TypeBase):
StringUUID, insert_default=lambda: str(uuid4()), default_factory=lambda: str(uuid4()), init=False
)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
install_permission: Mapped[InstallPermission] = mapped_column(
EnumText(InstallPermission, length=16),
install_permission: Mapped[TenantPluginInstallPermission] = mapped_column(
EnumText(TenantPluginInstallPermission, length=16),
nullable=False,
server_default="everyone",
default=InstallPermission.EVERYONE,
default=TenantPluginInstallPermission.EVERYONE,
)
debug_permission: Mapped[DebugPermission] = mapped_column(
EnumText(DebugPermission, length=16), nullable=False, server_default="noone", default=DebugPermission.NOBODY
debug_permission: Mapped[TenantPluginDebugPermission] = mapped_column(
EnumText(TenantPluginDebugPermission, length=16),
nullable=False,
server_default="noone",
default=TenantPluginDebugPermission.NOBODY,
)
class TenantPluginAutoUpgradeCategory(enum.StrEnum):
TOOL = "tool"
MODEL = "model"
EXTENSION = "extension"
AGENT_STRATEGY = "agent-strategy"
DATASOURCE = "datasource"
TRIGGER = "trigger"
class TenantPluginAutoUpgradeStrategySetting(enum.StrEnum):
DISABLED = "disabled"
FIX_ONLY = "fix_only"
LATEST = "latest"
class TenantPluginAutoUpgradeMode(enum.StrEnum):
ALL = "all"
PARTIAL = "partial"
EXCLUDE = "exclude"
class TenantPluginAutoUpgradeStrategy(TypeBase):
class PluginCategory(enum.StrEnum):
TOOL = "tool"
MODEL = "model"
EXTENSION = "extension"
AGENT_STRATEGY = "agent-strategy"
DATASOURCE = "datasource"
TRIGGER = "trigger"
class StrategySetting(enum.StrEnum):
DISABLED = "disabled"
FIX_ONLY = "fix_only"
LATEST = "latest"
class UpgradeMode(enum.StrEnum):
ALL = "all"
PARTIAL = "partial"
EXCLUDE = "exclude"
__tablename__ = "tenant_plugin_auto_upgrade_strategies"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="tenant_plugin_auto_upgrade_strategy_pkey"),
@ -431,20 +439,23 @@ class TenantPluginAutoUpgradeStrategy(TypeBase):
StringUUID, insert_default=lambda: str(uuid4()), default_factory=lambda: str(uuid4()), init=False
)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
category: Mapped[PluginCategory] = mapped_column(
EnumText(PluginCategory, length=32),
category: Mapped[TenantPluginAutoUpgradeCategory] = mapped_column(
EnumText(TenantPluginAutoUpgradeCategory, length=32),
nullable=False,
server_default="tool",
default=PluginCategory.TOOL,
default=TenantPluginAutoUpgradeCategory.TOOL,
)
strategy_setting: Mapped[StrategySetting] = mapped_column(
EnumText(StrategySetting, length=16),
strategy_setting: Mapped[TenantPluginAutoUpgradeStrategySetting] = mapped_column(
EnumText(TenantPluginAutoUpgradeStrategySetting, length=16),
nullable=False,
server_default="fix_only",
default=StrategySetting.FIX_ONLY,
default=TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
)
upgrade_mode: Mapped[UpgradeMode] = mapped_column(
EnumText(UpgradeMode, length=16), nullable=False, server_default="exclude", default=UpgradeMode.EXCLUDE
upgrade_mode: Mapped[TenantPluginAutoUpgradeMode] = mapped_column(
EnumText(TenantPluginAutoUpgradeMode, length=16),
nullable=False,
server_default="exclude",
default=TenantPluginAutoUpgradeMode.EXCLUDE,
)
exclude_plugins: Mapped[list[str]] = mapped_column(sa.JSON, nullable=False, default_factory=list)
include_plugins: Mapped[list[str]] = mapped_column(sa.JSON, nullable=False, default_factory=list)

View File

@ -141,9 +141,9 @@ Get account avatar url
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [EducationActivateResponse](#educationactivateresponse)<br> |
| Code | Description |
| ---- | ----------- |
| 200 | Success |
### [GET] /account/education/autocomplete
#### Parameters
@ -1411,7 +1411,7 @@ Infer CLI tool + ENV suggestions from a standardized Agent App skill
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [WorkspaceListResponse](#workspacelistresponse)<br> |
| 200 | Success | **application/json**: [WorkspacePaginationResponse](#workspacepaginationresponse)<br> |
### [GET] /api-based-extension
Get all API-based extensions for current tenant
@ -9983,7 +9983,7 @@ Create a new plugin endpoint
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Endpoint created successfully | **application/json**: [EndpointCreateResponse](#endpointcreateresponse)<br> |
| 200 | Endpoint created successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
| 403 | Admin privileges required | |
### ~~[POST] /workspaces/current/endpoints/create~~
@ -10002,7 +10002,7 @@ Deprecated legacy alias for creating a plugin endpoint. Use POST /workspaces/cur
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Endpoint created successfully | **application/json**: [EndpointCreateResponse](#endpointcreateresponse)<br> |
| 200 | Endpoint created successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
| 403 | Admin privileges required | |
### ~~[POST] /workspaces/current/endpoints/delete~~
@ -10021,7 +10021,7 @@ Deprecated legacy alias for deleting a plugin endpoint. Use DELETE /workspaces/c
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Endpoint deleted successfully | **application/json**: [EndpointDeleteResponse](#endpointdeleteresponse)<br> |
| 200 | Endpoint deleted successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
| 403 | Admin privileges required | |
### [POST] /workspaces/current/endpoints/disable
@ -10037,7 +10037,7 @@ Disable a plugin endpoint
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Endpoint disabled successfully | **application/json**: [EndpointDisableResponse](#endpointdisableresponse)<br> |
| 200 | Endpoint disabled successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
| 403 | Admin privileges required | |
### [POST] /workspaces/current/endpoints/enable
@ -10053,7 +10053,7 @@ Enable a plugin endpoint
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Endpoint enabled successfully | **application/json**: [EndpointEnableResponse](#endpointenableresponse)<br> |
| 200 | Endpoint enabled successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
| 403 | Admin privileges required | |
### [GET] /workspaces/current/endpoints/list
@ -10087,7 +10087,7 @@ List endpoints for a specific plugin
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [PluginEndpointListResponse](#pluginendpointlistresponse)<br> |
| 200 | Success | **application/json**: [EndpointListResponse](#endpointlistresponse)<br> |
### ~~[POST] /workspaces/current/endpoints/update~~
@ -10105,7 +10105,7 @@ Deprecated legacy alias for updating a plugin endpoint. Use PATCH /workspaces/cu
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Endpoint updated successfully | **application/json**: [EndpointUpdateResponse](#endpointupdateresponse)<br> |
| 200 | Endpoint updated successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
| 403 | Admin privileges required | |
### [DELETE] /workspaces/current/endpoints/{id}
@ -10121,7 +10121,7 @@ Delete a plugin endpoint
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Endpoint deleted successfully | **application/json**: [EndpointDeleteResponse](#endpointdeleteresponse)<br> |
| 200 | Endpoint deleted successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
| 403 | Admin privileges required | |
### [PATCH] /workspaces/current/endpoints/{id}
@ -10143,7 +10143,7 @@ Update a plugin endpoint
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Endpoint updated successfully | **application/json**: [EndpointUpdateResponse](#endpointupdateresponse)<br> |
| 200 | Endpoint updated successfully | **application/json**: [SuccessResponse](#successresponse)<br> |
| 403 | Admin privileges required | |
### [GET] /workspaces/current/members
@ -10203,7 +10203,7 @@ Update a plugin endpoint
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [MemberActionTenantResponse](#memberactiontenantresponse)<br> |
| 200 | Success | **application/json**: [MemberActionResponse](#memberactionresponse)<br> |
### [POST] /workspaces/current/members/{member_id}/owner-transfer
#### Parameters
@ -12471,9 +12471,15 @@ Returns permission flags that control workspace features like member invitations
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [WorkspaceMutationResponse](#workspacemutationresponse)<br> |
| 200 | Success | **application/json**: [WorkspaceTenantResultResponse](#workspacetenantresultresponse)<br> |
### [POST] /workspaces/custom-config/webapp-logo/upload
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **multipart/form-data**: { **"file"**: binary }<br> |
#### Responses
| Code | Description | Schema |
@ -12491,7 +12497,7 @@ Returns permission flags that control workspace features like member invitations
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [WorkspaceMutationResponse](#workspacemutationresponse)<br> |
| 200 | Success | **application/json**: [WorkspaceTenantResultResponse](#workspacetenantresultresponse)<br> |
### [POST] /workspaces/switch
#### Request Body
@ -16702,12 +16708,6 @@ Model class for provider custom model configuration.
| start_node_id | string | | Yes |
| start_node_title | string | | Yes |
#### DebugPermission
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| DebugPermission | string | | |
#### DeclaredArrayItem
Per-item shape for an ``array``-typed declared output.
@ -17034,12 +17034,6 @@ Request payload for bulk downloading documents as a zip archive.
| role | string | | Yes |
| token | string | | Yes |
#### EducationActivateResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| EducationActivateResponse | object | | |
#### EducationAutocompleteQuery
| Name | Type | Description | Required |
@ -17149,29 +17143,13 @@ Request payload for bulk downloading documents as a zip archive.
| plugin_unique_identifier | string | | Yes |
| settings | object | | Yes |
#### EndpointCreateResponse
#### EndpointDeclarationResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| success | boolean | Operation success | Yes |
#### EndpointDeleteResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| success | boolean | Operation success | Yes |
#### EndpointDisableResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| success | boolean | Operation success | Yes |
#### EndpointEnableResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| success | boolean | Operation success | Yes |
| hidden | boolean | | No |
| method | string | | Yes |
| path | string | | Yes |
#### EndpointIdPayload
@ -17187,6 +17165,23 @@ Request payload for bulk downloading documents as a zip archive.
| page_size | integer | | Yes |
| plugin_id | string | | Yes |
#### EndpointListItemResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| created_at | dateTime | | Yes |
| declaration | [EndpointProviderDeclarationResponse](#endpointproviderdeclarationresponse) | | No |
| enabled | boolean | | Yes |
| expired_at | dateTime | | Yes |
| hook_id | string | | Yes |
| id | string | | Yes |
| name | string | | Yes |
| plugin_id | string | | Yes |
| settings | object | | Yes |
| tenant_id | string | | Yes |
| updated_at | dateTime | | Yes |
| url | string | | Yes |
#### EndpointListQuery
| Name | Type | Description | Required |
@ -17198,7 +17193,59 @@ Request payload for bulk downloading documents as a zip archive.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| endpoints | [ object ] | Endpoint information | Yes |
| endpoints | [ [EndpointListItemResponse](#endpointlistitemresponse) ] | Endpoint information | Yes |
#### EndpointProviderConfigI18nResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| en_US | string | | Yes |
| ja_JP | string | | No |
| pt_BR | string | | No |
| zh_Hans | string | | No |
#### EndpointProviderConfigOptionResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| label | [EndpointProviderConfigI18nResponse](#endpointproviderconfigi18nresponse) | | Yes |
| value | string | | Yes |
#### EndpointProviderConfigResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| default | integer<br>string<br>number<br>boolean | | No |
| help | [EndpointProviderConfigI18nResponse](#endpointproviderconfigi18nresponse) | | No |
| label | [EndpointProviderConfigI18nResponse](#endpointproviderconfigi18nresponse) | | No |
| multiple | boolean | | No |
| name | string | | Yes |
| options | [ [EndpointProviderConfigOptionResponse](#endpointproviderconfigoptionresponse) ] | | No |
| placeholder | [EndpointProviderConfigI18nResponse](#endpointproviderconfigi18nresponse) | | No |
| required | boolean | | No |
| scope | [EndpointProviderConfigScope](#endpointproviderconfigscope) | | No |
| type | [ProviderConfigType](#providerconfigtype) | | Yes |
| url | string | | No |
#### EndpointProviderConfigScope
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| EndpointProviderConfigScope | string | | |
#### EndpointProviderDeclarationResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| endpoints | [ [EndpointDeclarationResponse](#endpointdeclarationresponse) ] | | No |
| settings | [ [EndpointProviderConfigResponse](#endpointproviderconfigresponse) ] | | No |
#### EndpointSettingsPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| name | string | | Yes |
| settings | object | | Yes |
#### EndpointUpdatePayload
@ -17207,12 +17254,6 @@ Request payload for bulk downloading documents as a zip archive.
| name | string | | Yes |
| settings | object | | Yes |
#### EndpointUpdateResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| success | boolean | Operation success | Yes |
#### EnvSuggestion
| Name | Type | Description | Required |
@ -18010,12 +18051,6 @@ Input field definition for snippet parameters.
| required | boolean | | No |
| type | string | | No |
#### InstallPermission
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| InstallPermission | string | | |
#### InstalledAppCreatePayload
| Name | Type | Description | Required |
@ -18321,7 +18356,7 @@ Enum class for large language model mode.
| marketplace_plugin_unique_identifier | string | | Yes |
| version | string | | No |
#### MemberActionTenantResponse
#### MemberActionResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
@ -19092,13 +19127,13 @@ Enum class for parameter type.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| auto_upgrade | [PluginAutoUpgradeSettingsPayload](#pluginautoupgradesettingspayload) | | Yes |
| category | [PluginCategory](#plugincategory) | | Yes |
| category | [TenantPluginAutoUpgradeCategory](#tenantpluginautoupgradecategory) | | Yes |
#### ParserAutoUpgradeFetch
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| category | [PluginCategory](#plugincategory) | | Yes |
| category | [TenantPluginAutoUpgradeCategory](#tenantpluginautoupgradecategory) | | Yes |
#### ParserCreateCredential
@ -19196,7 +19231,7 @@ Enum class for parameter type.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| category | [PluginCategory](#plugincategory) | | Yes |
| category | [TenantPluginAutoUpgradeCategory](#tenantpluginautoupgradecategory) | | Yes |
| plugin_id | string | | Yes |
#### ParserGetCredentials
@ -19284,8 +19319,8 @@ Enum class for parameter type.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| debug_permission | [DebugPermission](#debugpermission) | | No |
| install_permission | [InstallPermission](#installpermission) | | No |
| debug_permission | [TenantPluginDebugPermission](#tenantplugindebugpermission) | | No |
| install_permission | [TenantPluginInstallPermission](#tenantplugininstallpermission) | | No |
#### ParserPluginIdentifierQuery
@ -19494,7 +19529,7 @@ Shared permission levels for resources (datasets, credentials, etc.)
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| auto_upgrade | [PluginAutoUpgradeSettingsResponseModel](#pluginautoupgradesettingsresponsemodel) | | Yes |
| category | [PluginCategory](#plugincategory) | | Yes |
| category | [TenantPluginAutoUpgradeCategory](#tenantpluginautoupgradecategory) | | Yes |
#### PluginAutoUpgradeSettingsPayload
@ -19502,8 +19537,8 @@ Shared permission levels for resources (datasets, credentials, etc.)
| ---- | ---- | ----------- | -------- |
| exclude_plugins | [ string ] | | No |
| include_plugins | [ string ] | | No |
| strategy_setting | [StrategySetting](#strategysetting) | | No |
| upgrade_mode | [UpgradeMode](#upgrademode) | | No |
| strategy_setting | [TenantPluginAutoUpgradeStrategySetting](#tenantpluginautoupgradestrategysetting) | | No |
| upgrade_mode | [TenantPluginAutoUpgradeMode](#tenantpluginautoupgrademode) | | No |
| upgrade_time_of_day | integer | | No |
#### PluginAutoUpgradeSettingsResponseModel
@ -19512,8 +19547,8 @@ Shared permission levels for resources (datasets, credentials, etc.)
| ---- | ---- | ----------- | -------- |
| exclude_plugins | [ string ] | | Yes |
| include_plugins | [ string ] | | Yes |
| strategy_setting | [StrategySetting](#strategysetting) | | Yes |
| upgrade_mode | [UpgradeMode](#upgrademode) | | Yes |
| strategy_setting | [TenantPluginAutoUpgradeStrategySetting](#tenantpluginautoupgradestrategysetting) | | Yes |
| upgrade_mode | [TenantPluginAutoUpgradeMode](#tenantpluginautoupgrademode) | | Yes |
| upgrade_time_of_day | integer | | Yes |
#### PluginCategory
@ -19635,21 +19670,21 @@ Shared permission levels for resources (datasets, credentials, etc.)
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| current_identifier | string | | No |
| type | [Type](#type) | | Yes |
| type | [PluginDependencyType](#plugindependencytype) | | Yes |
| value | [Github](#github)<br>[Marketplace](#marketplace)<br>[Package](#package) | | Yes |
#### PluginDependencyType
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| PluginDependencyType | string | | |
#### PluginDynamicOptionsResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| options | | | Yes |
#### PluginEndpointListResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| endpoints | [ object ] | Endpoint information | Yes |
#### PluginInstallationItemResponse
| Name | Type | Description | Required |
@ -19730,7 +19765,13 @@ Shared permission levels for resources (datasets, credentials, etc.)
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| type | [core__plugin__entities__parameters__PluginParameterAutoGenerate__Type](#core__plugin__entities__parameters__pluginparameterautogenerate__type) | | Yes |
| type | [PluginParameterAutoGenerateType](#pluginparameterautogeneratetype) | | Yes |
#### PluginParameterAutoGenerateType
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| PluginParameterAutoGenerateType | string | | |
#### PluginParameterOption
@ -19750,15 +19791,15 @@ Shared permission levels for resources (datasets, credentials, etc.)
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| debug_permission | [DebugPermission](#debugpermission) | | Yes |
| install_permission | [InstallPermission](#installpermission) | | Yes |
| debug_permission | [TenantPluginDebugPermission](#tenantplugindebugpermission) | | Yes |
| install_permission | [TenantPluginInstallPermission](#tenantplugininstallpermission) | | Yes |
#### PluginPermissionSettingsPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| debug_permission | [DebugPermission](#debugpermission) | | No |
| install_permission | [InstallPermission](#installpermission) | | No |
| debug_permission | [TenantPluginDebugPermission](#tenantplugindebugpermission) | | No |
| install_permission | [TenantPluginInstallPermission](#tenantplugininstallpermission) | | No |
#### PluginReadmeResponse
@ -19840,9 +19881,15 @@ Model class for common provider settings like credentials
| placeholder | [I18nObject](#i18nobject) | | No |
| required | boolean | | No |
| scope | [AppSelectorScope](#appselectorscope)<br>[ModelSelectorScope](#modelselectorscope)<br>[ToolSelectorScope](#toolselectorscope) | | No |
| type | [core__entities__provider_entities__BasicProviderConfig__Type](#core__entities__provider_entities__basicproviderconfig__type) | The type of the credentials | Yes |
| type | [ProviderConfigType](#providerconfigtype) | The type of the credentials | Yes |
| url | string | | No |
#### ProviderConfigType
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| ProviderConfigType | string | | |
#### ProviderCredentialResponse
| Name | Type | Description | Required |
@ -20998,12 +21045,6 @@ Query parameters for listing snippet published workflows.
| paused | integer | | Yes |
| success | integer | | Yes |
#### StrategySetting
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| StrategySetting | string | | |
#### StringListSource
| Name | Type | Description | Required |
@ -21258,6 +21299,36 @@ Tag type
| ---- | ---- | ----------- | -------- |
| workspaces | [ [TenantListItemResponse](#tenantlistitemresponse) ] | | Yes |
#### TenantPluginAutoUpgradeCategory
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| TenantPluginAutoUpgradeCategory | string | | |
#### TenantPluginAutoUpgradeMode
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| TenantPluginAutoUpgradeMode | string | | |
#### TenantPluginAutoUpgradeStrategySetting
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| TenantPluginAutoUpgradeStrategySetting | string | | |
#### TenantPluginDebugPermission
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| TenantPluginDebugPermission | string | | |
#### TenantPluginInstallPermission
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| TenantPluginInstallPermission | string | | |
#### TextContentResponse
| Name | Type | Description | Required |
@ -21764,12 +21835,6 @@ Enum class for tool provider
| ---- | ---- | ----------- | -------- |
| TriggerSubscriptionListResponse | array | | |
#### Type
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| Type | string | | |
#### UnaddedModelConfiguration
Model class for provider unadded model configuration.
@ -21810,12 +21875,6 @@ Payload for updating a snippet.
| icon_info | [IconInfo](#iconinfo) | | No |
| name | string | | No |
#### UpgradeMode
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| UpgradeMode | string | | |
#### UploadConfig
| Name | Type | Description | Required |
@ -22946,7 +23005,13 @@ Workflow tool configuration
| limit | integer, <br>**Default:** 20 | | No |
| page | integer, <br>**Default:** 1 | | No |
#### WorkspaceListResponse
#### WorkspaceLogoUploadResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| id | string | | Yes |
#### WorkspacePaginationResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
@ -22956,19 +23021,6 @@ Workflow tool configuration
| page | integer | | Yes |
| total | integer | | Yes |
#### WorkspaceLogoUploadResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| id | string | | Yes |
#### WorkspaceMutationResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| result | string | | Yes |
| tenant | [TenantInfoResponse](#tenantinforesponse) | | Yes |
#### WorkspacePermissionResponse
| Name | Type | Description | Required |
@ -22983,6 +23035,13 @@ Workflow tool configuration
| ---- | ---- | ----------- | -------- |
| permission_keys | [ string ] | | No |
#### WorkspaceTenantResultResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| result | string | | Yes |
| tenant | [TenantInfoResponse](#tenantinforesponse) | | Yes |
#### _AccessControlLanguageQuery
| Name | Type | Description | Required |
@ -23051,18 +23110,6 @@ Workflow tool configuration
| ---- | ---- | ----------- | -------- |
| scope | [RBACResourceWhitelistScope](#rbacresourcewhitelistscope) | | Yes |
#### core__entities__provider_entities__BasicProviderConfig__Type
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| core__entities__provider_entities__BasicProviderConfig__Type | string | | |
#### core__plugin__entities__parameters__PluginParameterAutoGenerate__Type
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| core__plugin__entities__parameters__PluginParameterAutoGenerate__Type | string | | |
#### core__tools__entities__common_entities__I18nObject
Model class for i18n object.

View File

@ -941,9 +941,15 @@ Strict (extra='forbid').
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| current_identifier | string | | No |
| type | [Type](#type) | | Yes |
| type | [PluginDependencyType](#plugindependencytype) | | Yes |
| value | [Github](#github)<br>[Marketplace](#marketplace)<br>[Package](#package) | | Yes |
#### PluginDependencyType
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| PluginDependencyType | string | | |
#### RevokeResponse
| Name | Type | Description | Required |
@ -1024,12 +1030,6 @@ types it as a required `'success'` rather than an optional field.
| ---- | ---- | ----------- | -------- |
| result | string | | Yes |
#### Type
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| Type | string | | |
#### UsageInfo
| Name | Type | Description | Required |

View File

@ -8,7 +8,7 @@ from sqlalchemy import select
import app
from core.helper.marketplace import fetch_global_plugin_manifest
from extensions.ext_database import db
from models.account import TenantPluginAutoUpgradeStrategy
from models.account import TenantPluginAutoUpgradeStrategy, TenantPluginAutoUpgradeStrategySetting
from tasks import process_tenant_plugin_autoupgrade_check_task as check_task
logger = logging.getLogger(__name__)
@ -34,8 +34,7 @@ def check_upgradable_plugin_task():
TenantPluginAutoUpgradeStrategy.upgrade_time_of_day >= now_seconds_of_day,
TenantPluginAutoUpgradeStrategy.upgrade_time_of_day
< now_seconds_of_day + AUTO_UPGRADE_MINIMAL_CHECKING_INTERVAL,
TenantPluginAutoUpgradeStrategy.strategy_setting
!= TenantPluginAutoUpgradeStrategy.StrategySetting.DISABLED,
TenantPluginAutoUpgradeStrategy.strategy_setting != TenantPluginAutoUpgradeStrategySetting.DISABLED,
)
).all()

View File

@ -38,6 +38,8 @@ from models.account import (
Tenant,
TenantAccountJoin,
TenantAccountRole,
TenantPluginAutoUpgradeCategory,
TenantPluginAutoUpgradeMode,
TenantPluginAutoUpgradeStrategy,
TenantStatus,
)
@ -1257,13 +1259,13 @@ class TenantService:
session.add(tenant)
session.commit()
for category in TenantPluginAutoUpgradeStrategy.PluginCategory:
for category in TenantPluginAutoUpgradeCategory:
plugin_upgrade_strategy = TenantPluginAutoUpgradeStrategy(
tenant_id=tenant.id,
category=category,
strategy_setting=PluginAutoUpgradeService.default_strategy_setting_for_category(category),
upgrade_time_of_day=PluginAutoUpgradeService.default_upgrade_time_of_day(tenant.id),
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
upgrade_mode=TenantPluginAutoUpgradeMode.EXCLUDE,
exclude_plugins=[],
include_plugins=[],
)

View File

@ -2,7 +2,7 @@ import re
from configs import dify_config
from core.helper import marketplace
from core.plugin.entities.plugin import PluginDependency, PluginInstallationSource
from core.plugin.entities.plugin import PluginDependency, PluginDependencyType, PluginInstallationSource
from core.plugin.impl.plugin import PluginInstaller
from models.provider_ids import ModelProviderID, ToolProviderID
@ -55,7 +55,7 @@ class DependenciesAnalysisService:
unique_identifier = dependency.value.plugin_unique_identifier
if unique_identifier in missing_plugin_unique_identifiers:
# Extract version for Marketplace dependencies
if dependency.type == PluginDependency.Type.Marketplace:
if dependency.type == PluginDependencyType.Marketplace:
version_match = _VERSION_REGEX.search(unique_identifier)
if version_match:
dependency.value.version = version_match.group("version")
@ -84,7 +84,7 @@ class DependenciesAnalysisService:
if plugin.source == PluginInstallationSource.Github:
result.append(
PluginDependency(
type=PluginDependency.Type.Github,
type=PluginDependencyType.Github,
value=PluginDependency.Github(
repo=plugin.meta["repo"],
version=plugin.meta["version"],
@ -96,7 +96,7 @@ class DependenciesAnalysisService:
elif plugin.source == PluginInstallationSource.Marketplace:
result.append(
PluginDependency(
type=PluginDependency.Type.Marketplace,
type=PluginDependencyType.Marketplace,
value=PluginDependency.Marketplace(
marketplace_plugin_unique_identifier=plugin.plugin_unique_identifier
),
@ -105,7 +105,7 @@ class DependenciesAnalysisService:
elif plugin.source == PluginInstallationSource.Package:
result.append(
PluginDependency(
type=PluginDependency.Type.Package,
type=PluginDependencyType.Package,
value=PluginDependency.Package(plugin_unique_identifier=plugin.plugin_unique_identifier),
)
)
@ -130,7 +130,7 @@ class DependenciesAnalysisService:
deps = marketplace.batch_fetch_plugin_manifests(dependencies)
return [
PluginDependency(
type=PluginDependency.Type.Marketplace,
type=PluginDependencyType.Marketplace,
value=PluginDependency.Marketplace(marketplace_plugin_unique_identifier=dep.latest_package_identifier),
)
for dep in deps

View File

@ -14,11 +14,16 @@ from sqlalchemy.orm import Session
from core.db.session_factory import session_factory
from core.plugin.impl.plugin import PluginInstaller
from models.account import TenantPluginAutoUpgradeStrategy
from models.account import (
TenantPluginAutoUpgradeCategory,
TenantPluginAutoUpgradeMode,
TenantPluginAutoUpgradeStrategy,
TenantPluginAutoUpgradeStrategySetting,
)
logger = logging.getLogger(__name__)
PluginCategory = TenantPluginAutoUpgradeStrategy.PluginCategory
PluginCategory = TenantPluginAutoUpgradeCategory
PLUGIN_CATEGORIES = tuple(PluginCategory)
SECONDS_PER_DAY = 24 * 60 * 60
AUTO_UPGRADE_CHECK_SLOT_SECONDS = 15 * 60
@ -35,10 +40,10 @@ class PluginAutoUpgradeService:
@staticmethod
def default_strategy_setting_for_category(
category: PluginCategory,
) -> TenantPluginAutoUpgradeStrategy.StrategySetting:
) -> TenantPluginAutoUpgradeStrategySetting:
if category == PluginCategory.MODEL:
return TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST
return TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY
return TenantPluginAutoUpgradeStrategySetting.LATEST
return TenantPluginAutoUpgradeStrategySetting.FIX_ONLY
@staticmethod
def default_upgrade_time_of_day(tenant_id: str) -> int:
@ -102,9 +107,9 @@ class PluginAutoUpgradeService:
@staticmethod
def _has_default_strategy(strategy: TenantPluginAutoUpgradeStrategy) -> bool:
return (
strategy.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY
strategy.strategy_setting == TenantPluginAutoUpgradeStrategySetting.FIX_ONLY
and strategy.upgrade_time_of_day == 0
and strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
and strategy.upgrade_mode == TenantPluginAutoUpgradeMode.EXCLUDE
and not strategy.exclude_plugins
and not strategy.include_plugins
)
@ -114,7 +119,7 @@ class PluginAutoUpgradeService:
source_strategy: TenantPluginAutoUpgradeStrategy,
category: PluginCategory,
source_has_default_strategy: bool,
) -> TenantPluginAutoUpgradeStrategy.StrategySetting:
) -> TenantPluginAutoUpgradeStrategySetting:
# Only pure legacy defaults adopt the new model=latest default. User-edited
# strategies keep their original setting across all categories.
if source_has_default_strategy:
@ -266,9 +271,9 @@ class PluginAutoUpgradeService:
session: Session,
tenant_id: str,
category: PluginCategory,
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting,
strategy_setting: TenantPluginAutoUpgradeStrategySetting,
upgrade_time_of_day: int,
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode,
upgrade_mode: TenantPluginAutoUpgradeMode,
exclude_plugins: list[str],
include_plugins: list[str],
) -> None:
@ -294,9 +299,9 @@ class PluginAutoUpgradeService:
@staticmethod
def change_strategy(
tenant_id: str,
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting,
strategy_setting: TenantPluginAutoUpgradeStrategySetting,
upgrade_time_of_day: int,
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode,
upgrade_mode: TenantPluginAutoUpgradeMode,
exclude_plugins: list[str],
include_plugins: list[str],
category: PluginCategory,
@ -329,28 +334,28 @@ class PluginAutoUpgradeService:
session,
tenant_id,
category,
TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
0,
TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
TenantPluginAutoUpgradeMode.EXCLUDE,
[plugin_id],
[],
)
else:
if exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE:
if exist_strategy.upgrade_mode == TenantPluginAutoUpgradeMode.EXCLUDE:
# In exclude mode, disabling one plugin means adding it to exclude_plugins.
if plugin_id not in exist_strategy.exclude_plugins:
new_exclude_plugins = exist_strategy.exclude_plugins.copy()
new_exclude_plugins.append(plugin_id)
exist_strategy.exclude_plugins = new_exclude_plugins
elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL:
elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeMode.PARTIAL:
# In partial mode, disabling one plugin means removing it from include_plugins.
if plugin_id in exist_strategy.include_plugins:
new_include_plugins = exist_strategy.include_plugins.copy()
new_include_plugins.remove(plugin_id)
exist_strategy.include_plugins = new_include_plugins
elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL:
elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeMode.ALL:
# In all mode, switch to exclude mode so only this plugin is skipped.
exist_strategy.upgrade_mode = TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
exist_strategy.upgrade_mode = TenantPluginAutoUpgradeMode.EXCLUDE
exist_strategy.exclude_plugins = [plugin_id]
@staticmethod

View File

@ -1,7 +1,7 @@
from sqlalchemy import select
from core.db.session_factory import session_factory
from models.account import TenantPluginPermission
from models.account import TenantPluginDebugPermission, TenantPluginInstallPermission, TenantPluginPermission
class PluginPermissionService:
@ -15,8 +15,8 @@ class PluginPermissionService:
@staticmethod
def change_permission(
tenant_id: str,
install_permission: TenantPluginPermission.InstallPermission,
debug_permission: TenantPluginPermission.DebugPermission,
install_permission: TenantPluginInstallPermission,
debug_permission: TenantPluginDebugPermission,
):
with session_factory.create_session() as session, session.begin():
permission = session.scalar(

View File

@ -6,7 +6,7 @@ from httpx import get
from sqlalchemy import select
from sqlalchemy.orm import sessionmaker
from core.entities.provider_entities import ProviderConfig
from core.entities.provider_entities import ProviderConfig, ProviderConfigType
from core.tools.__base.tool_runtime import ToolRuntime
from core.tools.custom_tool.provider import ApiToolProviderController
from core.tools.entities.api_entities import ToolApiEntity, ToolProviderApiEntity
@ -52,7 +52,7 @@ class ApiToolManageService:
credentials_schema = [
ProviderConfig(
name="auth_type",
type=ProviderConfig.Type.SELECT,
type=ProviderConfigType.SELECT,
required=True,
default="none",
options=[
@ -63,7 +63,7 @@ class ApiToolManageService:
),
ProviderConfig(
name="api_key_header",
type=ProviderConfig.Type.TEXT_INPUT,
type=ProviderConfigType.TEXT_INPUT,
required=False,
placeholder=I18nObject(en_US="Enter api key header", zh_Hans="输入 api key headerX-API-KEY"),
default="api_key",
@ -71,7 +71,7 @@ class ApiToolManageService:
),
ProviderConfig(
name="api_key_value",
type=ProviderConfig.Type.TEXT_INPUT,
type=ProviderConfigType.TEXT_INPUT,
required=False,
placeholder=I18nObject(en_US="Enter api key", zh_Hans="输入 api key"),
default="",

View File

@ -459,13 +459,11 @@ class MCPToolManageService:
Returns:
JSON string of encrypted data
"""
from core.entities.provider_entities import BasicProviderConfig
from core.entities.provider_entities import BasicProviderConfig, ProviderConfigType
from core.tools.utils.encryption import create_provider_encrypter
# Create config for secret fields
config = [
BasicProviderConfig(type=BasicProviderConfig.Type.SECRET_INPUT, name=field) for field in secret_fields
]
config = [BasicProviderConfig(type=ProviderConfigType.SECRET_INPUT, name=field) for field in secret_fields]
encrypter_instance, _ = create_provider_encrypter(
tenant_id=tenant_id,

View File

@ -11,11 +11,15 @@ from core.plugin.entities.plugin import PluginInstallation, PluginInstallationSo
from core.plugin.impl.plugin import PluginInstaller
from core.plugin.plugin_service import PluginService
from extensions.ext_redis import redis_client
from models.account import TenantPluginAutoUpgradeStrategy
from models.account import (
TenantPluginAutoUpgradeCategory,
TenantPluginAutoUpgradeMode,
TenantPluginAutoUpgradeStrategySetting,
)
logger = logging.getLogger(__name__)
PluginCategory = TenantPluginAutoUpgradeStrategy.PluginCategory
PluginCategory = TenantPluginAutoUpgradeCategory
RETRY_TIMES_OF_ONE_PLUGIN_IN_ONE_TENANT = 3
CACHE_REDIS_KEY_PREFIX = "plugin_autoupgrade_check_task:cached_plugin_snapshot:"
CACHE_REDIS_TTL = 60 * 60 # 1 hour
@ -95,9 +99,9 @@ def _plugin_matches_category(plugin: PluginInstallation, category: str | None) -
@shared_task(queue="plugin")
def process_tenant_plugin_autoupgrade_check_task(
tenant_id: str,
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting,
strategy_setting: TenantPluginAutoUpgradeStrategySetting,
upgrade_time_of_day: int,
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode,
upgrade_mode: TenantPluginAutoUpgradeMode,
exclude_plugins: list[str],
include_plugins: list[str],
category: PluginCategory | str | None = None,
@ -113,14 +117,14 @@ def process_tenant_plugin_autoupgrade_check_task(
)
)
if strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.DISABLED:
if strategy_setting == TenantPluginAutoUpgradeStrategySetting.DISABLED:
return
# get plugin_ids to check
plugin_ids: list[tuple[str, str, str]] = [] # plugin_id, version, unique_identifier
click.echo(click.style(f"Upgrade mode: {upgrade_mode}", fg="green"))
if upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL and include_plugins:
if upgrade_mode == TenantPluginAutoUpgradeMode.PARTIAL and include_plugins:
all_plugins = manager.list_plugins(tenant_id)
for plugin in all_plugins:
@ -137,7 +141,7 @@ def process_tenant_plugin_autoupgrade_check_task(
)
)
elif upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE:
elif upgrade_mode == TenantPluginAutoUpgradeMode.EXCLUDE:
# get all plugins and remove excluded plugins
all_plugins = manager.list_plugins(tenant_id)
plugin_ids = [
@ -147,7 +151,7 @@ def process_tenant_plugin_autoupgrade_check_task(
and plugin.plugin_id not in exclude_plugins
and _plugin_matches_category(plugin, category_value)
]
elif upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL:
elif upgrade_mode == TenantPluginAutoUpgradeMode.ALL:
all_plugins = manager.list_plugins(tenant_id)
plugin_ids = [
(plugin.plugin_id, plugin.version, plugin.plugin_unique_identifier)
@ -187,8 +191,8 @@ def process_tenant_plugin_autoupgrade_check_task(
return False
version_checker = {
TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST: operator.ne,
TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY: fix_only_checker,
TenantPluginAutoUpgradeStrategySetting.LATEST: operator.ne,
TenantPluginAutoUpgradeStrategySetting.FIX_ONLY: fix_only_checker,
}
if version_checker[strategy_setting](latest_version, current_version):

View File

@ -3,11 +3,19 @@ from sqlalchemy import delete, func, select
from core.db.session_factory import session_factory
from models import Tenant
from models.account import TenantPluginAutoUpgradeStrategy, TenantPluginPermission
from models.account import (
TenantPluginAutoUpgradeCategory,
TenantPluginAutoUpgradeMode,
TenantPluginAutoUpgradeStrategy,
TenantPluginAutoUpgradeStrategySetting,
TenantPluginDebugPermission,
TenantPluginInstallPermission,
TenantPluginPermission,
)
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
from services.plugin.plugin_permission_service import PluginPermissionService
PLUGIN_CATEGORY = TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL
PLUGIN_CATEGORY = TenantPluginAutoUpgradeCategory.TOOL
@pytest.fixture
@ -36,31 +44,31 @@ class TestPluginPermissionLifecycle:
def test_change_creates_row(self, tenant):
result = PluginPermissionService.change_permission(
tenant,
TenantPluginPermission.InstallPermission.ADMINS,
TenantPluginPermission.DebugPermission.EVERYONE,
TenantPluginInstallPermission.ADMINS,
TenantPluginDebugPermission.EVERYONE,
)
assert result is True
perm = PluginPermissionService.get_permission(tenant)
assert perm is not None
assert perm.install_permission == TenantPluginPermission.InstallPermission.ADMINS
assert perm.debug_permission == TenantPluginPermission.DebugPermission.EVERYONE
assert perm.install_permission == TenantPluginInstallPermission.ADMINS
assert perm.debug_permission == TenantPluginDebugPermission.EVERYONE
def test_change_updates_existing_row(self, tenant):
PluginPermissionService.change_permission(
tenant,
TenantPluginPermission.InstallPermission.ADMINS,
TenantPluginPermission.DebugPermission.NOBODY,
TenantPluginInstallPermission.ADMINS,
TenantPluginDebugPermission.NOBODY,
)
PluginPermissionService.change_permission(
tenant,
TenantPluginPermission.InstallPermission.EVERYONE,
TenantPluginPermission.DebugPermission.ADMINS,
TenantPluginInstallPermission.EVERYONE,
TenantPluginDebugPermission.ADMINS,
)
perm = PluginPermissionService.get_permission(tenant)
assert perm is not None
assert perm.install_permission == TenantPluginPermission.InstallPermission.EVERYONE
assert perm.debug_permission == TenantPluginPermission.DebugPermission.ADMINS
assert perm.install_permission == TenantPluginInstallPermission.EVERYONE
assert perm.debug_permission == TenantPluginDebugPermission.ADMINS
with session_factory.create_session() as session:
count = session.scalar(
@ -78,9 +86,9 @@ class TestPluginAutoUpgradeLifecycle:
def test_change_creates_row(self, tenant):
result = PluginAutoUpgradeService.change_strategy(
tenant,
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST,
strategy_setting=TenantPluginAutoUpgradeStrategySetting.LATEST,
upgrade_time_of_day=3,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
upgrade_mode=TenantPluginAutoUpgradeMode.ALL,
exclude_plugins=[],
include_plugins=[],
category=PLUGIN_CATEGORY,
@ -89,24 +97,24 @@ class TestPluginAutoUpgradeLifecycle:
strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY)
assert strategy is not None
assert strategy.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST
assert strategy.strategy_setting == TenantPluginAutoUpgradeStrategySetting.LATEST
assert strategy.upgrade_time_of_day == 3
def test_change_updates_existing_row(self, tenant):
PluginAutoUpgradeService.change_strategy(
tenant,
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
strategy_setting=TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
upgrade_time_of_day=0,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
upgrade_mode=TenantPluginAutoUpgradeMode.ALL,
exclude_plugins=[],
include_plugins=[],
category=PLUGIN_CATEGORY,
)
PluginAutoUpgradeService.change_strategy(
tenant,
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST,
strategy_setting=TenantPluginAutoUpgradeStrategySetting.LATEST,
upgrade_time_of_day=12,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL,
upgrade_mode=TenantPluginAutoUpgradeMode.PARTIAL,
exclude_plugins=[],
include_plugins=["plugin-a"],
category=PLUGIN_CATEGORY,
@ -114,9 +122,9 @@ class TestPluginAutoUpgradeLifecycle:
strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY)
assert strategy is not None
assert strategy.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST
assert strategy.strategy_setting == TenantPluginAutoUpgradeStrategySetting.LATEST
assert strategy.upgrade_time_of_day == 12
assert strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL
assert strategy.upgrade_mode == TenantPluginAutoUpgradeMode.PARTIAL
assert strategy.include_plugins == ["plugin-a"]
def test_exclude_plugin_creates_strategy_when_none_exists(self, tenant):
@ -124,15 +132,15 @@ class TestPluginAutoUpgradeLifecycle:
strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY)
assert strategy is not None
assert strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
assert strategy.upgrade_mode == TenantPluginAutoUpgradeMode.EXCLUDE
assert "my-plugin" in strategy.exclude_plugins
def test_exclude_plugin_appends_in_exclude_mode(self, tenant):
PluginAutoUpgradeService.change_strategy(
tenant,
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
strategy_setting=TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
upgrade_time_of_day=0,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
upgrade_mode=TenantPluginAutoUpgradeMode.EXCLUDE,
exclude_plugins=["existing"],
include_plugins=[],
category=PLUGIN_CATEGORY,
@ -147,9 +155,9 @@ class TestPluginAutoUpgradeLifecycle:
def test_exclude_plugin_dedup_in_exclude_mode(self, tenant):
PluginAutoUpgradeService.change_strategy(
tenant,
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
strategy_setting=TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
upgrade_time_of_day=0,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
upgrade_mode=TenantPluginAutoUpgradeMode.EXCLUDE,
exclude_plugins=["same-plugin"],
include_plugins=[],
category=PLUGIN_CATEGORY,
@ -163,9 +171,9 @@ class TestPluginAutoUpgradeLifecycle:
def test_exclude_from_partial_mode_removes_from_include(self, tenant):
PluginAutoUpgradeService.change_strategy(
tenant,
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
strategy_setting=TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
upgrade_time_of_day=0,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL,
upgrade_mode=TenantPluginAutoUpgradeMode.PARTIAL,
exclude_plugins=[],
include_plugins=["p1", "p2"],
category=PLUGIN_CATEGORY,
@ -180,9 +188,9 @@ class TestPluginAutoUpgradeLifecycle:
def test_exclude_from_all_mode_switches_to_exclude(self, tenant):
PluginAutoUpgradeService.change_strategy(
tenant,
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST,
strategy_setting=TenantPluginAutoUpgradeStrategySetting.LATEST,
upgrade_time_of_day=0,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
upgrade_mode=TenantPluginAutoUpgradeMode.ALL,
exclude_plugins=[],
include_plugins=[],
category=PLUGIN_CATEGORY,
@ -191,5 +199,5 @@ class TestPluginAutoUpgradeLifecycle:
strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY)
assert strategy is not None
assert strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
assert strategy.upgrade_mode == TenantPluginAutoUpgradeMode.EXCLUDE
assert "excluded-plugin" in strategy.exclude_plugins

View File

@ -14,7 +14,7 @@ from controllers.console.datasets.rag_pipeline.rag_pipeline_import import (
RagPipelineImportCheckDependenciesApi,
RagPipelineImportConfirmApi,
)
from core.plugin.entities.plugin import PluginDependency
from core.plugin.entities.plugin import PluginDependency, PluginDependencyType
from models.dataset import Pipeline
from services.entities.dsl_entities import CheckDependenciesResult, ImportStatus
from services.rag_pipeline.rag_pipeline_dsl_service import RagPipelineImportInfo
@ -237,7 +237,7 @@ class TestRagPipelineImportCheckDependenciesApi:
pipeline = MagicMock(spec=Pipeline)
dependency = PluginDependency(
type=PluginDependency.Type.Marketplace,
type=PluginDependencyType.Marketplace,
value=PluginDependency.Marketplace(
marketplace_plugin_unique_identifier="langgenius/example:0.1.0",
version="0.1.0",

View File

@ -10,7 +10,13 @@ from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden
from controllers.console.workspace import plugin_permission_required
from models.account import Tenant, TenantPluginPermission, TenantStatus
from models.account import (
Tenant,
TenantPluginDebugPermission,
TenantPluginInstallPermission,
TenantPluginPermission,
TenantStatus,
)
def _create_tenant(db_session: Session) -> Tenant:
@ -24,8 +30,8 @@ def _create_tenant(db_session: Session) -> Tenant:
def _create_permission(
db_session: Session,
tenant_id: str,
install: TenantPluginPermission.InstallPermission = TenantPluginPermission.InstallPermission.EVERYONE,
debug: TenantPluginPermission.DebugPermission = TenantPluginPermission.DebugPermission.EVERYONE,
install: TenantPluginInstallPermission = TenantPluginInstallPermission.EVERYONE,
debug: TenantPluginDebugPermission = TenantPluginDebugPermission.EVERYONE,
) -> TenantPluginPermission:
perm = TenantPluginPermission(
tenant_id=tenant_id,
@ -59,8 +65,8 @@ class TestPluginPermissionRequired:
_create_permission(
db_session_with_containers,
tenant.id,
install=TenantPluginPermission.InstallPermission.NOBODY,
debug=TenantPluginPermission.DebugPermission.EVERYONE,
install=TenantPluginInstallPermission.NOBODY,
debug=TenantPluginDebugPermission.EVERYONE,
)
user = SimpleNamespace(is_admin_or_owner=True)
@ -81,8 +87,8 @@ class TestPluginPermissionRequired:
_create_permission(
db_session_with_containers,
tenant.id,
install=TenantPluginPermission.InstallPermission.ADMINS,
debug=TenantPluginPermission.DebugPermission.EVERYONE,
install=TenantPluginInstallPermission.ADMINS,
debug=TenantPluginDebugPermission.EVERYONE,
)
user = SimpleNamespace(is_admin_or_owner=False)
@ -103,8 +109,8 @@ class TestPluginPermissionRequired:
_create_permission(
db_session_with_containers,
tenant.id,
install=TenantPluginPermission.InstallPermission.ADMINS,
debug=TenantPluginPermission.DebugPermission.EVERYONE,
install=TenantPluginInstallPermission.ADMINS,
debug=TenantPluginDebugPermission.EVERYONE,
)
user = SimpleNamespace(is_admin_or_owner=True)
@ -124,8 +130,8 @@ class TestPluginPermissionRequired:
_create_permission(
db_session_with_containers,
tenant.id,
install=TenantPluginPermission.InstallPermission.EVERYONE,
debug=TenantPluginPermission.DebugPermission.NOBODY,
install=TenantPluginInstallPermission.EVERYONE,
debug=TenantPluginDebugPermission.NOBODY,
)
user = SimpleNamespace(is_admin_or_owner=True)
@ -146,8 +152,8 @@ class TestPluginPermissionRequired:
_create_permission(
db_session_with_containers,
tenant.id,
install=TenantPluginPermission.InstallPermission.EVERYONE,
debug=TenantPluginPermission.DebugPermission.ADMINS,
install=TenantPluginInstallPermission.EVERYONE,
debug=TenantPluginDebugPermission.ADMINS,
)
user = SimpleNamespace(is_admin_or_owner=False)
@ -168,8 +174,8 @@ class TestPluginPermissionRequired:
_create_permission(
db_session_with_containers,
tenant.id,
install=TenantPluginPermission.InstallPermission.EVERYONE,
debug=TenantPluginPermission.DebugPermission.ADMINS,
install=TenantPluginInstallPermission.EVERYONE,
debug=TenantPluginDebugPermission.ADMINS,
)
user = SimpleNamespace(is_admin_or_owner=True)

View File

@ -6,7 +6,7 @@ import pytest
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from models.account import TenantPluginPermission
from models.account import TenantPluginDebugPermission, TenantPluginInstallPermission, TenantPluginPermission
from services.plugin.plugin_permission_service import PluginPermissionService
@ -32,8 +32,8 @@ class TestGetPermission:
tenant_id = _tenant_id()
permission = TenantPluginPermission(
tenant_id=tenant_id,
install_permission=TenantPluginPermission.InstallPermission.ADMINS,
debug_permission=TenantPluginPermission.DebugPermission.EVERYONE,
install_permission=TenantPluginInstallPermission.ADMINS,
debug_permission=TenantPluginDebugPermission.EVERYONE,
)
db_session_with_containers.add(permission)
db_session_with_containers.commit()
@ -43,8 +43,8 @@ class TestGetPermission:
assert result is not None
assert result.id == permission.id
assert result.tenant_id == tenant_id
assert result.install_permission == TenantPluginPermission.InstallPermission.ADMINS
assert result.debug_permission == TenantPluginPermission.DebugPermission.EVERYONE
assert result.install_permission == TenantPluginInstallPermission.ADMINS
assert result.debug_permission == TenantPluginDebugPermission.EVERYONE
@pytest.mark.usefixtures("flask_app_with_containers")
def test_returns_none_when_not_found(self) -> None:
@ -61,36 +61,36 @@ class TestChangePermission:
result = PluginPermissionService.change_permission(
tenant_id,
TenantPluginPermission.InstallPermission.EVERYONE,
TenantPluginPermission.DebugPermission.EVERYONE,
TenantPluginInstallPermission.EVERYONE,
TenantPluginDebugPermission.EVERYONE,
)
permission = _get_permission(db_session_with_containers, tenant_id)
assert result is True
assert permission is not None
assert permission.install_permission == TenantPluginPermission.InstallPermission.EVERYONE
assert permission.debug_permission == TenantPluginPermission.DebugPermission.EVERYONE
assert permission.install_permission == TenantPluginInstallPermission.EVERYONE
assert permission.debug_permission == TenantPluginDebugPermission.EVERYONE
def test_updates_existing_permission(self, db_session_with_containers: Session) -> None:
tenant_id = _tenant_id()
existing = TenantPluginPermission(
tenant_id=tenant_id,
install_permission=TenantPluginPermission.InstallPermission.EVERYONE,
debug_permission=TenantPluginPermission.DebugPermission.EVERYONE,
install_permission=TenantPluginInstallPermission.EVERYONE,
debug_permission=TenantPluginDebugPermission.EVERYONE,
)
db_session_with_containers.add(existing)
db_session_with_containers.commit()
result = PluginPermissionService.change_permission(
tenant_id,
TenantPluginPermission.InstallPermission.ADMINS,
TenantPluginPermission.DebugPermission.ADMINS,
TenantPluginInstallPermission.ADMINS,
TenantPluginDebugPermission.ADMINS,
)
permission = _get_permission(db_session_with_containers, tenant_id)
assert result is True
assert permission is not None
assert permission.id == existing.id
assert permission.install_permission == TenantPluginPermission.InstallPermission.ADMINS
assert permission.debug_permission == TenantPluginPermission.DebugPermission.ADMINS
assert permission.install_permission == TenantPluginInstallPermission.ADMINS
assert permission.debug_permission == TenantPluginDebugPermission.ADMINS
assert _count_permissions(db_session_with_containers, tenant_id) == 1

View File

@ -1,4 +1,5 @@
import inspect
from datetime import UTC, datetime
from unittest.mock import patch
import pytest
@ -16,9 +17,39 @@ from controllers.console.workspace.endpoint import (
EndpointListApi,
EndpointListForSinglePluginApi,
)
from core.entities.provider_entities import ProviderConfig, ProviderConfigType
from core.plugin.entities.endpoint import EndpointEntityWithInstance, EndpointProviderDeclaration
from core.plugin.impl.exc import PluginPermissionDeniedError
def _endpoint_entity() -> EndpointEntityWithInstance:
now = datetime(2026, 1, 1, tzinfo=UTC)
return EndpointEntityWithInstance(
id="e1",
created_at=now,
updated_at=now,
tenant_id="t1",
plugin_id="p1",
settings={
"api_key": "pl********et",
"enabled": True,
"ids": ["a", "b"],
"nested": {"limit": 3},
},
expired_at=now,
declaration=EndpointProviderDeclaration(
settings=[
ProviderConfig(type=ProviderConfigType.SECRET_INPUT, name="api_key"),
ProviderConfig(type=ProviderConfigType.BOOLEAN, name="enabled"),
]
),
name="endpoint",
enabled=True,
url="https://example.test/hook-1",
hook_id="hook-1",
)
class TestEndpointCollectionApi:
def test_create_success(self, app: Flask):
api = EndpointCollectionApi()
@ -99,15 +130,40 @@ class TestEndpointListApi:
def test_list_success(self, app: Flask):
api = EndpointListApi()
method = inspect.unwrap(api.get)
endpoint_entity = _endpoint_entity()
with (
app.test_request_context("/?page=1&page_size=10"),
patch("controllers.console.workspace.endpoint.EndpointService.list_endpoints", return_value=[{"id": "e1"}]),
patch(
"controllers.console.workspace.endpoint.EndpointService.list_endpoints",
return_value=[endpoint_entity],
),
):
result = method(api, "t1", "u1")
assert "endpoints" in result
assert len(result["endpoints"]) == 1
endpoint = result["endpoints"][0]
assert endpoint["id"] == "e1"
assert endpoint["created_at"] == "2026-01-01T00:00:00Z"
assert endpoint["updated_at"] == "2026-01-01T00:00:00Z"
assert endpoint["settings"] == {
"api_key": "pl********et",
"enabled": True,
"ids": ["a", "b"],
"nested": {"limit": 3},
}
assert endpoint["tenant_id"] == "t1"
assert endpoint["plugin_id"] == "p1"
assert endpoint["expired_at"] == "2026-01-01T00:00:00Z"
assert endpoint["declaration"]["settings"][0]["type"] == "secret-input"
assert endpoint["declaration"]["settings"][0]["name"] == "api_key"
assert endpoint["declaration"]["settings"][1]["type"] == "boolean"
assert endpoint["declaration"]["settings"][1]["name"] == "enabled"
assert endpoint["declaration"]["endpoints"] == []
assert endpoint["name"] == "endpoint"
assert endpoint["enabled"] is True
assert endpoint["url"] == "https://example.test/hook-1"
assert endpoint["hook_id"] == "hook-1"
assert endpoint_entity.settings["api_key"] == "pl********et"
def test_list_invalid_query(self, app: Flask):
api = EndpointListApi()
@ -129,12 +185,14 @@ class TestEndpointListForSinglePluginApi:
app.test_request_context("/?page=1&page_size=10&plugin_id=p1"),
patch(
"controllers.console.workspace.endpoint.EndpointService.list_endpoints_for_single_plugin",
return_value=[{"id": "e1"}],
return_value=[_endpoint_entity()],
),
):
result = method(api, "t1", "u1")
assert "endpoints" in result
assert result["endpoints"][0]["id"] == "e1"
assert result["endpoints"][0]["settings"]["api_key"] == "pl********et"
assert result["endpoints"][0]["settings"]["nested"] == {"limit": 3}
def test_list_for_plugin_missing_param(self, app: Flask):
api = EndpointListForSinglePluginApi()

View File

@ -130,7 +130,7 @@ class TestMemberInviteEmailApi:
features.workspace_members.is_available.return_value = True
payload = {
"emails": ["a@test.com"],
"emails": ["A@TEST.com", "a@test.com"],
"role": "normal",
"language": "en-US",
}
@ -138,8 +138,10 @@ class TestMemberInviteEmailApi:
with (
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.members.FeatureService.get_features", return_value=features),
patch("controllers.console.workspace.members._count_new_member_invites", return_value=1),
patch("controllers.console.workspace.members.RegisterService.invite_new_member", return_value="token"),
patch("controllers.console.workspace.members._count_new_member_invites", return_value=1) as mock_count,
patch(
"controllers.console.workspace.members.RegisterService.invite_new_member", return_value="token"
) as mock_invite,
patch("controllers.console.workspace.members.dify_config.CONSOLE_WEB_URL", "http://x"),
patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", False),
patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", False),
@ -148,6 +150,10 @@ class TestMemberInviteEmailApi:
assert status == 201
assert result["result"] == "success"
assert result["invitation_results"][0]["email"] == "a@test.com"
mock_count.assert_not_called()
mock_invite.assert_called_once()
assert mock_invite.call_args.kwargs["email"] == "a@test.com"
def test_invite_limit_exceeded(self, app: Flask):
api = MemberInviteEmailApi()

View File

@ -44,7 +44,15 @@ from controllers.console.workspace.plugin import (
)
from core.plugin.entities.plugin import PluginInstallation
from core.plugin.impl.exc import PluginDaemonClientSideError
from models.account import Account, TenantAccountRole, TenantPluginAutoUpgradeStrategy, TenantPluginPermission
from models.account import (
Account,
TenantAccountRole,
TenantPluginAutoUpgradeCategory,
TenantPluginAutoUpgradeMode,
TenantPluginAutoUpgradeStrategySetting,
TenantPluginDebugPermission,
TenantPluginInstallPermission,
)
def _plugin_category_list_item(category: str = "tool") -> dict[str, Any]:
@ -393,8 +401,8 @@ class TestPluginChangePermissionApi:
user = _account(TenantAccountRole.NORMAL)
payload = {
"install_permission": TenantPluginPermission.InstallPermission.EVERYONE,
"debug_permission": TenantPluginPermission.DebugPermission.EVERYONE,
"install_permission": TenantPluginInstallPermission.EVERYONE,
"debug_permission": TenantPluginDebugPermission.EVERYONE,
}
with (
@ -410,8 +418,8 @@ class TestPluginChangePermissionApi:
user = _account()
payload = {
"install_permission": TenantPluginPermission.InstallPermission.EVERYONE,
"debug_permission": TenantPluginPermission.DebugPermission.EVERYONE,
"install_permission": TenantPluginInstallPermission.EVERYONE,
"debug_permission": TenantPluginDebugPermission.EVERYONE,
}
with (
@ -1020,11 +1028,11 @@ class TestPluginChangeAutoUpgradeApi:
user = _account()
payload = {
"category": TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value,
"category": TenantPluginAutoUpgradeCategory.TOOL.value,
"auto_upgrade": {
"strategy_setting": TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
"strategy_setting": TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
"upgrade_time_of_day": 0,
"upgrade_mode": TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
"upgrade_mode": TenantPluginAutoUpgradeMode.EXCLUDE,
"exclude_plugins": [],
"include_plugins": [],
},
@ -1048,11 +1056,11 @@ class TestPluginChangeAutoUpgradeApi:
user = _account()
payload = {
"category": TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL.value,
"category": TenantPluginAutoUpgradeCategory.MODEL.value,
"auto_upgrade": {
"strategy_setting": TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST,
"strategy_setting": TenantPluginAutoUpgradeStrategySetting.LATEST,
"upgrade_time_of_day": 3600,
"upgrade_mode": TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
"upgrade_mode": TenantPluginAutoUpgradeMode.ALL,
"exclude_plugins": [],
"include_plugins": [],
},
@ -1068,7 +1076,7 @@ class TestPluginChangeAutoUpgradeApi:
assert result["success"] is True
change.assert_called_once()
assert change.call_args.kwargs["category"] == TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL
assert change.call_args.kwargs["category"] == TenantPluginAutoUpgradeCategory.MODEL
def test_auto_upgrade_fail(self, app: Flask):
api = PluginChangeAutoUpgradeApi()
@ -1077,11 +1085,11 @@ class TestPluginChangeAutoUpgradeApi:
user = MagicMock(is_admin_or_owner=True)
payload = {
"category": TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value,
"category": TenantPluginAutoUpgradeCategory.TOOL.value,
"auto_upgrade": {
"strategy_setting": TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
"strategy_setting": TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
"upgrade_time_of_day": 0,
"upgrade_mode": TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
"upgrade_mode": TenantPluginAutoUpgradeMode.EXCLUDE,
"exclude_plugins": [],
"include_plugins": [],
},
@ -1102,16 +1110,16 @@ class TestPluginFetchAutoUpgradeApi:
method = unwrap(api.get)
auto_upgrade = MagicMock(
category=TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL,
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
category=TenantPluginAutoUpgradeCategory.TOOL,
strategy_setting=TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
upgrade_time_of_day=1,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
upgrade_mode=TenantPluginAutoUpgradeMode.EXCLUDE,
exclude_plugins=[],
include_plugins=[],
)
with (
app.test_request_context(f"/?category={TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value}"),
app.test_request_context(f"/?category={TenantPluginAutoUpgradeCategory.TOOL.value}"),
patch(
"controllers.console.workspace.plugin.PluginAutoUpgradeService.get_strategy",
return_value=auto_upgrade,
@ -1119,7 +1127,7 @@ class TestPluginFetchAutoUpgradeApi:
):
result = method(api, "t1")
assert result["category"] == TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL
assert result["category"] == TenantPluginAutoUpgradeCategory.TOOL
assert result["auto_upgrade"]["upgrade_time_of_day"] == 1
@ -1128,7 +1136,7 @@ class TestPluginAutoUpgradeExcludePluginApi:
api = PluginAutoUpgradeExcludePluginApi()
method = unwrap(api.post)
payload = {"plugin_id": "p", "category": TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value}
payload = {"plugin_id": "p", "category": TenantPluginAutoUpgradeCategory.TOOL.value}
with (
app.test_request_context("/", json=payload),
@ -1142,7 +1150,7 @@ class TestPluginAutoUpgradeExcludePluginApi:
api = PluginAutoUpgradeExcludePluginApi()
method = unwrap(api.post)
payload = {"plugin_id": "p", "category": TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value}
payload = {"plugin_id": "p", "category": TenantPluginAutoUpgradeCategory.TOOL.value}
with (
app.test_request_context("/", json=payload),

View File

@ -1,5 +1,6 @@
import inspect
import logging
from http import HTTPStatus
from io import BytesIO
from unittest.mock import MagicMock, patch
@ -26,7 +27,9 @@ from controllers.console.workspace.workspace import (
WebappLogoWorkspaceApi,
WorkspaceInfoApi,
WorkspaceListApi,
WorkspaceLogoUploadResponse,
WorkspacePermissionApi,
WorkspacePermissionResponse,
)
from enums.cloud_plan import CloudPlan
from libs.datetime_utils import naive_utc_now
@ -99,7 +102,7 @@ class TestTenantListApi:
):
result, status = method(api, "t1", user)
assert status == 200
assert status == HTTPStatus.OK
assert len(result["workspaces"]) == 2
assert result["workspaces"][0]["current"] is True
assert result["workspaces"][0]["plan"] == CloudPlan.TEAM
@ -146,7 +149,7 @@ class TestTenantListApi:
):
result, status = method(api, "t1", user)
assert status == 200
assert status == HTTPStatus.OK
assert result["workspaces"][0]["plan"] == CloudPlan.TEAM
assert result["workspaces"][1]["plan"] == CloudPlan.PROFESSIONAL
get_plan_bulk_mock.assert_called_once_with(["t1", "t2"])
@ -192,7 +195,7 @@ class TestTenantListApi:
):
result, status = method(api, "t2", user)
assert status == 200
assert status == HTTPStatus.OK
assert result["workspaces"][0]["plan"] == CloudPlan.TEAM
assert result["workspaces"][1]["plan"] == CloudPlan.TEAM
get_plan_bulk_mock.assert_called_once_with(["t1", "t2"])
@ -226,7 +229,7 @@ class TestTenantListApi:
):
result, status = method(api, "t1", user)
assert status == 200
assert status == HTTPStatus.OK
assert result["workspaces"][0]["plan"] == CloudPlan.SANDBOX
get_features_mock.assert_called_once_with("t1", exclude_vector_space=True)
@ -251,7 +254,7 @@ class TestTenantListApi:
):
result, status = method(api, "t2", user)
assert status == 200
assert status == HTTPStatus.OK
assert result["workspaces"][0]["plan"] == CloudPlan.SANDBOX
assert result["workspaces"][1]["plan"] == CloudPlan.SANDBOX
assert result["workspaces"][0]["current"] is False
@ -276,7 +279,7 @@ class TestTenantListApi:
):
result, status = method(api, None, user)
assert status == 200
assert status == HTTPStatus.OK
assert result["workspaces"] == []
get_features_mock.assert_not_called()
@ -295,7 +298,7 @@ class TestWorkspaceListApi:
):
result, status = method(api)
assert status == 200
assert status == HTTPStatus.OK
assert result["total"] == 1
assert result["has_more"] is False
@ -312,7 +315,7 @@ class TestWorkspaceListApi:
):
result, status = method(api)
assert status == 200
assert status == HTTPStatus.OK
assert result["has_more"] is True
@ -332,7 +335,7 @@ class TestTenantApi:
):
result, status = method(api, user)
assert status == 200
assert status == HTTPStatus.OK
assert result["id"] == "t1"
def test_post_archived_with_switch(self, app: Flask):
@ -386,7 +389,7 @@ class TestTenantApi:
result, status = method(api, user)
assert "Deprecated URL /info was used." in caplog.messages
assert status == 200
assert status == HTTPStatus.OK
class TestTenantInfoResponse:
@ -586,8 +589,9 @@ class TestWebappLogoWorkspaceApi:
result, status = method(api, user)
assert status == 201
assert result["id"] == "file1"
assert status == HTTPStatus.CREATED
assert result == {"id": "file1"}
assert WorkspaceLogoUploadResponse.model_validate(result).model_dump(mode="json") == {"id": "file1"}
def test_filename_missing(self, app: Flask):
api = WebappLogoWorkspaceApi()
@ -676,7 +680,7 @@ class TestWorkspaceInfoApi:
patch("controllers.console.workspace.workspace.db.session.commit"),
patch(
"controllers.console.workspace.workspace.WorkspaceService.get_tenant_info",
return_value={"name": "New Name"},
return_value={"id": "t1", "name": "New Name"},
),
):
result = method(api, "t1")
@ -716,8 +720,14 @@ class TestWorkspacePermissionApi:
):
result, status = method(api, "t1")
assert status == 200
assert result["workspace_id"] == "t1"
assert status == HTTPStatus.OK
expected = {
"workspace_id": "t1",
"allow_member_invite": True,
"allow_owner_transfer": False,
}
assert result == expected
assert WorkspacePermissionResponse.model_validate(result).model_dump(mode="json") == expected
def test_no_current_tenant(self, app: Flask):
api = WorkspacePermissionApi()

View File

@ -8,7 +8,7 @@ from core.datasource.entities.datasource_entities import (
DatasourceProviderEntityWithPlugin,
DatasourceProviderType,
)
from core.entities.provider_entities import ProviderConfig
from core.entities.provider_entities import ProviderConfig, ProviderConfigType
from core.tools.errors import ToolProviderCredentialValidationError
@ -149,7 +149,7 @@ class TestDatasourcePluginProviderController:
mock_config = MagicMock(spec=ProviderConfig)
mock_config.name = "text_field"
mock_config.required = True
mock_config.type = ProviderConfig.Type.TEXT_INPUT
mock_config.type = ProviderConfigType.TEXT_INPUT
mock_entity = MagicMock(spec=DatasourceProviderEntityWithPlugin)
mock_entity.credentials_schema = [mock_config]
@ -167,7 +167,7 @@ class TestDatasourcePluginProviderController:
mock_config = MagicMock(spec=ProviderConfig)
mock_config.name = "select_field"
mock_config.required = True
mock_config.type = ProviderConfig.Type.SELECT
mock_config.type = ProviderConfigType.SELECT
mock_config.options = [mock_option]
mock_entity = MagicMock(spec=DatasourceProviderEntityWithPlugin)
@ -206,7 +206,7 @@ class TestDatasourcePluginProviderController:
mock_config = MagicMock(spec=ProviderConfig)
mock_config.name = "valid_field"
mock_config.required = True
mock_config.type = ProviderConfig.Type.TEXT_INPUT
mock_config.type = ProviderConfigType.TEXT_INPUT
mock_entity = MagicMock(spec=DatasourceProviderEntityWithPlugin)
mock_entity.credentials_schema = [mock_config]
@ -243,7 +243,7 @@ class TestDatasourcePluginProviderController:
mock_config_text = MagicMock(spec=ProviderConfig)
mock_config_text.name = "text_def"
mock_config_text.required = False
mock_config_text.type = ProviderConfig.Type.TEXT_INPUT
mock_config_text.type = ProviderConfigType.TEXT_INPUT
mock_config_text.default = 123 # Int default, should be converted to str
mock_config_other = MagicMock(spec=ProviderConfig)

View File

@ -5,6 +5,7 @@ from core.entities.provider_entities import (
BasicProviderConfig,
ModelSettings,
ProviderConfig,
ProviderConfigType,
ProviderQuotaType,
)
from core.tools.entities.common_entities import I18nObject
@ -27,22 +28,22 @@ def test_provider_quota_type_value_of_rejects_unknown_values() -> None:
def test_basic_provider_config_type_value_of_handles_known_values() -> None:
# Arrange / Act
parameter_type = BasicProviderConfig.Type.value_of("text-input")
parameter_type = ProviderConfigType.value_of("text-input")
# Assert
assert parameter_type == BasicProviderConfig.Type.TEXT_INPUT
assert parameter_type == ProviderConfigType.TEXT_INPUT
def test_basic_provider_config_type_value_of_rejects_invalid_values() -> None:
# Arrange / Act / Assert
with pytest.raises(ValueError, match="invalid mode value"):
BasicProviderConfig.Type.value_of("unknown")
ProviderConfigType.value_of("unknown")
def test_provider_config_to_basic_provider_config_keeps_type_and_name() -> None:
# Arrange
provider_config = ProviderConfig(
type=BasicProviderConfig.Type.SELECT,
type=ProviderConfigType.SELECT,
name="workspace",
scope=AppSelectorScope.ALL,
options=[ProviderConfig.Option(value="all", label=I18nObject(en_US="All"))],
@ -53,7 +54,7 @@ def test_provider_config_to_basic_provider_config_keeps_type_and_name() -> None:
# Assert
assert isinstance(basic_config, BasicProviderConfig)
assert basic_config.type == BasicProviderConfig.Type.SELECT
assert basic_config.type == ProviderConfigType.SELECT
assert basic_config.name == "workspace"

View File

@ -17,7 +17,7 @@ import pytest
from packaging.version import Version
from requests import HTTPError
from core.plugin.entities.bundle import PluginBundleDependency
from core.plugin.entities.bundle import PluginBundleDependency, PluginBundleDependencyType
from core.plugin.entities.plugin import (
MissingPluginDependency,
PluginCategory,
@ -581,11 +581,11 @@ class TestDependencyResolution:
bundle_data = b"mock-bundle-data"
mock_dependencies = [
PluginBundleDependency(
type=PluginBundleDependency.Type.Marketplace,
type=PluginBundleDependencyType.Marketplace,
value=PluginBundleDependency.Marketplace(organization="org1", plugin="plugin1", version="1.0.0"),
),
PluginBundleDependency(
type=PluginBundleDependency.Type.Github,
type=PluginBundleDependencyType.Github,
value=PluginBundleDependency.Github(
repo_address="https://github.com/org/repo",
repo="org/repo",
@ -603,8 +603,8 @@ class TestDependencyResolution:
# Assert: Verify dependencies were extracted
assert len(result) == 2
assert result[0].type == PluginBundleDependency.Type.Marketplace
assert result[1].type == PluginBundleDependency.Type.Github
assert result[0].type == PluginBundleDependencyType.Marketplace
assert result[1].type == PluginBundleDependencyType.Github
mock_request.assert_called_once()
def test_fetch_missing_dependencies(self, plugin_installer):
@ -1129,7 +1129,7 @@ class TestPluginBundleOperations:
bundle_data = b"mock-marketplace-bundle"
mock_dependencies = [
PluginBundleDependency(
type=PluginBundleDependency.Type.Marketplace,
type=PluginBundleDependencyType.Marketplace,
value=PluginBundleDependency.Marketplace(
organization="langgenius", plugin="search-tool", version="1.2.0"
),
@ -1142,7 +1142,7 @@ class TestPluginBundleOperations:
# Assert: Verify marketplace dependency was extracted
assert len(result) == 1
assert result[0].type == PluginBundleDependency.Type.Marketplace
assert result[0].type == PluginBundleDependencyType.Marketplace
assert isinstance(result[0].value, PluginBundleDependency.Marketplace)
assert result[0].value.organization == "langgenius"
assert result[0].value.plugin == "search-tool"
@ -1158,7 +1158,7 @@ class TestPluginBundleOperations:
bundle_data = b"mock-github-bundle"
mock_dependencies = [
PluginBundleDependency(
type=PluginBundleDependency.Type.Github,
type=PluginBundleDependencyType.Github,
value=PluginBundleDependency.Github(
repo_address="https://github.com/example/plugin",
repo="example/plugin",
@ -1174,7 +1174,7 @@ class TestPluginBundleOperations:
# Assert: Verify GitHub dependency was extracted
assert len(result) == 1
assert result[0].type == PluginBundleDependency.Type.Github
assert result[0].type == PluginBundleDependencyType.Github
assert isinstance(result[0].value, PluginBundleDependency.Github)
assert result[0].value.repo == "example/plugin"
assert result[0].value.release == "v2.0.0"
@ -1204,7 +1204,7 @@ class TestPluginBundleOperations:
mock_dependencies = [
PluginBundleDependency(
type=PluginBundleDependency.Type.Package,
type=PluginBundleDependencyType.Package,
value=PluginBundleDependency.Package(
unique_identifier="org/bundled-plugin/1.5.0", manifest=mock_manifest
),
@ -1217,7 +1217,7 @@ class TestPluginBundleOperations:
# Assert: Verify package dependency was extracted with manifest
assert len(result) == 1
assert result[0].type == PluginBundleDependency.Type.Package
assert result[0].type == PluginBundleDependencyType.Package
assert isinstance(result[0].value, PluginBundleDependency.Package)
assert result[0].value.unique_identifier == "org/bundled-plugin/1.5.0"
assert result[0].value.manifest.name == "bundled-plugin"
@ -1233,11 +1233,11 @@ class TestPluginBundleOperations:
bundle_data = b"mock-mixed-bundle"
mock_dependencies = [
PluginBundleDependency(
type=PluginBundleDependency.Type.Marketplace,
type=PluginBundleDependencyType.Marketplace,
value=PluginBundleDependency.Marketplace(organization="org1", plugin="plugin1", version="1.0.0"),
),
PluginBundleDependency(
type=PluginBundleDependency.Type.Github,
type=PluginBundleDependencyType.Github,
value=PluginBundleDependency.Github(
repo_address="https://github.com/org2/plugin2",
repo="org2/plugin2",
@ -1253,8 +1253,8 @@ class TestPluginBundleOperations:
# Assert: Verify all dependency types were extracted
assert len(result) == 2
assert result[0].type == PluginBundleDependency.Type.Marketplace
assert result[1].type == PluginBundleDependency.Type.Github
assert result[0].type == PluginBundleDependencyType.Marketplace
assert result[1].type == PluginBundleDependencyType.Github
class TestPluginTaskStatusTransitions:

View File

@ -5,7 +5,7 @@ from typing import Any, override
import pytest
from core.entities.provider_entities import ProviderConfig
from core.entities.provider_entities import ProviderConfig, ProviderConfigType
from core.tools.__base.tool import Tool
from core.tools.__base.tool_provider import ToolProviderController
from core.tools.__base.tool_runtime import ToolRuntime
@ -66,7 +66,7 @@ def _provider_identity() -> ToolProviderIdentity:
def test_tool_provider_controller_get_credentials_schema_returns_deep_copy():
entity = ToolProviderEntity(
identity=_provider_identity(),
credentials_schema=[ProviderConfig(type=ProviderConfig.Type.TEXT_INPUT, name="api_key", required=False)],
credentials_schema=[ProviderConfig(type=ProviderConfigType.TEXT_INPUT, name="api_key", required=False)],
)
controller = _DummyController(entity=entity)
@ -88,10 +88,10 @@ def test_validate_credentials_format_covers_required_default_and_type_rules():
entity = ToolProviderEntity(
identity=_provider_identity(),
credentials_schema=[
ProviderConfig(type=ProviderConfig.Type.TEXT_INPUT, name="required_text", required=True),
ProviderConfig(type=ProviderConfig.Type.SECRET_INPUT, name="secret", required=False),
ProviderConfig(type=ProviderConfig.Type.SELECT, name="choice", required=False, options=select_options),
ProviderConfig(type=ProviderConfig.Type.TEXT_INPUT, name="with_default", required=False, default="x"),
ProviderConfig(type=ProviderConfigType.TEXT_INPUT, name="required_text", required=True),
ProviderConfig(type=ProviderConfigType.SECRET_INPUT, name="secret", required=False),
ProviderConfig(type=ProviderConfigType.SELECT, name="choice", required=False, options=select_options),
ProviderConfig(type=ProviderConfigType.TEXT_INPUT, name="with_default", required=False, default="x"),
],
)
controller = _DummyController(entity=entity)

View File

@ -5,7 +5,7 @@ from unittest.mock import Mock, patch
import pytest
from core.entities.provider_entities import BasicProviderConfig
from core.entities.provider_entities import BasicProviderConfig, ProviderConfigType
from core.helper.provider_encryption import ProviderConfigEncrypter
from core.tools.utils.encryption import create_tool_provider_encrypter
@ -31,7 +31,7 @@ def secret_field() -> BasicProviderConfig:
"""A SECRET_INPUT field named 'password'."""
return BasicProviderConfig(
name="password",
type=BasicProviderConfig.Type.SECRET_INPUT,
type=ProviderConfigType.SECRET_INPUT,
)
@ -40,7 +40,7 @@ def normal_field() -> BasicProviderConfig:
"""A TEXT_INPUT field named 'username'."""
return BasicProviderConfig(
name="username",
type=BasicProviderConfig.Type.TEXT_INPUT,
type=ProviderConfigType.TEXT_INPUT,
)
@ -185,7 +185,7 @@ def test_decrypt_swallow_exception_and_keep_original(encrypter_obj):
def test_create_tool_provider_encrypter_builds_cache_and_encrypter():
basic_config = BasicProviderConfig(name="key", type=BasicProviderConfig.Type.TEXT_INPUT)
basic_config = BasicProviderConfig(name="key", type=ProviderConfigType.TEXT_INPUT)
credential_schema_item = SimpleNamespace(to_basic_provider_config=lambda: basic_config)
controller = SimpleNamespace(
provider_type=SimpleNamespace(value="builtin"),

View File

@ -11,7 +11,7 @@ from unittest.mock import MagicMock, patch
import pytest
from core.plugin.entities.plugin import PluginDependency, PluginInstallationSource
from core.plugin.entities.plugin import PluginDependency, PluginDependencyType, PluginInstallationSource
from services.plugin.dependencies_analysis import DependenciesAnalysisService
@ -44,7 +44,7 @@ class TestAnalyzeModelProviderDependency:
class TestGetLeakedDependencies:
def _make_dependency(self, identifier: str, dep_type=PluginDependency.Type.Marketplace):
def _make_dependency(self, identifier: str, dep_type=PluginDependencyType.Marketplace):
return PluginDependency(
type=dep_type,
value=PluginDependency.Marketplace(marketplace_plugin_unique_identifier=identifier),
@ -110,7 +110,7 @@ class TestGenerateDependencies:
result = DependenciesAnalysisService.generate_dependencies("t1", ["p1"])
assert len(result) == 1
assert result[0].type == PluginDependency.Type.Github
assert result[0].type == PluginDependencyType.Github
assert result[0].value.repo == "org/repo"
@patch("services.plugin.dependencies_analysis.PluginInstaller")
@ -120,7 +120,7 @@ class TestGenerateDependencies:
result = DependenciesAnalysisService.generate_dependencies("t1", ["p1"])
assert result[0].type == PluginDependency.Type.Marketplace
assert result[0].type == PluginDependencyType.Marketplace
@patch("services.plugin.dependencies_analysis.PluginInstaller")
def test_package_source(self, mock_installer_cls):
@ -129,7 +129,7 @@ class TestGenerateDependencies:
result = DependenciesAnalysisService.generate_dependencies("t1", ["p1"])
assert result[0].type == PluginDependency.Type.Package
assert result[0].type == PluginDependencyType.Package
@patch("services.plugin.dependencies_analysis.PluginInstaller")
def test_remote_source_raises(self, mock_installer_cls):
@ -169,4 +169,4 @@ class TestGenerateLatestDependencies:
result = DependenciesAnalysisService.generate_latest_dependencies(["p1"])
assert len(result) == 1
assert result[0].type == PluginDependency.Type.Marketplace
assert result[0].type == PluginDependencyType.Marketplace

View File

@ -4,10 +4,14 @@ from unittest.mock import MagicMock, patch
import pytest
from models.account import TenantPluginAutoUpgradeStrategy
from models.account import (
TenantPluginAutoUpgradeCategory,
TenantPluginAutoUpgradeMode,
TenantPluginAutoUpgradeStrategySetting,
)
MODULE = "services.plugin.plugin_auto_upgrade_service"
PLUGIN_CATEGORY = TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL
PLUGIN_CATEGORY = TenantPluginAutoUpgradeCategory.TOOL
def _patched_session():
@ -57,9 +61,9 @@ class TestChangeStrategy:
result = PluginAutoUpgradeService.change_strategy(
"t1",
TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
3,
TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
TenantPluginAutoUpgradeMode.ALL,
[],
[],
category=PLUGIN_CATEGORY,
@ -78,18 +82,18 @@ class TestChangeStrategy:
result = PluginAutoUpgradeService.change_strategy(
"t1",
TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST,
TenantPluginAutoUpgradeStrategySetting.LATEST,
5,
TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL,
TenantPluginAutoUpgradeMode.PARTIAL,
["p1"],
["p2"],
category=PLUGIN_CATEGORY,
)
assert result is True
assert existing.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST
assert existing.strategy_setting == TenantPluginAutoUpgradeStrategySetting.LATEST
assert existing.upgrade_time_of_day == 5
assert existing.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL
assert existing.upgrade_mode == TenantPluginAutoUpgradeMode.PARTIAL
assert existing.exclude_plugins == ["p1"]
assert existing.include_plugins == ["p2"]
@ -102,10 +106,8 @@ class TestExcludePlugin:
with (
p1,
patch(f"{MODULE}.select"),
patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls,
patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy"),
):
strat_cls.StrategySetting.FIX_ONLY = "fix_only"
strat_cls.UpgradeMode.EXCLUDE = "exclude"
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
result = PluginAutoUpgradeService.exclude_plugin(
@ -120,14 +122,11 @@ class TestExcludePlugin:
def test_appends_to_exclude_list_in_exclude_mode(self):
p1, session = _patched_session()
existing = MagicMock()
existing.upgrade_mode = "exclude"
existing.upgrade_mode = TenantPluginAutoUpgradeMode.EXCLUDE
existing.exclude_plugins = ["p-existing"]
session.scalar.return_value = existing
with p1, patch(f"{MODULE}.select"), patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls:
strat_cls.UpgradeMode.EXCLUDE = "exclude"
strat_cls.UpgradeMode.PARTIAL = "partial"
strat_cls.UpgradeMode.ALL = "all"
with p1, patch(f"{MODULE}.select"):
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
result = PluginAutoUpgradeService.exclude_plugin("t1", "p-new", PLUGIN_CATEGORY)
@ -138,14 +137,11 @@ class TestExcludePlugin:
def test_removes_from_include_list_in_partial_mode(self):
p1, session = _patched_session()
existing = MagicMock()
existing.upgrade_mode = "partial"
existing.upgrade_mode = TenantPluginAutoUpgradeMode.PARTIAL
existing.include_plugins = ["p1", "p2"]
session.scalar.return_value = existing
with p1, patch(f"{MODULE}.select"), patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls:
strat_cls.UpgradeMode.EXCLUDE = "exclude"
strat_cls.UpgradeMode.PARTIAL = "partial"
strat_cls.UpgradeMode.ALL = "all"
with p1, patch(f"{MODULE}.select"):
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
result = PluginAutoUpgradeService.exclude_plugin("t1", "p1", PLUGIN_CATEGORY)
@ -156,32 +152,26 @@ class TestExcludePlugin:
def test_switches_to_exclude_mode_from_all(self):
p1, session = _patched_session()
existing = MagicMock()
existing.upgrade_mode = "all"
existing.upgrade_mode = TenantPluginAutoUpgradeMode.ALL
session.scalar.return_value = existing
with p1, patch(f"{MODULE}.select"), patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls:
strat_cls.UpgradeMode.EXCLUDE = "exclude"
strat_cls.UpgradeMode.PARTIAL = "partial"
strat_cls.UpgradeMode.ALL = "all"
with p1, patch(f"{MODULE}.select"):
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
result = PluginAutoUpgradeService.exclude_plugin("t1", "p1", PLUGIN_CATEGORY)
assert result is True
assert existing.upgrade_mode == "exclude"
assert existing.upgrade_mode == TenantPluginAutoUpgradeMode.EXCLUDE
assert existing.exclude_plugins == ["p1"]
def test_no_duplicate_in_exclude_list(self):
p1, session = _patched_session()
existing = MagicMock()
existing.upgrade_mode = "exclude"
existing.upgrade_mode = TenantPluginAutoUpgradeMode.EXCLUDE
existing.exclude_plugins = ["p1"]
session.scalar.return_value = existing
with p1, patch(f"{MODULE}.select"), patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls:
strat_cls.UpgradeMode.EXCLUDE = "exclude"
strat_cls.UpgradeMode.PARTIAL = "partial"
strat_cls.UpgradeMode.ALL = "all"
with p1, patch(f"{MODULE}.select"):
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
PluginAutoUpgradeService.exclude_plugin("t1", "p1", PLUGIN_CATEGORY)
@ -193,10 +183,10 @@ class TestBackfillStrategyCategories:
def test_creates_default_missing_categories_without_fetching_daemon(self):
p1, session = _patched_session()
tool_strategy = SimpleNamespace(
category=TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL,
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
category=TenantPluginAutoUpgradeCategory.TOOL,
strategy_setting=TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
upgrade_time_of_day=0,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
upgrade_mode=TenantPluginAutoUpgradeMode.EXCLUDE,
exclude_plugins=[],
include_plugins=[],
)
@ -209,17 +199,15 @@ class TestBackfillStrategyCategories:
result = PluginAutoUpgradeService.backfill_strategy_categories("t1")
expected_time = PluginAutoUpgradeService.default_upgrade_time_of_day("t1")
assert result.created_count == len(TenantPluginAutoUpgradeStrategy.PluginCategory) - 1
assert result.created_count == len(TenantPluginAutoUpgradeCategory) - 1
assert result.normalized is False
installer.list_plugins.assert_not_called()
assert tool_strategy.upgrade_time_of_day == expected_time
created_strategies = [call.args[0] for call in session.add.call_args_list]
model_strategy = next(
strategy
for strategy in created_strategies
if strategy.category == TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL
strategy for strategy in created_strategies if strategy.category == TenantPluginAutoUpgradeCategory.MODEL
)
assert model_strategy.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST
assert model_strategy.strategy_setting == TenantPluginAutoUpgradeStrategySetting.LATEST
assert model_strategy.upgrade_time_of_day == expected_time
def test_default_upgrade_time_is_aligned_to_fifteen_minutes(self):
@ -233,18 +221,18 @@ class TestBackfillStrategyCategories:
def test_creates_missing_categories_and_splits_known_plugins(self, caplog: pytest.LogCaptureFixture):
p1, session = _patched_session()
tool_strategy = SimpleNamespace(
category=TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL,
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
category=TenantPluginAutoUpgradeCategory.TOOL,
strategy_setting=TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
upgrade_time_of_day=0,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
upgrade_mode=TenantPluginAutoUpgradeMode.EXCLUDE,
exclude_plugins=["tool-plugin", "model-plugin", "unknown-plugin"],
include_plugins=["model-plugin", "tool-plugin"],
)
model_strategy = SimpleNamespace(
category=TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL,
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
category=TenantPluginAutoUpgradeCategory.MODEL,
strategy_setting=TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
upgrade_time_of_day=0,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
upgrade_mode=TenantPluginAutoUpgradeMode.EXCLUDE,
exclude_plugins=["tool-plugin", "model-plugin", "unknown-plugin"],
include_plugins=["model-plugin", "tool-plugin"],
)
@ -253,11 +241,11 @@ class TestBackfillStrategyCategories:
installed_plugins = [
SimpleNamespace(
plugin_id="tool-plugin",
declaration=SimpleNamespace(category=TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL),
declaration=SimpleNamespace(category=TenantPluginAutoUpgradeCategory.TOOL),
),
SimpleNamespace(
plugin_id="model-plugin",
declaration=SimpleNamespace(category=TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL),
declaration=SimpleNamespace(category=TenantPluginAutoUpgradeCategory.MODEL),
),
]
installer = MagicMock()
@ -272,9 +260,9 @@ class TestBackfillStrategyCategories:
result = PluginAutoUpgradeService.backfill_strategy_categories("t1")
assert result.created_count == len(TenantPluginAutoUpgradeStrategy.PluginCategory) - 2
assert result.created_count == len(TenantPluginAutoUpgradeCategory) - 2
assert result.normalized is True
assert session.add.call_count == len(TenantPluginAutoUpgradeStrategy.PluginCategory) - 2
assert session.add.call_count == len(TenantPluginAutoUpgradeCategory) - 2
assert tool_strategy.exclude_plugins == ["tool-plugin"]
assert tool_strategy.include_plugins == ["tool-plugin"]
assert model_strategy.exclude_plugins == ["model-plugin"]

View File

@ -87,11 +87,11 @@ def test_check_dependencies_returns_empty_when_no_redis_data(mocker: MockerFixtu
def test_check_dependencies_returns_leaked_deps_from_redis(mocker: MockerFixture) -> None:
from core.plugin.entities.plugin import PluginDependency
from core.plugin.entities.plugin import PluginDependency, PluginDependencyType
from services.rag_pipeline.rag_pipeline_dsl_service import CheckDependenciesPendingData
dep = PluginDependency(
type=PluginDependency.Type.Marketplace,
type=PluginDependencyType.Marketplace,
value=PluginDependency.Marketplace(marketplace_plugin_unique_identifier="test/plugin:0.1.0"),
)
pending_data = CheckDependenciesPendingData(
@ -1371,7 +1371,7 @@ def test_confirm_import_fails_when_no_knowledge_index_node(mocker: MockerFixture
def test_create_or_update_pipeline_saves_dependencies_to_redis(mocker: MockerFixture) -> None:
from core.plugin.entities.plugin import PluginDependency
from core.plugin.entities.plugin import PluginDependency, PluginDependencyType
session = cast(MagicMock, Mock())
service = RagPipelineDslService(session=cast(Session, session))
@ -1386,7 +1386,7 @@ def test_create_or_update_pipeline_saves_dependencies_to_redis(mocker: MockerFix
session.scalar.return_value = None
setex = mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.redis_client.setex")
dependency = PluginDependency(
type=PluginDependency.Type.Marketplace,
type=PluginDependencyType.Marketplace,
value=PluginDependency.Marketplace(marketplace_plugin_unique_identifier="langgenius/example:0.1.0"),
)

View File

@ -5,7 +5,11 @@ from unittest.mock import MagicMock, patch
from core.plugin.entities.marketplace import MarketplacePluginSnapshot
from core.plugin.entities.plugin import PluginCategory, PluginInstallationSource
from models.account import TenantPluginAutoUpgradeStrategy
from models.account import (
TenantPluginAutoUpgradeCategory,
TenantPluginAutoUpgradeMode,
TenantPluginAutoUpgradeStrategySetting,
)
MODULE = "tasks.process_tenant_plugin_autoupgrade_check_task"
@ -41,8 +45,8 @@ def _run_task(
*,
plugins: list,
manifests: list[MarketplacePluginSnapshot],
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
strategy_setting=TenantPluginAutoUpgradeStrategySetting.LATEST,
upgrade_mode=TenantPluginAutoUpgradeMode.ALL,
exclude_plugins=None,
include_plugins=None,
category=None,
@ -121,9 +125,9 @@ class TestUpgradeCallsMarketplaceService:
process_tenant_plugin_autoupgrade_check_task(
"tenant-1",
TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST,
TenantPluginAutoUpgradeStrategySetting.LATEST,
0,
TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
TenantPluginAutoUpgradeMode.ALL,
[],
[],
)
@ -136,7 +140,7 @@ class TestStrategySetting:
upgrade_mock, _ = _run_task(
plugins=[_make_plugin("acme/foo", "1.0.0")],
manifests=[_make_manifest("acme/foo", "1.0.1")],
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.DISABLED,
strategy_setting=TenantPluginAutoUpgradeStrategySetting.DISABLED,
)
upgrade_mock.assert_not_called()
@ -144,7 +148,7 @@ class TestStrategySetting:
upgrade_mock, calls = _run_task(
plugins=[_make_plugin("acme/foo", "1.0.0")],
manifests=[_make_manifest("acme/foo", "1.0.5")],
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
strategy_setting=TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
)
upgrade_mock.assert_called_once()
assert calls[0][2].endswith(":1.0.5@cafe1234")
@ -153,7 +157,7 @@ class TestStrategySetting:
upgrade_mock, _ = _run_task(
plugins=[_make_plugin("acme/foo", "1.0.0")],
manifests=[_make_manifest("acme/foo", "1.1.0")],
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
strategy_setting=TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
)
upgrade_mock.assert_not_called()
@ -161,7 +165,7 @@ class TestStrategySetting:
upgrade_mock, _ = _run_task(
plugins=[_make_plugin("acme/foo", "1.0.0")],
manifests=[_make_manifest("acme/foo", "2.0.0")],
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
strategy_setting=TenantPluginAutoUpgradeStrategySetting.FIX_ONLY,
)
upgrade_mock.assert_not_called()
@ -169,7 +173,7 @@ class TestStrategySetting:
upgrade_mock, _ = _run_task(
plugins=[_make_plugin("acme/foo", "1.0.0")],
manifests=[_make_manifest("acme/foo", "1.0.0")],
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST,
strategy_setting=TenantPluginAutoUpgradeStrategySetting.LATEST,
)
upgrade_mock.assert_not_called()
@ -188,7 +192,7 @@ class TestUpgradeMode:
upgrade_mock, calls = _run_task(
plugins=plugins,
manifests=manifests,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
upgrade_mode=TenantPluginAutoUpgradeMode.ALL,
)
assert upgrade_mock.call_count == 2
@ -208,7 +212,7 @@ class TestUpgradeMode:
upgrade_mock, calls = _run_task(
plugins=plugins,
manifests=manifests,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
upgrade_mode=TenantPluginAutoUpgradeMode.ALL,
)
assert upgrade_mock.call_count == 1
@ -227,7 +231,7 @@ class TestUpgradeMode:
upgrade_mock, calls = _run_task(
plugins=plugins,
manifests=manifests,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL,
upgrade_mode=TenantPluginAutoUpgradeMode.PARTIAL,
include_plugins=["acme/foo"],
)
@ -247,7 +251,7 @@ class TestUpgradeMode:
upgrade_mock, calls = _run_task(
plugins=plugins,
manifests=manifests,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
upgrade_mode=TenantPluginAutoUpgradeMode.EXCLUDE,
exclude_plugins=["acme/bar"],
)
@ -267,8 +271,8 @@ class TestUpgradeMode:
upgrade_mock, calls = _run_task(
plugins=plugins,
manifests=manifests,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
category=TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL,
upgrade_mode=TenantPluginAutoUpgradeMode.ALL,
category=TenantPluginAutoUpgradeCategory.MODEL,
)
upgrade_mock.assert_called_once()
@ -306,9 +310,9 @@ class TestErrorIsolation:
process_tenant_plugin_autoupgrade_check_task(
"tenant-1",
TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST,
TenantPluginAutoUpgradeStrategySetting.LATEST,
0,
TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL,
TenantPluginAutoUpgradeMode.ALL,
[],
[],
)

View File

@ -87,10 +87,6 @@ export type EducationActivatePayload = {
token: string
}
export type EducationActivateResponse = {
[key: string]: unknown
}
export type EducationAutocompleteResponse = {
curr_page?: number | null
data?: Array<string>
@ -301,7 +297,9 @@ export type PostAccountEducationData = {
}
export type PostAccountEducationResponses = {
200: EducationActivateResponse
200: {
[key: string]: unknown
}
}
export type PostAccountEducationResponse

View File

@ -127,11 +127,6 @@ export const zEducationActivatePayload = z.object({
token: z.string(),
})
/**
* EducationActivateResponse
*/
export const zEducationActivateResponse = z.record(z.string(), z.unknown())
/**
* EducationAutocompleteResponse
*/
@ -301,7 +296,7 @@ export const zPostAccountEducationBody = zEducationActivatePayload
/**
* Success
*/
export const zPostAccountEducationResponse = zEducationActivateResponse
export const zPostAccountEducationResponse = z.record(z.string(), z.unknown())
export const zGetAccountEducationAutocompleteQuery = z.object({
keywords: z.string(),

View File

@ -4,7 +4,7 @@ export type ClientOptions = {
baseUrl: `${string}://${string}/console/api` | (string & {})
}
export type WorkspaceListResponse = {
export type WorkspacePaginationResponse = {
data: Array<WorkspaceListItemResponse>
has_more: boolean
limit: number
@ -30,7 +30,7 @@ export type GetAllWorkspacesData = {
}
export type GetAllWorkspacesResponses = {
200: WorkspaceListResponse
200: WorkspacePaginationResponse
}
export type GetAllWorkspacesResponse = GetAllWorkspacesResponses[keyof GetAllWorkspacesResponses]

View File

@ -13,9 +13,9 @@ export const zWorkspaceListItemResponse = z.object({
})
/**
* WorkspaceListResponse
* WorkspacePaginationResponse
*/
export const zWorkspaceListResponse = z.object({
export const zWorkspacePaginationResponse = z.object({
data: z.array(zWorkspaceListItemResponse),
has_more: z.boolean(),
limit: z.int(),
@ -31,4 +31,4 @@ export const zGetAllWorkspacesQuery = z.object({
/**
* Success
*/
export const zGetAllWorkspacesResponse = zWorkspaceListResponse
export const zGetAllWorkspacesResponse = zWorkspacePaginationResponse

View File

@ -1372,7 +1372,7 @@ export type ImportStatus = 'completed' | 'completed-with-warnings' | 'failed' |
export type PluginDependency = {
current_identifier?: string | null
type: Type
type: PluginDependencyType
value: Github | Marketplace | Package
}
@ -2186,7 +2186,7 @@ export type ModelConfigPartial = {
export type LlmMode = 'chat' | 'completion'
export type Type = 'github' | 'marketplace' | 'package'
export type PluginDependencyType = 'github' | 'marketplace' | 'package'
export type Github = {
github_plugin_unique_identifier: string

View File

@ -2336,9 +2336,9 @@ export const zConversationDetail = z.object({
})
/**
* Type
* PluginDependencyType
*/
export const zType = z.enum(['github', 'marketplace', 'package'])
export const zPluginDependencyType = z.enum(['github', 'marketplace', 'package'])
/**
* Github
@ -2371,7 +2371,7 @@ export const zPackage = z.object({
*/
export const zPluginDependency = z.object({
current_identifier: z.string().nullish(),
type: zType,
type: zPluginDependencyType,
value: z.union([zGithub, zMarketplace, zPackage]),
})

View File

@ -415,7 +415,7 @@ export type PipelineTemplateItemResponse = {
export type PluginDependency = {
current_identifier?: string | null
type: Type
type: PluginDependencyType
value: Github | Marketplace | Package
}
@ -515,7 +515,7 @@ export type DatasetWeightedScoreResponse = {
weight_type?: string | null
}
export type Type = 'github' | 'marketplace' | 'package'
export type PluginDependencyType = 'github' | 'marketplace' | 'package'
export type Github = {
github_plugin_unique_identifier: string

View File

@ -545,9 +545,9 @@ export const zDatasetRerankingModelResponse = z.object({
})
/**
* Type
* PluginDependencyType
*/
export const zType = z.enum(['github', 'marketplace', 'package'])
export const zPluginDependencyType = z.enum(['github', 'marketplace', 'package'])
/**
* Github
@ -580,7 +580,7 @@ export const zPackage = z.object({
*/
export const zPluginDependency = z.object({
current_identifier: z.string().nullish(),
type: zType,
type: zPluginDependencyType,
value: z.union([zGithub, zMarketplace, zPackage]),
})

View File

@ -376,6 +376,7 @@ import {
zPostWorkspacesCurrentTriggerProviderBySubscriptionIdSubscriptionsUpdateResponse,
zPostWorkspacesCustomConfigBody,
zPostWorkspacesCustomConfigResponse,
zPostWorkspacesCustomConfigWebappLogoUploadBody,
zPostWorkspacesCustomConfigWebappLogoUploadResponse,
zPostWorkspacesInfoBody,
zPostWorkspacesInfoResponse,
@ -4175,6 +4176,7 @@ export const post72 = oc
successStatus: 201,
tags: ['console'],
})
.input(z.object({ body: zPostWorkspacesCustomConfigWebappLogoUploadBody }))
.output(zPostWorkspacesCustomConfigWebappLogoUploadResponse)
export const upload2 = {

View File

@ -133,7 +133,7 @@ export type EndpointCreatePayload = {
}
}
export type EndpointCreateResponse = {
export type SuccessResponse = {
success: boolean
}
@ -141,28 +141,8 @@ export type EndpointIdPayload = {
endpoint_id: string
}
export type EndpointDeleteResponse = {
success: boolean
}
export type EndpointDisableResponse = {
success: boolean
}
export type EndpointEnableResponse = {
success: boolean
}
export type EndpointListResponse = {
endpoints: Array<{
[key: string]: unknown
}>
}
export type PluginEndpointListResponse = {
endpoints: Array<{
[key: string]: unknown
}>
endpoints: Array<EndpointListItemResponse>
}
export type LegacyEndpointUpdatePayload = {
@ -173,10 +153,6 @@ export type LegacyEndpointUpdatePayload = {
}
}
export type EndpointUpdateResponse = {
success: boolean
}
export type EndpointUpdatePayload = {
name: string
settings: {
@ -216,7 +192,7 @@ export type SimpleResultDataResponse = {
result: string
}
export type MemberActionTenantResponse = {
export type MemberActionResponse = {
result: string
tenant_id: string
}
@ -383,7 +359,7 @@ export type BinaryFileResponse = Blob | File
export type ParserAutoUpgradeChange = {
auto_upgrade: PluginAutoUpgradeSettingsPayload
category: PluginCategory
category: TenantPluginAutoUpgradeCategory
}
export type PluginAutoUpgradeChangeResponse = {
@ -392,17 +368,13 @@ export type PluginAutoUpgradeChangeResponse = {
}
export type ParserExcludePlugin = {
category: PluginCategory
category: TenantPluginAutoUpgradeCategory
plugin_id: string
}
export type SuccessResponse = {
success: boolean
}
export type PluginAutoUpgradeFetchResponse = {
auto_upgrade: PluginAutoUpgradeSettingsResponseModel
category: PluginCategory
category: TenantPluginAutoUpgradeCategory
}
export type PluginDebuggingKeyResponse = {
@ -463,13 +435,13 @@ export type ParserDynamicOptionsWithCredentials = {
}
export type ParserPermissionChange = {
debug_permission?: DebugPermission
install_permission?: InstallPermission
debug_permission?: TenantPluginDebugPermission
install_permission?: TenantPluginInstallPermission
}
export type PluginPermissionResponse = {
debug_permission: DebugPermission
install_permission: InstallPermission
debug_permission: TenantPluginDebugPermission
install_permission: TenantPluginInstallPermission
}
export type PluginReadmeResponse = {
@ -913,7 +885,7 @@ export type WorkspaceCustomConfigPayload = {
replace_webapp_logo?: string | null
}
export type WorkspaceMutationResponse = {
export type WorkspaceTenantResultResponse = {
result: string
tenant: TenantInfoResponse
}
@ -1005,7 +977,7 @@ export type ImportStatus = 'completed' | 'completed-with-warnings' | 'failed' |
export type PluginDependency = {
current_identifier?: string | null
type: Type
type: PluginDependencyType
value: Github | Marketplace | Package
}
@ -1037,6 +1009,23 @@ export type Inner = {
provider?: string | null
}
export type EndpointListItemResponse = {
created_at: string
declaration?: EndpointProviderDeclarationResponse
enabled: boolean
expired_at: string
hook_id: string
id: string
name: string
plugin_id: string
settings: {
[key: string]: unknown
}
tenant_id: string
updated_at: string
url: string
}
export type MemberInviteResultResponse = {
email: string
message?: string | null
@ -1126,12 +1115,12 @@ export type ProviderWithModelsResponse = {
export type PluginAutoUpgradeSettingsPayload = {
exclude_plugins?: Array<string>
include_plugins?: Array<string>
strategy_setting?: StrategySetting
upgrade_mode?: UpgradeMode
strategy_setting?: TenantPluginAutoUpgradeStrategySetting
upgrade_mode?: TenantPluginAutoUpgradeMode
upgrade_time_of_day?: number
}
export type PluginCategory
export type TenantPluginAutoUpgradeCategory
= | 'agent-strategy'
| 'datasource'
| 'extension'
@ -1142,8 +1131,8 @@ export type PluginCategory
export type PluginAutoUpgradeSettingsResponseModel = {
exclude_plugins: Array<string>
include_plugins: Array<string>
strategy_setting: StrategySetting
upgrade_mode: UpgradeMode
strategy_setting: TenantPluginAutoUpgradeStrategySetting
upgrade_mode: TenantPluginAutoUpgradeMode
upgrade_time_of_day: number
}
@ -1175,9 +1164,9 @@ export type LatestPluginCache = {
version: string
}
export type DebugPermission = 'admins' | 'everyone' | 'noone'
export type TenantPluginDebugPermission = 'admins' | 'everyone' | 'noone'
export type InstallPermission = 'admins' | 'everyone' | 'noone'
export type TenantPluginInstallPermission = 'admins' | 'everyone' | 'noone'
export type PluginCategoryBuiltinToolProviderResponse = {
allow_delete: boolean
@ -1350,7 +1339,7 @@ export type ProviderConfig = {
placeholder?: I18nObject | null
required?: boolean
scope?: AppSelectorScope | ModelSelectorScope | ToolSelectorScope | null
type: CoreEntitiesProviderEntitiesBasicProviderConfigType
type: ProviderConfigType
url?: string | null
}
@ -1407,7 +1396,7 @@ export type TriggerProviderSubscriptionApiEntity = {
workflows_in_use: number
}
export type Type = 'github' | 'marketplace' | 'package'
export type PluginDependencyType = 'github' | 'marketplace' | 'package'
export type Github = {
github_plugin_unique_identifier: string
@ -1437,6 +1426,11 @@ export type SimpleProviderEntityResponse = {
tenant_id: string
}
export type EndpointProviderDeclarationResponse = {
endpoints?: Array<EndpointDeclarationResponse> | null
settings?: Array<EndpointProviderConfigResponse>
}
export type ConfigurateMethod = 'customizable-model' | 'predefined-model'
export type CustomConfigurationResponse = {
@ -1529,9 +1523,9 @@ export type ProviderModelWithStatusEntity = {
export type CustomConfigurationStatus = 'active' | 'no-configure'
export type StrategySetting = 'disabled' | 'fix_only' | 'latest'
export type TenantPluginAutoUpgradeStrategySetting = 'disabled' | 'fix_only' | 'latest'
export type UpgradeMode = 'all' | 'exclude' | 'partial'
export type TenantPluginAutoUpgradeMode = 'all' | 'exclude' | 'partial'
export type PluginDeclarationResponse = {
agent_strategy?: {
@ -1673,7 +1667,7 @@ export type ModelSelectorScope
export type ToolSelectorScope = 'all' | 'builtin' | 'custom' | 'workflow'
export type CoreEntitiesProviderEntitiesBasicProviderConfigType
export type ProviderConfigType
= | 'app-selector'
| 'array[tools]'
| 'boolean'
@ -1701,6 +1695,26 @@ export type AiModelEntityResponse = {
pricing?: PriceConfigResponse | null
}
export type EndpointDeclarationResponse = {
hidden?: boolean
method: string
path: string
}
export type EndpointProviderConfigResponse = {
default?: number | string | number | boolean | null
help?: EndpointProviderConfigI18nResponse | null
label?: EndpointProviderConfigI18nResponse | null
multiple?: boolean
name: string
options?: Array<EndpointProviderConfigOptionResponse> | null
placeholder?: EndpointProviderConfigI18nResponse | null
required?: boolean
scope?: EndpointProviderConfigScope | null
type: ProviderConfigType
url?: string | null
}
export type UnaddedModelConfiguration = {
model: string
model_type: ModelType
@ -1746,6 +1760,14 @@ export type QuotaConfiguration = {
restrict_models?: Array<RestrictModel>
}
export type PluginCategory
= | 'agent-strategy'
| 'datasource'
| 'extension'
| 'model'
| 'tool'
| 'trigger'
export type ProviderEntityResponse = {
background?: string | null
configurate_methods: Array<ConfigurateMethod>
@ -1766,7 +1788,7 @@ export type ProviderEntityResponse = {
}
export type PluginParameterAutoGenerate = {
type: CorePluginEntitiesParametersPluginParameterAutoGenerateType
type: PluginParameterAutoGenerateType
}
export type PluginParameterOption = {
@ -1800,6 +1822,33 @@ export type PriceConfigResponse = {
unit: string
}
export type EndpointProviderConfigI18nResponse = {
en_US: string
ja_JP?: string | null
pt_BR?: string | null
zh_Hans?: string | null
}
export type EndpointProviderConfigOptionResponse = {
label: EndpointProviderConfigI18nResponse
value: string
}
export type EndpointProviderConfigScope
= | 'all'
| 'builtin'
| 'chat'
| 'completion'
| 'custom'
| 'llm'
| 'moderation'
| 'rerank'
| 'speech2text'
| 'text-embedding'
| 'tts'
| 'vision'
| 'workflow'
export type FormOption = {
label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
show_on?: Array<FormShowOnObject>
@ -1821,7 +1870,7 @@ export type RestrictModel = {
model_type: ModelType
}
export type CorePluginEntitiesParametersPluginParameterAutoGenerateType = 'prompt_instruction'
export type PluginParameterAutoGenerateType = 'prompt_instruction'
export type AccountWithRoleListResponseWritable = {
accounts: Array<AccountWithRoleResponseWritable>
@ -2156,7 +2205,7 @@ export type PostWorkspacesCurrentEndpointsErrors = {
}
export type PostWorkspacesCurrentEndpointsResponses = {
200: EndpointCreateResponse
200: SuccessResponse
}
export type PostWorkspacesCurrentEndpointsResponse
@ -2174,7 +2223,7 @@ export type PostWorkspacesCurrentEndpointsCreateErrors = {
}
export type PostWorkspacesCurrentEndpointsCreateResponses = {
200: EndpointCreateResponse
200: SuccessResponse
}
export type PostWorkspacesCurrentEndpointsCreateResponse
@ -2192,7 +2241,7 @@ export type PostWorkspacesCurrentEndpointsDeleteErrors = {
}
export type PostWorkspacesCurrentEndpointsDeleteResponses = {
200: EndpointDeleteResponse
200: SuccessResponse
}
export type PostWorkspacesCurrentEndpointsDeleteResponse
@ -2210,7 +2259,7 @@ export type PostWorkspacesCurrentEndpointsDisableErrors = {
}
export type PostWorkspacesCurrentEndpointsDisableResponses = {
200: EndpointDisableResponse
200: SuccessResponse
}
export type PostWorkspacesCurrentEndpointsDisableResponse
@ -2228,7 +2277,7 @@ export type PostWorkspacesCurrentEndpointsEnableErrors = {
}
export type PostWorkspacesCurrentEndpointsEnableResponses = {
200: EndpointEnableResponse
200: SuccessResponse
}
export type PostWorkspacesCurrentEndpointsEnableResponse
@ -2263,7 +2312,7 @@ export type GetWorkspacesCurrentEndpointsListPluginData = {
}
export type GetWorkspacesCurrentEndpointsListPluginResponses = {
200: PluginEndpointListResponse
200: EndpointListResponse
}
export type GetWorkspacesCurrentEndpointsListPluginResponse
@ -2281,7 +2330,7 @@ export type PostWorkspacesCurrentEndpointsUpdateErrors = {
}
export type PostWorkspacesCurrentEndpointsUpdateResponses = {
200: EndpointUpdateResponse
200: SuccessResponse
}
export type PostWorkspacesCurrentEndpointsUpdateResponse
@ -2301,7 +2350,7 @@ export type DeleteWorkspacesCurrentEndpointsByIdErrors = {
}
export type DeleteWorkspacesCurrentEndpointsByIdResponses = {
200: EndpointDeleteResponse
200: SuccessResponse
}
export type DeleteWorkspacesCurrentEndpointsByIdResponse
@ -2321,7 +2370,7 @@ export type PatchWorkspacesCurrentEndpointsByIdErrors = {
}
export type PatchWorkspacesCurrentEndpointsByIdResponses = {
200: EndpointUpdateResponse
200: SuccessResponse
}
export type PatchWorkspacesCurrentEndpointsByIdResponse
@ -2393,7 +2442,7 @@ export type DeleteWorkspacesCurrentMembersByMemberIdData = {
}
export type DeleteWorkspacesCurrentMembersByMemberIdResponses = {
200: MemberActionTenantResponse
200: MemberActionResponse
}
export type DeleteWorkspacesCurrentMembersByMemberIdResponse
@ -4936,14 +4985,16 @@ export type PostWorkspacesCustomConfigData = {
}
export type PostWorkspacesCustomConfigResponses = {
200: WorkspaceMutationResponse
200: WorkspaceTenantResultResponse
}
export type PostWorkspacesCustomConfigResponse
= PostWorkspacesCustomConfigResponses[keyof PostWorkspacesCustomConfigResponses]
export type PostWorkspacesCustomConfigWebappLogoUploadData = {
body?: never
body: {
file: Blob | File
}
path?: never
query?: never
url: '/workspaces/custom-config/webapp-logo/upload'
@ -4964,7 +5015,7 @@ export type PostWorkspacesInfoData = {
}
export type PostWorkspacesInfoResponses = {
200: WorkspaceMutationResponse
200: WorkspaceTenantResultResponse
}
export type PostWorkspacesInfoResponse

View File

@ -56,9 +56,9 @@ export const zEndpointCreatePayload = z.object({
})
/**
* EndpointCreateResponse
* SuccessResponse
*/
export const zEndpointCreateResponse = z.object({
export const zSuccessResponse = z.object({
success: z.boolean(),
})
@ -69,41 +69,6 @@ export const zEndpointIdPayload = z.object({
endpoint_id: z.string(),
})
/**
* EndpointDeleteResponse
*/
export const zEndpointDeleteResponse = z.object({
success: z.boolean(),
})
/**
* EndpointDisableResponse
*/
export const zEndpointDisableResponse = z.object({
success: z.boolean(),
})
/**
* EndpointEnableResponse
*/
export const zEndpointEnableResponse = z.object({
success: z.boolean(),
})
/**
* EndpointListResponse
*/
export const zEndpointListResponse = z.object({
endpoints: z.array(z.record(z.string(), z.unknown())),
})
/**
* PluginEndpointListResponse
*/
export const zPluginEndpointListResponse = z.object({
endpoints: z.array(z.record(z.string(), z.unknown())),
})
/**
* LegacyEndpointUpdatePayload
*/
@ -113,13 +78,6 @@ export const zLegacyEndpointUpdatePayload = z.object({
settings: z.record(z.string(), z.unknown()),
})
/**
* EndpointUpdateResponse
*/
export const zEndpointUpdateResponse = z.object({
success: z.boolean(),
})
/**
* EndpointUpdatePayload
*/
@ -170,9 +128,9 @@ export const zSimpleResultDataResponse = z.object({
})
/**
* MemberActionTenantResponse
* MemberActionResponse
*/
export const zMemberActionTenantResponse = z.object({
export const zMemberActionResponse = z.object({
result: z.string(),
tenant_id: z.string(),
})
@ -296,13 +254,6 @@ export const zPluginAutoUpgradeChangeResponse = z.object({
success: z.boolean(),
})
/**
* SuccessResponse
*/
export const zSuccessResponse = z.object({
success: z.boolean(),
})
/**
* PluginDebuggingKeyResponse
*/
@ -724,9 +675,9 @@ export const zTenantInfoResponse = z.object({
})
/**
* WorkspaceMutationResponse
* WorkspaceTenantResultResponse
*/
export const zWorkspaceMutationResponse = z.object({
export const zWorkspaceTenantResultResponse = z.object({
result: z.string(),
tenant: zTenantInfoResponse,
})
@ -1073,9 +1024,9 @@ export const zModelCredentialResponse = z.object({
})
/**
* PluginCategory
* TenantPluginAutoUpgradeCategory
*/
export const zPluginCategory = z.enum([
export const zTenantPluginAutoUpgradeCategory = z.enum([
'agent-strategy',
'datasource',
'extension',
@ -1088,7 +1039,7 @@ export const zPluginCategory = z.enum([
* ParserExcludePlugin
*/
export const zParserExcludePlugin = z.object({
category: zPluginCategory,
category: zTenantPluginAutoUpgradeCategory,
plugin_id: z.string(),
})
@ -1112,29 +1063,29 @@ export const zPluginVersionsResponse = z.object({
})
/**
* DebugPermission
* TenantPluginDebugPermission
*/
export const zDebugPermission = z.enum(['admins', 'everyone', 'noone'])
export const zTenantPluginDebugPermission = z.enum(['admins', 'everyone', 'noone'])
/**
* InstallPermission
* TenantPluginInstallPermission
*/
export const zInstallPermission = z.enum(['admins', 'everyone', 'noone'])
export const zTenantPluginInstallPermission = z.enum(['admins', 'everyone', 'noone'])
/**
* ParserPermissionChange
*/
export const zParserPermissionChange = z.object({
debug_permission: zDebugPermission.optional().default('everyone'),
install_permission: zInstallPermission.optional().default('everyone'),
debug_permission: zTenantPluginDebugPermission.optional().default('everyone'),
install_permission: zTenantPluginInstallPermission.optional().default('everyone'),
})
/**
* PluginPermissionResponse
*/
export const zPluginPermissionResponse = z.object({
debug_permission: zDebugPermission,
install_permission: zInstallPermission,
debug_permission: zTenantPluginDebugPermission,
install_permission: zTenantPluginInstallPermission,
})
/**
@ -1492,9 +1443,9 @@ export const zTriggerProviderSubscriptionApiEntity = z.object({
export const zTriggerSubscriptionListResponse = z.array(zTriggerProviderSubscriptionApiEntity)
/**
* Type
* PluginDependencyType
*/
export const zType = z.enum(['github', 'marketplace', 'package'])
export const zPluginDependencyType = z.enum(['github', 'marketplace', 'package'])
/**
* Github
@ -1527,7 +1478,7 @@ export const zPackage = z.object({
*/
export const zPluginDependency = z.object({
current_identifier: z.string().nullish(),
type: zType,
type: zPluginDependencyType,
value: z.union([zGithub, zMarketplace, zPackage]),
})
@ -1709,14 +1660,14 @@ export const zProviderWithModelsDataResponse = z.object({
})
/**
* StrategySetting
* TenantPluginAutoUpgradeStrategySetting
*/
export const zStrategySetting = z.enum(['disabled', 'fix_only', 'latest'])
export const zTenantPluginAutoUpgradeStrategySetting = z.enum(['disabled', 'fix_only', 'latest'])
/**
* UpgradeMode
* TenantPluginAutoUpgradeMode
*/
export const zUpgradeMode = z.enum(['all', 'exclude', 'partial'])
export const zTenantPluginAutoUpgradeMode = z.enum(['all', 'exclude', 'partial'])
/**
* PluginAutoUpgradeSettingsPayload
@ -1724,8 +1675,8 @@ export const zUpgradeMode = z.enum(['all', 'exclude', 'partial'])
export const zPluginAutoUpgradeSettingsPayload = z.object({
exclude_plugins: z.array(z.string()).optional(),
include_plugins: z.array(z.string()).optional(),
strategy_setting: zStrategySetting.optional().default('fix_only'),
upgrade_mode: zUpgradeMode.optional().default('exclude'),
strategy_setting: zTenantPluginAutoUpgradeStrategySetting.optional().default('fix_only'),
upgrade_mode: zTenantPluginAutoUpgradeMode.optional().default('exclude'),
upgrade_time_of_day: z.int().optional().default(0),
})
@ -1734,7 +1685,7 @@ export const zPluginAutoUpgradeSettingsPayload = z.object({
*/
export const zParserAutoUpgradeChange = z.object({
auto_upgrade: zPluginAutoUpgradeSettingsPayload,
category: zPluginCategory,
category: zTenantPluginAutoUpgradeCategory,
})
/**
@ -1743,8 +1694,8 @@ export const zParserAutoUpgradeChange = z.object({
export const zPluginAutoUpgradeSettingsResponseModel = z.object({
exclude_plugins: z.array(z.string()),
include_plugins: z.array(z.string()),
strategy_setting: zStrategySetting,
upgrade_mode: zUpgradeMode,
strategy_setting: zTenantPluginAutoUpgradeStrategySetting,
upgrade_mode: zTenantPluginAutoUpgradeMode,
upgrade_time_of_day: z.int(),
})
@ -1753,7 +1704,7 @@ export const zPluginAutoUpgradeSettingsResponseModel = z.object({
*/
export const zPluginAutoUpgradeFetchResponse = z.object({
auto_upgrade: zPluginAutoUpgradeSettingsResponseModel,
category: zPluginCategory,
category: zTenantPluginAutoUpgradeCategory,
})
/**
@ -1988,9 +1939,9 @@ export const zModelSelectorScope = z.enum([
export const zToolSelectorScope = z.enum(['all', 'builtin', 'custom', 'workflow'])
/**
* Type
* ProviderConfigType
*/
export const zCoreEntitiesProviderEntitiesBasicProviderConfigType = z.enum([
export const zProviderConfigType = z.enum([
'app-selector',
'array[tools]',
'boolean',
@ -2015,7 +1966,7 @@ export const zProviderConfig = z.object({
placeholder: zI18nObject.nullish(),
required: z.boolean().optional().default(false),
scope: z.union([zAppSelectorScope, zModelSelectorScope, zToolSelectorScope]).nullish(),
type: zCoreEntitiesProviderEntitiesBasicProviderConfigType,
type: zProviderConfigType,
url: z.string().nullish(),
})
@ -2075,6 +2026,15 @@ export const zTriggerOAuthClientResponse = z.object({
system_configured: z.boolean(),
})
/**
* EndpointDeclarationResponse
*/
export const zEndpointDeclarationResponse = z.object({
hidden: z.boolean().optional().default(false),
method: z.string(),
path: z.string(),
})
/**
* UnaddedModelConfiguration
*
@ -2127,6 +2087,18 @@ export const zFieldModelSchema = z.object({
*/
export const zProviderQuotaType = z.enum(['free', 'paid', 'trial'])
/**
* PluginCategory
*/
export const zPluginCategory = z.enum([
'agent-strategy',
'datasource',
'extension',
'model',
'tool',
'trigger',
])
/**
* PluginParameterOption
*/
@ -2253,6 +2225,93 @@ export const zModelWithProviderListResponse = z.object({
data: z.array(zModelWithProviderEntityResponse),
})
/**
* EndpointProviderConfigI18nResponse
*/
export const zEndpointProviderConfigI18nResponse = z.object({
en_US: z.string(),
ja_JP: z.string().nullish(),
pt_BR: z.string().nullish(),
zh_Hans: z.string().nullish(),
})
/**
* EndpointProviderConfigOptionResponse
*/
export const zEndpointProviderConfigOptionResponse = z.object({
label: zEndpointProviderConfigI18nResponse,
value: z.string(),
})
/**
* EndpointProviderConfigScope
*/
export const zEndpointProviderConfigScope = z.enum([
'all',
'builtin',
'chat',
'completion',
'custom',
'llm',
'moderation',
'rerank',
'speech2text',
'text-embedding',
'tts',
'vision',
'workflow',
])
/**
* EndpointProviderConfigResponse
*/
export const zEndpointProviderConfigResponse = z.object({
default: z.union([z.int(), z.string(), z.number(), z.boolean()]).nullish(),
help: zEndpointProviderConfigI18nResponse.nullish(),
label: zEndpointProviderConfigI18nResponse.nullish(),
multiple: z.boolean().optional().default(false),
name: z.string(),
options: z.array(zEndpointProviderConfigOptionResponse).nullish(),
placeholder: zEndpointProviderConfigI18nResponse.nullish(),
required: z.boolean().optional().default(false),
scope: zEndpointProviderConfigScope.nullish(),
type: zProviderConfigType,
url: z.string().nullish(),
})
/**
* EndpointProviderDeclarationResponse
*/
export const zEndpointProviderDeclarationResponse = z.object({
endpoints: z.array(zEndpointDeclarationResponse).nullish(),
settings: z.array(zEndpointProviderConfigResponse).optional(),
})
/**
* EndpointListItemResponse
*/
export const zEndpointListItemResponse = z.object({
created_at: z.iso.datetime(),
declaration: zEndpointProviderDeclarationResponse.optional(),
enabled: z.boolean(),
expired_at: z.iso.datetime(),
hook_id: z.string(),
id: z.string(),
name: z.string(),
plugin_id: z.string(),
settings: z.record(z.string(), z.unknown()),
tenant_id: z.string(),
updated_at: z.iso.datetime(),
url: z.string(),
})
/**
* EndpointListResponse
*/
export const zEndpointListResponse = z.object({
endpoints: z.array(zEndpointListItemResponse),
})
/**
* FormShowOnObject
*
@ -2494,17 +2553,15 @@ export const zModelProviderListResponse = z.object({
})
/**
* Type
* PluginParameterAutoGenerateType
*/
export const zCorePluginEntitiesParametersPluginParameterAutoGenerateType = z.enum([
'prompt_instruction',
])
export const zPluginParameterAutoGenerateType = z.enum(['prompt_instruction'])
/**
* PluginParameterAutoGenerate
*/
export const zPluginParameterAutoGenerate = z.object({
type: zCorePluginEntitiesParametersPluginParameterAutoGenerateType,
type: zPluginParameterAutoGenerateType,
})
/**
@ -2748,35 +2805,35 @@ export const zPostWorkspacesCurrentEndpointsBody = zEndpointCreatePayload
/**
* Endpoint created successfully
*/
export const zPostWorkspacesCurrentEndpointsResponse = zEndpointCreateResponse
export const zPostWorkspacesCurrentEndpointsResponse = zSuccessResponse
export const zPostWorkspacesCurrentEndpointsCreateBody = zEndpointCreatePayload
/**
* Endpoint created successfully
*/
export const zPostWorkspacesCurrentEndpointsCreateResponse = zEndpointCreateResponse
export const zPostWorkspacesCurrentEndpointsCreateResponse = zSuccessResponse
export const zPostWorkspacesCurrentEndpointsDeleteBody = zEndpointIdPayload
/**
* Endpoint deleted successfully
*/
export const zPostWorkspacesCurrentEndpointsDeleteResponse = zEndpointDeleteResponse
export const zPostWorkspacesCurrentEndpointsDeleteResponse = zSuccessResponse
export const zPostWorkspacesCurrentEndpointsDisableBody = zEndpointIdPayload
/**
* Endpoint disabled successfully
*/
export const zPostWorkspacesCurrentEndpointsDisableResponse = zEndpointDisableResponse
export const zPostWorkspacesCurrentEndpointsDisableResponse = zSuccessResponse
export const zPostWorkspacesCurrentEndpointsEnableBody = zEndpointIdPayload
/**
* Endpoint enabled successfully
*/
export const zPostWorkspacesCurrentEndpointsEnableResponse = zEndpointEnableResponse
export const zPostWorkspacesCurrentEndpointsEnableResponse = zSuccessResponse
export const zGetWorkspacesCurrentEndpointsListQuery = z.object({
page: z.int().gte(1),
@ -2797,14 +2854,14 @@ export const zGetWorkspacesCurrentEndpointsListPluginQuery = z.object({
/**
* Success
*/
export const zGetWorkspacesCurrentEndpointsListPluginResponse = zPluginEndpointListResponse
export const zGetWorkspacesCurrentEndpointsListPluginResponse = zEndpointListResponse
export const zPostWorkspacesCurrentEndpointsUpdateBody = zLegacyEndpointUpdatePayload
/**
* Endpoint updated successfully
*/
export const zPostWorkspacesCurrentEndpointsUpdateResponse = zEndpointUpdateResponse
export const zPostWorkspacesCurrentEndpointsUpdateResponse = zSuccessResponse
export const zDeleteWorkspacesCurrentEndpointsByIdPath = z.object({
id: z.string(),
@ -2813,7 +2870,7 @@ export const zDeleteWorkspacesCurrentEndpointsByIdPath = z.object({
/**
* Endpoint deleted successfully
*/
export const zDeleteWorkspacesCurrentEndpointsByIdResponse = zEndpointDeleteResponse
export const zDeleteWorkspacesCurrentEndpointsByIdResponse = zSuccessResponse
export const zPatchWorkspacesCurrentEndpointsByIdBody = zEndpointUpdatePayload
@ -2824,7 +2881,7 @@ export const zPatchWorkspacesCurrentEndpointsByIdPath = z.object({
/**
* Endpoint updated successfully
*/
export const zPatchWorkspacesCurrentEndpointsByIdResponse = zEndpointUpdateResponse
export const zPatchWorkspacesCurrentEndpointsByIdResponse = zSuccessResponse
/**
* Success
@ -2861,7 +2918,7 @@ export const zDeleteWorkspacesCurrentMembersByMemberIdPath = z.object({
/**
* Success
*/
export const zDeleteWorkspacesCurrentMembersByMemberIdResponse = zMemberActionTenantResponse
export const zDeleteWorkspacesCurrentMembersByMemberIdResponse = zMemberActionResponse
export const zPostWorkspacesCurrentMembersByMemberIdOwnerTransferBody = zOwnerTransferPayload
@ -4442,7 +4499,11 @@ export const zPostWorkspacesCustomConfigBody = zWorkspaceCustomConfigPayload
/**
* Success
*/
export const zPostWorkspacesCustomConfigResponse = zWorkspaceMutationResponse
export const zPostWorkspacesCustomConfigResponse = zWorkspaceTenantResultResponse
export const zPostWorkspacesCustomConfigWebappLogoUploadBody = z.object({
file: z.custom<Blob | File>(),
})
/**
* Logo uploaded
@ -4454,7 +4515,7 @@ export const zPostWorkspacesInfoBody = zWorkspaceInfoPayload
/**
* Success
*/
export const zPostWorkspacesInfoResponse = zWorkspaceMutationResponse
export const zPostWorkspacesInfoResponse = zWorkspaceTenantResultResponse
export const zPostWorkspacesSwitchBody = zSwitchWorkspacePayload

View File

@ -369,10 +369,12 @@ export type PermittedExternalAppsListResponse = {
export type PluginDependency = {
current_identifier?: string | null
type: Type
type: PluginDependencyType
value: Github | Marketplace | Package
}
export type PluginDependencyType = 'github' | 'marketplace' | 'package'
export type RevokeResponse = {
status: string
}
@ -415,8 +417,6 @@ export type TaskStopResponse = {
result: 'success'
}
export type Type = 'github' | 'marketplace' | 'package'
export type UsageInfo = {
completion_tokens?: number
prompt_tokens?: number

View File

@ -450,6 +450,27 @@ export const zPermittedExternalAppsListResponse = z.object({
total: z.int(),
})
/**
* PluginDependencyType
*/
export const zPluginDependencyType = z.enum(['github', 'marketplace', 'package'])
/**
* PluginDependency
*/
export const zPluginDependency = z.object({
current_identifier: z.string().nullish(),
type: zPluginDependencyType,
value: z.union([zGithub, zMarketplace, zPackage]),
})
/**
* CheckDependenciesResult
*/
export const zCheckDependenciesResult = z.object({
leaked_dependencies: z.array(zPluginDependency).optional(),
})
/**
* RevokeResponse
*/
@ -567,27 +588,6 @@ export const zTaskStopResponse = z.object({
result: z.literal('success'),
})
/**
* Type
*/
export const zType = z.enum(['github', 'marketplace', 'package'])
/**
* PluginDependency
*/
export const zPluginDependency = z.object({
current_identifier: z.string().nullish(),
type: zType,
value: z.union([zGithub, zMarketplace, zPackage]),
})
/**
* CheckDependenciesResult
*/
export const zCheckDependenciesResult = z.object({
leaked_dependencies: z.array(zPluginDependency).optional(),
})
/**
* UsageInfo
*/