From 43356043bb467d19bef64fb707292997f1195344 Mon Sep 17 00:00:00 2001 From: niansia Date: Tue, 6 Oct 2026 14:40:04 +0800 Subject: [PATCH 1/4] feat(dashboard): export logs from the console and settings pages Add GET /api/v1/logs/export, which packs the log files and the in-memory logs into a zip archive. The files come from StorageCleaner, so log and trace log paths configured outside data/logs are included too, and the in-memory cache is split into memory/logs.txt and memory/traces.jsonl. Add an Export Logs button to the console page and to the log and cache cleanup panel in Settings. Closes #10407 --- astrbot/core/utils/storage_cleaner.py | 12 +++- astrbot/dashboard/api/logs.py | 23 ++++++- astrbot/dashboard/services/log_service.py | 61 +++++++++++++++++++ .../src/api/generated/openapi-v1/sdk.gen.ts | 12 +++- .../src/api/generated/openapi-v1/types.gen.ts | 4 ++ dashboard/src/api/v1.ts | 5 ++ .../src/components/shared/LogExportButton.vue | 48 +++++++++++++++ .../components/shared/StorageCleanupPanel.vue | 2 + .../i18n/locales/en-US/features/console.json | 5 ++ .../i18n/locales/ja-JP/features/console.json | 5 ++ .../i18n/locales/ru-RU/features/console.json | 5 ++ .../i18n/locales/zh-CN/features/console.json | 5 ++ dashboard/src/views/ConsolePage.vue | 2 + docs/en/use/webui.md | 2 + docs/zh/use/webui.md | 2 + openspec/openapi-v1.yaml | 15 +++++ tests/test_fastapi_v1_dashboard.py | 53 ++++++++++++++++ 17 files changed, 256 insertions(+), 5 deletions(-) create mode 100644 dashboard/src/components/shared/LogExportButton.vue diff --git a/astrbot/core/utils/storage_cleaner.py b/astrbot/core/utils/storage_cleaner.py index 134071dce9e..b7e5a4a7eb5 100644 --- a/astrbot/core/utils/storage_cleaner.py +++ b/astrbot/core/utils/storage_cleaner.py @@ -76,7 +76,7 @@ def cleanup(self, target: str = "all") -> dict: def _build_status(self, target: str) -> dict: if target == self.TARGET_LOGS: - files = self._collect_log_files() + files = self.collect_log_files() primary_path = self._data_dir / "logs" elif target == self.TARGET_CACHE: files = self._collect_cache_files() @@ -94,7 +94,7 @@ def _build_status(self, target: str) -> dict: def _cleanup_target(self, target: str) -> dict: if target == self.TARGET_LOGS: - files = self._collect_log_files() + files = self.collect_log_files() active_log_files = self._active_log_files() elif target == self.TARGET_CACHE: files = self._collect_cache_files() @@ -151,7 +151,13 @@ def _cleanup_target(self, target: str) -> dict: "failed_files": failed_files, } - def _collect_log_files(self) -> set[Path]: + def collect_log_files(self) -> set[Path]: + """Collect files under data/logs plus the configured log files. + + Returns: + The log files, including rotated ones of the configured log and + trace log paths, even when those are outside data/logs. + """ files = set(self._iter_files(self._data_dir / "logs")) for log_path in self._configured_log_paths(): files.update(self._iter_log_family_files(log_path)) diff --git a/astrbot/dashboard/api/logs.py b/astrbot/dashboard/api/logs.py index de5e6b5729e..ac4ad066619 100644 --- a/astrbot/dashboard/api/logs.py +++ b/astrbot/dashboard/api/logs.py @@ -1,7 +1,11 @@ from __future__ import annotations +import asyncio +import time + from fastapi import APIRouter, Depends, Header, Request -from fastapi.responses import StreamingResponse +from fastapi.responses import FileResponse, StreamingResponse +from starlette.background import BackgroundTask from astrbot.dashboard.responses import ApiError, ok from astrbot.dashboard.schemas import TraceSettingsRequest @@ -80,6 +84,23 @@ async def live_logs( return _log_stream_response(last_event_id, service) +@router.get("/logs/export") +async def export_logs( + _auth: AuthContext = Depends(require_system_scope), + service: LogService = Depends(get_service), +): + try: + archive_path = await asyncio.to_thread(service.export_logs) + except LogServiceError as exc: + _raise_log_error(exc) + return FileResponse( + archive_path, + media_type="application/zip", + filename=f"astrbot-logs-{time.strftime('%Y%m%d-%H%M%S')}.zip", + background=BackgroundTask(archive_path.unlink, missing_ok=True), + ) + + @router.get("/trace/settings") async def get_trace_settings( _auth: AuthContext = Depends(require_system_scope), diff --git a/astrbot/dashboard/services/log_service.py b/astrbot/dashboard/services/log_service.py index 52cbcc05d82..b4707a2e8df 100644 --- a/astrbot/dashboard/services/log_service.py +++ b/astrbot/dashboard/services/log_service.py @@ -3,10 +3,15 @@ import asyncio import json import time +import uuid +import zipfile from collections.abc import AsyncGenerator +from pathlib import Path from astrbot.core import LogBroker, logger from astrbot.core.config.astrbot_config import AstrBotConfig +from astrbot.core.utils.astrbot_path import get_astrbot_data_path, get_astrbot_temp_path +from astrbot.core.utils.storage_cleaner import StorageCleaner class LogServiceError(Exception): @@ -69,6 +74,62 @@ def get_log_history(self) -> dict: logger.error(f"获取日志历史失败: {exc}") raise LogServiceError(f"获取日志历史失败: {exc}") from exc + def export_logs(self) -> Path: + """Pack the log files and the in-memory logs into a zip archive. + + The archive holds the files found by ``StorageCleaner`` (data/logs plus + the configured log and trace log files) and the in-memory cache, split + into ``memory/logs.txt`` and ``memory/traces.jsonl``. + + Returns: + Path of the archive in the temp directory. The caller removes it + once it has been sent. + + Raises: + LogServiceError: If the archive cannot be written. + """ + cached = list(self.log_broker.log_cache) + logs = [ + str(item.get("data", "")) for item in cached if item.get("type") != "trace" + ] + traces = [item for item in cached if item.get("type") == "trace"] + data_dir = Path(get_astrbot_data_path()).resolve() + export_dir = Path(get_astrbot_temp_path()) / "log_exports" + export_dir.mkdir(parents=True, exist_ok=True) + archive_path = export_dir / f"astrbot-logs-{uuid.uuid4().hex}.zip" + log_files = { + path.resolve() for path in StorageCleaner(self.config).collect_log_files() + } + try: + with zipfile.ZipFile(archive_path, "w", zipfile.ZIP_DEFLATED) as archive: + for file_path in sorted(log_files): + if file_path.is_relative_to(data_dir): + arcname = file_path.relative_to(data_dir).as_posix() + else: + arcname = f"external/{file_path.name}" + try: + archive.write(file_path, arcname) + except OSError as exc: + # A log file can be rotated away while we are packing. + logger.warning( + f"Skipped unreadable log file {file_path}: {exc}" + ) + archive.writestr( + "memory/logs.txt", "".join(f"{line}\n" for line in logs) + ) + archive.writestr( + "memory/traces.jsonl", + "".join( + json.dumps(trace, ensure_ascii=False, default=str) + "\n" + for trace in traces + ), + ) + except Exception as exc: + archive_path.unlink(missing_ok=True) + logger.error(f"Failed to export logs: {exc}") + raise LogServiceError(f"Failed to export logs: {exc}") from exc + return archive_path + def get_trace_settings(self) -> dict: try: return {"trace_enable": self.config.get("trace_enable", True)} diff --git a/dashboard/src/api/generated/openapi-v1/sdk.gen.ts b/dashboard/src/api/generated/openapi-v1/sdk.gen.ts index 00c85e29c80..759fd6c5df7 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, ListPluginPagesByIdData, ListPluginPagesByIdError, ListPluginPagesByIdResponse, GetPluginPageByIdData, GetPluginPageByIdError, GetPluginPageByIdResponse, GetPluginPageAssetByIdData, GetPluginPageAssetByIdError, GetPluginPageAssetByIdResponse, 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, ListPluginPagesData, ListPluginPagesError, ListPluginPagesResponse, GetPluginPageData, GetPluginPageError, GetPluginPageResponse, GetPluginPageAssetData, GetPluginPageAssetError, GetPluginPageAssetResponse, ListPluginViewsByIdData, ListPluginViewsByIdError, ListPluginViewsByIdResponse, GetPluginViewByIdData, GetPluginViewByIdError, GetPluginViewByIdResponse, GetPluginViewAssetByIdData, GetPluginViewAssetByIdError, GetPluginViewAssetByIdResponse, ListPluginViewsData, ListPluginViewsError, ListPluginViewsResponse, GetPluginViewData, GetPluginViewError, GetPluginViewResponse, GetPluginViewAssetData, GetPluginViewAssetError, GetPluginViewAssetResponse, 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, 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, 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, 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, ListPluginPagesData, ListPluginPagesError, ListPluginPagesResponse, GetPluginPageData, GetPluginPageError, GetPluginPageResponse, GetPluginPageAssetData, GetPluginPageAssetError, GetPluginPageAssetResponse, ListPluginViewsByIdData, ListPluginViewsByIdError, ListPluginViewsByIdResponse, GetPluginViewByIdData, GetPluginViewByIdError, GetPluginViewByIdResponse, GetPluginViewAssetByIdData, GetPluginViewAssetByIdError, GetPluginViewAssetByIdResponse, ListPluginViewsData, ListPluginViewsError, ListPluginViewsResponse, GetPluginViewData, GetPluginViewError, GetPluginViewResponse, GetPluginViewAssetData, GetPluginViewAssetError, GetPluginViewAssetResponse, 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, ExportLogsError, ExportLogsResponse, 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()); @@ -3144,6 +3144,16 @@ export const getLogHistory = (options?: Op }); }; +/** + * Export log files and in-memory logs as a zip archive + */ +export const exportLogs = (options?: OptionsLegacyParser) => { + return (options?.client ?? client).get({ + ...options, + url: '/api/v1/logs/export' + }); +}; + /** * Get trace settings */ diff --git a/dashboard/src/api/generated/openapi-v1/types.gen.ts b/dashboard/src/api/generated/openapi-v1/types.gen.ts index 80720abd81b..6d1ec56b17e 100644 --- a/dashboard/src/api/generated/openapi-v1/types.gen.ts +++ b/dashboard/src/api/generated/openapi-v1/types.gen.ts @@ -3748,6 +3748,10 @@ export type GetLogHistoryResponse = (string); export type GetLogHistoryError = unknown; +export type ExportLogsResponse = ((Blob | File)); + +export type ExportLogsError = unknown; + export type GetTraceSettingsResponse = (SuccessEnvelope); export type GetTraceSettingsError = unknown; diff --git a/dashboard/src/api/v1.ts b/dashboard/src/api/v1.ts index 9c6e11324cb..fd7817e14ed 100644 --- a/dashboard/src/api/v1.ts +++ b/dashboard/src/api/v1.ts @@ -1234,6 +1234,11 @@ export const logApi = { history() { return typed<{ logs?: OpenConfig[] }>(openApiV1.getLogHistory()); }, + exportArchive() { + return openApiV1.exportLogs({ + responseType: 'blob', + }) as Promise>; + }, liveUrl() { return '/api/v1/logs/live'; }, diff --git a/dashboard/src/components/shared/LogExportButton.vue b/dashboard/src/components/shared/LogExportButton.vue new file mode 100644 index 00000000000..6fea6832145 --- /dev/null +++ b/dashboard/src/components/shared/LogExportButton.vue @@ -0,0 +1,48 @@ + + + diff --git a/dashboard/src/components/shared/StorageCleanupPanel.vue b/dashboard/src/components/shared/StorageCleanupPanel.vue index 9ecc8683c2d..3ad1052c6cb 100644 --- a/dashboard/src/components/shared/StorageCleanupPanel.vue +++ b/dashboard/src/components/shared/StorageCleanupPanel.vue @@ -12,6 +12,7 @@ {{ formatBytes(storageStatus.total_bytes || 0) }} + diff --git a/dashboard/src/i18n/locales/en-US/features/console.json b/dashboard/src/i18n/locales/en-US/features/console.json index d8e5134763e..4f2f2ebaed7 100644 --- a/dashboard/src/i18n/locales/en-US/features/console.json +++ b/dashboard/src/i18n/locales/en-US/features/console.json @@ -42,9 +42,15 @@ "requestFailed": "Request failed." }, "exportLogs": { - "button": "Export Logs", - "hint": "Packs the log files under data/logs and the recent in-memory logs. Without \"Enable File Logging\", no new log files are written, so the archive mostly has the in-memory logs. Logs may contain chat messages and user IDs, so check them before sharing publicly.", - "failed": "Failed to export logs." + "button": "Export diagnostics", + "hint": "Export logs, traces, configuration, plugin status and environment details.", + "failed": "Could not export diagnostics. Please try again.", + "contents": "Includes available disk and rotated logs, up to 500 cached events, available trace history, AstrBot configuration, plugin status and environment details. Console filters do not affect the export. Existing data can be exported even when file logging or tracing is disabled.", + "privacy": "Recognized passwords, keys and tokens are masked, and public IPs use consistent aliases throughout the bundle. Local and other non-public addresses remain. Logs, traces and settings may still contain conversations, account identifiers or other personal data. Review before sharing. The bundle is downloaded locally.", + "cancel": "Cancel", + "redact": "Redact credentials (recommended)", + "rawPrivacy": "Redaction is off: the bundle retains original passwords, keys, tokens, public IPs and other sensitive information. Use only for private diagnosis with someone you trust; do not upload it to public issues or group chats.", + "download": "Download bundle" }, "debugHint": { "text": "Debug logs can be enabled in \"Configuration File → System → Console Log Level\"" diff --git a/dashboard/src/i18n/locales/ja-JP/features/console.json b/dashboard/src/i18n/locales/ja-JP/features/console.json index eb524a1496e..276aa944272 100644 --- a/dashboard/src/i18n/locales/ja-JP/features/console.json +++ b/dashboard/src/i18n/locales/ja-JP/features/console.json @@ -42,9 +42,15 @@ "requestFailed": "リクエストに失敗しました。" }, "exportLogs": { - "button": "ログをエクスポート", - "hint": "data/logs 内のログファイルとメモリ上の最近のログをまとめます。「ファイルログを有効化」がオフの場合は新しいログファイルが書き込まれないため、主にメモリ上のログになります。ログにはチャット内容やユーザー ID が含まれることがあるため、公開する前に確認してください。", - "failed": "ログのエクスポートに失敗しました。" + "button": "診断パッケージを出力", + "hint": "ログ、トレース、設定、プラグイン状態、実行環境を出力します。", + "failed": "診断パッケージの出力に失敗しました。もう一度お試しください。", + "contents": "既存のファイルログとローテーションログ、最大 500 件のキャッシュ、利用可能なトレース履歴、AstrBot の設定、プラグイン状態、実行環境を含みます。画面のフィルターは適用されません。ファイルログやトレースが無効でも既存データを出力できます。", + "privacy": "識別されたパスワード、キー、トークンはマスクされ、公開 IP はアーカイブ全体で同じ番号に置換されます。内部・ループバックなどの非公開アドレスは保持されます。ログ、トレース、設定には会話やアカウントなどの個人情報が残る場合があります。共有前にご確認ください。ファイルはローカルにダウンロードされます。", + "cancel": "キャンセル", + "redact": "認証情報をマスクする(推奨)", + "rawPrivacy": "マスクが無効です。パスワード、キー、トークン、公開 IP などの機密情報が元のまま含まれます。信頼できる相手との非公開の診断にのみ使用し、公開 Issue やグループチャットに投稿しないでください。", + "download": "診断パッケージをダウンロード" }, "debugHint": { "text": "Debug ログは「設定ファイル → システム → コンソールログレベル」で有効にできます" diff --git a/dashboard/src/i18n/locales/ru-RU/features/console.json b/dashboard/src/i18n/locales/ru-RU/features/console.json index 227adf99cae..f49d956a896 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/console.json +++ b/dashboard/src/i18n/locales/ru-RU/features/console.json @@ -1,4 +1,4 @@ -{ +{ "title": "Логи платформы", "hideUserChat": { "label": "Скрыть чаты пользователей", @@ -42,9 +42,15 @@ "requestFailed": "Ошибка запроса." }, "exportLogs": { - "button": "Экспорт логов", - "hint": "Упаковывает файлы логов из data/logs и последние логи из памяти. Если «Включить логирование в файл» выключено, новые файлы логов не пишутся, поэтому в архиве в основном логи из памяти. Логи могут содержать сообщения чата и ID пользователей — проверьте их перед публикацией.", - "failed": "Не удалось экспортировать логи." + "button": "Экспорт диагностики", + "hint": "Экспорт журналов, трассировок, настроек, состояния плагинов и среды.", + "failed": "Не удалось экспортировать диагностику. Повторите попытку.", + "contents": "Архив содержит доступные файлы журналов, включая ротацию, до 500 записей из памяти, доступную историю трассировок, настройки AstrBot, состояние плагинов и сведения о среде. Фильтры консоли не влияют на экспорт. Существующие данные доступны и при отключённом журналировании или трассировке.", + "privacy": "Распознанные пароли, ключи и токены скрываются, а публичные IP заменяются едиными обозначениями во всём архиве. Локальные и другие непубличные адреса сохраняются. Журналы, трассировки и настройки могут содержать переписку, идентификаторы и другие личные данные. Проверьте файлы перед отправкой. Архив скачивается локально.", + "cancel": "Отмена", + "redact": "Скрывать учётные данные (рекомендуется)", + "rawPrivacy": "Маскирование отключено: архив содержит исходные пароли, ключи, токены, публичные IP и другие конфиденциальные данные. Используйте его только для частной диагностики с доверенным человеком; не публикуйте в открытых задачах или групповых чатах.", + "download": "Скачать архив" }, "debugHint": { "text": "Для отображения Debug-логов необходимо установить соответствующий уровень в «Конфигурация → Система → Уровень логирования»" diff --git a/dashboard/src/i18n/locales/zh-CN/features/console.json b/dashboard/src/i18n/locales/zh-CN/features/console.json index df33c408c4c..3ec96e5ddb0 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/console.json +++ b/dashboard/src/i18n/locales/zh-CN/features/console.json @@ -42,9 +42,15 @@ "requestFailed": "请求失败。" }, "exportLogs": { - "button": "导出日志", - "hint": "打包 data/logs 下的日志文件和内存中最近的日志。未开启「启用文件日志」时不会写入新的日志文件,压缩包里主要是内存中的日志;日志里可能有聊天内容和用户 ID,公开分享前请先检查。", - "failed": "导出日志失败。" + "button": "导出诊断包", + "hint": "导出排障所需的日志、Trace、配置、插件状态和运行环境。", + "failed": "诊断包导出失败,请稍后重试。", + "contents": "包含现有磁盘及轮转日志、最近最多 500 条缓存、可用的 Trace 历史、AstrBot 配置、插件状态和运行环境。导出不受页面筛选影响;未开启文件日志或 Trace 也可导出已有内容。", + "privacy": "已识别的密码、密钥和 Token 将自动遮盖,公网 IP 替换为包内统一编号;内网、回环等非公网地址保留。日志、Trace 和设置仍可能包含聊天内容、账号及其他个人信息,分享前请检查。文件仅下载到本地。", + "cancel": "取消", + "redact": "脱敏导出(推荐)", + "rawPrivacy": "已关闭脱敏:诊断包将保留原始密码、密钥、Token、公网 IP 等敏感信息。仅用于可信的私下排障,请勿上传至公开 Issue 或群聊。", + "download": "下载诊断包" }, "debugHint": { "text": "Debug 日志需要在「配置文件 → 系统 → 控制台日志级别」中开启" diff --git a/docs/en/use/webui.md b/docs/en/use/webui.md index 0a68e0de279..465fd956095 100644 --- a/docs/en/use/webui.md +++ b/docs/en/use/webui.md @@ -168,7 +168,7 @@ Use the `Conversations` tab to find and manage saved conversation records: The `Logs` tab shows AstrBot runtime logs in real time. You can filter by log level and install missing Pip packages from this page. To view DEBUG logs, first set `Console Log Level` to `DEBUG` under `System Settings → General → Logs`. -Click `Export Logs` in the top right to download a zip archive with the log files under `data/logs` (log files configured elsewhere through `Log File Path` and `Trace Log File Path` are included too) and the recent in-memory logs and traces, which you can hand to developers when reporting a problem. `Enable File Logging` is off by default. In that case no new log files are written, so the archive mostly has the recent in-memory logs (plus any older log files still on disk). For full logs, turn on `Enable File Logging` under `System Settings → General → Logs`, reproduce the problem, and then export. Logs may contain chat messages and user IDs, so check them before sharing publicly. The log and cache cleanup panel under `System Settings → General → Cache` has the same `Export Logs` button. +Click `Export diagnostics` and confirm the content and privacy notice to download a ZIP. It includes available disk and rotated logs, configured custom logs, up to 500 cached events, available trace history, configuration profiles and routes, plugin versions and status, environment details and a manifest. Existing data remains exportable when file logging or tracing is disabled; missing trace history is recorded without enabling tracing. Trace history is limited to the latest 50 traces and 16 MiB in total. Disk logs are limited to 256 MiB of source data, skipping individual files larger than 64 MiB and, in redacted mode, oversized lines. Truncation and missing items are recorded. Raw databases and plugin source code are excluded. The dialog enables `Redact credentials (recommended)` by default. Recognized credentials are masked, and public IPv4/IPv6 addresses use consistent aliases such as `[PUBLIC_IP_1]` across the bundle. Alternate spellings of the same address share an alias; protocols, ports, paths, local and other non-public addresses remain. No original-address mapping is included. Conversations and personal data may remain; review before sharing. Uncheck this option when diagnosis requires original values. This retains passwords, keys, tokens, public IPs and other sensitive data, and adds `raw` to the filename; share only privately with someone you trust. Reopening the dialog resets the option to enabled. Only authenticated dashboard administrators can download bundles; developer API keys are not accepted. The button in `System Settings → General → Cache` uses the same export. Open `Filters` next to the search box to filter by level, source (System/Plugin), individual plugin, and bot platform, or exclude user-chat logs. Combine these conditions with `Search logs`. Plugin menus prefer display names and show plugin IDs as subtitles; `System` replaces the former Core source label. diff --git a/docs/zh/use/webui.md b/docs/zh/use/webui.md index 9e9ffbf1ff3..6ef574921b8 100644 --- a/docs/zh/use/webui.md +++ b/docs/zh/use/webui.md @@ -168,7 +168,7 @@ ChatUI 支持以下常用能力: `日志` 页面显示 AstrBot 的实时运行日志,可以按日志级别筛选,也可以从页面中安装缺失的 Pip 库。需要查看 DEBUG 日志时,请先在 `系统设置 → 常规 → 日志` 中将 `控制台日志级别` 设为 `DEBUG`。 -点击右上角的 `导出日志` 可以下载一个 zip 压缩包,里面有 `data/logs` 下的日志文件(`日志文件路径` 和 `Trace 日志文件路径` 设在其他位置时也会一起打包)以及内存中最近的日志和追踪,反馈问题时可以直接提供给开发者。`启用文件日志` 默认是关闭的,此时不会写入新的日志文件,压缩包里主要是内存中最近的日志(以及之前留下的日志文件),需要完整日志时请先在 `系统设置 → 常规 → 日志` 中开启 `启用文件日志`,复现问题后再导出。日志中可能包含聊天内容和用户 ID,公开分享前请先检查。`系统设置 → 常规 → 缓存` 的日志与缓存清理面板中也有同样的 `导出日志` 按钮。 +点击右上角的 `导出诊断包`,确认内容与隐私提示后下载 ZIP。包内包含现有磁盘及轮转日志、自定义路径日志、最近最多 500 条缓存、可用的 Trace 历史、配置及路由、插件版本与状态、运行环境和导出清单。未开启文件日志或 Trace 时,仍可导出已有内容;没有 Trace 历史时会在清单中注明,不会自动开启 Trace。Trace 历史最多导出最近 50 条、合计 16 MiB;磁盘日志合计最多读取 256 MiB、单文件超过 64 MiB 时会跳过,脱敏模式下过长的日志行也会跳过,截断和缺失会记入清单,不包含原始数据库或插件源码。导出弹窗默认勾选 `脱敏导出(推荐)`,自动遮盖已识别的凭据,公网 IPv4/IPv6 地址替换为包内一致的 `[PUBLIC_IP_1]` 等编号,保留协议、端口、路径及内网、回环等非公网地址;同一 IP 的不同写法使用同一编号,不在包内附带原始 IP 对照表,但仍可能包含聊天内容和个人信息,分享前请检查。排障需要原始内容时可取消勾选;此时会保留密码、密钥、Token、公网 IP 等敏感信息,文件名带有 `raw` 标记,请仅私下提供给可信的诊断人员,勿公开上传。每次打开弹窗都会恢复默认勾选。仅控制台管理员可下载,开发者 API Key 不可使用。`系统设置 → 常规 → 缓存` 面板中的按钮使用同一功能。 点击搜索框左侧的“过滤器”,在日志容器左侧按日志级别、来源(系统/插件)、具体插件、机器人平台筛选,也可排除用户对话日志;这些条件可与“搜索日志”叠加使用。插件优先显示 display name,菜单副标题显示插件 ID;“系统”对应原先的 Core 来源。 diff --git a/openspec/openapi-v1.yaml b/openspec/openapi-v1.yaml index 40b8c6db5c2..3a22c1d3e80 100644 --- a/openspec/openapi-v1.yaml +++ b/openspec/openapi-v1.yaml @@ -5090,12 +5090,20 @@ paths: /api/v1/logs/export: get: tags: [Logs] - summary: Export log files and in-memory logs as a zip archive + summary: Export a diagnostic bundle + description: Requires a dashboard administrator session. Developer API keys are not accepted. Includes available disk and memory logs, trace history, configuration, plugin status and environment details. Credentials are masked by default; setting redact=false exports original sensitive values. operationId: exportLogs + security: + - BearerAuth: [] x-astrbot-scope: system + parameters: + - name: redact + in: query + description: Mask recognized credentials. Disable only for private diagnosis. + schema: {type: boolean, default: true} responses: "200": - description: Log archive + description: Diagnostic ZIP. With redact=false it contains original sensitive values; personal data may remain in either mode. content: application/zip: schema: diff --git a/tests/test_fastapi_v1_dashboard.py b/tests/test_fastapi_v1_dashboard.py index a77ef70aa28..ba9fb8bd9dd 100644 --- a/tests/test_fastapi_v1_dashboard.py +++ b/tests/test_fastapi_v1_dashboard.py @@ -4,8 +4,8 @@ import json import os import stat -import zipfile import time +import zipfile from dataclasses import dataclass from pathlib import Path from types import SimpleNamespace @@ -4381,9 +4381,11 @@ async def test_v1_log_export_packs_log_files_and_memory_logs( assert response.status_code == 200 assert response.headers["content-type"] == "application/zip" - assert "astrbot-logs-" in response.headers["content-disposition"] + assert "astrbot-diagnostics-" in response.headers["content-disposition"] with zipfile.ZipFile(io.BytesIO(response.content)) as archive: assert sorted(archive.namelist()) == [ + "config.json", + "environment.json", "external/2/custom.log", "external/custom.1.log", "external/custom.log", @@ -4391,6 +4393,8 @@ async def test_v1_log_export_packs_log_files_and_memory_logs( "manifest.json", "memory/logs.txt", "memory/traces.jsonl", + "plugins.json", + "traces/history.jsonl", ] assert archive.read("external/custom.log") == b"current" assert archive.read("external/custom.1.log") == b"rotated" @@ -4398,7 +4402,8 @@ async def test_v1_log_export_packs_log_files_and_memory_logs( assert archive.read("logs/event_loop_watchdog.log") == b"watchdog" assert archive.read("memory/logs.txt").decode() == "[INFO] hello\n" traces = archive.read("memory/traces.jsonl").decode().splitlines() - assert [json.loads(line)["span_id"] for line in traces] == ["span-1"] + # Current LogBroker only retains ordinary logs; traces are persisted. + assert traces == [] manifest = json.loads(archive.read("manifest.json")) assert sorted(manifest["files"]) == [ "external/2/custom.log", @@ -4407,7 +4412,7 @@ async def test_v1_log_export_packs_log_files_and_memory_logs( "logs/event_loop_watchdog.log", ] assert manifest["skipped"] == [] - assert (manifest["memory_logs"], manifest["memory_traces"]) == (1, 1) + assert (manifest["memory_logs"], manifest["memory_traces"]) == (1, 0) assert str(tmp_path) not in json.dumps(manifest) assert not any((tmp_path / "data" / "temp" / "log_exports").iterdir()) @@ -4514,12 +4519,225 @@ async def test_v1_log_export_runs_one_export_at_a_time( @pytest.mark.asyncio -async def test_v1_log_export_requires_authentication(asgi_client: httpx.AsyncClient): - response = await asgi_client.get("/api/v1/logs/export") +@pytest.mark.parametrize("redact", [True, False]) +async def test_v1_log_export_requires_authentication(asgi_client, redact): + response = await asgi_client.get("/api/v1/logs/export", params={"redact": redact}) assert response.status_code == 401 -async def test_v1_persistent_trace_routes(asgi_client, monkeypatch, tmp_path): + +@pytest.mark.asyncio +@pytest.mark.parametrize("redact", [True, False]) +@pytest.mark.parametrize( + "method", ["developer_header", "developer_bearer", "query", "expired"] +) +async def test_diagnostic_export_rejects_non_admin_auth( + method, redact, fake_db, asgi_app, asgi_client +): + from unittest.mock import Mock + + fake_db.add_api_key("diagnostic-developer-key", ["system", "config", "*"]) + export = Mock( + side_effect=AssertionError("Export must not run before authentication") + ) + asgi_app.state.services.logs.export_logs = export + options = { + "developer_header": {"headers": {"X-API-Key": "diagnostic-developer-key"}}, + "developer_bearer": { + "headers": {"Authorization": "Bearer diagnostic-developer-key"} + }, + "query": {"params": {"api_key": "diagnostic-developer-key"}}, + "expired": { + "headers": { + "Authorization": "Bearer " + + jwt.encode( + {"username": "admin", "exp": 1}, JWT_SECRET, algorithm="HS256" + ) + } + }, + } + options[method].setdefault("params", {})["redact"] = redact + response = await asgi_client.get("/api/v1/logs/export", **options[method]) + assert response.status_code == 401 + export.assert_not_called() + + +@pytest.mark.asyncio +@pytest.mark.parametrize("redact", [True, False]) +async def test_diagnostic_export_respects_redaction_for_every_section( + monkeypatch, tmp_path, asgi_app, asgi_client, redact +): + from urllib.parse import quote + + monkeypatch.setenv("ASTRBOT_ROOT", str(tmp_path)) + service = asgi_app.state.services.logs + config = service.config + config["trace_enable"] = False + secret = "private/configured+credential" + plugin_secret = "private-plugin-credential" + trace_secret = "previous-provider-credential" + config["diagnostic_network"] = { + "public_endpoint": "http://8.8.8.8:8080/v1", + "public_ipv6": "https://[2606:4700:4700::1111]:443/v1", + "local_endpoint": "http://192.168.1.2:6185/health", + } + config["provider_sources"] = [ + {"key": [secret], "api_base": "https://example.com/v1"} + ] + config.setdefault("dashboard", {}).update( + password="private-password", + jwt_secret="private-jwt-secret", + password_storage_upgraded=True, + password_change_required=False, + ) + config["max_tokens"] = 4096 + plugin = service.core.plugin_manager.context.get_all_stars()[0] + plugin.config = {"api_key": plugin_secret} + logs = tmp_path / "data/logs" + logs.mkdir(parents=True) + (logs / "astrbot.log").write_text( + f"{secret}\n{quote(secret, safe='')}\n{plugin_secret}\n{trace_secret}\nhttp://8.8.8.8:8080/v1 https://[2606:4700:4700::1111]:443/v1 http://192.168.1.2:6185/health\nAuthorization: Bearer unknown-header-token\nCookie: session=unknown; second=secret\n-----BEGIN PRIVATE KEY-----\nprivate-key-body\n-----END PRIVATE KEY-----\n", + encoding="utf-8", + ) + (logs / "astrbot.1.log").write_text(plugin_secret, encoding="utf-8") + (logs / "binary.log").write_bytes(b"\x00binary-private-data") + service.log_broker.publish({"time": 1, "level": "INFO", "data": f"{secret} peer=8.8.8.8"}) + service.log_broker.publish( + {"type": "trace", "time": 2, "fields": {"api_key": secret, "enabled": True}} + ) + (logs / "traces.db").touch() + + class Store: + def list_traces(self, **_kwargs): + return {"traces": [{"trace_id": "a" * 32}], "total": 1} + + def get_trace(self, _trace_id): + return { + "trace": {"trace_id": "a" * 32}, + "spans": [ + {"attributes": {"api_key": trace_secret, "message": plugin_secret, "peer": "::ffff:8.8.8.8", "ipv6": "2606:4700:4700:0:0:0:0:1111"}} + ], + } + + monkeypatch.setattr( + "astrbot.dashboard.services.log_service.get_trace_store", lambda *_: Store() + ) + response = await asgi_client.get( + "/api/v1/logs/export", + headers=_jwt_headers(), + params={} if redact else {"redact": "false"}, + ) + assert response.status_code == 200 + assert response.headers["cache-control"] == "no-store" + assert ("raw-" in response.headers["content-disposition"]) is (not redact) + with zipfile.ZipFile(io.BytesIO(response.content)) as archive: + all_text = "\n".join(archive.read(name).decode() for name in archive.namelist()) + for private in ( + secret, + quote(secret, safe=""), + plugin_secret, + trace_secret, + "private-password", + "private-jwt-secret", + "unknown-header-token", + "session=unknown", + "second=secret", + "private-key-body", + ): + assert (private not in all_text) is redact + assert "binary-private-data" not in all_text + if not redact: + assert archive.read("logs/astrbot.log") == (logs / "astrbot.log").read_bytes() + exported = json.loads(archive.read("config.json"))["profiles"]["default"] + if redact: + assert exported["diagnostic_network"]["public_endpoint"] == "http://[PUBLIC_IP_1]:8080/v1" + assert exported["diagnostic_network"]["public_ipv6"] == "https://[PUBLIC_IP_2]:443/v1" + assert b"peer=[PUBLIC_IP_1]" in archive.read("memory/logs.txt") + trace = json.loads(archive.read("traces/history.jsonl"))["spans"][0]["attributes"] + assert trace["peer"] == "[PUBLIC_IP_1]" + assert trace["ipv6"] == "[PUBLIC_IP_2]" + assert "8.8.8.8" not in all_text + assert "2606:4700:4700" not in all_text + else: + assert exported["diagnostic_network"] == config["diagnostic_network"] + assert "::ffff:8.8.8.8" in all_text + assert exported["diagnostic_network"]["local_endpoint"] == "http://192.168.1.2:6185/health" + assert exported["dashboard"]["password_storage_upgraded"] is True + assert exported["dashboard"]["password_change_required"] is False + assert exported["max_tokens"] == 4096 + assert exported["provider_sources"][0]["key"] == ["[REDACTED]" if redact else secret] + assert json.loads(archive.read("plugins.json"))["plugins"][0]["enabled"] is True + manifest = json.loads(archive.read("manifest.json")) + assert manifest["trace_history"]["count"] == 1 + assert manifest["trace_enabled"] is False + assert manifest["redacted"] is redact + assert "logs/traces.db" not in archive.namelist() + assert "logs/binary.log" not in archive.namelist() + assert archive.read("memory/traces.jsonl") == b"" + + +@pytest.mark.asyncio +async def test_diagnostic_export_without_trace_does_not_create_or_enable_it( + monkeypatch, tmp_path, asgi_app, asgi_client +): + from unittest.mock import Mock + + monkeypatch.setenv("ASTRBOT_ROOT", str(tmp_path)) + asgi_app.state.services.logs.config["trace_enable"] = False + getter = Mock(side_effect=AssertionError("No database should be opened")) + monkeypatch.setattr( + "astrbot.dashboard.services.log_service.get_trace_store", getter + ) + response = await asgi_client.get("/api/v1/logs/export", headers=_jwt_headers()) + assert response.status_code == 200 + with zipfile.ZipFile(io.BytesIO(response.content)) as archive: + manifest = json.loads(archive.read("manifest.json")) + assert manifest["trace_enabled"] is False + assert manifest["trace_history"]["status"] == "disabled_no_history" + assert archive.read("traces/history.jsonl") == b"" + getter.assert_not_called() + assert not (tmp_path / "data/logs/traces.db").exists() + assert asgi_app.state.services.logs.config["trace_enable"] is False + + +@pytest.mark.asyncio +async def test_diagnostic_export_authenticates_dashboard_cookie( + monkeypatch, tmp_path, asgi_client +): + monkeypatch.setenv("ASTRBOT_ROOT", str(tmp_path)) + token = _jwt_headers()["Authorization"].removeprefix("Bearer ") + asgi_client.cookies.set(DASHBOARD_JWT_COOKIE_NAME, token) + response = await asgi_client.get("/api/v1/logs/export") + assert response.status_code == 200 + + +@pytest.mark.asyncio +async def test_diagnostic_export_redaction_failure_is_closed_and_unlocks( + monkeypatch, tmp_path, asgi_app, asgi_client +): + from unittest.mock import Mock + + from astrbot.dashboard.services import log_service + + monkeypatch.setenv("ASTRBOT_ROOT", str(tmp_path)) + original = log_service.collect_diagnostics + monkeypatch.setattr( + log_service, + "collect_diagnostics", + Mock(side_effect=RuntimeError("private-value-in-error")), + ) + response = await asgi_client.get("/api/v1/logs/export", headers=_jwt_headers()) + assert response.status_code == 400 + assert "private-value-in-error" not in response.text + assert not asgi_app.state.services.logs._export_lock.locked() + assert not list((tmp_path / "data/temp/log_exports").glob("*.zip")) + monkeypatch.setattr(log_service, "collect_diagnostics", original) + response = await asgi_client.get("/api/v1/logs/export", headers=_jwt_headers()) + assert response.status_code == 200 + + +@pytest.mark.asyncio +async def test_v1_persistent_trace_routes(asgi_client, asgi_app, monkeypatch, tmp_path): """Verify durable history/detail validation and dashboard authentication.""" from astrbot.core.utils.trace_store import TraceStore from astrbot.dashboard.services import log_service @@ -4598,6 +4816,19 @@ async def test_v1_persistent_trace_routes(asgi_client, monkeypatch, tmp_path): assert ( await asgi_client.get("/api/v1/trace/settings", headers=headers) ).status_code == 200 + monkeypatch.setenv("ASTRBOT_ROOT", str(tmp_path)) + asgi_app.state.services.logs.config.update( + trace_enable=False, trace_db_path=str(tmp_path / "traces.db") + ) + response = await asgi_client.get("/api/v1/logs/export", headers=headers) + assert response.status_code == 200 + with zipfile.ZipFile(io.BytesIO(response.content)) as archive: + manifest = json.loads(archive.read("manifest.json")) + assert manifest["trace_enabled"] is False + assert manifest["trace_history"]["count"] == 1 + history = json.loads(archive.read("traces/history.jsonl")) + assert history["trace"]["trace_id"] == trace_id + assert history["spans"][0]["name"] == "message" finally: store.close() @@ -4613,15 +4844,24 @@ async def test_v1_trace_storage_cleanup_target(asgi_client, monkeypatch): } cleanup = AsyncMock(return_value={"removed_traces": 3, "status": usage}) monkeypatch.setattr(stat_service.StatService, "cleanup_storage", cleanup) - monkeypatch.setattr(stat_service.StatService, "get_storage_status", AsyncMock(return_value=usage)) + monkeypatch.setattr( + stat_service.StatService, "get_storage_status", AsyncMock(return_value=usage) + ) token = jwt.encode( {"username": "astrbot", "exp": int(time.time()) + 600}, - JWT_SECRET, algorithm="HS256", + JWT_SECRET, + algorithm="HS256", ) headers = {"Authorization": f"Bearer {token}"} - assert (await asgi_client.post("/api/v1/stats/storage/cleanup", json={"target": "traces"})).status_code == 401 + assert ( + await asgi_client.post( + "/api/v1/stats/storage/cleanup", json={"target": "traces"} + ) + ).status_code == 401 response = await asgi_client.post( - "/api/v1/stats/storage/cleanup", json={"target": "traces"}, headers=headers, + "/api/v1/stats/storage/cleanup", + json={"target": "traces"}, + headers=headers, ) assert response.status_code == 200 assert response.json()["data"]["removed_traces"] == 3 @@ -4629,4 +4869,8 @@ async def test_v1_trace_storage_cleanup_target(asgi_client, monkeypatch): response = await asgi_client.get("/api/v1/stats/storage", headers=headers) assert response.json()["data"]["total_bytes"] == 60 assert response.json()["data"]["traces"]["size_bytes"] == 30 - assert (await asgi_client.post("/api/v1/stats/storage/cleanup", json={"target": "invalid"}, headers=headers)).status_code == 422 + assert ( + await asgi_client.post( + "/api/v1/stats/storage/cleanup", json={"target": "invalid"}, headers=headers + ) + ).status_code == 422 diff --git a/tests/unit/test_diagnostic_redaction.py b/tests/unit/test_diagnostic_redaction.py new file mode 100644 index 00000000000..4e86b0a64c3 --- /dev/null +++ b/tests/unit/test_diagnostic_redaction.py @@ -0,0 +1,157 @@ +"""Credential and stream-boundary regressions for diagnostic exports.""" + +import io +import json + +import pytest + +from astrbot.dashboard.services.diagnostic_service import DiagnosticRedactor + + +@pytest.mark.parametrize("flag", [True, False]) +def test_status_flags_and_configuration_shape_survive_redaction(flag): + original = { + "password_storage_upgraded": flag, + "password_change_required": flag, + "max_tokens": 4096, + "tokenizer": "demo", + "recovery_code_hash": "private-recovery-hash", + "app_key": "private-app-key", + "provider": {"key": ["configured-private-credential", ""]}, + } + redactor = DiagnosticRedactor() + redactor.collect(original) + exported = redactor.redact(original) + assert exported["password_storage_upgraded"] is flag + assert exported["password_change_required"] is flag + assert exported["max_tokens"] == 4096 + assert exported["tokenizer"] == "demo" + assert exported["recovery_code_hash"] == "[REDACTED]" + assert exported["app_key"] == "[REDACTED]" + assert exported["provider"]["key"] == ["[REDACTED]", ""] + assert original["provider"]["key"][0] == "configured-private-credential" + + +def test_redacts_known_escaped_and_unconfigured_credentials(): + secret = 'secret with "quotes" and /spaces+' + redactor = DiagnosticRedactor() + redactor.collect({"api_key": secret}) + text = ( + secret + "\n" + json.dumps(secret) + "\n" + "https://user:private@example.com/?key=unknown-query-key&x=1\n" + "Bearer unknown-header-token\nCookie: a=private; b=other\n" + "sk-abcdefghijklmnopqrstuvwxyz\n" + "-----BEGIN OPENSSH PRIVATE KEY-----\nprivate-key-body\n-----END OPENSSH PRIVATE KEY-----" + ) + result = redactor.text(text) + for private in ( + secret, + "quotes", + "user:private", + "unknown-query-key", + "unknown-header-token", + "a=private", + "b=other", + "sk-abc", + "private-key-body", + ): + assert private not in result + assert "&x=1" in result + + +def test_stream_drops_oversized_lines_without_leaking_suffixes(monkeypatch): + monkeypatch.setattr( + "astrbot.dashboard.services.diagnostic_service.MAX_LOG_LINE_BYTES", 32 + ) + redactor = DiagnosticRedactor() + content = b"a" * 100 + b"private-tail\nnormal log\n" + output = io.BytesIO() + assert redactor.copy_log(io.BytesIO(content), output, len(content)) is True + assert output.getvalue() == b"normal log\n" + + +def test_stream_redacts_private_key_blocks_and_keeps_unterminated_final_line(): + content = b"normal\n-----BEGIN PRIVATE KEY-----\nsecret-body\n-----END PRIVATE KEY-----\nlast line" + output = io.BytesIO() + redactor = DiagnosticRedactor() + assert redactor.copy_log(io.BytesIO(content), output, len(content)) is False + assert output.getvalue() == b"normal\n[REDACTED]\nlast line" + + +def test_stream_does_not_emit_a_credential_fragment_at_size_boundary(): + source = io.BytesIO(b"normal\nBearer private-token-without-newline") + output = io.BytesIO() + assert DiagnosticRedactor().copy_log(source, output, 16) is True + assert output.getvalue() == b"normal\n" + + +def test_disabled_redaction_preserves_structured_values_and_original_log_bytes(): + redactor = DiagnosticRedactor(enabled=False) + value = { + "password": "original-secret", + "password_storage_upgraded": True, + "host": "8.8.8.8", + } + redactor.collect(value) + assert redactor.redact(value) == value + assert redactor.text("Bearer original-secret") == "Bearer original-secret" + content = b"\x1b[32m8.8.8.8 [2606:4700:4700::1111]:443\r\n-----BEGIN PRIVATE KEY-----\noriginal-secret\n\xff" + output = io.BytesIO() + assert redactor.copy_log(io.BytesIO(content), output, len(content)) is False + assert output.getvalue() == content + + +def test_public_ip_aliases_are_shared_across_config_logs_and_trace_spellings(): + redactor = DiagnosticRedactor() + config = redactor.redact( + { + "api_base": "http://8.8.8.8:8080/v1", + "ipv6": "https://[2606:4700:4700::1111]:443/v1", + } + ) + assert config == { + "api_base": "http://[PUBLIC_IP_1]:8080/v1", + "ipv6": "https://[PUBLIC_IP_2]:443/v1", + } + assert ( + redactor.text("client:8.8.8.8, peer=[::ffff:8.8.8.8]:80.") + == "client:[PUBLIC_IP_1], peer=[PUBLIC_IP_1]:80." + ) + assert redactor.redact({"peer": "2606:4700:4700:0:0:0:0:1111"}) == { + "peer": "[PUBLIC_IP_2]" + } + output = io.BytesIO() + content = b"peer=2606:4700:4700::1111. next=1.1.1.1:53\n" + redactor.copy_log(io.BytesIO(content), output, len(content)) + assert output.getvalue() == b"peer=[PUBLIC_IP_2]. next=[PUBLIC_IP_3]:53\n" + assert DiagnosticRedactor().text("1.1.1.1") == "[PUBLIC_IP_1]" + + +@pytest.mark.parametrize( + "value", + [ + "0.0.0.0", + "127.0.0.1:6185", + "10.0.0.1:8080", + "172.16.1.1", + "192.168.1.1", + "169.254.1.2", + "100.64.1.2", + "224.0.0.1", + "::", + "::1", + "[::1]:6185", + "[fe80::1%eth0]:8080", + "fc00::1", + "fd12::1", + "ff02::1", + "::ffff:192.168.1.1", + "version=v1.2.3.4", + "bad=999.1.1.1", + "bad=1.2.3.4.5", + "12:34:56", + "dead:beef:hello", + ], +) +def test_local_non_global_and_non_ip_text_remains_unchanged(value): + assert DiagnosticRedactor().text(value) == value