From d2f1c7a6267e6b4e3418f652ffd82ce692761986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=E2=82=82=E2=82=82H=E2=82=82=E2=82=85NO=E2=82=86?= Date: Thu, 18 Jun 2026 02:57:52 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=90=8C=E6=AD=A5=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E7=BD=AE=E9=A1=B6=E5=81=8F=E5=A5=BD=E5=88=B0=E5=90=8E=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/dashboard/api/app.py | 4 + astrbot/dashboard/api/plugin_preferences.py | 55 +++++ astrbot/dashboard/api/router.py | 2 + astrbot/dashboard/schemas.py | 4 + .../services/plugin_preference_service.py | 90 ++++++++ .../src/api/generated/openapi-v1/sdk.gen.ts | 22 +- .../src/api/generated/openapi-v1/types.gen.ts | 25 ++ dashboard/src/api/v1.ts | 19 ++ .../views/extension/InstalledPluginsTab.vue | 124 +++++++++- .../extension/extensionPreferenceStorage.mjs | 2 +- .../extension/pluginPinnedPreferenceSync.mjs | 47 ++++ .../tests/pluginPinnedPreferenceSync.test.mjs | 97 ++++++++ openspec/openapi-v1.yaml | 62 +++++ tests/test_fastapi_v1_dashboard.py | 215 ++++++++++++++++++ 14 files changed, 757 insertions(+), 11 deletions(-) create mode 100644 astrbot/dashboard/api/plugin_preferences.py create mode 100644 astrbot/dashboard/services/plugin_preference_service.py create mode 100644 dashboard/src/views/extension/pluginPinnedPreferenceSync.mjs create mode 100644 dashboard/tests/pluginPinnedPreferenceSync.test.mjs diff --git a/astrbot/dashboard/api/app.py b/astrbot/dashboard/api/app.py index 00ceb11cfc..e252c330b4 100644 --- a/astrbot/dashboard/api/app.py +++ b/astrbot/dashboard/api/app.py @@ -33,6 +33,9 @@ from astrbot.dashboard.services.persona_service import PersonaService from astrbot.dashboard.services.platform_service import PlatformService from astrbot.dashboard.services.plugin_page_service import PluginPageService +from astrbot.dashboard.services.plugin_preference_service import ( + PluginPreferenceService, +) from astrbot.dashboard.services.plugin_service import PluginService from astrbot.dashboard.services.session_management_service import ( SessionManagementService, @@ -123,6 +126,7 @@ def create_dashboard_asgi_app( providers=ProviderConfigService(core_lifecycle), personas=PersonaService(core_lifecycle), plugins=PluginService(core_lifecycle, core_lifecycle.plugin_manager), + plugin_preferences=PluginPreferenceService(db), plugin_pages=PluginPageService( core_lifecycle.plugin_manager, core_lifecycle=core_lifecycle, diff --git a/astrbot/dashboard/api/plugin_preferences.py b/astrbot/dashboard/api/plugin_preferences.py new file mode 100644 index 0000000000..a471e90f97 --- /dev/null +++ b/astrbot/dashboard/api/plugin_preferences.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from fastapi import APIRouter, Depends, Request + +from astrbot.dashboard.responses import ApiError, ok +from astrbot.dashboard.schemas import PluginPinnedExtensionsRequest +from astrbot.dashboard.services.plugin_preference_service import ( + PluginPreferenceService, +) + +from .auth import AuthContext, require_scope + +router = APIRouter(tags=["Plugin Preferences"]) + + +async def require_plugin_scope(request: Request) -> AuthContext: + """校验当前请求具有 plugin scope 权限。""" + return await require_scope(request, "plugin") + + +def get_plugin_preference_service(request: Request) -> PluginPreferenceService: + """从应用状态获取插件偏好服务。""" + return request.app.state.services.plugin_preferences + + +@router.get("/plugins/preferences/pinned") +async def get_pinned_extensions( + _auth: AuthContext = Depends(require_plugin_scope), + service: PluginPreferenceService = Depends(get_plugin_preference_service), +): + """获取 Dashboard 全局置顶插件列表。""" + try: + pinned, preference_exists = await service.get_pinned_extensions() + except Exception as exc: + raise ApiError("加载插件置顶偏好失败", status_code=500) from exc + return ok( + { + "pinned_extensions": pinned, + "preference_exists": preference_exists, + } + ) + + +@router.put("/plugins/preferences/pinned") +async def set_pinned_extensions( + payload: PluginPinnedExtensionsRequest, + _auth: AuthContext = Depends(require_plugin_scope), + service: PluginPreferenceService = Depends(get_plugin_preference_service), +): + """更新 Dashboard 全局置顶插件列表。""" + try: + pinned = await service.set_pinned_extensions(payload.pinned_extensions) + except Exception as exc: + raise ApiError("保存插件置顶偏好失败", status_code=500) from exc + return ok({"pinned_extensions": pinned, "preference_exists": True}) diff --git a/astrbot/dashboard/api/router.py b/astrbot/dashboard/api/router.py index b59ea62858..9ce56357f5 100644 --- a/astrbot/dashboard/api/router.py +++ b/astrbot/dashboard/api/router.py @@ -19,6 +19,7 @@ from .open_api import router as open_api_router from .personas import router as personas_router from .platform import router as platform_router +from .plugin_preferences import router as plugin_preferences_router from .plugins import router as plugins_router from .providers import router as providers_router from .sessions import router as sessions_router @@ -41,6 +42,7 @@ def build_api_router() -> APIRouter: router.include_router(bots_router) router.include_router(providers_router) router.include_router(plugins_router) + router.include_router(plugin_preferences_router) router.include_router(chat_router) router.include_router(chat_projects_router) router.include_router(conversations_router) diff --git a/astrbot/dashboard/schemas.py b/astrbot/dashboard/schemas.py index f29fcda7ec..f05ebf095c 100644 --- a/astrbot/dashboard/schemas.py +++ b/astrbot/dashboard/schemas.py @@ -534,6 +534,10 @@ class PluginUninstallRequest(OpenModel): delete_data: bool | None = None +class PluginPinnedExtensionsRequest(OpenModel): + pinned_extensions: list[Any] | None = None + + class PluginConfigFileDeleteRequest(OpenModel): path: str | None = None file: str | None = None diff --git a/astrbot/dashboard/services/plugin_preference_service.py b/astrbot/dashboard/services/plugin_preference_service.py new file mode 100644 index 0000000000..afbf7cef7c --- /dev/null +++ b/astrbot/dashboard/services/plugin_preference_service.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +from astrbot.core.db import BaseDatabase + +PREFERENCE_SCOPE = "global" +PREFERENCE_SCOPE_ID = "global" +PREFERENCE_KEY = "plugin_pinned_extensions" + + +class PluginPreferenceService: + """Dashboard 插件全局偏好服务。 + + 当前仅用于持久化插件置顶顺序。数据存储在 preferences 表中, + 使用 global scope,因此是 Dashboard 全局偏好,不按登录用户隔离。 + """ + + def __init__(self, db: BaseDatabase) -> None: + """初始化服务。 + + Args: + db: 数据库访问对象,直接使用 BaseDatabase 的 preference 方法。 + """ + self.db = db + + @staticmethod + def normalize_pinned_extensions(value: object) -> list[str]: + """将任意输入归一化为置顶插件名称列表。 + + 过滤规则: + - 仅保留非空字符串; + - 去除首尾空白; + - 按出现顺序去重; + - 脏数据或非列表输入兜底为空列表。 + + Args: + value: 待归一化的原始值。 + + Returns: + 归一化后的插件名称列表。 + """ + if not isinstance(value, list): + return [] + + seen: set[str] = set() + result: list[str] = [] + for item in value: + if not isinstance(item, str): + continue + name = item.strip() + if not name or name in seen: + continue + seen.add(name) + result.append(name) + return result + + async def get_pinned_extensions(self) -> tuple[list[str], bool]: + """从数据库读取置顶插件列表。 + + Returns: + 已归一化的置顶插件名称列表,以及该偏好记录是否存在。 + """ + preference = await self.db.get_preference( + PREFERENCE_SCOPE, + PREFERENCE_SCOPE_ID, + PREFERENCE_KEY, + ) + + preference_exists = preference is not None + if not preference_exists or not isinstance(preference.value, dict): + return [], preference_exists + + return self.normalize_pinned_extensions(preference.value.get("val")), True + + async def set_pinned_extensions(self, names: object) -> list[str]: + """保存置顶插件列表到数据库。 + + Args: + names: 待保存的插件名称列表,可为任意内容,会先归一化。 + + Returns: + 归一化后实际保存的插件名称列表。 + """ + normalized = self.normalize_pinned_extensions(names) + await self.db.insert_preference_or_update( + PREFERENCE_SCOPE, + PREFERENCE_SCOPE_ID, + PREFERENCE_KEY, + {"val": normalized}, + ) + return normalized diff --git a/dashboard/src/api/generated/openapi-v1/sdk.gen.ts b/dashboard/src/api/generated/openapi-v1/sdk.gen.ts index 8cde55cf98..009be2cae5 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, 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, AddChatProjectSessionData, AddChatProjectSessionError, AddChatProjectSessionResponse, RemoveChatProjectSessionData, RemoveChatProjectSessionError, RemoveChatProjectSessionResponse, SendImMessageData, SendImMessageError, SendImMessageResponse, ListImBotsError, ListImBotsResponse, UploadFileData, UploadFileError, UploadFileResponse, 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, ListPluginPagesByIdData, ListPluginPagesByIdError, ListPluginPagesByIdResponse, GetPluginPageByIdData, GetPluginPageByIdError, GetPluginPageByIdResponse, GetPluginPageAssetByIdData, GetPluginPageAssetByIdError, GetPluginPageAssetByIdResponse, GetPluginData, GetPluginError, GetPluginResponse, UninstallPluginData, UninstallPluginError, UninstallPluginResponse, GetPluginConfigData, GetPluginConfigError, GetPluginConfigResponse, UpdatePluginConfigData, UpdatePluginConfigError, UpdatePluginConfigResponse, GetPluginConfigSchemaData, GetPluginConfigSchemaError, GetPluginConfigSchemaResponse, ListPluginConfigFilesData, ListPluginConfigFilesError, ListPluginConfigFilesResponse, UploadPluginConfigFilesData, UploadPluginConfigFilesError, UploadPluginConfigFilesResponse, DeletePluginConfigFileData, DeletePluginConfigFileError, DeletePluginConfigFileResponse, GetPluginReadmeData, GetPluginReadmeError, GetPluginReadmeResponse, GetPluginChangelogData, GetPluginChangelogError, GetPluginChangelogResponse, ReloadPluginData, ReloadPluginError, ReloadPluginResponse, SetPluginEnabledData, SetPluginEnabledError, SetPluginEnabledResponse, UpdatePluginData, UpdatePluginError, UpdatePluginResponse, UpdatePluginsData, UpdatePluginsError, UpdatePluginsResponse, CheckPluginVersionSupportData, CheckPluginVersionSupportError, CheckPluginVersionSupportResponse, ListFailedPluginsError, ListFailedPluginsResponse, UninstallFailedPluginData, UninstallFailedPluginError, UninstallFailedPluginResponse, ReloadFailedPluginData, ReloadFailedPluginError, ReloadFailedPluginResponse, InstallPluginFromGithubData, InstallPluginFromGithubError, InstallPluginFromGithubResponse, 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, ListPluginPagesData, ListPluginPagesError, ListPluginPagesResponse, GetPluginPageData, GetPluginPageError, GetPluginPageResponse, GetPluginPageAssetData, GetPluginPageAssetError, GetPluginPageAssetResponse, 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, 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, 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, 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, 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, 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, AddChatProjectSessionData, AddChatProjectSessionError, AddChatProjectSessionResponse, RemoveChatProjectSessionData, RemoveChatProjectSessionError, RemoveChatProjectSessionResponse, SendImMessageData, SendImMessageError, SendImMessageResponse, ListImBotsError, ListImBotsResponse, UploadFileData, UploadFileError, UploadFileResponse, 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, ListPluginPagesByIdData, ListPluginPagesByIdError, ListPluginPagesByIdResponse, GetPluginPageByIdData, GetPluginPageByIdError, GetPluginPageByIdResponse, GetPluginPageAssetByIdData, GetPluginPageAssetByIdError, GetPluginPageAssetByIdResponse, GetPluginData, GetPluginError, GetPluginResponse, UninstallPluginData, UninstallPluginError, UninstallPluginResponse, GetPluginConfigData, GetPluginConfigError, GetPluginConfigResponse, UpdatePluginConfigData, UpdatePluginConfigError, UpdatePluginConfigResponse, GetPluginConfigSchemaData, GetPluginConfigSchemaError, GetPluginConfigSchemaResponse, ListPluginConfigFilesData, ListPluginConfigFilesError, ListPluginConfigFilesResponse, UploadPluginConfigFilesData, UploadPluginConfigFilesError, UploadPluginConfigFilesResponse, DeletePluginConfigFileData, DeletePluginConfigFileError, DeletePluginConfigFileResponse, GetPluginReadmeData, GetPluginReadmeError, GetPluginReadmeResponse, GetPluginChangelogData, GetPluginChangelogError, GetPluginChangelogResponse, ReloadPluginData, ReloadPluginError, ReloadPluginResponse, SetPluginEnabledData, SetPluginEnabledError, SetPluginEnabledResponse, UpdatePluginData, UpdatePluginError, UpdatePluginResponse, UpdatePluginsData, UpdatePluginsError, UpdatePluginsResponse, CheckPluginVersionSupportData, CheckPluginVersionSupportError, CheckPluginVersionSupportResponse, ListFailedPluginsError, ListFailedPluginsResponse, UninstallFailedPluginData, UninstallFailedPluginError, UninstallFailedPluginResponse, ReloadFailedPluginData, ReloadFailedPluginError, ReloadFailedPluginResponse, GetPinnedExtensionsError, GetPinnedExtensionsResponse, UpdatePinnedExtensionsData, UpdatePinnedExtensionsError, UpdatePinnedExtensionsResponse, InstallPluginFromGithubData, InstallPluginFromGithubError, InstallPluginFromGithubResponse, 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, ListPluginPagesData, ListPluginPagesError, ListPluginPagesResponse, GetPluginPageData, GetPluginPageError, GetPluginPageResponse, GetPluginPageAssetData, GetPluginPageAssetError, GetPluginPageAssetResponse, 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, 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, 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, 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, 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()); @@ -1400,6 +1400,26 @@ export const reloadFailedPlugin = (options }); }; +/** + * Get pinned extension names + */ +export const getPinnedExtensions = (options?: OptionsLegacyParser) => { + return (options?.client ?? client).get({ + ...options, + url: '/api/v1/plugins/preferences/pinned' + }); +}; + +/** + * Update pinned extension names + */ +export const updatePinnedExtensions = (options: OptionsLegacyParser) => { + return (options?.client ?? client).put({ + ...options, + url: '/api/v1/plugins/preferences/pinned' + }); +}; + /** * Install a plugin from GitHub */ diff --git a/dashboard/src/api/generated/openapi-v1/types.gen.ts b/dashboard/src/api/generated/openapi-v1/types.gen.ts index ede087696a..876c0cd893 100644 --- a/dashboard/src/api/generated/openapi-v1/types.gen.ts +++ b/dashboard/src/api/generated/openapi-v1/types.gen.ts @@ -459,6 +459,19 @@ export type PluginGithubInstallRequest = { ignore_version_check?: boolean; }; +export type PluginPinnedExtensionsData = { + pinned_extensions: Array<(string)>; + preference_exists: boolean; +}; + +export type PluginPinnedExtensionsEnvelope = SuccessEnvelope & { + data: PluginPinnedExtensionsData; +}; + +export type PluginPinnedExtensionsRequest = { + pinned_extensions?: Array; +}; + export type PluginSourceRequest = { id?: string; name?: string; @@ -1923,6 +1936,18 @@ export type ReloadFailedPluginResponse = (SuccessEnvelope); export type ReloadFailedPluginError = unknown; +export type GetPinnedExtensionsResponse = (PluginPinnedExtensionsEnvelope); + +export type GetPinnedExtensionsError = unknown; + +export type UpdatePinnedExtensionsData = { + body: PluginPinnedExtensionsRequest; +}; + +export type UpdatePinnedExtensionsResponse = (PluginPinnedExtensionsEnvelope); + +export type UpdatePinnedExtensionsError = unknown; + export type InstallPluginFromGithubData = { body: PluginGithubInstallRequest; }; diff --git a/dashboard/src/api/v1.ts b/dashboard/src/api/v1.ts index b0b248de50..5449636a73 100644 --- a/dashboard/src/api/v1.ts +++ b/dashboard/src/api/v1.ts @@ -66,6 +66,11 @@ export interface ApiEnvelope { data: T; } +export interface PluginPinnedExtensionsData { + pinned_extensions: string[]; + preference_exists: boolean; +} + export const UPGRADE_RECOVERY_EVENT = 'astrbot-upgrade-recovery'; export const UPGRADE_RECOVERY_TOKEN_KEY = 'astrbot-upgrade-recovery-token'; @@ -1296,6 +1301,20 @@ export const pluginApi = { }, }; +export const pluginPreferencesApi = { + getPinnedExtensions() { + return apiV1Client.get>( + '/plugins/preferences/pinned', + ); + }, + updatePinnedExtensions(names: unknown[]) { + return apiV1Client.put>( + '/plugins/preferences/pinned', + { pinned_extensions: names }, + ); + }, +}; + export const pluginExtensionApi = { get(pluginPath: string, config?: AxiosRequestConfig) { return apiV1Client.get>( diff --git a/dashboard/src/views/extension/InstalledPluginsTab.vue b/dashboard/src/views/extension/InstalledPluginsTab.vue index 5e41bc68b1..48ab987391 100644 --- a/dashboard/src/views/extension/InstalledPluginsTab.vue +++ b/dashboard/src/views/extension/InstalledPluginsTab.vue @@ -1,11 +1,14 @@ diff --git a/dashboard/src/views/extension/extensionPreferenceStorage.mjs b/dashboard/src/views/extension/extensionPreferenceStorage.mjs index c0ce80bdd0..6020c071b3 100644 --- a/dashboard/src/views/extension/extensionPreferenceStorage.mjs +++ b/dashboard/src/views/extension/extensionPreferenceStorage.mjs @@ -40,7 +40,7 @@ const getStorageForWrite = (storageOverride) => { } }; -const normalizePinnedExtensions = (value) => { +export const normalizePinnedExtensions = (value) => { if (!Array.isArray(value)) { return []; } diff --git a/dashboard/src/views/extension/pluginPinnedPreferenceSync.mjs b/dashboard/src/views/extension/pluginPinnedPreferenceSync.mjs new file mode 100644 index 0000000000..2d90fd7f72 --- /dev/null +++ b/dashboard/src/views/extension/pluginPinnedPreferenceSync.mjs @@ -0,0 +1,47 @@ +/** + * 插件置顶偏好同步逻辑。 + * + * 该模块不依赖 Vue,只负责处理本地 localStorage 列表与后端列表的归一化与冲突决策, + * 便于在 node --test 中单元测试。 + */ + +import { normalizePinnedExtensions } from "./extensionPreferenceStorage.mjs"; + +export { normalizePinnedExtensions }; + +/** + * 根据后端列表与本地列表决定最终应展示的置顶顺序。 + * + * 规则: + * - 后端已有偏好记录时,以后端为准,即使列表为空也不迁移; + * - 后端没有偏好记录且本地有旧列表时,使用本地列表,并触发一次性迁移; + * - 两者都为空时,结果为空数组,不迁移。 + * + * @param {Object} options + * @param {string[]} options.localNames 从 localStorage 读取的本地列表(已归一化)。 + * @param {string[]} options.remoteNames 从后端拉取的列表(已归一化)。 + * @param {boolean} [options.preferenceExists] 后端是否存在置顶偏好记录。 + * @returns {{ names: string[]; shouldMigrate: boolean; migrateNames?: string[] }} + */ +export const resolvePinnedExtensionNames = ({ + localNames, + remoteNames, + preferenceExists, +}) => { + const normalizedRemote = normalizePinnedExtensions(remoteNames); + const normalizedLocal = normalizePinnedExtensions(localNames); + + if (preferenceExists === true || normalizedRemote.length > 0) { + return { names: normalizedRemote, shouldMigrate: false }; + } + + if (preferenceExists === false && normalizedLocal.length > 0) { + return { + names: normalizedLocal, + shouldMigrate: true, + migrateNames: normalizedLocal, + }; + } + + return { names: [], shouldMigrate: false }; +}; diff --git a/dashboard/tests/pluginPinnedPreferenceSync.test.mjs b/dashboard/tests/pluginPinnedPreferenceSync.test.mjs new file mode 100644 index 0000000000..4d6f556f64 --- /dev/null +++ b/dashboard/tests/pluginPinnedPreferenceSync.test.mjs @@ -0,0 +1,97 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { + normalizePinnedExtensions, + resolvePinnedExtensionNames, +} from '../src/views/extension/pluginPinnedPreferenceSync.mjs'; + +test('normalizePinnedExtensions filters empty, non-string, and duplicate items', () => { + const input = ['alpha', ' beta ', 'alpha', '', null, undefined, 1, 'beta']; + assert.deepEqual(normalizePinnedExtensions(input), ['alpha', 'beta']); +}); + +test('normalizePinnedExtensions returns empty array for non-array input', () => { + assert.deepEqual(normalizePinnedExtensions(null), []); + assert.deepEqual(normalizePinnedExtensions({ key: 'value' }), []); + assert.deepEqual(normalizePinnedExtensions('alpha,beta'), []); +}); + +test('resolvePinnedExtensionNames prefers remote list when present', () => { + const result = resolvePinnedExtensionNames({ + localNames: ['local-a', 'local-b'], + remoteNames: ['remote-a', 'remote-b'], + preferenceExists: true, + }); + + assert.deepEqual(result.names, ['remote-a', 'remote-b']); + assert.equal(result.shouldMigrate, false); +}); + +test('resolvePinnedExtensionNames migrates local list when remote record is missing', () => { + const result = resolvePinnedExtensionNames({ + localNames: ['local-a', 'local-b', 'local-a', ''], + remoteNames: [], + preferenceExists: false, + }); + + assert.deepEqual(result.names, ['local-a', 'local-b']); + assert.equal(result.shouldMigrate, true); + assert.deepEqual(result.migrateNames, ['local-a', 'local-b']); +}); + +test('resolvePinnedExtensionNames keeps empty remote record instead of migrating local list', () => { + const result = resolvePinnedExtensionNames({ + localNames: ['local-a', 'local-b'], + remoteNames: [], + preferenceExists: true, + }); + + assert.deepEqual(result.names, []); + assert.equal(result.shouldMigrate, false); +}); + +test('resolvePinnedExtensionNames returns empty list when both sides are empty', () => { + const result = resolvePinnedExtensionNames({ + localNames: [], + remoteNames: [], + preferenceExists: false, + }); + + assert.deepEqual(result.names, []); + assert.equal(result.shouldMigrate, false); +}); + +test('resolvePinnedExtensionNames ignores dirty missing remote data and falls back to local', () => { + const result = resolvePinnedExtensionNames({ + localNames: ['local-a'], + remoteNames: ['', null, 1], + preferenceExists: false, + }); + + assert.deepEqual(result.names, ['local-a']); + assert.equal(result.shouldMigrate, true); + assert.deepEqual(result.migrateNames, ['local-a']); +}); + +test('resolvePinnedExtensionNames does not migrate when remote existence is unknown', () => { + const result = resolvePinnedExtensionNames({ + localNames: ['local-a'], + remoteNames: [], + preferenceExists: undefined, + }); + + assert.deepEqual(result.names, []); + assert.equal(result.shouldMigrate, false); +}); + +test('resolvePinnedExtensionNames treats dirty existing remote data as empty record', () => { + const result = resolvePinnedExtensionNames({ + localNames: ['local-a'], + remoteNames: ['', null, 1], + preferenceExists: true, + }); + + assert.deepEqual(result.names, []); + assert.equal(result.shouldMigrate, false); +}); diff --git a/openspec/openapi-v1.yaml b/openspec/openapi-v1.yaml index 26b788ad92..39610dc3c0 100644 --- a/openspec/openapi-v1.yaml +++ b/openspec/openapi-v1.yaml @@ -32,6 +32,7 @@ tags: - name: Plugins - name: Plugin Sources - name: Plugin Pages + - name: Plugin Preferences - name: Commands - name: Tools - name: MCP @@ -2248,6 +2249,38 @@ paths: "200": $ref: "#/components/responses/Ok" + /api/v1/plugins/preferences/pinned: + get: + tags: [Plugin Preferences] + summary: Get pinned extension names + operationId: getPinnedExtensions + x-astrbot-scope: plugin + responses: + "200": + description: Pinned extension preference + content: + application/json: + schema: + $ref: "#/components/schemas/PluginPinnedExtensionsEnvelope" + put: + tags: [Plugin Preferences] + summary: Update pinned extension names + operationId: updatePinnedExtensions + x-astrbot-scope: plugin + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/PluginPinnedExtensionsRequest" + responses: + "200": + description: Pinned extension preference + content: + application/json: + schema: + $ref: "#/components/schemas/PluginPinnedExtensionsEnvelope" + /api/v1/plugins/install/github: post: tags: [Plugins] @@ -5484,6 +5517,35 @@ components: format: uri additionalProperties: false + PluginPinnedExtensionsRequest: + type: object + properties: + pinned_extensions: + type: array + items: {} + additionalProperties: false + + PluginPinnedExtensionsData: + type: object + required: [pinned_extensions, preference_exists] + properties: + pinned_extensions: + type: array + items: + type: string + preference_exists: + type: boolean + additionalProperties: false + + PluginPinnedExtensionsEnvelope: + allOf: + - $ref: "#/components/schemas/SuccessEnvelope" + - type: object + required: [data] + properties: + data: + $ref: "#/components/schemas/PluginPinnedExtensionsData" + CommandPatchRequest: type: object properties: diff --git a/tests/test_fastapi_v1_dashboard.py b/tests/test_fastapi_v1_dashboard.py index 7e73869dd7..1be43777f0 100644 --- a/tests/test_fastapi_v1_dashboard.py +++ b/tests/test_fastapi_v1_dashboard.py @@ -78,6 +78,36 @@ def __init__(self) -> None: self.touched_key_ids: list[str] = [] self.umo_ids = ["webchat:FriendMessage:webchat!user!session-1"] self.preferences: list[object] = [] + self._preferences: dict[tuple[str, str, str], object] = {} + self.raise_on_get_preference = False + self.raise_on_insert_preference = False + + async def get_preference(self, scope: str, scope_id: str, key: str): + if self.raise_on_get_preference: + raise RuntimeError("preference database unavailable") + return self._preferences.get((scope, scope_id, key)) + + async def insert_preference_or_update( + self, scope: str, scope_id: str, key: str, value: dict + ): + from types import SimpleNamespace + + if self.raise_on_insert_preference: + raise RuntimeError("preference database unavailable") + + pref = self._preferences.get((scope, scope_id, key)) + if pref is not None: + pref.value = value + else: + pref = SimpleNamespace( + scope=scope, + scope_id=scope_id, + key=key, + value=value, + ) + self._preferences[(scope, scope_id, key)] = pref + self.preferences.append(pref) + return pref async def get_active_api_key_by_hash(self, key_hash: str) -> FakeApiKey | None: return self.api_keys.get(key_hash) @@ -2548,3 +2578,188 @@ async def test_v1_platform_webhook_is_public_route( "method": "POST", "payload": {"challenge": "ping"}, } + + +@pytest.mark.asyncio +async def test_v1_plugin_preferences_pinned_initial_empty( + asgi_client: httpx.AsyncClient, +): + response = await asgi_client.get( + "/api/v1/plugins/preferences/pinned", + headers=_jwt_headers(), + ) + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "ok" + assert data["data"] == { + "pinned_extensions": [], + "preference_exists": False, + } + + +@pytest.mark.asyncio +async def test_v1_plugin_preferences_pinned_put_normalizes_and_gets( + asgi_client: httpx.AsyncClient, +): + headers = _jwt_headers() + + put_response = await asgi_client.put( + "/api/v1/plugins/preferences/pinned", + json={"pinned_extensions": ["alpha", "beta", "alpha", "", None]}, + headers=headers, + ) + + assert put_response.status_code == 200 + put_data = put_response.json() + assert put_data["status"] == "ok" + assert put_data["data"] == { + "pinned_extensions": ["alpha", "beta"], + "preference_exists": True, + } + + get_response = await asgi_client.get( + "/api/v1/plugins/preferences/pinned", + headers=headers, + ) + + assert get_response.status_code == 200 + get_data = get_response.json() + assert get_data["status"] == "ok" + assert get_data["data"] == { + "pinned_extensions": ["alpha", "beta"], + "preference_exists": True, + } + + +@pytest.mark.asyncio +async def test_v1_plugin_preferences_pinned_distinguishes_existing_empty( + asgi_client: httpx.AsyncClient, +): + headers = _jwt_headers() + + put_response = await asgi_client.put( + "/api/v1/plugins/preferences/pinned", + json={"pinned_extensions": []}, + headers=headers, + ) + get_response = await asgi_client.get( + "/api/v1/plugins/preferences/pinned", + headers=headers, + ) + + assert put_response.status_code == 200 + assert put_response.json()["data"] == { + "pinned_extensions": [], + "preference_exists": True, + } + assert get_response.status_code == 200 + assert get_response.json()["data"] == { + "pinned_extensions": [], + "preference_exists": True, + } + + +@pytest.mark.asyncio +async def test_v1_plugin_preferences_pinned_get_db_error_is_not_empty( + asgi_client: httpx.AsyncClient, + fake_db: FakeDb, +): + fake_db.raise_on_get_preference = True + + response = await asgi_client.get( + "/api/v1/plugins/preferences/pinned", + headers=_jwt_headers(), + ) + + assert response.status_code == 500 + data = response.json() + assert data["status"] == "error" + assert data["message"] == "加载插件置顶偏好失败" + + +@pytest.mark.asyncio +async def test_v1_plugin_preferences_pinned_put_db_error_is_not_success( + asgi_client: httpx.AsyncClient, + fake_db: FakeDb, +): + fake_db.raise_on_insert_preference = True + + response = await asgi_client.put( + "/api/v1/plugins/preferences/pinned", + json={"pinned_extensions": ["alpha"]}, + headers=_jwt_headers(), + ) + + assert response.status_code == 500 + data = response.json() + assert data["status"] == "error" + assert data["message"] == "保存插件置顶偏好失败" + + +@pytest.mark.asyncio +async def test_v1_plugin_preferences_pinned_requires_auth( + asgi_client: httpx.AsyncClient, +): + get_response = await asgi_client.get("/api/v1/plugins/preferences/pinned") + put_response = await asgi_client.put( + "/api/v1/plugins/preferences/pinned", + json={"pinned_extensions": ["alpha"]}, + ) + + assert get_response.status_code == 401 + assert put_response.status_code == 401 + + +@pytest.mark.asyncio +async def test_v1_plugin_preferences_pinned_accepts_plugin_api_key( + asgi_client: httpx.AsyncClient, + fake_db: FakeDb, +): + raw_key = "abk_plugin_preferences_plugin" + fake_db.add_api_key(raw_key, scopes=["plugin"]) + headers = {"X-API-Key": raw_key} + + put_response = await asgi_client.put( + "/api/v1/plugins/preferences/pinned", + json={"pinned_extensions": ["alpha", "beta"]}, + headers=headers, + ) + get_response = await asgi_client.get( + "/api/v1/plugins/preferences/pinned", + headers=headers, + ) + + assert put_response.status_code == 200 + assert put_response.json()["data"] == { + "pinned_extensions": ["alpha", "beta"], + "preference_exists": True, + } + assert get_response.status_code == 200 + assert get_response.json()["data"] == { + "pinned_extensions": ["alpha", "beta"], + "preference_exists": True, + } + + +@pytest.mark.asyncio +async def test_v1_plugin_preferences_pinned_rejects_non_plugin_api_key( + asgi_client: httpx.AsyncClient, + fake_db: FakeDb, +): + raw_key = "abk_plugin_preferences_bot" + fake_db.add_api_key(raw_key, scopes=["bot"]) + headers = {"X-API-Key": raw_key} + + put_response = await asgi_client.put( + "/api/v1/plugins/preferences/pinned", + json={"pinned_extensions": ["alpha"]}, + headers=headers, + ) + get_response = await asgi_client.get( + "/api/v1/plugins/preferences/pinned", + headers=headers, + ) + + assert put_response.status_code == 403 + assert get_response.status_code == 403