diff --git a/astrbot/dashboard/api/plugins.py b/astrbot/dashboard/api/plugins.py index 4cdf0fccfa0..e456235079c 100644 --- a/astrbot/dashboard/api/plugins.py +++ b/astrbot/dashboard/api/plugins.py @@ -794,6 +794,24 @@ async def list_plugin_config_files_by_id( ) +@router.get("/plugins/config-files/preview") +def preview_plugin_config_image_by_id( + plugin_id: str = Query(...), + config_key: str = Query(...), + path: str = Query(...), + _auth: AuthContext = Depends(require_plugin_scope), + service: ConfigFileService = Depends(get_config_file_service), +): + # A synchronous route keeps image decoding off the ASGI event loop. + return Response( + service.preview_config_image( + name=plugin_id, key_path=config_key, rel_path=path + ), + media_type="image/png", + headers={"Cache-Control": "no-store", "X-Content-Type-Options": "nosniff"}, + ) + + @router.post("/plugins/config-files") async def upload_plugin_config_files_by_id( request: Request, diff --git a/astrbot/dashboard/services/config_service.py b/astrbot/dashboard/services/config_service.py index 1050356c81b..f89b55ab320 100644 --- a/astrbot/dashboard/services/config_service.py +++ b/astrbot/dashboard/services/config_service.py @@ -3,6 +3,7 @@ import asyncio import copy import inspect +import io import os import traceback from datetime import datetime, timezone @@ -10,6 +11,8 @@ from typing import Any from zoneinfo import ZoneInfo, ZoneInfoNotFoundError +from PIL import Image, ImageOps + from astrbot.core import file_token_service, logger from astrbot.core.computer import computer_client from astrbot.core.computer.booters.local import LocalShellComponent @@ -47,6 +50,8 @@ ("dashboard", "totp", "recovery_code_hash"), ) MAX_FILE_BYTES = 500 * 1024 * 1024 +MAX_PREVIEW_FILE_BYTES = 10 * 1024 * 1024 +MAX_PREVIEW_PIXELS = 20_000_000 def try_cast(value: Any, type_: str): @@ -1314,6 +1319,93 @@ def list_config_files_from_dashboard_args(self, args) -> dict: key_path=args.get("key"), ) + def preview_config_image(self, *, name: str, key_path: str, rel_path: str) -> bytes: + """Render a bounded, metadata-free preview of a file field's image. + + Args: + name: Registered plugin name. + key_path: File configuration field in the plugin's schema. + rel_path: Uploaded file path relative to the plugin data directory. + + Returns: + A PNG thumbnail with a maximum edge of 1024 pixels. + + Raises: + ValueError: The plugin, field, or path is invalid. + ApiError: The file is missing, too large, or not a supported image. + """ + _, name, key_path, _, config = self.resolve_config_file_scope( + scope="plugin", name=name, key_path=key_path + ) + meta = _get_schema_item(getattr(config, "schema", None), key_path) + if not meta or meta.get("type") != "file": + raise ValueError("Config item not found or not file type") + + normalized = _normalize_rel_path(rel_path) + folder = f"files/{_config_key_to_folder(key_path)}" + if ( + rel_path.startswith(("/", "\\")) + or not normalized + or not normalized.startswith(f"{folder}/") + ): + raise ValueError("Invalid path parameter") + + try: + plugin_root = self._plugin_root_path(name) + target = self._safe_plugin_path(plugin_root, normalized) + if target is None: + raise ValueError("Invalid path parameter") + # Keep this boundary unresolved: a symlink for the field directory + # must not grant access to another field's files. + target.relative_to(plugin_root / folder) + except (OSError, RuntimeError, ValueError) as exc: + raise ValueError("Invalid path parameter") from exc + + try: + if not target.exists(): + raise ApiError("Image file not found", status_code=404) + if not target.is_file(): + raise ValueError("Invalid path parameter") + with target.open("rb") as source: + data = source.read(MAX_PREVIEW_FILE_BYTES + 1) + except FileNotFoundError as exc: + raise ApiError("Image file not found", status_code=404) from exc + except OSError as exc: + raise ApiError("Image file cannot be read", status_code=503) from exc + if len(data) > MAX_PREVIEW_FILE_BYTES: + raise ApiError("Image file is too large to preview", status_code=413) + try: + with Image.open(io.BytesIO(data)) as image: + if image.format not in { + "PNG", + "JPEG", + "WEBP", + "GIF", + "BMP", + "DIB", + "TIFF", + "ICO", + }: + raise ApiError("Unsupported image format", status_code=415) + if image.width * image.height > MAX_PREVIEW_PIXELS: + raise ApiError("Image dimensions are too large", status_code=413) + # Animated images intentionally preview only the first frame. + preview = ImageOps.exif_transpose(image) + preview.thumbnail((1024, 1024)) + preview = preview.convert( + "RGBA" + if "A" in preview.getbands() or "transparency" in preview.info + else "RGB" + ) + preview.info.clear() + output = io.BytesIO() + preview.save(output, format="PNG") + return output.getvalue() + except Image.DecompressionBombError as exc: + raise ApiError("Image dimensions are too large", status_code=413) from exc + except (OSError, SyntaxError, EOFError) as exc: + raise ApiError("Image preview is unavailable", status_code=415) from exc + @staticmethod def _allowed_file_extensions(meta: dict) -> list[str]: file_types = meta.get("file_types") diff --git a/dashboard/src/api/generated/openapi-v1/sdk.gen.ts b/dashboard/src/api/generated/openapi-v1/sdk.gen.ts index b52847650f9..fc902bb3b46 100644 --- a/dashboard/src/api/generated/openapi-v1/sdk.gen.ts +++ b/dashboard/src/api/generated/openapi-v1/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import { createClient, createConfig, type OptionsLegacyParser, formDataBodySerializer } from '@hey-api/client-axios'; -import type { LoginData, LoginError, LoginResponse, LogoutError, LogoutResponse, GetAuthSetupStatusError, GetAuthSetupStatusResponse, SetupAuthData, SetupAuthError, SetupAuthResponse, SetupTotpData, SetupTotpError, SetupTotpResponse, RecoverTotpError, RecoverTotpResponse, UpdateAuthAccountData, UpdateAuthAccountError, UpdateAuthAccountResponse, ListApiKeysError, ListApiKeysResponse, CreateApiKeyData, CreateApiKeyError, CreateApiKeyResponse, RevokeApiKeyData, RevokeApiKeyError, RevokeApiKeyResponse, DeleteApiKeyData, DeleteApiKeyError, DeleteApiKeyResponse, GetSystemConfigSchemaError, GetSystemConfigSchemaResponse, GetSystemConfigError, GetSystemConfigResponse, UpdateSystemConfigData, UpdateSystemConfigError, UpdateSystemConfigResponse, GetSystemConfigRuntimeError, GetSystemConfigRuntimeResponse, GetConfigProfileSchemaError, GetConfigProfileSchemaResponse, ListConfigProfilesError, ListConfigProfilesResponse, CreateConfigProfileData, CreateConfigProfileError, CreateConfigProfileResponse, GetConfigProfileData, GetConfigProfileError, GetConfigProfileResponse, UpdateConfigProfileContentData, UpdateConfigProfileContentError, UpdateConfigProfileContentResponse, RenameConfigProfileData, RenameConfigProfileError, RenameConfigProfileResponse, DeleteConfigProfileData, DeleteConfigProfileError, DeleteConfigProfileResponse, ListConfigRoutesError, ListConfigRoutesResponse, ReplaceConfigRoutesData, ReplaceConfigRoutesError, ReplaceConfigRoutesResponse, UpsertConfigRouteData, UpsertConfigRouteError, UpsertConfigRouteResponse, DeleteConfigRouteData, DeleteConfigRouteError, DeleteConfigRouteResponse, ListBotTypesError, ListBotTypesResponse, RegisterBotTypeData, RegisterBotTypeError, RegisterBotTypeResponse, ListBotsData, ListBotsError, ListBotsResponse, CreateBotData, CreateBotError, CreateBotResponse, ListBotStatsError, ListBotStatsResponse, GetBotByIdData, GetBotByIdError, GetBotByIdResponse, UpdateBotByIdData, UpdateBotByIdError, UpdateBotByIdResponse, DeleteBotByIdData, DeleteBotByIdError, DeleteBotByIdResponse, SetBotEnabledByIdData, SetBotEnabledByIdError, SetBotEnabledByIdResponse, TestBotByIdData, TestBotByIdError, TestBotByIdResponse, GetBotData, GetBotError, GetBotResponse, UpdateBotData, UpdateBotError, UpdateBotResponse, DeleteBotData, DeleteBotError, DeleteBotResponse, SetBotEnabledData, SetBotEnabledError, SetBotEnabledResponse, TestBotData, TestBotError, TestBotResponse, GetProviderSchemaError, GetProviderSchemaResponse, ListProviderSourcesError, ListProviderSourcesResponse, CreateProviderSourceData, CreateProviderSourceError, CreateProviderSourceResponse, GetProviderSourceByIdData, GetProviderSourceByIdError, GetProviderSourceByIdResponse, UpsertProviderSourceByIdData, UpsertProviderSourceByIdError, UpsertProviderSourceByIdResponse, DeleteProviderSourceByIdData, DeleteProviderSourceByIdError, DeleteProviderSourceByIdResponse, ListProviderSourceModelsByIdData, ListProviderSourceModelsByIdError, ListProviderSourceModelsByIdResponse, ListProvidersBySourceIdData, ListProvidersBySourceIdError, ListProvidersBySourceIdResponse, CreateProviderInSourceByIdData, CreateProviderInSourceByIdError, CreateProviderInSourceByIdResponse, GetProviderSourceData, GetProviderSourceError, GetProviderSourceResponse, UpsertProviderSourceData, UpsertProviderSourceError, UpsertProviderSourceResponse, DeleteProviderSourceData, DeleteProviderSourceError, DeleteProviderSourceResponse, ListProviderSourceModelsData, ListProviderSourceModelsError, ListProviderSourceModelsResponse, ListProvidersBySourceData, ListProvidersBySourceError, ListProvidersBySourceResponse, CreateProviderInSourceData, CreateProviderInSourceError, CreateProviderInSourceResponse, ListProvidersData, ListProvidersError, ListProvidersResponse, CreateProviderData, CreateProviderError, CreateProviderResponse, GetProviderByIdData, GetProviderByIdError, GetProviderByIdResponse, UpdateProviderByIdData, UpdateProviderByIdError, UpdateProviderByIdResponse, DeleteProviderByIdData, DeleteProviderByIdError, DeleteProviderByIdResponse, SetProviderEnabledByIdData, SetProviderEnabledByIdError, SetProviderEnabledByIdResponse, TestProviderByIdData, TestProviderByIdError, TestProviderByIdResponse, GetProviderEmbeddingDimensionByIdData, GetProviderEmbeddingDimensionByIdError, GetProviderEmbeddingDimensionByIdResponse, GetProviderData, GetProviderError, GetProviderResponse, UpdateProviderData, UpdateProviderError, UpdateProviderResponse, DeleteProviderData, DeleteProviderError, DeleteProviderResponse, SetProviderEnabledData, SetProviderEnabledError, SetProviderEnabledResponse, TestProviderData, TestProviderError, TestProviderResponse, GetProviderEmbeddingDimensionData, GetProviderEmbeddingDimensionError, GetProviderEmbeddingDimensionResponse, SendChatMessageData, SendChatMessageError, SendChatMessageResponse, OpenChatWebSocketData, OpenLiveChatWebSocketData, OpenUnifiedChatWebSocketData, ListChatSessionsData, ListChatSessionsError, ListChatSessionsResponse, CreateChatSessionData, CreateChatSessionError, CreateChatSessionResponse, BatchDeleteChatSessionsData, BatchDeleteChatSessionsError, BatchDeleteChatSessionsResponse, GetChatSessionData, GetChatSessionError, GetChatSessionResponse, UpdateChatSessionData, UpdateChatSessionError, UpdateChatSessionResponse, DeleteChatSessionData, DeleteChatSessionError, DeleteChatSessionResponse, StopChatSessionData, StopChatSessionError, StopChatSessionResponse, ResumeChatRunData, ResumeChatRunError, ResumeChatRunResponse, UpdateChatMessageData, UpdateChatMessageError, UpdateChatMessageResponse, RegenerateChatMessageData, RegenerateChatMessageError, RegenerateChatMessageResponse, ListChatConfigsError, ListChatConfigsResponse, CreateChatThreadData, CreateChatThreadError, CreateChatThreadResponse, GetChatThreadData, GetChatThreadError, GetChatThreadResponse, DeleteChatThreadData, DeleteChatThreadError, DeleteChatThreadResponse, SendChatThreadMessageData, SendChatThreadMessageError, SendChatThreadMessageResponse, ListChatProjectsError, ListChatProjectsResponse, CreateChatProjectData, CreateChatProjectError, CreateChatProjectResponse, GetChatProjectData, GetChatProjectError, GetChatProjectResponse, UpdateChatProjectData, UpdateChatProjectError, UpdateChatProjectResponse, DeleteChatProjectData, DeleteChatProjectError, DeleteChatProjectResponse, ListChatProjectSessionsData, ListChatProjectSessionsError, ListChatProjectSessionsResponse, ListChatProjectWorkspaceFilesData, ListChatProjectWorkspaceFilesError, ListChatProjectWorkspaceFilesResponse, GetChatProjectWorkspaceFileData, GetChatProjectWorkspaceFileError, GetChatProjectWorkspaceFileResponse, DownloadChatProjectWorkspaceFileData, DownloadChatProjectWorkspaceFileError, DownloadChatProjectWorkspaceFileResponse, AddChatProjectSessionData, AddChatProjectSessionError, AddChatProjectSessionResponse, RemoveChatProjectSessionData, RemoveChatProjectSessionError, RemoveChatProjectSessionResponse, SendImMessageData, SendImMessageError, SendImMessageResponse, ListImBotsError, ListImBotsResponse, UploadFileData, UploadFileError, UploadFileResponse, InitFileUploadData, InitFileUploadError, InitFileUploadResponse, UploadFileChunkData, UploadFileChunkError, UploadFileChunkResponse, CompleteFileUploadData, CompleteFileUploadError, CompleteFileUploadResponse, AbortFileUploadData, AbortFileUploadError, AbortFileUploadResponse, StatusFileUploadData, StatusFileUploadError, StatusFileUploadResponse, UploadOpenApiFileData, UploadOpenApiFileError, UploadOpenApiFileResponse, DownloadOpenApiFileData, DownloadOpenApiFileError, DownloadOpenApiFileResponse, GetFileByNameData, GetFileByNameError, GetFileByNameResponse, GetTokenFileData, GetTokenFileError, GetTokenFileResponse, GetAttachmentData, GetAttachmentError, GetAttachmentResponse, DeleteAttachmentData, DeleteAttachmentError, DeleteAttachmentResponse, DownloadAttachmentData, DownloadAttachmentError, DownloadAttachmentResponse, ListPluginsData, ListPluginsError, ListPluginsResponse, GetPluginByIdData, GetPluginByIdError, GetPluginByIdResponse, UninstallPluginByIdData, UninstallPluginByIdError, UninstallPluginByIdResponse, GetPluginConfigByIdData, GetPluginConfigByIdError, GetPluginConfigByIdResponse, UpdatePluginConfigByIdData, UpdatePluginConfigByIdError, UpdatePluginConfigByIdResponse, GetPluginConfigSchemaByIdData, GetPluginConfigSchemaByIdError, GetPluginConfigSchemaByIdResponse, ListPluginConfigFilesByIdData, ListPluginConfigFilesByIdError, ListPluginConfigFilesByIdResponse, UploadPluginConfigFilesByIdData, UploadPluginConfigFilesByIdError, UploadPluginConfigFilesByIdResponse, DeletePluginConfigFileByIdData, DeletePluginConfigFileByIdError, DeletePluginConfigFileByIdResponse, GetPluginReadmeByIdData, GetPluginReadmeByIdError, GetPluginReadmeByIdResponse, GetPluginChangelogByIdData, GetPluginChangelogByIdError, GetPluginChangelogByIdResponse, ReloadPluginByIdData, ReloadPluginByIdError, ReloadPluginByIdResponse, SetPluginEnabledByIdData, SetPluginEnabledByIdError, SetPluginEnabledByIdResponse, GetPluginData, GetPluginError, GetPluginResponse, UninstallPluginData, UninstallPluginError, UninstallPluginResponse, GetPluginConfigData, GetPluginConfigError, GetPluginConfigResponse, UpdatePluginConfigData, UpdatePluginConfigError, UpdatePluginConfigResponse, UpdatePluginLogLevelData, UpdatePluginLogLevelError, UpdatePluginLogLevelResponse, GetPluginConfigSchemaData, GetPluginConfigSchemaError, GetPluginConfigSchemaResponse, ListPluginConfigFilesData, ListPluginConfigFilesError, ListPluginConfigFilesResponse, UploadPluginConfigFilesData, UploadPluginConfigFilesError, UploadPluginConfigFilesResponse, DeletePluginConfigFileData, DeletePluginConfigFileError, DeletePluginConfigFileResponse, GetPluginReadmeData, GetPluginReadmeError, GetPluginReadmeResponse, GetPluginChangelogData, GetPluginChangelogError, GetPluginChangelogResponse, ReloadPluginData, ReloadPluginError, ReloadPluginResponse, BindPluginSourceData, BindPluginSourceError, BindPluginSourceResponse, SetPluginEnabledData, SetPluginEnabledError, SetPluginEnabledResponse, UpdatePluginData, UpdatePluginError, UpdatePluginResponse, UpdatePluginsData, UpdatePluginsError, UpdatePluginsResponse, CheckPluginVersionSupportData, CheckPluginVersionSupportError, CheckPluginVersionSupportResponse, ValidatePluginRepoData, ValidatePluginRepoError, ValidatePluginRepoResponse, ListFailedPluginsError, ListFailedPluginsResponse, UninstallFailedPluginData, UninstallFailedPluginError, UninstallFailedPluginResponse, ReloadFailedPluginData, ReloadFailedPluginError, ReloadFailedPluginResponse, InstallPluginFromGithubData, InstallPluginFromGithubError, InstallPluginFromGithubResponse, InstallPluginFromGitData, InstallPluginFromGitError, InstallPluginFromGitResponse, InstallPluginFromUrlData, InstallPluginFromUrlError, InstallPluginFromUrlResponse, InstallPluginFromUploadData, InstallPluginFromUploadError, InstallPluginFromUploadResponse, ListPluginMarketData, ListPluginMarketError, ListPluginMarketResponse, ListPluginMarketCategoriesError, ListPluginMarketCategoriesResponse, ListPluginSourcesError, ListPluginSourcesResponse, CreatePluginSourceData, CreatePluginSourceError, CreatePluginSourceResponse, ReplacePluginSourcesData, ReplacePluginSourcesError, ReplacePluginSourcesResponse, DeletePluginSourceData, DeletePluginSourceError, DeletePluginSourceResponse, DeletePluginSourceByIdData, DeletePluginSourceByIdError, DeletePluginSourceByIdResponse, ListPluginViewsByIdData, ListPluginViewsByIdError, ListPluginViewsByIdResponse, ListPluginPagesByIdData, ListPluginPagesByIdError, ListPluginPagesByIdResponse, GetPluginViewByIdData, GetPluginViewByIdError, GetPluginViewByIdResponse, GetPluginPageByIdData, GetPluginPageByIdError, GetPluginPageByIdResponse, ListPluginViewsData, ListPluginViewsError, ListPluginViewsResponse, ListPluginPagesData, ListPluginPagesError, ListPluginPagesResponse, GetPluginViewData, GetPluginViewError, GetPluginViewResponse, GetPluginPageData, GetPluginPageError, GetPluginPageResponse, GetPluginViewTokenAssetData, GetPluginViewTokenAssetError, GetPluginViewTokenAssetResponse, GetPluginPageBridgeSdkError, GetPluginPageBridgeSdkResponse, GetPluginExtensionRouteData, GetPluginExtensionRouteError, GetPluginExtensionRouteResponse, PostPluginExtensionRouteData, PostPluginExtensionRouteError, PostPluginExtensionRouteResponse, PutPluginExtensionRouteData, PutPluginExtensionRouteError, PutPluginExtensionRouteResponse, PatchPluginExtensionRouteData, PatchPluginExtensionRouteError, PatchPluginExtensionRouteResponse, DeletePluginExtensionRouteData, DeletePluginExtensionRouteError, DeletePluginExtensionRouteResponse, ListCommandsData, ListCommandsError, ListCommandsResponse, UpdateCommandData, UpdateCommandError, UpdateCommandResponse, ListCommandConflictsError, ListCommandConflictsResponse, ListToolsData, ListToolsError, ListToolsResponse, SetToolEnabledData, SetToolEnabledError, SetToolEnabledResponse, SetToolPermissionData, SetToolPermissionError, SetToolPermissionResponse, ListMcpServersError, ListMcpServersResponse, CreateMcpServerData, CreateMcpServerError, CreateMcpServerResponse, UpdateMcpServerByNameData, UpdateMcpServerByNameError, UpdateMcpServerByNameResponse, DeleteMcpServerByNameData, DeleteMcpServerByNameError, DeleteMcpServerByNameResponse, SetMcpServerEnabledByNameData, SetMcpServerEnabledByNameError, SetMcpServerEnabledByNameResponse, TestMcpServerByNameData, TestMcpServerByNameError, TestMcpServerByNameResponse, UpdateMcpServerData, UpdateMcpServerError, UpdateMcpServerResponse, DeleteMcpServerData, DeleteMcpServerError, DeleteMcpServerResponse, SetMcpServerEnabledData, SetMcpServerEnabledError, SetMcpServerEnabledResponse, TestMcpServerData, TestMcpServerError, TestMcpServerResponse, SyncModelScopeMcpServersData, SyncModelScopeMcpServersError, SyncModelScopeMcpServersResponse, ListSkillsData, ListSkillsError, ListSkillsResponse, UploadSkillData, UploadSkillError, UploadSkillResponse, UploadSkillsBatchData, UploadSkillsBatchError, UploadSkillsBatchResponse, UpdateSkillByNameData, UpdateSkillByNameError, UpdateSkillByNameResponse, DeleteSkillByNameData, DeleteSkillByNameError, DeleteSkillByNameResponse, DownloadSkillByNameData, DownloadSkillByNameError, DownloadSkillByNameResponse, ListSkillFilesByNameData, ListSkillFilesByNameError, ListSkillFilesByNameResponse, GetSkillFileByNameData, GetSkillFileByNameError, GetSkillFileByNameResponse, UpdateSkillFileByNameData, UpdateSkillFileByNameError, UpdateSkillFileByNameResponse, UpdateSkillData, UpdateSkillError, UpdateSkillResponse, DeleteSkillData, DeleteSkillError, DeleteSkillResponse, DownloadSkillData, DownloadSkillError, DownloadSkillResponse, ListSkillFilesData, ListSkillFilesError, ListSkillFilesResponse, GetSkillFileData, GetSkillFileError, GetSkillFileResponse, UpdateSkillFileData, UpdateSkillFileError, UpdateSkillFileResponse, ListNeoSkillCandidatesData, ListNeoSkillCandidatesError, ListNeoSkillCandidatesResponse, ListNeoSkillReleasesData, ListNeoSkillReleasesError, ListNeoSkillReleasesResponse, GetNeoSkillPayloadData, GetNeoSkillPayloadError, GetNeoSkillPayloadResponse, EvaluateNeoSkillCandidateData, EvaluateNeoSkillCandidateError, EvaluateNeoSkillCandidateResponse, PromoteNeoSkillCandidateData, PromoteNeoSkillCandidateError, PromoteNeoSkillCandidateResponse, RollbackNeoSkillReleaseData, RollbackNeoSkillReleaseError, RollbackNeoSkillReleaseResponse, SyncNeoSkillReleaseData, SyncNeoSkillReleaseError, SyncNeoSkillReleaseResponse, DeleteNeoSkillCandidateData, DeleteNeoSkillCandidateError, DeleteNeoSkillCandidateResponse, DeleteNeoSkillReleaseData, DeleteNeoSkillReleaseError, DeleteNeoSkillReleaseResponse, ListKnowledgeBasesData, ListKnowledgeBasesError, ListKnowledgeBasesResponse, CreateKnowledgeBaseData, CreateKnowledgeBaseError, CreateKnowledgeBaseResponse, GetKnowledgeBaseData, GetKnowledgeBaseError, GetKnowledgeBaseResponse, UpdateKnowledgeBaseData, UpdateKnowledgeBaseError, UpdateKnowledgeBaseResponse, DeleteKnowledgeBaseData, DeleteKnowledgeBaseError, DeleteKnowledgeBaseResponse, GetKnowledgeBaseStatsData, GetKnowledgeBaseStatsError, GetKnowledgeBaseStatsResponse, ListKnowledgeDocumentsData, ListKnowledgeDocumentsError, ListKnowledgeDocumentsResponse, UploadKnowledgeDocumentData, UploadKnowledgeDocumentError, UploadKnowledgeDocumentResponse, ImportKnowledgeDocumentsData, ImportKnowledgeDocumentsError, ImportKnowledgeDocumentsResponse, ImportKnowledgeDocumentFromUrlData, ImportKnowledgeDocumentFromUrlError, ImportKnowledgeDocumentFromUrlResponse, GetKnowledgeDocumentData, GetKnowledgeDocumentError, GetKnowledgeDocumentResponse, DeleteKnowledgeDocumentData, DeleteKnowledgeDocumentError, DeleteKnowledgeDocumentResponse, ListKnowledgeChunksData, ListKnowledgeChunksError, ListKnowledgeChunksResponse, DeleteKnowledgeChunkData, DeleteKnowledgeChunkError, DeleteKnowledgeChunkResponse, RetrieveKnowledgeBaseData, RetrieveKnowledgeBaseError, RetrieveKnowledgeBaseResponse, GetKnowledgeTaskData, GetKnowledgeTaskError, GetKnowledgeTaskResponse, GetPersonaTreeError, GetPersonaTreeResponse, ListPersonasData, ListPersonasError, ListPersonasResponse, CreatePersonaData, CreatePersonaError, CreatePersonaResponse, GetPersonaByIdData, GetPersonaByIdError, GetPersonaByIdResponse, UpdatePersonaByIdData, UpdatePersonaByIdError, UpdatePersonaByIdResponse, DeletePersonaByIdData, DeletePersonaByIdError, DeletePersonaByIdResponse, GetPersonaData, GetPersonaError, GetPersonaResponse, UpdatePersonaData, UpdatePersonaError, UpdatePersonaResponse, DeletePersonaData, DeletePersonaError, DeletePersonaResponse, ListPersonaFoldersData, ListPersonaFoldersError, ListPersonaFoldersResponse, CreatePersonaFolderData, CreatePersonaFolderError, CreatePersonaFolderResponse, UpdatePersonaFolderData, UpdatePersonaFolderError, UpdatePersonaFolderResponse, DeletePersonaFolderData, DeletePersonaFolderError, DeletePersonaFolderResponse, MovePersonaItemData, MovePersonaItemError, MovePersonaItemResponse, ReorderPersonaItemsData, ReorderPersonaItemsError, ReorderPersonaItemsResponse, ListSessionsData, ListSessionsError, ListSessionsResponse, ListActiveUmosError, ListActiveUmosResponse, ListSessionRulesData, ListSessionRulesError, ListSessionRulesResponse, UpsertSessionRuleData, UpsertSessionRuleError, UpsertSessionRuleResponse, DeleteSessionRulesData, DeleteSessionRulesError, DeleteSessionRulesResponse, BatchUpdateSessionProviderData, BatchUpdateSessionProviderError, BatchUpdateSessionProviderResponse, BatchUpdateSessionServiceData, BatchUpdateSessionServiceError, BatchUpdateSessionServiceResponse, ListSessionGroupsError, ListSessionGroupsResponse, CreateSessionGroupData, CreateSessionGroupError, CreateSessionGroupResponse, UpdateSessionGroupData, UpdateSessionGroupError, UpdateSessionGroupResponse, DeleteSessionGroupData, DeleteSessionGroupError, DeleteSessionGroupResponse, ListConversationsData, ListConversationsError, ListConversationsResponse, GetConversationFilterOptionsError, GetConversationFilterOptionsResponse, BatchDeleteConversationsData, BatchDeleteConversationsError, BatchDeleteConversationsResponse, GetConversationData, GetConversationError, GetConversationResponse, UpdateConversationData, UpdateConversationError, UpdateConversationResponse, DeleteConversationData, DeleteConversationError, DeleteConversationResponse, ReplaceConversationMessagesData, ReplaceConversationMessagesError, ReplaceConversationMessagesResponse, ExportConversationsData, ExportConversationsError, ExportConversationsResponse, GetStatsData, GetStatsError, GetStatsResponse, GetProviderTokenStatsData, GetProviderTokenStatsError, GetProviderTokenStatsResponse, GetVersionError, GetVersionResponse, GetPublicVersionsError, GetPublicVersionsResponse, GetFirstNoticeData, GetFirstNoticeError, GetFirstNoticeResponse, TestGhproxyConnectionData, TestGhproxyConnectionError, TestGhproxyConnectionResponse, ListChangelogVersionsError, ListChangelogVersionsResponse, GetChangelogData, GetChangelogError, GetChangelogResponse, GetStartTimeError, GetStartTimeResponse, GetStorageStatusError, GetStorageStatusResponse, CleanupStorageData, CleanupStorageError, CleanupStorageResponse, RestartCoreError, RestartCoreResponse, ListBackupsData, ListBackupsError, ListBackupsResponse, CreateBackupData, CreateBackupError, CreateBackupResponse, UploadBackupData, UploadBackupError, UploadBackupResponse, InitBackupUploadData, InitBackupUploadError, InitBackupUploadResponse, UploadBackupChunkData, UploadBackupChunkError, UploadBackupChunkResponse, CompleteBackupUploadData, CompleteBackupUploadError, CompleteBackupUploadResponse, AbortBackupUploadData, AbortBackupUploadError, AbortBackupUploadResponse, StatusBackupUploadData, StatusBackupUploadError, StatusBackupUploadResponse, GetBackupProgressData, GetBackupProgressError, GetBackupProgressResponse, DownloadBackupData, DownloadBackupError, DownloadBackupResponse, RenameBackupData, RenameBackupError, RenameBackupResponse, DeleteBackupData, DeleteBackupError, DeleteBackupResponse, CheckBackupData, CheckBackupError, CheckBackupResponse, ImportBackupData, ImportBackupError, ImportBackupResponse, CheckUpdateError, CheckUpdateResponse, ListReleasesData, ListReleasesError, ListReleasesResponse, UpdateCoreData, UpdateCoreError, UpdateCoreResponse, UpdateDashboardData, UpdateDashboardError, UpdateDashboardResponse, GetUpdateProgressData, GetUpdateProgressError, GetUpdateProgressResponse, InstallPipPackageData, InstallPipPackageError, InstallPipPackageResponse, ListCronJobsData, ListCronJobsError, ListCronJobsResponse, CreateCronJobData, CreateCronJobError, CreateCronJobResponse, UpdateCronJobData, UpdateCronJobError, UpdateCronJobResponse, DeleteCronJobData, DeleteCronJobError, DeleteCronJobResponse, RunCronJobData, RunCronJobError, RunCronJobResponse, StreamLiveLogsError, StreamLiveLogsResponse, GetLogHistoryError, GetLogHistoryResponse, ExportLogsData, ExportLogsError, ExportLogsResponse, ListTracesData, ListTracesError, ListTracesResponse, GetTraceDetailData, GetTraceDetailError, GetTraceDetailResponse, GetTraceSettingsError, GetTraceSettingsResponse, UpdateTraceSettingsData, UpdateTraceSettingsError, UpdateTraceSettingsResponse, ListT2iTemplatesError, ListT2iTemplatesResponse, CreateT2iTemplateData, CreateT2iTemplateError, CreateT2iTemplateResponse, GetActiveT2iTemplateError, GetActiveT2iTemplateResponse, SetActiveT2iTemplateData, SetActiveT2iTemplateError, SetActiveT2iTemplateResponse, ResetDefaultT2iTemplateError, ResetDefaultT2iTemplateResponse, GetT2iTemplateData, GetT2iTemplateError, GetT2iTemplateResponse, UpdateT2iTemplateData, UpdateT2iTemplateError, UpdateT2iTemplateResponse, DeleteT2iTemplateData, DeleteT2iTemplateError, DeleteT2iTemplateResponse, GetSubagentConfigError, GetSubagentConfigResponse, UpdateSubagentConfigData, UpdateSubagentConfigError, UpdateSubagentConfigResponse, ListSubagentAvailableToolsError, ListSubagentAvailableToolsResponse, VerifyPlatformWebhookData, VerifyPlatformWebhookError, VerifyPlatformWebhookResponse, ReceivePlatformWebhookData, ReceivePlatformWebhookError, ReceivePlatformWebhookResponse } from './types.gen'; +import type { LoginData, LoginError, LoginResponse, LogoutError, LogoutResponse, GetAuthSetupStatusError, GetAuthSetupStatusResponse, SetupAuthData, SetupAuthError, SetupAuthResponse, SetupTotpData, SetupTotpError, SetupTotpResponse, RecoverTotpError, RecoverTotpResponse, UpdateAuthAccountData, UpdateAuthAccountError, UpdateAuthAccountResponse, ListApiKeysError, ListApiKeysResponse, CreateApiKeyData, CreateApiKeyError, CreateApiKeyResponse, RevokeApiKeyData, RevokeApiKeyError, RevokeApiKeyResponse, DeleteApiKeyData, DeleteApiKeyError, DeleteApiKeyResponse, GetSystemConfigSchemaError, GetSystemConfigSchemaResponse, GetSystemConfigError, GetSystemConfigResponse, UpdateSystemConfigData, UpdateSystemConfigError, UpdateSystemConfigResponse, GetSystemConfigRuntimeError, GetSystemConfigRuntimeResponse, GetConfigProfileSchemaError, GetConfigProfileSchemaResponse, ListConfigProfilesError, ListConfigProfilesResponse, CreateConfigProfileData, CreateConfigProfileError, CreateConfigProfileResponse, GetConfigProfileData, GetConfigProfileError, GetConfigProfileResponse, UpdateConfigProfileContentData, UpdateConfigProfileContentError, UpdateConfigProfileContentResponse, RenameConfigProfileData, RenameConfigProfileError, RenameConfigProfileResponse, DeleteConfigProfileData, DeleteConfigProfileError, DeleteConfigProfileResponse, ListConfigRoutesError, ListConfigRoutesResponse, ReplaceConfigRoutesData, ReplaceConfigRoutesError, ReplaceConfigRoutesResponse, UpsertConfigRouteData, UpsertConfigRouteError, UpsertConfigRouteResponse, DeleteConfigRouteData, DeleteConfigRouteError, DeleteConfigRouteResponse, ListBotTypesError, ListBotTypesResponse, RegisterBotTypeData, RegisterBotTypeError, RegisterBotTypeResponse, ListBotsData, ListBotsError, ListBotsResponse, CreateBotData, CreateBotError, CreateBotResponse, ListBotStatsError, ListBotStatsResponse, GetBotByIdData, GetBotByIdError, GetBotByIdResponse, UpdateBotByIdData, UpdateBotByIdError, UpdateBotByIdResponse, DeleteBotByIdData, DeleteBotByIdError, DeleteBotByIdResponse, SetBotEnabledByIdData, SetBotEnabledByIdError, SetBotEnabledByIdResponse, TestBotByIdData, TestBotByIdError, TestBotByIdResponse, GetBotData, GetBotError, GetBotResponse, UpdateBotData, UpdateBotError, UpdateBotResponse, DeleteBotData, DeleteBotError, DeleteBotResponse, SetBotEnabledData, SetBotEnabledError, SetBotEnabledResponse, TestBotData, TestBotError, TestBotResponse, GetProviderSchemaError, GetProviderSchemaResponse, ListProviderSourcesError, ListProviderSourcesResponse, CreateProviderSourceData, CreateProviderSourceError, CreateProviderSourceResponse, GetProviderSourceByIdData, GetProviderSourceByIdError, GetProviderSourceByIdResponse, UpsertProviderSourceByIdData, UpsertProviderSourceByIdError, UpsertProviderSourceByIdResponse, DeleteProviderSourceByIdData, DeleteProviderSourceByIdError, DeleteProviderSourceByIdResponse, ListProviderSourceModelsByIdData, ListProviderSourceModelsByIdError, ListProviderSourceModelsByIdResponse, ListProvidersBySourceIdData, ListProvidersBySourceIdError, ListProvidersBySourceIdResponse, CreateProviderInSourceByIdData, CreateProviderInSourceByIdError, CreateProviderInSourceByIdResponse, GetProviderSourceData, GetProviderSourceError, GetProviderSourceResponse, UpsertProviderSourceData, UpsertProviderSourceError, UpsertProviderSourceResponse, DeleteProviderSourceData, DeleteProviderSourceError, DeleteProviderSourceResponse, ListProviderSourceModelsData, ListProviderSourceModelsError, ListProviderSourceModelsResponse, ListProvidersBySourceData, ListProvidersBySourceError, ListProvidersBySourceResponse, CreateProviderInSourceData, CreateProviderInSourceError, CreateProviderInSourceResponse, ListProvidersData, ListProvidersError, ListProvidersResponse, CreateProviderData, CreateProviderError, CreateProviderResponse, GetProviderByIdData, GetProviderByIdError, GetProviderByIdResponse, UpdateProviderByIdData, UpdateProviderByIdError, UpdateProviderByIdResponse, DeleteProviderByIdData, DeleteProviderByIdError, DeleteProviderByIdResponse, SetProviderEnabledByIdData, SetProviderEnabledByIdError, SetProviderEnabledByIdResponse, TestProviderByIdData, TestProviderByIdError, TestProviderByIdResponse, GetProviderEmbeddingDimensionByIdData, GetProviderEmbeddingDimensionByIdError, GetProviderEmbeddingDimensionByIdResponse, GetProviderData, GetProviderError, GetProviderResponse, UpdateProviderData, UpdateProviderError, UpdateProviderResponse, DeleteProviderData, DeleteProviderError, DeleteProviderResponse, SetProviderEnabledData, SetProviderEnabledError, SetProviderEnabledResponse, TestProviderData, TestProviderError, TestProviderResponse, GetProviderEmbeddingDimensionData, GetProviderEmbeddingDimensionError, GetProviderEmbeddingDimensionResponse, SendChatMessageData, SendChatMessageError, SendChatMessageResponse, OpenChatWebSocketData, OpenLiveChatWebSocketData, OpenUnifiedChatWebSocketData, ListChatSessionsData, ListChatSessionsError, ListChatSessionsResponse, CreateChatSessionData, CreateChatSessionError, CreateChatSessionResponse, BatchDeleteChatSessionsData, BatchDeleteChatSessionsError, BatchDeleteChatSessionsResponse, GetChatSessionData, GetChatSessionError, GetChatSessionResponse, UpdateChatSessionData, UpdateChatSessionError, UpdateChatSessionResponse, DeleteChatSessionData, DeleteChatSessionError, DeleteChatSessionResponse, StopChatSessionData, StopChatSessionError, StopChatSessionResponse, ResumeChatRunData, ResumeChatRunError, ResumeChatRunResponse, UpdateChatMessageData, UpdateChatMessageError, UpdateChatMessageResponse, RegenerateChatMessageData, RegenerateChatMessageError, RegenerateChatMessageResponse, ListChatConfigsError, ListChatConfigsResponse, CreateChatThreadData, CreateChatThreadError, CreateChatThreadResponse, GetChatThreadData, GetChatThreadError, GetChatThreadResponse, DeleteChatThreadData, DeleteChatThreadError, DeleteChatThreadResponse, SendChatThreadMessageData, SendChatThreadMessageError, SendChatThreadMessageResponse, ListChatProjectsError, ListChatProjectsResponse, CreateChatProjectData, CreateChatProjectError, CreateChatProjectResponse, GetChatProjectData, GetChatProjectError, GetChatProjectResponse, UpdateChatProjectData, UpdateChatProjectError, UpdateChatProjectResponse, DeleteChatProjectData, DeleteChatProjectError, DeleteChatProjectResponse, ListChatProjectSessionsData, ListChatProjectSessionsError, ListChatProjectSessionsResponse, ListChatProjectWorkspaceFilesData, ListChatProjectWorkspaceFilesError, ListChatProjectWorkspaceFilesResponse, GetChatProjectWorkspaceFileData, GetChatProjectWorkspaceFileError, GetChatProjectWorkspaceFileResponse, DownloadChatProjectWorkspaceFileData, DownloadChatProjectWorkspaceFileError, DownloadChatProjectWorkspaceFileResponse, AddChatProjectSessionData, AddChatProjectSessionError, AddChatProjectSessionResponse, RemoveChatProjectSessionData, RemoveChatProjectSessionError, RemoveChatProjectSessionResponse, SendImMessageData, SendImMessageError, SendImMessageResponse, ListImBotsError, ListImBotsResponse, UploadFileData, UploadFileError, UploadFileResponse, InitFileUploadData, InitFileUploadError, InitFileUploadResponse, UploadFileChunkData, UploadFileChunkError, UploadFileChunkResponse, CompleteFileUploadData, CompleteFileUploadError, CompleteFileUploadResponse, AbortFileUploadData, AbortFileUploadError, AbortFileUploadResponse, StatusFileUploadData, StatusFileUploadError, StatusFileUploadResponse, UploadOpenApiFileData, UploadOpenApiFileError, UploadOpenApiFileResponse, DownloadOpenApiFileData, DownloadOpenApiFileError, DownloadOpenApiFileResponse, GetFileByNameData, GetFileByNameError, GetFileByNameResponse, GetTokenFileData, GetTokenFileError, GetTokenFileResponse, GetAttachmentData, GetAttachmentError, GetAttachmentResponse, DeleteAttachmentData, DeleteAttachmentError, DeleteAttachmentResponse, DownloadAttachmentData, DownloadAttachmentError, DownloadAttachmentResponse, ListPluginsData, ListPluginsError, ListPluginsResponse, GetPluginByIdData, GetPluginByIdError, GetPluginByIdResponse, UninstallPluginByIdData, UninstallPluginByIdError, UninstallPluginByIdResponse, GetPluginConfigByIdData, GetPluginConfigByIdError, GetPluginConfigByIdResponse, UpdatePluginConfigByIdData, UpdatePluginConfigByIdError, UpdatePluginConfigByIdResponse, GetPluginConfigSchemaByIdData, GetPluginConfigSchemaByIdError, GetPluginConfigSchemaByIdResponse, ListPluginConfigFilesByIdData, ListPluginConfigFilesByIdError, ListPluginConfigFilesByIdResponse, UploadPluginConfigFilesByIdData, UploadPluginConfigFilesByIdError, UploadPluginConfigFilesByIdResponse, DeletePluginConfigFileByIdData, DeletePluginConfigFileByIdError, DeletePluginConfigFileByIdResponse, PreviewPluginConfigImageByIdData, PreviewPluginConfigImageByIdError, PreviewPluginConfigImageByIdResponse, GetPluginReadmeByIdData, GetPluginReadmeByIdError, GetPluginReadmeByIdResponse, GetPluginChangelogByIdData, GetPluginChangelogByIdError, GetPluginChangelogByIdResponse, ReloadPluginByIdData, ReloadPluginByIdError, ReloadPluginByIdResponse, SetPluginEnabledByIdData, SetPluginEnabledByIdError, SetPluginEnabledByIdResponse, GetPluginData, GetPluginError, GetPluginResponse, UninstallPluginData, UninstallPluginError, UninstallPluginResponse, GetPluginConfigData, GetPluginConfigError, GetPluginConfigResponse, UpdatePluginConfigData, UpdatePluginConfigError, UpdatePluginConfigResponse, UpdatePluginLogLevelData, UpdatePluginLogLevelError, UpdatePluginLogLevelResponse, GetPluginConfigSchemaData, GetPluginConfigSchemaError, GetPluginConfigSchemaResponse, ListPluginConfigFilesData, ListPluginConfigFilesError, ListPluginConfigFilesResponse, UploadPluginConfigFilesData, UploadPluginConfigFilesError, UploadPluginConfigFilesResponse, DeletePluginConfigFileData, DeletePluginConfigFileError, DeletePluginConfigFileResponse, GetPluginReadmeData, GetPluginReadmeError, GetPluginReadmeResponse, GetPluginChangelogData, GetPluginChangelogError, GetPluginChangelogResponse, ReloadPluginData, ReloadPluginError, ReloadPluginResponse, BindPluginSourceData, BindPluginSourceError, BindPluginSourceResponse, SetPluginEnabledData, SetPluginEnabledError, SetPluginEnabledResponse, UpdatePluginData, UpdatePluginError, UpdatePluginResponse, UpdatePluginsData, UpdatePluginsError, UpdatePluginsResponse, CheckPluginVersionSupportData, CheckPluginVersionSupportError, CheckPluginVersionSupportResponse, ValidatePluginRepoData, ValidatePluginRepoError, ValidatePluginRepoResponse, ListFailedPluginsError, ListFailedPluginsResponse, UninstallFailedPluginData, UninstallFailedPluginError, UninstallFailedPluginResponse, ReloadFailedPluginData, ReloadFailedPluginError, ReloadFailedPluginResponse, InstallPluginFromGithubData, InstallPluginFromGithubError, InstallPluginFromGithubResponse, InstallPluginFromGitData, InstallPluginFromGitError, InstallPluginFromGitResponse, InstallPluginFromUrlData, InstallPluginFromUrlError, InstallPluginFromUrlResponse, InstallPluginFromUploadData, InstallPluginFromUploadError, InstallPluginFromUploadResponse, ListPluginMarketData, ListPluginMarketError, ListPluginMarketResponse, ListPluginMarketCategoriesError, ListPluginMarketCategoriesResponse, ListPluginSourcesError, ListPluginSourcesResponse, CreatePluginSourceData, CreatePluginSourceError, CreatePluginSourceResponse, ReplacePluginSourcesData, ReplacePluginSourcesError, ReplacePluginSourcesResponse, DeletePluginSourceData, DeletePluginSourceError, DeletePluginSourceResponse, DeletePluginSourceByIdData, DeletePluginSourceByIdError, DeletePluginSourceByIdResponse, ListPluginViewsByIdData, ListPluginViewsByIdError, ListPluginViewsByIdResponse, ListPluginPagesByIdData, ListPluginPagesByIdError, ListPluginPagesByIdResponse, GetPluginViewByIdData, GetPluginViewByIdError, GetPluginViewByIdResponse, GetPluginPageByIdData, GetPluginPageByIdError, GetPluginPageByIdResponse, ListPluginViewsData, ListPluginViewsError, ListPluginViewsResponse, ListPluginPagesData, ListPluginPagesError, ListPluginPagesResponse, GetPluginViewData, GetPluginViewError, GetPluginViewResponse, GetPluginPageData, GetPluginPageError, GetPluginPageResponse, GetPluginViewTokenAssetData, GetPluginViewTokenAssetError, GetPluginViewTokenAssetResponse, GetPluginPageBridgeSdkError, GetPluginPageBridgeSdkResponse, GetPluginExtensionRouteData, GetPluginExtensionRouteError, GetPluginExtensionRouteResponse, PostPluginExtensionRouteData, PostPluginExtensionRouteError, PostPluginExtensionRouteResponse, PutPluginExtensionRouteData, PutPluginExtensionRouteError, PutPluginExtensionRouteResponse, PatchPluginExtensionRouteData, PatchPluginExtensionRouteError, PatchPluginExtensionRouteResponse, DeletePluginExtensionRouteData, DeletePluginExtensionRouteError, DeletePluginExtensionRouteResponse, ListCommandsData, ListCommandsError, ListCommandsResponse, UpdateCommandData, UpdateCommandError, UpdateCommandResponse, ListCommandConflictsError, ListCommandConflictsResponse, ListToolsData, ListToolsError, ListToolsResponse, SetToolEnabledData, SetToolEnabledError, SetToolEnabledResponse, SetToolPermissionData, SetToolPermissionError, SetToolPermissionResponse, ListMcpServersError, ListMcpServersResponse, CreateMcpServerData, CreateMcpServerError, CreateMcpServerResponse, UpdateMcpServerByNameData, UpdateMcpServerByNameError, UpdateMcpServerByNameResponse, DeleteMcpServerByNameData, DeleteMcpServerByNameError, DeleteMcpServerByNameResponse, SetMcpServerEnabledByNameData, SetMcpServerEnabledByNameError, SetMcpServerEnabledByNameResponse, TestMcpServerByNameData, TestMcpServerByNameError, TestMcpServerByNameResponse, UpdateMcpServerData, UpdateMcpServerError, UpdateMcpServerResponse, DeleteMcpServerData, DeleteMcpServerError, DeleteMcpServerResponse, SetMcpServerEnabledData, SetMcpServerEnabledError, SetMcpServerEnabledResponse, TestMcpServerData, TestMcpServerError, TestMcpServerResponse, SyncModelScopeMcpServersData, SyncModelScopeMcpServersError, SyncModelScopeMcpServersResponse, ListSkillsData, ListSkillsError, ListSkillsResponse, UploadSkillData, UploadSkillError, UploadSkillResponse, UploadSkillsBatchData, UploadSkillsBatchError, UploadSkillsBatchResponse, UpdateSkillByNameData, UpdateSkillByNameError, UpdateSkillByNameResponse, DeleteSkillByNameData, DeleteSkillByNameError, DeleteSkillByNameResponse, DownloadSkillByNameData, DownloadSkillByNameError, DownloadSkillByNameResponse, ListSkillFilesByNameData, ListSkillFilesByNameError, ListSkillFilesByNameResponse, GetSkillFileByNameData, GetSkillFileByNameError, GetSkillFileByNameResponse, UpdateSkillFileByNameData, UpdateSkillFileByNameError, UpdateSkillFileByNameResponse, UpdateSkillData, UpdateSkillError, UpdateSkillResponse, DeleteSkillData, DeleteSkillError, DeleteSkillResponse, DownloadSkillData, DownloadSkillError, DownloadSkillResponse, ListSkillFilesData, ListSkillFilesError, ListSkillFilesResponse, GetSkillFileData, GetSkillFileError, GetSkillFileResponse, UpdateSkillFileData, UpdateSkillFileError, UpdateSkillFileResponse, ListNeoSkillCandidatesData, ListNeoSkillCandidatesError, ListNeoSkillCandidatesResponse, ListNeoSkillReleasesData, ListNeoSkillReleasesError, ListNeoSkillReleasesResponse, GetNeoSkillPayloadData, GetNeoSkillPayloadError, GetNeoSkillPayloadResponse, EvaluateNeoSkillCandidateData, EvaluateNeoSkillCandidateError, EvaluateNeoSkillCandidateResponse, PromoteNeoSkillCandidateData, PromoteNeoSkillCandidateError, PromoteNeoSkillCandidateResponse, RollbackNeoSkillReleaseData, RollbackNeoSkillReleaseError, RollbackNeoSkillReleaseResponse, SyncNeoSkillReleaseData, SyncNeoSkillReleaseError, SyncNeoSkillReleaseResponse, DeleteNeoSkillCandidateData, DeleteNeoSkillCandidateError, DeleteNeoSkillCandidateResponse, DeleteNeoSkillReleaseData, DeleteNeoSkillReleaseError, DeleteNeoSkillReleaseResponse, ListKnowledgeBasesData, ListKnowledgeBasesError, ListKnowledgeBasesResponse, CreateKnowledgeBaseData, CreateKnowledgeBaseError, CreateKnowledgeBaseResponse, GetKnowledgeBaseData, GetKnowledgeBaseError, GetKnowledgeBaseResponse, UpdateKnowledgeBaseData, UpdateKnowledgeBaseError, UpdateKnowledgeBaseResponse, DeleteKnowledgeBaseData, DeleteKnowledgeBaseError, DeleteKnowledgeBaseResponse, GetKnowledgeBaseStatsData, GetKnowledgeBaseStatsError, GetKnowledgeBaseStatsResponse, ListKnowledgeDocumentsData, ListKnowledgeDocumentsError, ListKnowledgeDocumentsResponse, UploadKnowledgeDocumentData, UploadKnowledgeDocumentError, UploadKnowledgeDocumentResponse, ImportKnowledgeDocumentsData, ImportKnowledgeDocumentsError, ImportKnowledgeDocumentsResponse, ImportKnowledgeDocumentFromUrlData, ImportKnowledgeDocumentFromUrlError, ImportKnowledgeDocumentFromUrlResponse, GetKnowledgeDocumentData, GetKnowledgeDocumentError, GetKnowledgeDocumentResponse, DeleteKnowledgeDocumentData, DeleteKnowledgeDocumentError, DeleteKnowledgeDocumentResponse, ListKnowledgeChunksData, ListKnowledgeChunksError, ListKnowledgeChunksResponse, DeleteKnowledgeChunkData, DeleteKnowledgeChunkError, DeleteKnowledgeChunkResponse, RetrieveKnowledgeBaseData, RetrieveKnowledgeBaseError, RetrieveKnowledgeBaseResponse, GetKnowledgeTaskData, GetKnowledgeTaskError, GetKnowledgeTaskResponse, GetPersonaTreeError, GetPersonaTreeResponse, ListPersonasData, ListPersonasError, ListPersonasResponse, CreatePersonaData, CreatePersonaError, CreatePersonaResponse, GetPersonaByIdData, GetPersonaByIdError, GetPersonaByIdResponse, UpdatePersonaByIdData, UpdatePersonaByIdError, UpdatePersonaByIdResponse, DeletePersonaByIdData, DeletePersonaByIdError, DeletePersonaByIdResponse, GetPersonaData, GetPersonaError, GetPersonaResponse, UpdatePersonaData, UpdatePersonaError, UpdatePersonaResponse, DeletePersonaData, DeletePersonaError, DeletePersonaResponse, ListPersonaFoldersData, ListPersonaFoldersError, ListPersonaFoldersResponse, CreatePersonaFolderData, CreatePersonaFolderError, CreatePersonaFolderResponse, UpdatePersonaFolderData, UpdatePersonaFolderError, UpdatePersonaFolderResponse, DeletePersonaFolderData, DeletePersonaFolderError, DeletePersonaFolderResponse, MovePersonaItemData, MovePersonaItemError, MovePersonaItemResponse, ReorderPersonaItemsData, ReorderPersonaItemsError, ReorderPersonaItemsResponse, ListSessionsData, ListSessionsError, ListSessionsResponse, ListActiveUmosError, ListActiveUmosResponse, ListSessionRulesData, ListSessionRulesError, ListSessionRulesResponse, UpsertSessionRuleData, UpsertSessionRuleError, UpsertSessionRuleResponse, DeleteSessionRulesData, DeleteSessionRulesError, DeleteSessionRulesResponse, BatchUpdateSessionProviderData, BatchUpdateSessionProviderError, BatchUpdateSessionProviderResponse, BatchUpdateSessionServiceData, BatchUpdateSessionServiceError, BatchUpdateSessionServiceResponse, ListSessionGroupsError, ListSessionGroupsResponse, CreateSessionGroupData, CreateSessionGroupError, CreateSessionGroupResponse, UpdateSessionGroupData, UpdateSessionGroupError, UpdateSessionGroupResponse, DeleteSessionGroupData, DeleteSessionGroupError, DeleteSessionGroupResponse, ListConversationsData, ListConversationsError, ListConversationsResponse, GetConversationFilterOptionsError, GetConversationFilterOptionsResponse, BatchDeleteConversationsData, BatchDeleteConversationsError, BatchDeleteConversationsResponse, GetConversationData, GetConversationError, GetConversationResponse, UpdateConversationData, UpdateConversationError, UpdateConversationResponse, DeleteConversationData, DeleteConversationError, DeleteConversationResponse, ReplaceConversationMessagesData, ReplaceConversationMessagesError, ReplaceConversationMessagesResponse, ExportConversationsData, ExportConversationsError, ExportConversationsResponse, GetStatsData, GetStatsError, GetStatsResponse, GetProviderTokenStatsData, GetProviderTokenStatsError, GetProviderTokenStatsResponse, GetVersionError, GetVersionResponse, GetPublicVersionsError, GetPublicVersionsResponse, GetFirstNoticeData, GetFirstNoticeError, GetFirstNoticeResponse, TestGhproxyConnectionData, TestGhproxyConnectionError, TestGhproxyConnectionResponse, ListChangelogVersionsError, ListChangelogVersionsResponse, GetChangelogData, GetChangelogError, GetChangelogResponse, GetStartTimeError, GetStartTimeResponse, GetStorageStatusError, GetStorageStatusResponse, CleanupStorageData, CleanupStorageError, CleanupStorageResponse, RestartCoreError, RestartCoreResponse, ListBackupsData, ListBackupsError, ListBackupsResponse, CreateBackupData, CreateBackupError, CreateBackupResponse, UploadBackupData, UploadBackupError, UploadBackupResponse, InitBackupUploadData, InitBackupUploadError, InitBackupUploadResponse, UploadBackupChunkData, UploadBackupChunkError, UploadBackupChunkResponse, CompleteBackupUploadData, CompleteBackupUploadError, CompleteBackupUploadResponse, AbortBackupUploadData, AbortBackupUploadError, AbortBackupUploadResponse, StatusBackupUploadData, StatusBackupUploadError, StatusBackupUploadResponse, GetBackupProgressData, GetBackupProgressError, GetBackupProgressResponse, DownloadBackupData, DownloadBackupError, DownloadBackupResponse, RenameBackupData, RenameBackupError, RenameBackupResponse, DeleteBackupData, DeleteBackupError, DeleteBackupResponse, CheckBackupData, CheckBackupError, CheckBackupResponse, ImportBackupData, ImportBackupError, ImportBackupResponse, CheckUpdateError, CheckUpdateResponse, ListReleasesData, ListReleasesError, ListReleasesResponse, UpdateCoreData, UpdateCoreError, UpdateCoreResponse, UpdateDashboardData, UpdateDashboardError, UpdateDashboardResponse, GetUpdateProgressData, GetUpdateProgressError, GetUpdateProgressResponse, InstallPipPackageData, InstallPipPackageError, InstallPipPackageResponse, ListCronJobsData, ListCronJobsError, ListCronJobsResponse, CreateCronJobData, CreateCronJobError, CreateCronJobResponse, UpdateCronJobData, UpdateCronJobError, UpdateCronJobResponse, DeleteCronJobData, DeleteCronJobError, DeleteCronJobResponse, RunCronJobData, RunCronJobError, RunCronJobResponse, StreamLiveLogsError, StreamLiveLogsResponse, GetLogHistoryError, GetLogHistoryResponse, ExportLogsData, ExportLogsError, ExportLogsResponse, ListTracesData, ListTracesError, ListTracesResponse, GetTraceDetailData, GetTraceDetailError, GetTraceDetailResponse, GetTraceSettingsError, GetTraceSettingsResponse, UpdateTraceSettingsData, UpdateTraceSettingsError, UpdateTraceSettingsResponse, ListT2iTemplatesError, ListT2iTemplatesResponse, CreateT2iTemplateData, CreateT2iTemplateError, CreateT2iTemplateResponse, GetActiveT2iTemplateError, GetActiveT2iTemplateResponse, SetActiveT2iTemplateData, SetActiveT2iTemplateError, SetActiveT2iTemplateResponse, ResetDefaultT2iTemplateError, ResetDefaultT2iTemplateResponse, GetT2iTemplateData, GetT2iTemplateError, GetT2iTemplateResponse, UpdateT2iTemplateData, UpdateT2iTemplateError, UpdateT2iTemplateResponse, DeleteT2iTemplateData, DeleteT2iTemplateError, DeleteT2iTemplateResponse, GetSubagentConfigError, GetSubagentConfigResponse, UpdateSubagentConfigData, UpdateSubagentConfigError, UpdateSubagentConfigResponse, ListSubagentAvailableToolsError, ListSubagentAvailableToolsResponse, VerifyPlatformWebhookData, VerifyPlatformWebhookError, VerifyPlatformWebhookResponse, ReceivePlatformWebhookData, ReceivePlatformWebhookError, ReceivePlatformWebhookResponse } from './types.gen'; export const client = createClient(createConfig()); @@ -1267,6 +1267,17 @@ export const deletePluginConfigFileById = }); }; +/** + * Preview an uploaded image in a plugin file configuration field + * Requires plugin scope. Returns a metadata-free PNG with a maximum edge of 1024 pixels. Only raster images up to 10 MiB and 20 million pixels inside the selected field's upload directory are accepted. Animated images preview their first frame. The original file is not modified. + */ +export const previewPluginConfigImageById = (options: OptionsLegacyParser) => { + return (options?.client ?? client).get({ + ...options, + url: '/api/v1/plugins/config-files/preview' + }); +}; + /** * Get plugin README content by query ID */ diff --git a/dashboard/src/api/generated/openapi-v1/types.gen.ts b/dashboard/src/api/generated/openapi-v1/types.gen.ts index 98286cbbbe0..fbdcc191f0f 100644 --- a/dashboard/src/api/generated/openapi-v1/types.gen.ts +++ b/dashboard/src/api/generated/openapi-v1/types.gen.ts @@ -1911,6 +1911,21 @@ export type DeletePluginConfigFileByIdResponse = (SuccessEnvelope); export type DeletePluginConfigFileByIdError = unknown; +export type PreviewPluginConfigImageByIdData = { + query: { + config_key: string; + /** + * Uploaded path relative to the plugin data directory + */ + path: string; + plugin_id: string; + }; +}; + +export type PreviewPluginConfigImageByIdResponse = ((Blob | File)); + +export type PreviewPluginConfigImageByIdError = (unknown); + export type GetPluginReadmeByIdData = { query: { plugin_id: string; diff --git a/dashboard/src/api/v1.ts b/dashboard/src/api/v1.ts index a0dae2cab70..38ec0de7f49 100644 --- a/dashboard/src/api/v1.ts +++ b/dashboard/src/api/v1.ts @@ -1349,6 +1349,13 @@ export const pluginApi = { }), ); }, + previewConfigImage(pluginId: string, configKey: string, path: string, signal: AbortSignal) { + return openApiV1.previewPluginConfigImageById({ + query: { plugin_id: pluginId, config_key: configKey, path }, + responseType: 'blob', + signal, + }) as Promise>; + }, uploadConfigFiles(pluginId: string, configKey: string, formData: FormData) { return typed( openApiV1.uploadPluginConfigFilesById({ diff --git a/dashboard/src/components/shared/ConfigImagePreview.vue b/dashboard/src/components/shared/ConfigImagePreview.vue new file mode 100644 index 00000000000..fdd76e89816 --- /dev/null +++ b/dashboard/src/components/shared/ConfigImagePreview.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/dashboard/src/components/shared/FileConfigItem.vue b/dashboard/src/components/shared/FileConfigItem.vue index ee688a656f6..ae5b241a105 100644 --- a/dashboard/src/components/shared/FileConfigItem.vue +++ b/dashboard/src/components/shared/FileConfigItem.vue @@ -25,7 +25,13 @@ {{ getDisplayName(item.path) }} @@ -77,6 +83,7 @@ import { computed, ref, watch } from 'vue' import { pluginApi } from '@/api/v1' import { useToast } from '@/utils/toast' import { useModuleI18n } from '@/i18n/composables' +import ConfigImagePreview from './ConfigImagePreview.vue' const props = defineProps({ modelValue: { @@ -109,6 +116,7 @@ const loadingFiles = ref(false) const MAX_FILE_BYTES = 500 * 1024 * 1024 const MAX_FILE_MB = 500 const directoryFiles = ref([]) +const previewRevision = ref(0) const fileList = computed({ get: () => (Array.isArray(props.modelValue) ? props.modelValue : []), @@ -277,6 +285,7 @@ const uploadFiles = async (files) => { const updatedDirectory = new Set(directoryFiles.value) uploaded.forEach((path) => updatedDirectory.add(path)) directoryFiles.value = Array.from(updatedDirectory) + previewRevision.value += 1 toast.success(tm('fileUpload.uploadSuccess', { count: uploaded.length })) } @@ -382,6 +391,14 @@ watch( word-break: break-word; } +.file-thumbnail { + display: flex; + align-items: center; + justify-content: center; + width: 56px; + height: 48px; +} + .upload-item { cursor: pointer; transition: background 0.2s ease; diff --git a/dashboard/src/i18n/locales/en-US/features/config.json b/dashboard/src/i18n/locales/en-US/features/config.json index d045814ef31..70218b44152 100644 --- a/dashboard/src/i18n/locales/en-US/features/config.json +++ b/dashboard/src/i18n/locales/en-US/features/config.json @@ -172,6 +172,10 @@ "fileUpload": { "button": "Manage Files", "dialogTitle": "Uploaded Files", + "previewImage": "Preview image: {name}", + "closePreview": "Close image preview", + "loadingPreview": "Loading image preview", + "previewUnavailable": "Image preview unavailable", "dropzone": "Upload new file", "allowedTypes": "Allowed types: {types}", "empty": "No files uploaded", diff --git a/dashboard/src/i18n/locales/ja-JP/features/config.json b/dashboard/src/i18n/locales/ja-JP/features/config.json index b975394f36e..d43fa70d39d 100644 --- a/dashboard/src/i18n/locales/ja-JP/features/config.json +++ b/dashboard/src/i18n/locales/ja-JP/features/config.json @@ -170,6 +170,10 @@ "title": "設定ファイルを編集" }, "fileUpload": { + "previewImage": "画像をプレビュー:{name}", + "closePreview": "画像プレビューを閉じる", + "loadingPreview": "画像プレビューを読み込み中", + "previewUnavailable": "画像をプレビューできません", "button": "ファイルを管理", "dialogTitle": "アップロード済みファイル", "dropzone": "新しいファイルをアップロード", diff --git a/dashboard/src/i18n/locales/ru-RU/features/config.json b/dashboard/src/i18n/locales/ru-RU/features/config.json index bfa71b75733..385b394962b 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/config.json +++ b/dashboard/src/i18n/locales/ru-RU/features/config.json @@ -170,6 +170,10 @@ "title": "Редактирование файла" }, "fileUpload": { + "previewImage": "Просмотр изображения: {name}", + "closePreview": "Закрыть просмотр изображения", + "loadingPreview": "Загрузка изображения", + "previewUnavailable": "Предпросмотр изображения недоступен", "button": "Файлы", "dialogTitle": "Загруженные файлы", "dropzone": "Загрузить файлы", diff --git a/dashboard/src/i18n/locales/zh-CN/features/config.json b/dashboard/src/i18n/locales/zh-CN/features/config.json index d06713cfd78..e20a837c70e 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/config.json +++ b/dashboard/src/i18n/locales/zh-CN/features/config.json @@ -170,6 +170,10 @@ "title": "编辑配置文件" }, "fileUpload": { + "previewImage": "预览图片:{name}", + "closePreview": "关闭图片预览", + "loadingPreview": "正在加载图片预览", + "previewUnavailable": "图片无法预览", "button": "管理文件", "dialogTitle": "已上传文件", "dropzone": "上传新文件", diff --git a/docs/en/dev/openapi-scopes.md b/docs/en/dev/openapi-scopes.md index fbe942c4d2a..8d93be80a66 100644 --- a/docs/en/dev/openapi-scopes.md +++ b/docs/en/dev/openapi-scopes.md @@ -236,6 +236,7 @@ Manage plugins, plugin configuration, plugin sources, and marketplace data. | `GET` | `/api/v1/plugins/config-files` | — | | `POST` | `/api/v1/plugins/config-files` | — | | `DELETE` | `/api/v1/plugins/config-files` | — | +| `GET` | `/api/v1/plugins/config-files/preview` | — | | `GET` | `/api/v1/plugins/config/schema` | — | | `PATCH` | `/api/v1/plugins/enabled` | — | | `GET` | `/api/v1/plugins/extensions/{plugin_path}` | — | diff --git a/docs/en/dev/star/guides/plugin-config.md b/docs/en/dev/star/guides/plugin-config.md index 892c560740a..5a371146573 100644 --- a/docs/en/dev/star/guides/plugin-config.md +++ b/docs/en/dev/star/guides/plugin-config.md @@ -120,6 +120,20 @@ Introduced in v4.13.0, this allows plugins to define file-upload configuration i } ``` +In the WebUI, open **Extensions → Plugins → Installed**, click the plugin's gear +icon (**Extension Config**), and click **Manage Files** for a `file` item. +Uploaded PNG/APNG, JPEG (including `.jpe`/`.jfif`), WebP, GIF, BMP/DIB, TIFF, and ICO +images show thumbnails; click a +thumbnail to open a larger preview, and press Escape or the close button to +return to the list. Non-image, missing, invalid, and oversized files remain +manageable without a preview. + +Previews require the same plugin authentication as file management and are +restricted to the selected field's upload directory. They are limited to 10 MiB +of input and 20 million decoded pixels, rendered as PNG with a maximum edge of +1024 pixels, and do not modify the uploaded file. Animated images show their first +frame. Existing upload, add-to-config, delete, and save behavior is unchanged. + ### `dict` type schema Used to visualize editing a Python `dict` type configuration. For example, AstrBot Core's custom extra body parameter configuration: diff --git a/docs/en/use/webui.md b/docs/en/use/webui.md index 5d22390afb5..25ed5a63d7c 100644 --- a/docs/en/use/webui.md +++ b/docs/en/use/webui.md @@ -145,6 +145,13 @@ Select `Extensions` in the sidebar. The top tabs are `Plugins`, `Skills`, `MCP S On either plugin list, you can also click `Install Plugin` (+) in the bottom right corner to manually install plugins via URL or file upload. +For a plugin with file-upload settings, click its gear icon (`Extension Config`), +then `Manage Files` beside the file setting. Supported uploaded images have +clickable thumbnails. Close the larger preview with Escape or its close button. +Other files keep their usual management actions. See the +[file configuration reference](/en/dev/star/guides/plugin-config#file-type-schema) +for supported formats and preview limits. + > Due to the plugin update mechanism, the AstrBot Team cannot fully guarantee the security of plugins in the plugin market. Please carefully verify them. The AstrBot Team is not responsible for any losses caused by plugins. ### Handling Plugin Load Failures diff --git a/docs/public/openapi.json b/docs/public/openapi.json index 5b45fa67ab2..3a6b3154ecb 100644 --- a/docs/public/openapi.json +++ b/docs/public/openapi.json @@ -3168,6 +3168,92 @@ "description": "**Required scope:** `plugin`" } }, + "/api/v1/plugins/config-files/preview": { + "get": { + "tags": [ + "Plugins" + ], + "summary": "Preview an uploaded image in a plugin file configuration field", + "description": "Requires plugin scope. Returns a metadata-free PNG with a maximum edge of 1024 pixels. Only raster images up to 10 MiB and 20 million pixels inside the selected field's upload directory are accepted. Animated images preview their first frame. The original file is not modified.\n\n**Required scope:** `plugin`", + "operationId": "previewPluginConfigImageById", + "x-astrbot-scope": "plugin", + "parameters": [ + { + "name": "plugin_id", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "config_key", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "path", + "in": "query", + "required": true, + "description": "Uploaded path relative to the plugin data directory", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Image preview", + "headers": { + "Cache-Control": { + "schema": { + "type": "string", + "const": "no-store" + } + }, + "X-Content-Type-Options": { + "schema": { + "type": "string", + "const": "nosniff" + } + } + }, + "content": { + "image/png": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "Invalid plugin, configuration field, or file path" + }, + "401": { + "description": "Authentication required" + }, + "403": { + "description": "Plugin scope required" + }, + "404": { + "description": "Image file not found" + }, + "413": { + "description": "Image exceeds the input size or pixel limit" + }, + "415": { + "description": "Unsupported or invalid image" + }, + "503": { + "description": "Image storage temporarily unavailable" + } + } + } + }, "/api/v1/plugins/readme": { "get": { "tags": [ diff --git a/docs/zh/dev/openapi-scopes.md b/docs/zh/dev/openapi-scopes.md index 9ad0f5ee0e2..eca40c902c1 100644 --- a/docs/zh/dev/openapi-scopes.md +++ b/docs/zh/dev/openapi-scopes.md @@ -236,6 +236,7 @@ outline: deep | `GET` | `/api/v1/plugins/config-files` | — | | `POST` | `/api/v1/plugins/config-files` | — | | `DELETE` | `/api/v1/plugins/config-files` | — | +| `GET` | `/api/v1/plugins/config-files/preview` | — | | `GET` | `/api/v1/plugins/config/schema` | — | | `PATCH` | `/api/v1/plugins/enabled` | — | | `GET` | `/api/v1/plugins/extensions/{plugin_path}` | — | diff --git a/docs/zh/dev/star/guides/plugin-config.md b/docs/zh/dev/star/guides/plugin-config.md index d1236bc3a73..a2d7ad62da2 100644 --- a/docs/zh/dev/star/guides/plugin-config.md +++ b/docs/zh/dev/star/guides/plugin-config.md @@ -120,6 +120,15 @@ API Key、访问令牌和密码等敏感字符串应设置 `"secret": true`。 } ``` +在 WebUI 的 **插件 → 插件 → 已安装** 中,点击插件的齿轮图标(**插件配置**), +再点击 `file` 配置项的 **管理文件**。已上传的 PNG/APNG、JPEG(含 `.jpe`/`.jfif`)、WebP、GIF、BMP/DIB、TIFF +和 ICO 图片会显示缩略图,点击缩略图可查看大图,按 Escape 或关闭按钮返回列表。 +非图片、缺失、损坏或超出预览限制的文件仍可按原方式管理。 + +预览使用与文件管理相同的插件认证,且只能读取当前配置项上传目录内的图片。 +输入限制为 10 MiB、解码后不超过 2000 万像素,输出最长边不超过 1024 像素的 PNG, +不会修改原始上传文件。动图仅预览首帧。上传、加入配置、删除及保存配置的原有行为不变。 + ### dict 类型的 schema 用于可视化编辑一个 Python 的 dict 类型的配置。如 AstrBot Core 中的自定义请求体参数配置项: diff --git a/docs/zh/use/webui.md b/docs/zh/use/webui.md index 9b08fdda139..577374cd2c2 100644 --- a/docs/zh/use/webui.md +++ b/docs/zh/use/webui.md @@ -145,6 +145,11 @@ ChatUI 支持以下常用能力: 在已安装或插件市场页面,也可以点击右下角 `安装插件`(+)按钮,以 URL / 文件上传的方式手动安装插件。 +如果插件提供文件上传配置,点击其齿轮图标(`插件配置`),再点击文件配置项旁的 +`管理文件`。支持预览的已上传图片会显示可点击的缩略图;按 Escape 或关闭按钮退出 +大图预览。其他文件仍保留原有管理操作。支持的格式及预览限制见 +[文件配置说明](/zh/dev/star/guides/plugin-config#file-类型的-schema)。 + > 由于插件更新机制,AstrBot Team 无法完全保证插件市场中插件的安全性,请您仔细甄别。因为插件原因造成损失的,AstrBot Team 不予负责。 ### 插件加载失败处理 diff --git a/openspec/openapi-v1.yaml b/openspec/openapi-v1.yaml index 3eb7b95a3b8..b42b2841973 100644 --- a/openspec/openapi-v1.yaml +++ b/openspec/openapi-v1.yaml @@ -2083,6 +2083,66 @@ paths: "200": $ref: "#/components/responses/Ok" + /api/v1/plugins/config-files/preview: + get: + tags: [Plugins] + summary: Preview an uploaded image in a plugin file configuration field + description: >- + Requires plugin scope. Returns a metadata-free PNG with a maximum edge + of 1024 pixels. Only raster images up to 10 MiB and 20 million pixels + inside the selected field's upload directory are accepted. Animated + images preview their first frame. The original file is not modified. + operationId: previewPluginConfigImageById + x-astrbot-scope: plugin + parameters: + - name: plugin_id + in: query + required: true + schema: + type: string + - name: config_key + in: query + required: true + schema: + type: string + - name: path + in: query + required: true + description: Uploaded path relative to the plugin data directory + schema: + type: string + responses: + "200": + description: Image preview + headers: + Cache-Control: + schema: + type: string + const: no-store + X-Content-Type-Options: + schema: + type: string + const: nosniff + content: + image/png: + schema: + type: string + format: binary + "400": + description: Invalid plugin, configuration field, or file path + "401": + description: Authentication required + "403": + description: Plugin scope required + "404": + description: Image file not found + "413": + description: Image exceeds the input size or pixel limit + "415": + description: Unsupported or invalid image + "503": + description: Image storage temporarily unavailable + /api/v1/plugins/readme: get: tags: [Plugins] diff --git a/tests/test_fastapi_v1_dashboard.py b/tests/test_fastapi_v1_dashboard.py index de7a6f01636..7d720058bbb 100644 --- a/tests/test_fastapi_v1_dashboard.py +++ b/tests/test_fastapi_v1_dashboard.py @@ -3497,6 +3497,323 @@ async def test_v1_plugin_config_file_routes_reach_service_layer( assert delete_response.json()["status"] == "error" +@pytest.fixture +def plugin_config_preview_files(asgi_app, monkeypatch, tmp_path): + """Register a synthetic file field and its real uploaded image files.""" + root = tmp_path / "plugin_data" + image_dir = root / "preview-demo" / "files" / "images" + image_dir.mkdir(parents=True) + PILImage.new("RGB", (1600, 800), "blue").save(image_dir / "landscape.png") + PILImage.new("RGB", (40, 80), "orange").save(image_dir / "portrait.webp") + (image_dir / "invalid.png").write_bytes(b"not an image") + (image_dir / "active.svg").write_text( + '', + encoding="utf-8", + ) + other_dir = root / "preview-demo" / "files" / "other" + other_dir.mkdir() + PILImage.new("RGB", (10, 10), "red").save(other_dir / "private.png") + config = SimpleNamespace( + schema={ + "images": {"type": "file"}, + "other": {"type": "file"}, + "alias": {"type": "file"}, + "text": {"type": "string"}, + } + ) + service = asgi_app.state.services.config_files + monkeypatch.setattr( + config_service, "get_astrbot_plugin_data_path", lambda: str(root) + ) + monkeypatch.setattr( + service, + "get_plugin_metadata_by_name", + lambda name: SimpleNamespace(config=config) if name == "preview-demo" else None, + ) + return root, image_dir + + +@pytest.mark.asyncio +@pytest.mark.parametrize("filename", ["landscape.png", "portrait.webp"]) +async def test_v1_plugin_config_image_preview( + asgi_client, plugin_config_preview_files, filename +): + """Serve a bounded PNG preview without modifying or exposing the source.""" + _, image_dir = plugin_config_preview_files + original = (image_dir / filename).read_bytes() + response = await asgi_client.get( + "/api/v1/plugins/config-files/preview", + params={ + "plugin_id": "preview-demo", + "config_key": "images", + "path": f"files/images/{filename}", + }, + headers=_jwt_headers(), + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "image/png" + assert response.headers["cache-control"] == "no-store" + assert response.headers["x-content-type-options"] == "nosniff" + with PILImage.open(io.BytesIO(response.content)) as image: + assert image.format == "PNG" + assert max(image.size) <= 1024 + if filename == "landscape.png": + assert image.size == (1024, 512) + assert (image_dir / filename).read_bytes() == original + + +@pytest.mark.asyncio +async def test_v1_plugin_config_image_preview_requires_plugin_scope( + asgi_client, fake_db, plugin_config_preview_files +): + """Reuse plugin authentication rather than allowing public image URLs.""" + params = { + "plugin_id": "preview-demo", + "config_key": "images", + "path": "files/images/landscape.png", + } + anonymous = await asgi_client.get( + "/api/v1/plugins/config-files/preview", params=params + ) + assert anonymous.status_code == 401 + fake_db.add_api_key("preview-wrong-scope", ["file"]) + denied = await asgi_client.get( + "/api/v1/plugins/config-files/preview", + params=params, + headers={"Authorization": "Bearer preview-wrong-scope"}, + ) + assert denied.status_code == 403 + fake_db.add_api_key("preview-plugin-scope", ["plugin"]) + allowed = await asgi_client.get( + "/api/v1/plugins/config-files/preview", + params=params, + headers={"Authorization": "Bearer preview-plugin-scope"}, + ) + assert allowed.status_code == 200 + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("plugin_id", "config_key", "path", "status"), + [ + ("preview-demo", "images", "../private.png", 400), + ("preview-demo", "images", "files/images/../other/private.png", 400), + ("preview-demo", "images", "files/other/private.png", 400), + ("preview-demo", "images", "files/images-other/private.png", 400), + ("preview-demo", "images", "main.py", 400), + ("preview-demo", "images", "/etc/passwd", 400), + ("preview-demo", "images", "/files/images/landscape.png", 400), + ("preview-demo", "images", "files/images/missing.png", 404), + ("preview-demo", "images", "files/images", 400), + ("preview-demo", "text", "files/images/landscape.png", 400), + ("preview-demo", "unknown", "files/images/landscape.png", 400), + ("unknown-plugin", "images", "files/images/landscape.png", 400), + ("preview-demo", "images", "files/images/invalid.png", 415), + ("preview-demo", "images", "files/images/active.svg", 415), + ], +) +async def test_v1_plugin_config_image_preview_rejects_unsafe_inputs( + asgi_client, plugin_config_preview_files, plugin_id, config_key, path, status +): + """Reject paths outside the selected field and files that are not images.""" + response = await asgi_client.get( + "/api/v1/plugins/config-files/preview", + params={"plugin_id": plugin_id, "config_key": config_key, "path": path}, + headers=_jwt_headers(), + ) + assert response.status_code == status + assert response.json()["status"] == "error" + assert str(plugin_config_preview_files[0]) not in response.text + + +@pytest.mark.asyncio +@pytest.mark.parametrize("link_kind", ["file", "directory", "field-directory", "loop"]) +async def test_v1_plugin_config_image_preview_rejects_symlink_escape( + asgi_client, plugin_config_preview_files, require_symlink, link_kind +): + """Resolve symlinks before enforcing the selected field's directory.""" + root, image_dir = plugin_config_preview_files + if link_kind == "file": + (image_dir / "linked.png").symlink_to( + root / "preview-demo" / "files" / "other" / "private.png" + ) + path = "files/images/linked.png" + elif link_kind == "directory": + (image_dir / "linked").symlink_to(image_dir.parent / "other") + path = "files/images/linked/private.png" + elif link_kind == "field-directory": + alias = root / "preview-demo" / "files" / "alias" + alias.symlink_to(image_dir) + path = "files/alias/landscape.png" + else: + (image_dir / "loop.png").symlink_to(image_dir / "loop.png") + path = "files/images/loop.png" + response = await asgi_client.get( + "/api/v1/plugins/config-files/preview", + params={ + "plugin_id": "preview-demo", + "config_key": "alias" if link_kind == "field-directory" else "images", + "path": path, + }, + headers=_jwt_headers(), + ) + assert response.status_code == 400 + + +@pytest.mark.asyncio +@pytest.mark.parametrize("limit", ["bytes", "pixels"]) +async def test_v1_plugin_config_image_preview_is_bounded( + asgi_client, plugin_config_preview_files, monkeypatch, limit +): + """Bound compressed input and decoded dimensions independently.""" + monkeypatch.setattr( + config_service, + "MAX_PREVIEW_FILE_BYTES" if limit == "bytes" else "MAX_PREVIEW_PIXELS", + 32, + raising=False, + ) + response = await asgi_client.get( + "/api/v1/plugins/config-files/preview", + params={ + "plugin_id": "preview-demo", + "config_key": "images", + "path": "files/images/landscape.png", + }, + headers=_jwt_headers(), + ) + assert response.status_code == 413 + + +@pytest.mark.asyncio +@pytest.mark.parametrize("format_", ["JPEG", "GIF", "BMP", "DIB", "TIFF", "ICO"]) +async def test_v1_plugin_config_image_preview_supported_raster_formats( + asgi_client, plugin_config_preview_files, format_ +): + """Decode supported raster formats rather than trusting the extension.""" + _, image_dir = plugin_config_preview_files + path = image_dir / "sample.image" + PILImage.new("RGB", (32, 32), "blue").save(path, format=format_) + response = await asgi_client.get( + "/api/v1/plugins/config-files/preview", + params={ + "plugin_id": "preview-demo", + "config_key": "images", + "path": "files/images/sample.image", + }, + headers=_jwt_headers(), + ) + assert response.status_code == 200 + with PILImage.open(io.BytesIO(response.content)) as image: + assert image.size == (32, 32) + assert image.format == "PNG" + + +@pytest.mark.asyncio +async def test_v1_plugin_config_image_preview_applies_orientation_and_strips_metadata( + asgi_client, plugin_config_preview_files +): + """Apply EXIF orientation without returning private source metadata.""" + _, image_dir = plugin_config_preview_files + image = PILImage.new("RGB", (80, 40), "blue") + exif = image.getexif() + exif[274] = 6 + exif[270] = "private fixture metadata" + image.save(image_dir / "rotated.jpg", exif=exif) + response = await asgi_client.get( + "/api/v1/plugins/config-files/preview", + params={ + "plugin_id": "preview-demo", + "config_key": "images", + "path": "files/images/rotated.jpg", + }, + headers=_jwt_headers(), + ) + assert response.status_code == 200 + with PILImage.open(io.BytesIO(response.content)) as preview: + assert preview.size == (40, 80) + assert not preview.getexif() + assert not preview.info + with PILImage.open(image_dir / "rotated.jpg") as original: + assert original.size == (80, 40) + assert original.getexif()[270] == "private fixture metadata" + + +@pytest.mark.asyncio +async def test_v1_plugin_config_image_preview_uses_first_animation_frame( + asgi_client, plugin_config_preview_files +): + """Keep animation previews bounded to one frame.""" + _, image_dir = plugin_config_preview_files + PILImage.new("RGB", (32, 32), "blue").save( + image_dir / "animated.gif", + save_all=True, + append_images=[PILImage.new("RGB", (32, 32), "red")], + duration=100, + loop=0, + ) + response = await asgi_client.get( + "/api/v1/plugins/config-files/preview", + params={ + "plugin_id": "preview-demo", + "config_key": "images", + "path": "files/images/animated.gif", + }, + headers=_jwt_headers(), + ) + assert response.status_code == 200 + with PILImage.open(io.BytesIO(response.content)) as preview: + assert not getattr(preview, "is_animated", False) + assert preview.convert("RGB").getpixel((0, 0)) == (0, 0, 255) + + +@pytest.mark.asyncio +async def test_v1_plugin_config_image_preview_rejects_decompression_bombs( + asgi_client, plugin_config_preview_files, monkeypatch +): + """Handle Pillow's decoder guard without weakening its global limit.""" + monkeypatch.setattr(PILImage, "MAX_IMAGE_PIXELS", 100) + response = await asgi_client.get( + "/api/v1/plugins/config-files/preview", + params={ + "plugin_id": "preview-demo", + "config_key": "images", + "path": "files/images/landscape.png", + }, + headers=_jwt_headers(), + ) + assert response.status_code == 413 + assert PILImage.MAX_IMAGE_PIXELS == 100 + + +@pytest.mark.asyncio +@pytest.mark.parametrize("storage_error", [PermissionError, OSError]) +async def test_v1_plugin_config_image_preview_distinguishes_storage_failure( + asgi_client, plugin_config_preview_files, monkeypatch, storage_error +): + """Report storage outages separately from corrupt or unsupported images.""" + _, image_dir = plugin_config_preview_files + original_open = Path.open + + def failing_open(path, *args, **kwargs): + if path == image_dir / "landscape.png": + raise storage_error(f"Private storage failure: {image_dir}") + return original_open(path, *args, **kwargs) + + monkeypatch.setattr(Path, "open", failing_open) + response = await asgi_client.get( + "/api/v1/plugins/config-files/preview", + params={ + "plugin_id": "preview-demo", + "config_key": "images", + "path": "files/images/landscape.png", + }, + headers=_jwt_headers(), + ) + assert response.status_code == 503 + assert response.json()["message"] == "Image file cannot be read" + assert str(image_dir) not in response.text + + @pytest.mark.asyncio async def test_v1_safe_plugin_routes_accept_slash_ids( asgi_app: FastAPI,