diff --git a/astrbot/dashboard/api/stats.py b/astrbot/dashboard/api/stats.py index cdcb505a4e7..78b3943c2e7 100644 --- a/astrbot/dashboard/api/stats.py +++ b/astrbot/dashboard/api/stats.py @@ -4,7 +4,11 @@ from astrbot.dashboard.async_utils import run_maybe_async from astrbot.dashboard.responses import ApiError, ok -from astrbot.dashboard.schemas import GhProxyTestRequest, StorageCleanupRequest +from astrbot.dashboard.schemas import ( + GhProxyTestRequest, + PypiIndexTestRequest, + StorageCleanupRequest, +) from astrbot.dashboard.services.stat_service import StatService, StatServiceError from .auth import AuthContext, require_dashboard_user, require_scope @@ -104,6 +108,33 @@ async def test_ghproxy_connection( return await _run(service.test_ghproxy_connection(payload.proxy_url)) +@router.post("/stats/pypi-index/test") +async def test_pypi_index_connection( + payload: PypiIndexTestRequest, + _auth: AuthContext = Depends(require_system_scope), + service: StatService = Depends(get_service), +): + return await _run(service.test_pypi_index_connection(payload.index_url)) + + +@router.post("/stats/pypi-index/speed") +async def measure_pypi_index_speed( + payload: PypiIndexTestRequest, + _auth: AuthContext = Depends(require_system_scope), + service: StatService = Depends(get_service), +): + return await _run(service.measure_pypi_index_speed(payload.index_url)) + + +@router.post("/stats/ghproxy/speed") +async def measure_ghproxy_speed( + payload: GhProxyTestRequest, + _auth: AuthContext = Depends(require_system_scope), + service: StatService = Depends(get_service), +): + return await _run(service.measure_ghproxy_speed(payload.proxy_url)) + + @router.get("/changelogs") async def list_changelog_versions( _auth: AuthContext = Depends(require_system_scope), diff --git a/astrbot/dashboard/schemas.py b/astrbot/dashboard/schemas.py index 2f2b2e91807..957c22beec4 100644 --- a/astrbot/dashboard/schemas.py +++ b/astrbot/dashboard/schemas.py @@ -222,6 +222,10 @@ class GhProxyTestRequest(BaseModel): proxy_url: str | None = None +class PypiIndexTestRequest(BaseModel): + index_url: str | None = None + + class OpenApiChatRequest(OpenModel): locale: str | None = None message: Any = None diff --git a/astrbot/dashboard/services/stat_service.py b/astrbot/dashboard/services/stat_service.py index 2f90dab6c72..73815872969 100644 --- a/astrbot/dashboard/services/stat_service.py +++ b/astrbot/dashboard/services/stat_service.py @@ -10,10 +10,13 @@ from collections import defaultdict from datetime import datetime, timedelta, timezone from functools import cmp_to_key +from html.parser import HTMLParser from pathlib import Path +from urllib.parse import urljoin, urlparse import aiohttp import psutil +from packaging.version import InvalidVersion, Version from sqlmodel import col, func, select from astrbot.core import DEMO_MODE, logger @@ -50,6 +53,22 @@ class StatServiceError(Exception): pass +# Speed tests download real payloads, so these budgets stop a single click +# from pulling tens of megabytes. +_SPEED_SAMPLE_SECONDS = 6.0 +_SPEED_SAMPLE_MAX_BYTES = 4 * 1024 * 1024 +# The simple index page of one package lists every release ever published, so +# it can grow without bound on a misbehaving or hostile mirror. +_PACKAGE_PAGE_MAX_BYTES = 8 * 1024 * 1024 +_PIP_USER_AGENT = "pip/24.3.1" +# A real repository asset the dashboard downloads (the t2i runtime bundle), +# large enough to produce a meaningful throughput number. +_GITHUB_SPEED_TARGET_URL = ( + "https://github.com/AstrBotDevs/AstrBot/raw/refs/heads/master/" + "astrbot/core/utils/t2i/template/shiki_runtime.iife.js" +) + + class StatService: def __init__( self, @@ -523,11 +542,12 @@ async def get_provider_token_stats(self, days: int) -> dict: async def test_ghproxy_connection(self, proxy_url: str | None) -> dict: try: - if not proxy_url: - raise StatServiceError("proxy_url is required") - - proxy_url = proxy_url.rstrip("/") - test_url = f"{proxy_url}/https://github.com/AstrBotDevs/AstrBot/raw/refs/heads/master/.python-version" + target_url = "https://github.com/AstrBotDevs/AstrBot/raw/refs/heads/master/.python-version" + # An empty proxy_url means "no acceleration", so GitHub is tested directly. + normalized_proxy = (proxy_url or "").strip().rstrip("/") + test_url = ( + f"{normalized_proxy}/{target_url}" if normalized_proxy else target_url + ) start_time = time.time() async with ( @@ -550,6 +570,245 @@ async def test_ghproxy_connection(self, proxy_url: str | None) -> dict: logger.error(traceback.format_exc()) raise StatServiceError(f"Error: {exc!s}") from exc + async def test_pypi_index_connection(self, index_url: str | None) -> dict: + """Check whether a PyPI index (mirror) answers requests. + + Only the response headers are awaited: a simple index page can be very + large, so the body is never downloaded. + + Args: + index_url: Base URL of the PyPI index, e.g. + ``https://mirrors.aliyun.com/pypi/simple/``. + + Returns: + A dict with the round trip latency in milliseconds. + + Raises: + StatServiceError: If the URL is missing, malformed, or unreachable. + """ + try: + normalized_url = (index_url or "").strip().rstrip("/") + if not normalized_url: + raise StatServiceError("index_url is required") + + parsed = urlparse(normalized_url) + if parsed.scheme not in ("http", "https") or not parsed.netloc: + raise StatServiceError("index_url must be a valid http(s) URL") + + start_time = time.time() + async with ( + aiohttp.ClientSession() as session, + session.get( + f"{normalized_url}/", + # Some mirrors reject non-pip user agents, so mimic pip itself. + headers={"User-Agent": _PIP_USER_AGENT}, + timeout=aiohttp.ClientTimeout(total=10), + ) as response, + ): + if response.status == 200: + end_time = time.time() + return { + "latency": round((end_time - start_time) * 1000, 2), + } + raise StatServiceError(f"Failed. Status code: {response.status}") + except StatServiceError: + raise + except Exception as exc: + logger.error(traceback.format_exc()) + raise StatServiceError(f"Error: {exc!s}") from exc + + async def measure_pypi_index_speed(self, index_url: str | None) -> dict: + """Download a real wheel from a PyPI index and report the throughput. + + The speed test pulls an actual package file, so the result reflects what + ``pip install`` would experience instead of a bare round trip. Only the + first few megabytes and seconds are sampled so slow mirrors answer fast. + + The package listing page is streamed with a hard byte budget, so a + misbehaving mirror cannot push an unbounded page into memory. + + Args: + index_url: Base URL of the PyPI index, e.g. + ``https://mirrors.aliyun.com/pypi/simple/``. + + Returns: + A dict with the throughput in MB/s, the sampled byte count, and the + elapsed seconds. + + Raises: + StatServiceError: If the URL is missing, malformed, or unreachable. + """ + try: + normalized_url = (index_url or "").strip().rstrip("/") + if not normalized_url: + raise StatServiceError("index_url is required") + + parsed = urlparse(normalized_url) + if parsed.scheme not in ("http", "https") or not parsed.netloc: + raise StatServiceError("index_url must be a valid http(s) URL") + + async with aiohttp.ClientSession() as session: + # pip ships a single platform independent wheel and every mirror + # serves the same file, which keeps the numbers comparable. + package_page_url = f"{normalized_url}/pip/" + async with session.get( + package_page_url, + headers={"User-Agent": _PIP_USER_AGENT}, + timeout=aiohttp.ClientTimeout(total=15), + ) as response: + if response.status != 200: + raise StatServiceError( + f"Failed. Status code: {response.status}" + ) + # Stream the page and reject it as soon as it exceeds the + # budget, so an oversized or endless response cannot be + # buffered into memory first. + page_bytes = bytearray() + async for chunk in response.content.iter_chunked(65536): + page_bytes += chunk + if len(page_bytes) > _PACKAGE_PAGE_MAX_BYTES: + raise StatServiceError( + "Package page from this index is too large " + f"(over {_PACKAGE_PAGE_MAX_BYTES // 1048576} MB)" + ) + page = page_bytes.decode( + response.charset or "utf-8", errors="replace" + ) + + class _AnchorHrefParser(HTMLParser): + """Collect ``href`` values of anchor tags from a simple index. + + A real parser is used instead of a regex so that valid markup + with single quotes, unquoted values, or uppercase ``HREF`` + is handled; tag and attribute names arrive lowercased. + """ + + def __init__(self) -> None: + super().__init__(convert_charrefs=True) + self.hrefs: list[str] = [] + + def handle_starttag( + self, tag: str, attrs: list[tuple[str, str | None]] + ) -> None: + if tag != "a": + return + for name, value in attrs: + if name == "href" and value: + self.hrefs.append(value) + + anchor_parser = _AnchorHrefParser() + anchor_parser.feed(page) + anchor_parser.close() + + wheels: list[tuple[Version, str]] = [] + for href in anchor_parser.hrefs: + # Simple indexes append a "#sha256=..." fragment. + href = href.split("#", 1)[0] + file_name = href.rsplit("/", 1)[-1] + suffix = "-py3-none-any.whl" + if not ( + file_name.startswith("pip-") and file_name.endswith(suffix) + ): + continue + try: + version = Version(file_name[len("pip-") : -len(suffix)]) + except InvalidVersion: + continue + wheels.append((version, urljoin(package_page_url, href))) + + if not wheels: + raise StatServiceError("No pip wheel found on this index") + wheels.sort(key=lambda wheel: wheel[0]) + wheel_url = wheels[-1][1] + + start_time = time.time() + received = 0 + async with session.get( + wheel_url, + headers={"User-Agent": _PIP_USER_AGENT}, + timeout=aiohttp.ClientTimeout(total=_SPEED_SAMPLE_SECONDS + 5), + ) as response: + if response.status != 200: + raise StatServiceError( + f"Failed. Status code: {response.status}" + ) + async for chunk in response.content.iter_chunked(65536): + received += len(chunk) + if ( + received >= _SPEED_SAMPLE_MAX_BYTES + or time.time() - start_time > _SPEED_SAMPLE_SECONDS + ): + break + + duration = time.time() - start_time + if not received or duration <= 0: + raise StatServiceError("No data received") + return { + "speed": round(received / 1048576 / duration, 2), + "bytes": received, + "duration": round(duration, 2), + } + except StatServiceError: + raise + except Exception as exc: + logger.error(traceback.format_exc()) + raise StatServiceError(f"Error: {exc!s}") from exc + + async def measure_ghproxy_speed(self, proxy_url: str | None) -> dict: + """Download a repository file through a GitHub proxy and report throughput. + + Args: + proxy_url: Proxy prefix such as ``https://gh-proxy.com``; an empty + value measures the direct route. + + Returns: + A dict with the throughput in MB/s, the sampled byte count, and the + elapsed seconds. + + Raises: + StatServiceError: If the download fails. + """ + try: + normalized_proxy = (proxy_url or "").strip().rstrip("/") + test_url = ( + f"{normalized_proxy}/{_GITHUB_SPEED_TARGET_URL}" + if normalized_proxy + else _GITHUB_SPEED_TARGET_URL + ) + + start_time = time.time() + received = 0 + async with ( + aiohttp.ClientSession() as session, + session.get( + test_url, + timeout=aiohttp.ClientTimeout(total=_SPEED_SAMPLE_SECONDS + 5), + ) as response, + ): + if response.status != 200: + raise StatServiceError(f"Failed. Status code: {response.status}") + async for chunk in response.content.iter_chunked(65536): + received += len(chunk) + if ( + received >= _SPEED_SAMPLE_MAX_BYTES + or time.time() - start_time > _SPEED_SAMPLE_SECONDS + ): + break + + duration = time.time() - start_time + if not received or duration <= 0: + raise StatServiceError("No data received") + return { + "speed": round(received / 1048576 / duration, 2), + "bytes": received, + "duration": round(duration, 2), + } + except StatServiceError: + raise + except Exception as exc: + logger.error(traceback.format_exc()) + raise StatServiceError(f"Error: {exc!s}") from exc + def get_changelog(self, version: str | None) -> dict: try: if not version: diff --git a/dashboard/src/api/generated/openapi-v1/sdk.gen.ts b/dashboard/src/api/generated/openapi-v1/sdk.gen.ts index 4a3ec831515..68cfef1c6e8 100644 --- a/dashboard/src/api/generated/openapi-v1/sdk.gen.ts +++ b/dashboard/src/api/generated/openapi-v1/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import { createClient, createConfig, type OptionsLegacyParser, formDataBodySerializer } from '@hey-api/client-axios'; -import type { LoginData, LoginError, LoginResponse, LogoutError, LogoutResponse, GetAuthSetupStatusError, GetAuthSetupStatusResponse, SetupAuthData, SetupAuthError, SetupAuthResponse, SetupTotpData, SetupTotpError, SetupTotpResponse, RecoverTotpError, RecoverTotpResponse, UpdateAuthAccountData, UpdateAuthAccountError, UpdateAuthAccountResponse, ListApiKeysError, ListApiKeysResponse, CreateApiKeyData, CreateApiKeyError, CreateApiKeyResponse, RevokeApiKeyData, RevokeApiKeyError, RevokeApiKeyResponse, DeleteApiKeyData, DeleteApiKeyError, DeleteApiKeyResponse, GetSystemConfigSchemaError, GetSystemConfigSchemaResponse, GetSystemConfigError, GetSystemConfigResponse, UpdateSystemConfigData, UpdateSystemConfigError, UpdateSystemConfigResponse, GetSystemConfigRuntimeError, GetSystemConfigRuntimeResponse, GetConfigProfileSchemaError, GetConfigProfileSchemaResponse, ListConfigProfilesError, ListConfigProfilesResponse, CreateConfigProfileData, CreateConfigProfileError, CreateConfigProfileResponse, GetConfigProfileData, GetConfigProfileError, GetConfigProfileResponse, UpdateConfigProfileContentData, UpdateConfigProfileContentError, UpdateConfigProfileContentResponse, RenameConfigProfileData, RenameConfigProfileError, RenameConfigProfileResponse, DeleteConfigProfileData, DeleteConfigProfileError, DeleteConfigProfileResponse, ListConfigRoutesError, ListConfigRoutesResponse, ReplaceConfigRoutesData, ReplaceConfigRoutesError, ReplaceConfigRoutesResponse, UpsertConfigRouteData, UpsertConfigRouteError, UpsertConfigRouteResponse, DeleteConfigRouteData, DeleteConfigRouteError, DeleteConfigRouteResponse, ListBotTypesError, ListBotTypesResponse, RegisterBotTypeData, RegisterBotTypeError, RegisterBotTypeResponse, ListBotsData, ListBotsError, ListBotsResponse, CreateBotData, CreateBotError, CreateBotResponse, ListBotStatsError, ListBotStatsResponse, GetBotByIdData, GetBotByIdError, GetBotByIdResponse, UpdateBotByIdData, UpdateBotByIdError, UpdateBotByIdResponse, DeleteBotByIdData, DeleteBotByIdError, DeleteBotByIdResponse, SetBotEnabledByIdData, SetBotEnabledByIdError, SetBotEnabledByIdResponse, TestBotByIdData, TestBotByIdError, TestBotByIdResponse, GetBotData, GetBotError, GetBotResponse, UpdateBotData, UpdateBotError, UpdateBotResponse, DeleteBotData, DeleteBotError, DeleteBotResponse, SetBotEnabledData, SetBotEnabledError, SetBotEnabledResponse, TestBotData, TestBotError, TestBotResponse, GetProviderSchemaError, GetProviderSchemaResponse, ListProviderSourcesError, ListProviderSourcesResponse, CreateProviderSourceData, CreateProviderSourceError, CreateProviderSourceResponse, GetProviderSourceByIdData, GetProviderSourceByIdError, GetProviderSourceByIdResponse, UpsertProviderSourceByIdData, UpsertProviderSourceByIdError, UpsertProviderSourceByIdResponse, DeleteProviderSourceByIdData, DeleteProviderSourceByIdError, DeleteProviderSourceByIdResponse, ListProviderSourceModelsByIdData, ListProviderSourceModelsByIdError, ListProviderSourceModelsByIdResponse, ListProvidersBySourceIdData, ListProvidersBySourceIdError, ListProvidersBySourceIdResponse, CreateProviderInSourceByIdData, CreateProviderInSourceByIdError, CreateProviderInSourceByIdResponse, GetProviderSourceData, GetProviderSourceError, GetProviderSourceResponse, UpsertProviderSourceData, UpsertProviderSourceError, UpsertProviderSourceResponse, DeleteProviderSourceData, DeleteProviderSourceError, DeleteProviderSourceResponse, ListProviderSourceModelsData, ListProviderSourceModelsError, ListProviderSourceModelsResponse, ListProvidersBySourceData, ListProvidersBySourceError, ListProvidersBySourceResponse, CreateProviderInSourceData, CreateProviderInSourceError, CreateProviderInSourceResponse, ListProvidersData, ListProvidersError, ListProvidersResponse, CreateProviderData, CreateProviderError, CreateProviderResponse, GetProviderByIdData, GetProviderByIdError, GetProviderByIdResponse, UpdateProviderByIdData, UpdateProviderByIdError, UpdateProviderByIdResponse, DeleteProviderByIdData, DeleteProviderByIdError, DeleteProviderByIdResponse, SetProviderEnabledByIdData, SetProviderEnabledByIdError, SetProviderEnabledByIdResponse, TestProviderByIdData, TestProviderByIdError, TestProviderByIdResponse, GetProviderEmbeddingDimensionByIdData, GetProviderEmbeddingDimensionByIdError, GetProviderEmbeddingDimensionByIdResponse, GetProviderData, GetProviderError, GetProviderResponse, UpdateProviderData, UpdateProviderError, UpdateProviderResponse, DeleteProviderData, DeleteProviderError, DeleteProviderResponse, SetProviderEnabledData, SetProviderEnabledError, SetProviderEnabledResponse, TestProviderData, TestProviderError, TestProviderResponse, GetProviderEmbeddingDimensionData, GetProviderEmbeddingDimensionError, GetProviderEmbeddingDimensionResponse, SendChatMessageData, SendChatMessageError, SendChatMessageResponse, OpenChatWebSocketData, OpenLiveChatWebSocketData, OpenUnifiedChatWebSocketData, ListChatSessionsData, ListChatSessionsError, ListChatSessionsResponse, CreateChatSessionData, CreateChatSessionError, CreateChatSessionResponse, BatchDeleteChatSessionsData, BatchDeleteChatSessionsError, BatchDeleteChatSessionsResponse, GetChatSessionData, GetChatSessionError, GetChatSessionResponse, UpdateChatSessionData, UpdateChatSessionError, UpdateChatSessionResponse, DeleteChatSessionData, DeleteChatSessionError, DeleteChatSessionResponse, StopChatSessionData, StopChatSessionError, StopChatSessionResponse, ResumeChatRunData, ResumeChatRunError, ResumeChatRunResponse, UpdateChatMessageData, UpdateChatMessageError, UpdateChatMessageResponse, RegenerateChatMessageData, RegenerateChatMessageError, RegenerateChatMessageResponse, ListChatConfigsError, ListChatConfigsResponse, CreateChatThreadData, CreateChatThreadError, CreateChatThreadResponse, GetChatThreadData, GetChatThreadError, GetChatThreadResponse, DeleteChatThreadData, DeleteChatThreadError, DeleteChatThreadResponse, SendChatThreadMessageData, SendChatThreadMessageError, SendChatThreadMessageResponse, ListChatProjectsError, ListChatProjectsResponse, CreateChatProjectData, CreateChatProjectError, CreateChatProjectResponse, GetChatProjectData, GetChatProjectError, GetChatProjectResponse, UpdateChatProjectData, UpdateChatProjectError, UpdateChatProjectResponse, DeleteChatProjectData, DeleteChatProjectError, DeleteChatProjectResponse, ListChatProjectSessionsData, ListChatProjectSessionsError, ListChatProjectSessionsResponse, ListChatProjectWorkspaceFilesData, ListChatProjectWorkspaceFilesError, ListChatProjectWorkspaceFilesResponse, GetChatProjectWorkspaceFileData, GetChatProjectWorkspaceFileError, GetChatProjectWorkspaceFileResponse, DownloadChatProjectWorkspaceFileData, DownloadChatProjectWorkspaceFileError, DownloadChatProjectWorkspaceFileResponse, AddChatProjectSessionData, AddChatProjectSessionError, AddChatProjectSessionResponse, RemoveChatProjectSessionData, RemoveChatProjectSessionError, RemoveChatProjectSessionResponse, SendImMessageData, SendImMessageError, SendImMessageResponse, ListImBotsError, ListImBotsResponse, UploadFileData, UploadFileError, UploadFileResponse, InitFileUploadData, InitFileUploadError, InitFileUploadResponse, UploadFileChunkData, UploadFileChunkError, UploadFileChunkResponse, CompleteFileUploadData, CompleteFileUploadError, CompleteFileUploadResponse, AbortFileUploadData, AbortFileUploadError, AbortFileUploadResponse, StatusFileUploadData, StatusFileUploadError, StatusFileUploadResponse, UploadOpenApiFileData, UploadOpenApiFileError, UploadOpenApiFileResponse, DownloadOpenApiFileData, DownloadOpenApiFileError, DownloadOpenApiFileResponse, GetFileByNameData, GetFileByNameError, GetFileByNameResponse, GetTokenFileData, GetTokenFileError, GetTokenFileResponse, GetAttachmentData, GetAttachmentError, GetAttachmentResponse, DeleteAttachmentData, DeleteAttachmentError, DeleteAttachmentResponse, DownloadAttachmentData, DownloadAttachmentError, DownloadAttachmentResponse, ListPluginsData, ListPluginsError, ListPluginsResponse, GetPluginByIdData, GetPluginByIdError, GetPluginByIdResponse, UninstallPluginByIdData, UninstallPluginByIdError, UninstallPluginByIdResponse, GetPluginConfigByIdData, GetPluginConfigByIdError, GetPluginConfigByIdResponse, UpdatePluginConfigByIdData, UpdatePluginConfigByIdError, UpdatePluginConfigByIdResponse, GetPluginConfigSchemaByIdData, GetPluginConfigSchemaByIdError, GetPluginConfigSchemaByIdResponse, ListPluginConfigFilesByIdData, ListPluginConfigFilesByIdError, ListPluginConfigFilesByIdResponse, UploadPluginConfigFilesByIdData, UploadPluginConfigFilesByIdError, UploadPluginConfigFilesByIdResponse, DeletePluginConfigFileByIdData, DeletePluginConfigFileByIdError, DeletePluginConfigFileByIdResponse, GetPluginReadmeByIdData, GetPluginReadmeByIdError, GetPluginReadmeByIdResponse, GetPluginChangelogByIdData, GetPluginChangelogByIdError, GetPluginChangelogByIdResponse, ReloadPluginByIdData, ReloadPluginByIdError, ReloadPluginByIdResponse, SetPluginEnabledByIdData, SetPluginEnabledByIdError, SetPluginEnabledByIdResponse, GetPluginData, GetPluginError, GetPluginResponse, UninstallPluginData, UninstallPluginError, UninstallPluginResponse, GetPluginConfigData, GetPluginConfigError, GetPluginConfigResponse, UpdatePluginConfigData, UpdatePluginConfigError, UpdatePluginConfigResponse, UpdatePluginLogLevelData, UpdatePluginLogLevelError, UpdatePluginLogLevelResponse, GetPluginConfigSchemaData, GetPluginConfigSchemaError, GetPluginConfigSchemaResponse, ListPluginConfigFilesData, ListPluginConfigFilesError, ListPluginConfigFilesResponse, UploadPluginConfigFilesData, UploadPluginConfigFilesError, UploadPluginConfigFilesResponse, DeletePluginConfigFileData, DeletePluginConfigFileError, DeletePluginConfigFileResponse, GetPluginReadmeData, GetPluginReadmeError, GetPluginReadmeResponse, GetPluginChangelogData, GetPluginChangelogError, GetPluginChangelogResponse, ReloadPluginData, ReloadPluginError, ReloadPluginResponse, BindPluginSourceData, BindPluginSourceError, BindPluginSourceResponse, SetPluginEnabledData, SetPluginEnabledError, SetPluginEnabledResponse, UpdatePluginData, UpdatePluginError, UpdatePluginResponse, UpdatePluginsData, UpdatePluginsError, UpdatePluginsResponse, CheckPluginVersionSupportData, CheckPluginVersionSupportError, CheckPluginVersionSupportResponse, ValidatePluginRepoData, ValidatePluginRepoError, ValidatePluginRepoResponse, ListFailedPluginsError, ListFailedPluginsResponse, UninstallFailedPluginData, UninstallFailedPluginError, UninstallFailedPluginResponse, ReloadFailedPluginData, ReloadFailedPluginError, ReloadFailedPluginResponse, InstallPluginFromGithubData, InstallPluginFromGithubError, InstallPluginFromGithubResponse, InstallPluginFromGitData, InstallPluginFromGitError, InstallPluginFromGitResponse, InstallPluginFromUrlData, InstallPluginFromUrlError, InstallPluginFromUrlResponse, InstallPluginFromUploadData, InstallPluginFromUploadError, InstallPluginFromUploadResponse, ListPluginMarketData, ListPluginMarketError, ListPluginMarketResponse, ListPluginMarketCategoriesError, ListPluginMarketCategoriesResponse, ListPluginSourcesError, ListPluginSourcesResponse, CreatePluginSourceData, CreatePluginSourceError, CreatePluginSourceResponse, ReplacePluginSourcesData, ReplacePluginSourcesError, ReplacePluginSourcesResponse, DeletePluginSourceData, DeletePluginSourceError, DeletePluginSourceResponse, DeletePluginSourceByIdData, DeletePluginSourceByIdError, DeletePluginSourceByIdResponse, ListPluginViewsByIdData, ListPluginViewsByIdError, ListPluginViewsByIdResponse, ListPluginPagesByIdData, ListPluginPagesByIdError, ListPluginPagesByIdResponse, GetPluginViewByIdData, GetPluginViewByIdError, GetPluginViewByIdResponse, GetPluginPageByIdData, GetPluginPageByIdError, GetPluginPageByIdResponse, ListPluginViewsData, ListPluginViewsError, ListPluginViewsResponse, ListPluginPagesData, ListPluginPagesError, ListPluginPagesResponse, GetPluginViewData, GetPluginViewError, GetPluginViewResponse, GetPluginPageData, GetPluginPageError, GetPluginPageResponse, GetPluginViewTokenAssetData, GetPluginViewTokenAssetError, GetPluginViewTokenAssetResponse, GetPluginPageBridgeSdkError, GetPluginPageBridgeSdkResponse, GetPluginExtensionRouteData, GetPluginExtensionRouteError, GetPluginExtensionRouteResponse, PostPluginExtensionRouteData, PostPluginExtensionRouteError, PostPluginExtensionRouteResponse, PutPluginExtensionRouteData, PutPluginExtensionRouteError, PutPluginExtensionRouteResponse, PatchPluginExtensionRouteData, PatchPluginExtensionRouteError, PatchPluginExtensionRouteResponse, DeletePluginExtensionRouteData, DeletePluginExtensionRouteError, DeletePluginExtensionRouteResponse, ListCommandsData, ListCommandsError, ListCommandsResponse, UpdateCommandData, UpdateCommandError, UpdateCommandResponse, ListCommandConflictsError, ListCommandConflictsResponse, ListToolsData, ListToolsError, ListToolsResponse, SetToolEnabledData, SetToolEnabledError, SetToolEnabledResponse, SetToolPermissionData, SetToolPermissionError, SetToolPermissionResponse, ListMcpServersError, ListMcpServersResponse, CreateMcpServerData, CreateMcpServerError, CreateMcpServerResponse, UpdateMcpServerByNameData, UpdateMcpServerByNameError, UpdateMcpServerByNameResponse, DeleteMcpServerByNameData, DeleteMcpServerByNameError, DeleteMcpServerByNameResponse, SetMcpServerEnabledByNameData, SetMcpServerEnabledByNameError, SetMcpServerEnabledByNameResponse, TestMcpServerByNameData, TestMcpServerByNameError, TestMcpServerByNameResponse, UpdateMcpServerData, UpdateMcpServerError, UpdateMcpServerResponse, DeleteMcpServerData, DeleteMcpServerError, DeleteMcpServerResponse, SetMcpServerEnabledData, SetMcpServerEnabledError, SetMcpServerEnabledResponse, TestMcpServerData, TestMcpServerError, TestMcpServerResponse, SyncModelScopeMcpServersData, SyncModelScopeMcpServersError, SyncModelScopeMcpServersResponse, ListSkillsData, ListSkillsError, ListSkillsResponse, UploadSkillData, UploadSkillError, UploadSkillResponse, UploadSkillsBatchData, UploadSkillsBatchError, UploadSkillsBatchResponse, UpdateSkillByNameData, UpdateSkillByNameError, UpdateSkillByNameResponse, DeleteSkillByNameData, DeleteSkillByNameError, DeleteSkillByNameResponse, DownloadSkillByNameData, DownloadSkillByNameError, DownloadSkillByNameResponse, ListSkillFilesByNameData, ListSkillFilesByNameError, ListSkillFilesByNameResponse, GetSkillFileByNameData, GetSkillFileByNameError, GetSkillFileByNameResponse, UpdateSkillFileByNameData, UpdateSkillFileByNameError, UpdateSkillFileByNameResponse, UpdateSkillData, UpdateSkillError, UpdateSkillResponse, DeleteSkillData, DeleteSkillError, DeleteSkillResponse, DownloadSkillData, DownloadSkillError, DownloadSkillResponse, ListSkillFilesData, ListSkillFilesError, ListSkillFilesResponse, GetSkillFileData, GetSkillFileError, GetSkillFileResponse, UpdateSkillFileData, UpdateSkillFileError, UpdateSkillFileResponse, ListNeoSkillCandidatesData, ListNeoSkillCandidatesError, ListNeoSkillCandidatesResponse, ListNeoSkillReleasesData, ListNeoSkillReleasesError, ListNeoSkillReleasesResponse, GetNeoSkillPayloadData, GetNeoSkillPayloadError, GetNeoSkillPayloadResponse, EvaluateNeoSkillCandidateData, EvaluateNeoSkillCandidateError, EvaluateNeoSkillCandidateResponse, PromoteNeoSkillCandidateData, PromoteNeoSkillCandidateError, PromoteNeoSkillCandidateResponse, RollbackNeoSkillReleaseData, RollbackNeoSkillReleaseError, RollbackNeoSkillReleaseResponse, SyncNeoSkillReleaseData, SyncNeoSkillReleaseError, SyncNeoSkillReleaseResponse, DeleteNeoSkillCandidateData, DeleteNeoSkillCandidateError, DeleteNeoSkillCandidateResponse, DeleteNeoSkillReleaseData, DeleteNeoSkillReleaseError, DeleteNeoSkillReleaseResponse, ListKnowledgeBasesData, ListKnowledgeBasesError, ListKnowledgeBasesResponse, CreateKnowledgeBaseData, CreateKnowledgeBaseError, CreateKnowledgeBaseResponse, GetKnowledgeBaseData, GetKnowledgeBaseError, GetKnowledgeBaseResponse, UpdateKnowledgeBaseData, UpdateKnowledgeBaseError, UpdateKnowledgeBaseResponse, DeleteKnowledgeBaseData, DeleteKnowledgeBaseError, DeleteKnowledgeBaseResponse, GetKnowledgeBaseStatsData, GetKnowledgeBaseStatsError, GetKnowledgeBaseStatsResponse, ListKnowledgeDocumentsData, ListKnowledgeDocumentsError, ListKnowledgeDocumentsResponse, UploadKnowledgeDocumentData, UploadKnowledgeDocumentError, UploadKnowledgeDocumentResponse, ImportKnowledgeDocumentsData, ImportKnowledgeDocumentsError, ImportKnowledgeDocumentsResponse, ImportKnowledgeDocumentFromUrlData, ImportKnowledgeDocumentFromUrlError, ImportKnowledgeDocumentFromUrlResponse, GetKnowledgeDocumentData, GetKnowledgeDocumentError, GetKnowledgeDocumentResponse, DeleteKnowledgeDocumentData, DeleteKnowledgeDocumentError, DeleteKnowledgeDocumentResponse, ListKnowledgeChunksData, ListKnowledgeChunksError, ListKnowledgeChunksResponse, DeleteKnowledgeChunkData, DeleteKnowledgeChunkError, DeleteKnowledgeChunkResponse, RetrieveKnowledgeBaseData, RetrieveKnowledgeBaseError, RetrieveKnowledgeBaseResponse, GetKnowledgeTaskData, GetKnowledgeTaskError, GetKnowledgeTaskResponse, GetPersonaTreeError, GetPersonaTreeResponse, ListPersonasData, ListPersonasError, ListPersonasResponse, CreatePersonaData, CreatePersonaError, CreatePersonaResponse, GetPersonaByIdData, GetPersonaByIdError, GetPersonaByIdResponse, UpdatePersonaByIdData, UpdatePersonaByIdError, UpdatePersonaByIdResponse, DeletePersonaByIdData, DeletePersonaByIdError, DeletePersonaByIdResponse, GetPersonaData, GetPersonaError, GetPersonaResponse, UpdatePersonaData, UpdatePersonaError, UpdatePersonaResponse, DeletePersonaData, DeletePersonaError, DeletePersonaResponse, ListPersonaFoldersData, ListPersonaFoldersError, ListPersonaFoldersResponse, CreatePersonaFolderData, CreatePersonaFolderError, CreatePersonaFolderResponse, UpdatePersonaFolderData, UpdatePersonaFolderError, UpdatePersonaFolderResponse, DeletePersonaFolderData, DeletePersonaFolderError, DeletePersonaFolderResponse, MovePersonaItemData, MovePersonaItemError, MovePersonaItemResponse, ReorderPersonaItemsData, ReorderPersonaItemsError, ReorderPersonaItemsResponse, ListSessionsData, ListSessionsError, ListSessionsResponse, ListActiveUmosData, ListActiveUmosError, ListActiveUmosResponse, ListSessionSendersData, ListSessionSendersError, ListSessionSendersResponse, ListSessionRulesData, ListSessionRulesError, ListSessionRulesResponse, UpsertSessionRuleData, UpsertSessionRuleError, UpsertSessionRuleResponse, DeleteSessionRulesData, DeleteSessionRulesError, DeleteSessionRulesResponse, BatchUpdateSessionProviderData, BatchUpdateSessionProviderError, BatchUpdateSessionProviderResponse, BatchUpdateSessionServiceData, BatchUpdateSessionServiceError, BatchUpdateSessionServiceResponse, ListSessionGroupsError, ListSessionGroupsResponse, CreateSessionGroupData, CreateSessionGroupError, CreateSessionGroupResponse, UpdateSessionGroupData, UpdateSessionGroupError, UpdateSessionGroupResponse, DeleteSessionGroupData, DeleteSessionGroupError, DeleteSessionGroupResponse, ListConversationsData, ListConversationsError, ListConversationsResponse, GetConversationFilterOptionsError, GetConversationFilterOptionsResponse, BatchDeleteConversationsData, BatchDeleteConversationsError, BatchDeleteConversationsResponse, GetConversationData, GetConversationError, GetConversationResponse, UpdateConversationData, UpdateConversationError, UpdateConversationResponse, DeleteConversationData, DeleteConversationError, DeleteConversationResponse, ReplaceConversationMessagesData, ReplaceConversationMessagesError, ReplaceConversationMessagesResponse, ExportConversationsData, ExportConversationsError, ExportConversationsResponse, GetStatsData, GetStatsError, GetStatsResponse, GetProviderTokenStatsData, GetProviderTokenStatsError, GetProviderTokenStatsResponse, GetVersionError, GetVersionResponse, GetPublicVersionsError, GetPublicVersionsResponse, GetFirstNoticeData, GetFirstNoticeError, GetFirstNoticeResponse, TestGhproxyConnectionData, TestGhproxyConnectionError, TestGhproxyConnectionResponse, ListChangelogVersionsError, ListChangelogVersionsResponse, GetChangelogData, GetChangelogError, GetChangelogResponse, GetStartTimeError, GetStartTimeResponse, GetStorageStatusError, GetStorageStatusResponse, CleanupStorageData, CleanupStorageError, CleanupStorageResponse, RestartCoreError, RestartCoreResponse, ListBackupsData, ListBackupsError, ListBackupsResponse, CreateBackupData, CreateBackupError, CreateBackupResponse, UploadBackupData, UploadBackupError, UploadBackupResponse, InitBackupUploadData, InitBackupUploadError, InitBackupUploadResponse, UploadBackupChunkData, UploadBackupChunkError, UploadBackupChunkResponse, CompleteBackupUploadData, CompleteBackupUploadError, CompleteBackupUploadResponse, AbortBackupUploadData, AbortBackupUploadError, AbortBackupUploadResponse, StatusBackupUploadData, StatusBackupUploadError, StatusBackupUploadResponse, GetBackupProgressData, GetBackupProgressError, GetBackupProgressResponse, DownloadBackupData, DownloadBackupError, DownloadBackupResponse, RenameBackupData, RenameBackupError, RenameBackupResponse, DeleteBackupData, DeleteBackupError, DeleteBackupResponse, CheckBackupData, CheckBackupError, CheckBackupResponse, ImportBackupData, ImportBackupError, ImportBackupResponse, CheckUpdateError, CheckUpdateResponse, ListReleasesData, ListReleasesError, ListReleasesResponse, UpdateCoreData, UpdateCoreError, UpdateCoreResponse, UpdateDashboardData, UpdateDashboardError, UpdateDashboardResponse, GetUpdateProgressData, GetUpdateProgressError, GetUpdateProgressResponse, InstallPipPackageData, InstallPipPackageError, InstallPipPackageResponse, ListCronJobsData, ListCronJobsError, ListCronJobsResponse, CreateCronJobData, CreateCronJobError, CreateCronJobResponse, UpdateCronJobData, UpdateCronJobError, UpdateCronJobResponse, DeleteCronJobData, DeleteCronJobError, DeleteCronJobResponse, RunCronJobData, RunCronJobError, RunCronJobResponse, StreamLiveLogsError, StreamLiveLogsResponse, GetLogHistoryError, GetLogHistoryResponse, ExportLogsData, ExportLogsError, ExportLogsResponse, ListTracesData, ListTracesError, ListTracesResponse, GetTraceDetailData, GetTraceDetailError, GetTraceDetailResponse, GetTraceSettingsError, GetTraceSettingsResponse, UpdateTraceSettingsData, UpdateTraceSettingsError, UpdateTraceSettingsResponse, ListT2iTemplatesError, ListT2iTemplatesResponse, CreateT2iTemplateData, CreateT2iTemplateError, CreateT2iTemplateResponse, GetActiveT2iTemplateError, GetActiveT2iTemplateResponse, SetActiveT2iTemplateData, SetActiveT2iTemplateError, SetActiveT2iTemplateResponse, ResetDefaultT2iTemplateError, ResetDefaultT2iTemplateResponse, GetT2iTemplateData, GetT2iTemplateError, GetT2iTemplateResponse, UpdateT2iTemplateData, UpdateT2iTemplateError, UpdateT2iTemplateResponse, DeleteT2iTemplateData, DeleteT2iTemplateError, DeleteT2iTemplateResponse, GetSubagentConfigError, GetSubagentConfigResponse, UpdateSubagentConfigData, UpdateSubagentConfigError, UpdateSubagentConfigResponse, ListSubagentAvailableToolsError, ListSubagentAvailableToolsResponse, VerifyPlatformWebhookData, VerifyPlatformWebhookError, VerifyPlatformWebhookResponse, ReceivePlatformWebhookData, ReceivePlatformWebhookError, ReceivePlatformWebhookResponse } from './types.gen'; +import type { LoginData, LoginError, LoginResponse, LogoutError, LogoutResponse, GetAuthSetupStatusError, GetAuthSetupStatusResponse, SetupAuthData, SetupAuthError, SetupAuthResponse, SetupTotpData, SetupTotpError, SetupTotpResponse, RecoverTotpError, RecoverTotpResponse, UpdateAuthAccountData, UpdateAuthAccountError, UpdateAuthAccountResponse, ListApiKeysError, ListApiKeysResponse, CreateApiKeyData, CreateApiKeyError, CreateApiKeyResponse, RevokeApiKeyData, RevokeApiKeyError, RevokeApiKeyResponse, DeleteApiKeyData, DeleteApiKeyError, DeleteApiKeyResponse, GetSystemConfigSchemaError, GetSystemConfigSchemaResponse, GetSystemConfigError, GetSystemConfigResponse, UpdateSystemConfigData, UpdateSystemConfigError, UpdateSystemConfigResponse, GetSystemConfigRuntimeError, GetSystemConfigRuntimeResponse, GetConfigProfileSchemaError, GetConfigProfileSchemaResponse, ListConfigProfilesError, ListConfigProfilesResponse, CreateConfigProfileData, CreateConfigProfileError, CreateConfigProfileResponse, GetConfigProfileData, GetConfigProfileError, GetConfigProfileResponse, UpdateConfigProfileContentData, UpdateConfigProfileContentError, UpdateConfigProfileContentResponse, RenameConfigProfileData, RenameConfigProfileError, RenameConfigProfileResponse, DeleteConfigProfileData, DeleteConfigProfileError, DeleteConfigProfileResponse, ListConfigRoutesError, ListConfigRoutesResponse, ReplaceConfigRoutesData, ReplaceConfigRoutesError, ReplaceConfigRoutesResponse, UpsertConfigRouteData, UpsertConfigRouteError, UpsertConfigRouteResponse, DeleteConfigRouteData, DeleteConfigRouteError, DeleteConfigRouteResponse, ListBotTypesError, ListBotTypesResponse, RegisterBotTypeData, RegisterBotTypeError, RegisterBotTypeResponse, ListBotsData, ListBotsError, ListBotsResponse, CreateBotData, CreateBotError, CreateBotResponse, ListBotStatsError, ListBotStatsResponse, GetBotByIdData, GetBotByIdError, GetBotByIdResponse, UpdateBotByIdData, UpdateBotByIdError, UpdateBotByIdResponse, DeleteBotByIdData, DeleteBotByIdError, DeleteBotByIdResponse, SetBotEnabledByIdData, SetBotEnabledByIdError, SetBotEnabledByIdResponse, TestBotByIdData, TestBotByIdError, TestBotByIdResponse, GetBotData, GetBotError, GetBotResponse, UpdateBotData, UpdateBotError, UpdateBotResponse, DeleteBotData, DeleteBotError, DeleteBotResponse, SetBotEnabledData, SetBotEnabledError, SetBotEnabledResponse, TestBotData, TestBotError, TestBotResponse, GetProviderSchemaError, GetProviderSchemaResponse, ListProviderSourcesError, ListProviderSourcesResponse, CreateProviderSourceData, CreateProviderSourceError, CreateProviderSourceResponse, GetProviderSourceByIdData, GetProviderSourceByIdError, GetProviderSourceByIdResponse, UpsertProviderSourceByIdData, UpsertProviderSourceByIdError, UpsertProviderSourceByIdResponse, DeleteProviderSourceByIdData, DeleteProviderSourceByIdError, DeleteProviderSourceByIdResponse, ListProviderSourceModelsByIdData, ListProviderSourceModelsByIdError, ListProviderSourceModelsByIdResponse, ListProvidersBySourceIdData, ListProvidersBySourceIdError, ListProvidersBySourceIdResponse, CreateProviderInSourceByIdData, CreateProviderInSourceByIdError, CreateProviderInSourceByIdResponse, GetProviderSourceData, GetProviderSourceError, GetProviderSourceResponse, UpsertProviderSourceData, UpsertProviderSourceError, UpsertProviderSourceResponse, DeleteProviderSourceData, DeleteProviderSourceError, DeleteProviderSourceResponse, ListProviderSourceModelsData, ListProviderSourceModelsError, ListProviderSourceModelsResponse, ListProvidersBySourceData, ListProvidersBySourceError, ListProvidersBySourceResponse, CreateProviderInSourceData, CreateProviderInSourceError, CreateProviderInSourceResponse, ListProvidersData, ListProvidersError, ListProvidersResponse, CreateProviderData, CreateProviderError, CreateProviderResponse, GetProviderByIdData, GetProviderByIdError, GetProviderByIdResponse, UpdateProviderByIdData, UpdateProviderByIdError, UpdateProviderByIdResponse, DeleteProviderByIdData, DeleteProviderByIdError, DeleteProviderByIdResponse, SetProviderEnabledByIdData, SetProviderEnabledByIdError, SetProviderEnabledByIdResponse, TestProviderByIdData, TestProviderByIdError, TestProviderByIdResponse, GetProviderEmbeddingDimensionByIdData, GetProviderEmbeddingDimensionByIdError, GetProviderEmbeddingDimensionByIdResponse, GetProviderData, GetProviderError, GetProviderResponse, UpdateProviderData, UpdateProviderError, UpdateProviderResponse, DeleteProviderData, DeleteProviderError, DeleteProviderResponse, SetProviderEnabledData, SetProviderEnabledError, SetProviderEnabledResponse, TestProviderData, TestProviderError, TestProviderResponse, GetProviderEmbeddingDimensionData, GetProviderEmbeddingDimensionError, GetProviderEmbeddingDimensionResponse, SendChatMessageData, SendChatMessageError, SendChatMessageResponse, OpenChatWebSocketData, OpenLiveChatWebSocketData, OpenUnifiedChatWebSocketData, ListChatSessionsData, ListChatSessionsError, ListChatSessionsResponse, CreateChatSessionData, CreateChatSessionError, CreateChatSessionResponse, BatchDeleteChatSessionsData, BatchDeleteChatSessionsError, BatchDeleteChatSessionsResponse, GetChatSessionData, GetChatSessionError, GetChatSessionResponse, UpdateChatSessionData, UpdateChatSessionError, UpdateChatSessionResponse, DeleteChatSessionData, DeleteChatSessionError, DeleteChatSessionResponse, StopChatSessionData, StopChatSessionError, StopChatSessionResponse, ResumeChatRunData, ResumeChatRunError, ResumeChatRunResponse, UpdateChatMessageData, UpdateChatMessageError, UpdateChatMessageResponse, RegenerateChatMessageData, RegenerateChatMessageError, RegenerateChatMessageResponse, ListChatConfigsError, ListChatConfigsResponse, CreateChatThreadData, CreateChatThreadError, CreateChatThreadResponse, GetChatThreadData, GetChatThreadError, GetChatThreadResponse, DeleteChatThreadData, DeleteChatThreadError, DeleteChatThreadResponse, SendChatThreadMessageData, SendChatThreadMessageError, SendChatThreadMessageResponse, ListChatProjectsError, ListChatProjectsResponse, CreateChatProjectData, CreateChatProjectError, CreateChatProjectResponse, GetChatProjectData, GetChatProjectError, GetChatProjectResponse, UpdateChatProjectData, UpdateChatProjectError, UpdateChatProjectResponse, DeleteChatProjectData, DeleteChatProjectError, DeleteChatProjectResponse, ListChatProjectSessionsData, ListChatProjectSessionsError, ListChatProjectSessionsResponse, ListChatProjectWorkspaceFilesData, ListChatProjectWorkspaceFilesError, ListChatProjectWorkspaceFilesResponse, GetChatProjectWorkspaceFileData, GetChatProjectWorkspaceFileError, GetChatProjectWorkspaceFileResponse, DownloadChatProjectWorkspaceFileData, DownloadChatProjectWorkspaceFileError, DownloadChatProjectWorkspaceFileResponse, AddChatProjectSessionData, AddChatProjectSessionError, AddChatProjectSessionResponse, RemoveChatProjectSessionData, RemoveChatProjectSessionError, RemoveChatProjectSessionResponse, SendImMessageData, SendImMessageError, SendImMessageResponse, ListImBotsError, ListImBotsResponse, UploadFileData, UploadFileError, UploadFileResponse, InitFileUploadData, InitFileUploadError, InitFileUploadResponse, UploadFileChunkData, UploadFileChunkError, UploadFileChunkResponse, CompleteFileUploadData, CompleteFileUploadError, CompleteFileUploadResponse, AbortFileUploadData, AbortFileUploadError, AbortFileUploadResponse, StatusFileUploadData, StatusFileUploadError, StatusFileUploadResponse, UploadOpenApiFileData, UploadOpenApiFileError, UploadOpenApiFileResponse, DownloadOpenApiFileData, DownloadOpenApiFileError, DownloadOpenApiFileResponse, GetFileByNameData, GetFileByNameError, GetFileByNameResponse, GetTokenFileData, GetTokenFileError, GetTokenFileResponse, GetAttachmentData, GetAttachmentError, GetAttachmentResponse, DeleteAttachmentData, DeleteAttachmentError, DeleteAttachmentResponse, DownloadAttachmentData, DownloadAttachmentError, DownloadAttachmentResponse, ListPluginsData, ListPluginsError, ListPluginsResponse, GetPluginByIdData, GetPluginByIdError, GetPluginByIdResponse, UninstallPluginByIdData, UninstallPluginByIdError, UninstallPluginByIdResponse, GetPluginConfigByIdData, GetPluginConfigByIdError, GetPluginConfigByIdResponse, UpdatePluginConfigByIdData, UpdatePluginConfigByIdError, UpdatePluginConfigByIdResponse, GetPluginConfigSchemaByIdData, GetPluginConfigSchemaByIdError, GetPluginConfigSchemaByIdResponse, ListPluginConfigFilesByIdData, ListPluginConfigFilesByIdError, ListPluginConfigFilesByIdResponse, UploadPluginConfigFilesByIdData, UploadPluginConfigFilesByIdError, UploadPluginConfigFilesByIdResponse, DeletePluginConfigFileByIdData, DeletePluginConfigFileByIdError, DeletePluginConfigFileByIdResponse, GetPluginReadmeByIdData, GetPluginReadmeByIdError, GetPluginReadmeByIdResponse, GetPluginChangelogByIdData, GetPluginChangelogByIdError, GetPluginChangelogByIdResponse, ReloadPluginByIdData, ReloadPluginByIdError, ReloadPluginByIdResponse, SetPluginEnabledByIdData, SetPluginEnabledByIdError, SetPluginEnabledByIdResponse, GetPluginData, GetPluginError, GetPluginResponse, UninstallPluginData, UninstallPluginError, UninstallPluginResponse, GetPluginConfigData, GetPluginConfigError, GetPluginConfigResponse, UpdatePluginConfigData, UpdatePluginConfigError, UpdatePluginConfigResponse, UpdatePluginLogLevelData, UpdatePluginLogLevelError, UpdatePluginLogLevelResponse, GetPluginConfigSchemaData, GetPluginConfigSchemaError, GetPluginConfigSchemaResponse, ListPluginConfigFilesData, ListPluginConfigFilesError, ListPluginConfigFilesResponse, UploadPluginConfigFilesData, UploadPluginConfigFilesError, UploadPluginConfigFilesResponse, DeletePluginConfigFileData, DeletePluginConfigFileError, DeletePluginConfigFileResponse, GetPluginReadmeData, GetPluginReadmeError, GetPluginReadmeResponse, GetPluginChangelogData, GetPluginChangelogError, GetPluginChangelogResponse, ReloadPluginData, ReloadPluginError, ReloadPluginResponse, BindPluginSourceData, BindPluginSourceError, BindPluginSourceResponse, SetPluginEnabledData, SetPluginEnabledError, SetPluginEnabledResponse, UpdatePluginData, UpdatePluginError, UpdatePluginResponse, UpdatePluginsData, UpdatePluginsError, UpdatePluginsResponse, CheckPluginVersionSupportData, CheckPluginVersionSupportError, CheckPluginVersionSupportResponse, ValidatePluginRepoData, ValidatePluginRepoError, ValidatePluginRepoResponse, ListFailedPluginsError, ListFailedPluginsResponse, UninstallFailedPluginData, UninstallFailedPluginError, UninstallFailedPluginResponse, ReloadFailedPluginData, ReloadFailedPluginError, ReloadFailedPluginResponse, InstallPluginFromGithubData, InstallPluginFromGithubError, InstallPluginFromGithubResponse, InstallPluginFromGitData, InstallPluginFromGitError, InstallPluginFromGitResponse, InstallPluginFromUrlData, InstallPluginFromUrlError, InstallPluginFromUrlResponse, InstallPluginFromUploadData, InstallPluginFromUploadError, InstallPluginFromUploadResponse, ListPluginMarketData, ListPluginMarketError, ListPluginMarketResponse, ListPluginMarketCategoriesError, ListPluginMarketCategoriesResponse, ListPluginSourcesError, ListPluginSourcesResponse, CreatePluginSourceData, CreatePluginSourceError, CreatePluginSourceResponse, ReplacePluginSourcesData, ReplacePluginSourcesError, ReplacePluginSourcesResponse, DeletePluginSourceData, DeletePluginSourceError, DeletePluginSourceResponse, DeletePluginSourceByIdData, DeletePluginSourceByIdError, DeletePluginSourceByIdResponse, ListPluginViewsByIdData, ListPluginViewsByIdError, ListPluginViewsByIdResponse, ListPluginPagesByIdData, ListPluginPagesByIdError, ListPluginPagesByIdResponse, GetPluginViewByIdData, GetPluginViewByIdError, GetPluginViewByIdResponse, GetPluginPageByIdData, GetPluginPageByIdError, GetPluginPageByIdResponse, ListPluginViewsData, ListPluginViewsError, ListPluginViewsResponse, ListPluginPagesData, ListPluginPagesError, ListPluginPagesResponse, GetPluginViewData, GetPluginViewError, GetPluginViewResponse, GetPluginPageData, GetPluginPageError, GetPluginPageResponse, GetPluginViewTokenAssetData, GetPluginViewTokenAssetError, GetPluginViewTokenAssetResponse, GetPluginPageBridgeSdkError, GetPluginPageBridgeSdkResponse, GetPluginExtensionRouteData, GetPluginExtensionRouteError, GetPluginExtensionRouteResponse, PostPluginExtensionRouteData, PostPluginExtensionRouteError, PostPluginExtensionRouteResponse, PutPluginExtensionRouteData, PutPluginExtensionRouteError, PutPluginExtensionRouteResponse, PatchPluginExtensionRouteData, PatchPluginExtensionRouteError, PatchPluginExtensionRouteResponse, DeletePluginExtensionRouteData, DeletePluginExtensionRouteError, DeletePluginExtensionRouteResponse, ListCommandsData, ListCommandsError, ListCommandsResponse, UpdateCommandData, UpdateCommandError, UpdateCommandResponse, ListCommandConflictsError, ListCommandConflictsResponse, ListToolsData, ListToolsError, ListToolsResponse, SetToolEnabledData, SetToolEnabledError, SetToolEnabledResponse, SetToolPermissionData, SetToolPermissionError, SetToolPermissionResponse, ListMcpServersError, ListMcpServersResponse, CreateMcpServerData, CreateMcpServerError, CreateMcpServerResponse, UpdateMcpServerByNameData, UpdateMcpServerByNameError, UpdateMcpServerByNameResponse, DeleteMcpServerByNameData, DeleteMcpServerByNameError, DeleteMcpServerByNameResponse, SetMcpServerEnabledByNameData, SetMcpServerEnabledByNameError, SetMcpServerEnabledByNameResponse, TestMcpServerByNameData, TestMcpServerByNameError, TestMcpServerByNameResponse, UpdateMcpServerData, UpdateMcpServerError, UpdateMcpServerResponse, DeleteMcpServerData, DeleteMcpServerError, DeleteMcpServerResponse, SetMcpServerEnabledData, SetMcpServerEnabledError, SetMcpServerEnabledResponse, TestMcpServerData, TestMcpServerError, TestMcpServerResponse, SyncModelScopeMcpServersData, SyncModelScopeMcpServersError, SyncModelScopeMcpServersResponse, ListSkillsData, ListSkillsError, ListSkillsResponse, UploadSkillData, UploadSkillError, UploadSkillResponse, UploadSkillsBatchData, UploadSkillsBatchError, UploadSkillsBatchResponse, UpdateSkillByNameData, UpdateSkillByNameError, UpdateSkillByNameResponse, DeleteSkillByNameData, DeleteSkillByNameError, DeleteSkillByNameResponse, DownloadSkillByNameData, DownloadSkillByNameError, DownloadSkillByNameResponse, ListSkillFilesByNameData, ListSkillFilesByNameError, ListSkillFilesByNameResponse, GetSkillFileByNameData, GetSkillFileByNameError, GetSkillFileByNameResponse, UpdateSkillFileByNameData, UpdateSkillFileByNameError, UpdateSkillFileByNameResponse, UpdateSkillData, UpdateSkillError, UpdateSkillResponse, DeleteSkillData, DeleteSkillError, DeleteSkillResponse, DownloadSkillData, DownloadSkillError, DownloadSkillResponse, ListSkillFilesData, ListSkillFilesError, ListSkillFilesResponse, GetSkillFileData, GetSkillFileError, GetSkillFileResponse, UpdateSkillFileData, UpdateSkillFileError, UpdateSkillFileResponse, ListNeoSkillCandidatesData, ListNeoSkillCandidatesError, ListNeoSkillCandidatesResponse, ListNeoSkillReleasesData, ListNeoSkillReleasesError, ListNeoSkillReleasesResponse, GetNeoSkillPayloadData, GetNeoSkillPayloadError, GetNeoSkillPayloadResponse, EvaluateNeoSkillCandidateData, EvaluateNeoSkillCandidateError, EvaluateNeoSkillCandidateResponse, PromoteNeoSkillCandidateData, PromoteNeoSkillCandidateError, PromoteNeoSkillCandidateResponse, RollbackNeoSkillReleaseData, RollbackNeoSkillReleaseError, RollbackNeoSkillReleaseResponse, SyncNeoSkillReleaseData, SyncNeoSkillReleaseError, SyncNeoSkillReleaseResponse, DeleteNeoSkillCandidateData, DeleteNeoSkillCandidateError, DeleteNeoSkillCandidateResponse, DeleteNeoSkillReleaseData, DeleteNeoSkillReleaseError, DeleteNeoSkillReleaseResponse, ListKnowledgeBasesData, ListKnowledgeBasesError, ListKnowledgeBasesResponse, CreateKnowledgeBaseData, CreateKnowledgeBaseError, CreateKnowledgeBaseResponse, GetKnowledgeBaseData, GetKnowledgeBaseError, GetKnowledgeBaseResponse, UpdateKnowledgeBaseData, UpdateKnowledgeBaseError, UpdateKnowledgeBaseResponse, DeleteKnowledgeBaseData, DeleteKnowledgeBaseError, DeleteKnowledgeBaseResponse, GetKnowledgeBaseStatsData, GetKnowledgeBaseStatsError, GetKnowledgeBaseStatsResponse, ListKnowledgeDocumentsData, ListKnowledgeDocumentsError, ListKnowledgeDocumentsResponse, UploadKnowledgeDocumentData, UploadKnowledgeDocumentError, UploadKnowledgeDocumentResponse, ImportKnowledgeDocumentsData, ImportKnowledgeDocumentsError, ImportKnowledgeDocumentsResponse, ImportKnowledgeDocumentFromUrlData, ImportKnowledgeDocumentFromUrlError, ImportKnowledgeDocumentFromUrlResponse, GetKnowledgeDocumentData, GetKnowledgeDocumentError, GetKnowledgeDocumentResponse, DeleteKnowledgeDocumentData, DeleteKnowledgeDocumentError, DeleteKnowledgeDocumentResponse, ListKnowledgeChunksData, ListKnowledgeChunksError, ListKnowledgeChunksResponse, DeleteKnowledgeChunkData, DeleteKnowledgeChunkError, DeleteKnowledgeChunkResponse, RetrieveKnowledgeBaseData, RetrieveKnowledgeBaseError, RetrieveKnowledgeBaseResponse, GetKnowledgeTaskData, GetKnowledgeTaskError, GetKnowledgeTaskResponse, GetPersonaTreeError, GetPersonaTreeResponse, ListPersonasData, ListPersonasError, ListPersonasResponse, CreatePersonaData, CreatePersonaError, CreatePersonaResponse, GetPersonaByIdData, GetPersonaByIdError, GetPersonaByIdResponse, UpdatePersonaByIdData, UpdatePersonaByIdError, UpdatePersonaByIdResponse, DeletePersonaByIdData, DeletePersonaByIdError, DeletePersonaByIdResponse, GetPersonaData, GetPersonaError, GetPersonaResponse, UpdatePersonaData, UpdatePersonaError, UpdatePersonaResponse, DeletePersonaData, DeletePersonaError, DeletePersonaResponse, ListPersonaFoldersData, ListPersonaFoldersError, ListPersonaFoldersResponse, CreatePersonaFolderData, CreatePersonaFolderError, CreatePersonaFolderResponse, UpdatePersonaFolderData, UpdatePersonaFolderError, UpdatePersonaFolderResponse, DeletePersonaFolderData, DeletePersonaFolderError, DeletePersonaFolderResponse, MovePersonaItemData, MovePersonaItemError, MovePersonaItemResponse, ReorderPersonaItemsData, ReorderPersonaItemsError, ReorderPersonaItemsResponse, ListSessionsData, ListSessionsError, ListSessionsResponse, ListActiveUmosData, ListActiveUmosError, ListActiveUmosResponse, ListSessionSendersData, ListSessionSendersError, ListSessionSendersResponse, 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, TestPypiIndexConnectionData, TestPypiIndexConnectionError, TestPypiIndexConnectionResponse, MeasurePypiIndexSpeedData, MeasurePypiIndexSpeedError, MeasurePypiIndexSpeedResponse, MeasureGhproxySpeedData, MeasureGhproxySpeedError, MeasureGhproxySpeedResponse, ListChangelogVersionsError, ListChangelogVersionsResponse, GetChangelogData, GetChangelogError, GetChangelogResponse, GetStartTimeError, GetStartTimeResponse, GetStorageStatusError, GetStorageStatusResponse, CleanupStorageData, CleanupStorageError, CleanupStorageResponse, RestartCoreError, RestartCoreResponse, ListBackupsData, ListBackupsError, ListBackupsResponse, CreateBackupData, CreateBackupError, CreateBackupResponse, UploadBackupData, UploadBackupError, UploadBackupResponse, InitBackupUploadData, InitBackupUploadError, InitBackupUploadResponse, UploadBackupChunkData, UploadBackupChunkError, UploadBackupChunkResponse, CompleteBackupUploadData, CompleteBackupUploadError, CompleteBackupUploadResponse, AbortBackupUploadData, AbortBackupUploadError, AbortBackupUploadResponse, StatusBackupUploadData, StatusBackupUploadError, StatusBackupUploadResponse, GetBackupProgressData, GetBackupProgressError, GetBackupProgressResponse, DownloadBackupData, DownloadBackupError, DownloadBackupResponse, RenameBackupData, RenameBackupError, RenameBackupResponse, DeleteBackupData, DeleteBackupError, DeleteBackupResponse, CheckBackupData, CheckBackupError, CheckBackupResponse, ImportBackupData, ImportBackupError, ImportBackupResponse, CheckUpdateError, CheckUpdateResponse, ListReleasesData, ListReleasesError, ListReleasesResponse, UpdateCoreData, UpdateCoreError, UpdateCoreResponse, UpdateDashboardData, UpdateDashboardError, UpdateDashboardResponse, GetUpdateProgressData, GetUpdateProgressError, GetUpdateProgressResponse, InstallPipPackageData, InstallPipPackageError, InstallPipPackageResponse, ListCronJobsData, ListCronJobsError, ListCronJobsResponse, CreateCronJobData, CreateCronJobError, CreateCronJobResponse, UpdateCronJobData, UpdateCronJobError, UpdateCronJobResponse, DeleteCronJobData, DeleteCronJobError, DeleteCronJobResponse, RunCronJobData, RunCronJobError, RunCronJobResponse, StreamLiveLogsError, StreamLiveLogsResponse, GetLogHistoryError, GetLogHistoryResponse, ExportLogsData, ExportLogsError, ExportLogsResponse, ListTracesData, ListTracesError, ListTracesResponse, GetTraceDetailData, GetTraceDetailError, GetTraceDetailResponse, GetTraceSettingsError, GetTraceSettingsResponse, UpdateTraceSettingsData, UpdateTraceSettingsError, UpdateTraceSettingsResponse, ListT2iTemplatesError, ListT2iTemplatesResponse, CreateT2iTemplateData, CreateT2iTemplateError, CreateT2iTemplateResponse, GetActiveT2iTemplateError, GetActiveT2iTemplateResponse, SetActiveT2iTemplateData, SetActiveT2iTemplateError, SetActiveT2iTemplateResponse, ResetDefaultT2iTemplateError, ResetDefaultT2iTemplateResponse, GetT2iTemplateData, GetT2iTemplateError, GetT2iTemplateResponse, UpdateT2iTemplateData, UpdateT2iTemplateError, UpdateT2iTemplateResponse, DeleteT2iTemplateData, DeleteT2iTemplateError, DeleteT2iTemplateResponse, GetSubagentConfigError, GetSubagentConfigResponse, UpdateSubagentConfigData, UpdateSubagentConfigError, UpdateSubagentConfigResponse, ListSubagentAvailableToolsError, ListSubagentAvailableToolsResponse, VerifyPlatformWebhookData, VerifyPlatformWebhookError, VerifyPlatformWebhookResponse, ReceivePlatformWebhookData, ReceivePlatformWebhookError, ReceivePlatformWebhookResponse } from './types.gen'; export const client = createClient(createConfig()); @@ -2794,6 +2794,36 @@ export const testGhproxyConnection = (opti }); }; +/** + * Test a PyPI index (mirror) endpoint + */ +export const testPypiIndexConnection = (options: OptionsLegacyParser) => { + return (options?.client ?? client).post({ + ...options, + url: '/api/v1/stats/pypi-index/test' + }); +}; + +/** + * Download a wheel from a PyPI index and measure the throughput + */ +export const measurePypiIndexSpeed = (options: OptionsLegacyParser) => { + return (options?.client ?? client).post({ + ...options, + url: '/api/v1/stats/pypi-index/speed' + }); +}; + +/** + * Download a repository file through a GitHub proxy and measure the throughput + */ +export const measureGhproxySpeed = (options: OptionsLegacyParser) => { + return (options?.client ?? client).post({ + ...options, + url: '/api/v1/stats/ghproxy/speed' + }); +}; + /** * List available changelog versions */ diff --git a/dashboard/src/api/generated/openapi-v1/types.gen.ts b/dashboard/src/api/generated/openapi-v1/types.gen.ts index 0ff1ad2af07..248e19a4d06 100644 --- a/dashboard/src/api/generated/openapi-v1/types.gen.ts +++ b/dashboard/src/api/generated/openapi-v1/types.gen.ts @@ -619,6 +619,10 @@ export type ProviderSourceConfigRequest = { config: DynamicConfig; }; +export type PypiIndexTestRequest = { + index_url: string; +}; + export type RenameRequest = { name: string; }; @@ -3505,6 +3509,30 @@ export type TestGhproxyConnectionResponse = (SuccessEnvelope); export type TestGhproxyConnectionError = unknown; +export type TestPypiIndexConnectionData = { + body: PypiIndexTestRequest; +}; + +export type TestPypiIndexConnectionResponse = (SuccessEnvelope); + +export type TestPypiIndexConnectionError = unknown; + +export type MeasurePypiIndexSpeedData = { + body: PypiIndexTestRequest; +}; + +export type MeasurePypiIndexSpeedResponse = (SuccessEnvelope); + +export type MeasurePypiIndexSpeedError = unknown; + +export type MeasureGhproxySpeedData = { + body: GhproxyTestRequest; +}; + +export type MeasureGhproxySpeedResponse = (SuccessEnvelope); + +export type MeasureGhproxySpeedError = unknown; + export type ListChangelogVersionsResponse = (SuccessEnvelope); export type ListChangelogVersionsError = unknown; diff --git a/dashboard/src/api/v1.ts b/dashboard/src/api/v1.ts index 734f18a804b..43bf2f6cebf 100644 --- a/dashboard/src/api/v1.ts +++ b/dashboard/src/api/v1.ts @@ -47,6 +47,7 @@ import { type PluginValidateRepoRequest, type PluginConfigFileDeleteRequest, type ProviderConfigRequest, + type PypiIndexTestRequest, type RuntimeInfo, type BatchSessionProviderRequest, type BatchSessionServiceRequest, @@ -1863,6 +1864,21 @@ export const statsApi = { ), ); }, + testPypiIndex(payload: PypiIndexTestRequest) { + return typed<{ latency?: number }>( + openApiV1.testPypiIndexConnection({ body: payload }), + ); + }, + measureGhproxySpeed(payload: GhproxyTestRequest) { + return typed<{ speed?: number; bytes?: number; duration?: number }>( + openApiV1.measureGhproxySpeed({ body: payload }), + ); + }, + measurePypiIndexSpeed(payload: PypiIndexTestRequest) { + return typed<{ speed?: number; bytes?: number; duration?: number }>( + openApiV1.measurePypiIndexSpeed({ body: payload }), + ); + }, startTime() { const v1Request = typed(openApiV1.getStartTime()); const legacyRequest = httpClient.get>( diff --git a/dashboard/src/components/shared/ProxySelector.vue b/dashboard/src/components/shared/ProxySelector.vue index 33033cfa415..8cb77d5d084 100644 --- a/dashboard/src/components/shared/ProxySelector.vue +++ b/dashboard/src/components/shared/ProxySelector.vue @@ -1,52 +1,85 @@ @@ -63,20 +96,47 @@ export default { data() { return { githubProxies: [ - "https://edgeone.gh-proxy.com", - "https://hk.gh-proxy.com", - "https://gh-proxy.com", "https://gh.dpik.top", + "https://gh-proxy.com", + "https://hk.gh-proxy.com", + "https://edgeone.gh-proxy.com", ], githubProxyRadioControl: "0", // the index of the selected proxy selectedGitHubProxy: "", + // Kept apart from selectedGitHubProxy so a typed URL survives preset + // switches and can still be read and tested while it is not selected. + customProxyValue: "", radioValue: "0", // 0: 不使用, 1: 使用 - loadingTestingConnection: false, + testing: false, testingProxies: {}, proxyStatus: {}, + proxySpeed: {}, initializing: true, } }, + computed: { + // The radio list is flat: "none", an index of githubProxies, or "custom". + // It is projected onto the persisted radioValue / githubProxyRadioControl + // pair that other pages still read from localStorage. + selection: { + get() { + if (String(this.radioValue) === "0") { + return "none"; + } + const control = String(this.githubProxyRadioControl); + return control === "-1" ? "custom" : control; + }, + set(value) { + const normalized = String(value); + if (normalized === "none") { + this.radioValue = "0"; + return; + } + this.radioValue = "1"; + this.githubProxyRadioControl = normalized === "custom" ? "-1" : normalized; + }, + }, + }, methods: { getProxyByControl(control) { const normalizedControl = String(control); @@ -89,46 +149,158 @@ export default { } return this.githubProxies[index] || ""; }, - async testSingleProxy(idx) { - this.testingProxies[idx] = true; - - const proxy = this.githubProxies[idx]; - + onCustomInput(value) { + this.customProxyValue = value; + // Only a selected custom option feeds the saved setting; otherwise + // the box is just a place to keep and test a URL. + if (this.selection === "custom") { + this.selectedGitHubProxy = String(value || "").trim(); + } + }, + // statusKey groups one probe result: an index of githubProxies, "direct" + // for the no-acceleration route, or "custom" for a user supplied URL. + async testProxy(proxyUrl, statusKey) { try { const response = await statsApi.testGhproxy({ - proxy_url: proxy + proxy_url: proxyUrl || "" }); - console.log(response.data); if (response.status === 200) { - this.proxyStatus[idx] = { + this.proxyStatus[statusKey] = { available: true, latency: Math.round(response.data.data.latency) }; } else { - this.proxyStatus[idx] = { + this.proxyStatus[statusKey] = { available: false, latency: 0 }; } } catch (error) { - this.proxyStatus[idx] = { + this.proxyStatus[statusKey] = { available: false, latency: 0 }; + } + }, + + // Every route this selector can probe: each preset mirror, the direct + // route, and a valid custom URL. + collectTargets() { + const targets = this.githubProxies.map((proxy, idx) => ({ + url: proxy, + key: String(idx), + })); + // An empty proxy_url tests the direct route, so "no proxy" is + // measured on the same footing as the mirrors. + targets.push({ url: "", key: "direct" }); + + const customProxy = (this.customProxyValue || "") + .trim() + .replace(/\/+$/, ""); + if (/^https?:\/\//i.test(customProxy)) { + targets.push({ url: customProxy, key: "custom" }); + } + + return targets; + }, + + // One route, latency then throughput. A stage argument lets the caller + // run the two stages as separate sweeps across all routes. + async testTarget(target, stage = "both") { + this.testingProxies[target.key] = true; + + try { + if (stage !== "speed") { + await this.testProxy(target.url, target.key); + } + if (stage !== "latency") { + await this.measureProxy(target.url, target.key); + } + } finally { + this.testingProxies[target.key] = false; + } + }, + + // Plain click tests every route one after another so results appear + // route by route, Ctrl/Cmd+click re-tests only the selected route, and + // Shift+click runs every route at once. + async onTestClick(event) { + if (this.testing) { + return; + } + + const mode = event.shiftKey + ? "parallel" + : event.ctrlKey || event.metaKey + ? "single" + : "serial"; + + this.testing = true; + + try { + if (mode === "single") { + // The radio uses "none" for the direct route while the test + // keys it as "direct", so the two names are mapped here. + const selectedKey = this.selection === "none" ? "direct" : this.selection; + const target = this.collectTargets().find((item) => item.key === selectedKey); + if (target) { + // Keep the other results: this only refreshes one route. + await this.testTarget(target); + } + return; + } + + const targets = this.collectTargets(); + // The radio names the direct route "none" while the test keys + // it as "direct", so the two names are mapped here as well. + const startIndex = targets.findIndex( + (target) => + target.key === + (this.selection === "none" ? "direct" : this.selection) + ); + // Start the sweep at the selected route so its result lands + // first, then keep the listed order of the remaining routes. + const ordered = + startIndex > 0 + ? [...targets.slice(startIndex), ...targets.slice(0, startIndex)] + : targets; + this.proxyStatus = {}; + this.proxySpeed = {}; + + if (mode === "parallel") { + // Every latency probe finishes before any download starts, + // so a saturating speed sample cannot skew a sibling's + // latency. + await Promise.all( + ordered.map((target) => this.testTarget(target, "latency")) + ); + await Promise.all( + ordered.map((target) => this.testTarget(target, "speed")) + ); + return; + } + + for (const target of ordered) { + await this.testTarget(target); + } } finally { - this.testingProxies[idx] = false; + this.testing = false; } }, - - async testAllProxies() { - this.loadingTestingConnection = true; - - const promises = this.githubProxies.map((proxy, idx) => - this.testSingleProxy(idx) - ); - - await Promise.all(promises); - this.loadingTestingConnection = false; + // Real download throughput of a repository asset in MB/s, so the number + // reflects the bandwidth a route delivers instead of its round trip. + async measureProxy(proxyUrl, speedKey) { + try { + const response = await statsApi.measureGhproxySpeed({ + proxy_url: proxyUrl || "" + }); + this.proxySpeed[speedKey] = + response.status === 200 + ? Number(response.data.data.speed) || 0 + : 0; + } catch (error) { + this.proxySpeed[speedKey] = 0; + } }, }, mounted() { @@ -137,6 +309,7 @@ export default { const savedProxy = localStorage.getItem('selectedGitHubProxy') || ""; const savedRadio = localStorage.getItem('githubProxyRadioValue') || "0"; const savedControl = String(localStorage.getItem('githubProxyRadioControl') || "0"); + const savedCustom = localStorage.getItem('selectedGitHubProxyCustom') || ""; this.radioValue = savedRadio; this.githubProxyRadioControl = savedControl; @@ -151,6 +324,9 @@ export default { this.selectedGitHubProxy = ""; } + this.customProxyValue = + savedCustom || (savedControl === "-1" ? savedProxy : ""); + this.initializing = false; }, watch: { @@ -163,6 +339,12 @@ export default { } localStorage.setItem('selectedGitHubProxy', newVal); }, + customProxyValue: function (newVal) { + if (this.initializing) { + return; + } + localStorage.setItem('selectedGitHubProxyCustom', newVal || ""); + }, radioValue: function (newVal) { if (this.initializing) { return; @@ -184,9 +366,11 @@ export default { this.selectedGitHubProxy = ""; return; } - if (normalizedVal !== "-1") { - this.selectedGitHubProxy = this.getProxyByControl(normalizedVal); + if (normalizedVal === "-1") { + this.selectedGitHubProxy = String(this.customProxyValue || "").trim(); + return; } + this.selectedGitHubProxy = this.getProxyByControl(normalizedVal); } } } @@ -198,6 +382,17 @@ export default { min-width: 0; } +.proxy-selector__header { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} + +.proxy-selector__test-button { + flex: 0 0 auto; +} + .proxy-selector__title { margin: 0; color: rgb(var(--v-theme-on-surface)); @@ -206,10 +401,6 @@ export default { line-height: 1.4; } -.proxy-selector__list { - margin-left: 16px; -} - .proxy-selector :deep(.v-label) { min-width: 0; font-size: 0.875rem; @@ -219,6 +410,7 @@ export default { .proxy-selector__option-label { display: flex; + flex-wrap: wrap; align-items: center; gap: 8px; min-width: 0; @@ -234,9 +426,13 @@ export default { display: flex; flex: 0 0 auto; align-items: center; + gap: 8px; } +/* A fixed width keeps this box as long as the PyPI one, and stops percentage + sizing from collapsing it inside the flex label. */ .proxy-selector__custom-input { - width: min(100%, 420px); + flex: 0 0 auto; + width: 272px; } diff --git a/dashboard/src/components/shared/PypiMirrorSelector.vue b/dashboard/src/components/shared/PypiMirrorSelector.vue new file mode 100644 index 00000000000..14fcf6e3f9e --- /dev/null +++ b/dashboard/src/components/shared/PypiMirrorSelector.vue @@ -0,0 +1,348 @@ + + + + + diff --git a/dashboard/src/i18n/locales/en-US/features/settings.json b/dashboard/src/i18n/locales/en-US/features/settings.json index 6a7840a1d89..3a1c9990b4b 100644 --- a/dashboard/src/i18n/locales/en-US/features/settings.json +++ b/dashboard/src/i18n/locales/en-US/features/settings.json @@ -79,14 +79,39 @@ "subtitle": "Set the GitHub proxy address used when downloading plugins or updating AstrBot. This is effective in mainland China's network environment. Can be customized, input takes effect in real time. All addresses do not guarantee stability. If errors occur when updating plugins/projects, please first check if the proxy address is working properly.", "label": "Select GitHub Proxy Address" }, + "pypiMirror": { + "title": "PyPI Package Index", + "subtitle": "PyPI index used by pip when installing AstrBot's Python dependencies. Pick a mirror or a custom URL. Restart AstrBot for changes to take effect.", + "label": "Select PyPI Source", + "custom": "Custom", + "customPlaceholder": "https://example.com/pypi/simple", + "testConnection": "Test Connectivity", + "testTip": { + "serial": "Click: test one source at a time, starting from the selected one", + "single": "Ctrl / ⌘ + Click: test only the selected source", + "parallel": "Shift + Click: test all sources in parallel" + }, + "unavailable": "Unavailable", + "presets": { + "official": "Official", + "aliyun": "Aliyun", + "ustc": "USTC", + "tsinghua": "Tsinghua", + "tencent": "Tencent Cloud" + } + }, "proxySelector": { - "title": "GitHub Proxy", - "noProxy": "Don't use GitHub Proxy", - "useProxy": "Use GitHub Proxy", - "testConnection": "Test Connection", - "available": "Available", + "title": "Select GitHub Mirror", + "noProxy": "Don't use GitHub Mirror", + "testConnection": "Test Connectivity", + "testTip": { + "serial": "Click: test one source at a time, starting from the selected one", + "single": "Ctrl / ⌘ + Click: test only the selected source", + "parallel": "Shift + Click: test all sources in parallel" + }, "unavailable": "Unavailable", - "custom": "Custom" + "custom": "Custom", + "customPlaceholder": "https://example.com" } }, "theme": { diff --git a/dashboard/src/i18n/locales/ja-JP/features/settings.json b/dashboard/src/i18n/locales/ja-JP/features/settings.json index 477b703716b..e357ce5c9e5 100644 --- a/dashboard/src/i18n/locales/ja-JP/features/settings.json +++ b/dashboard/src/i18n/locales/ja-JP/features/settings.json @@ -79,14 +79,39 @@ "subtitle": "プラグインのダウンロードや AstrBot の更新に使用する GitHub プロキシアドレスを設定します。中国本土のネットワーク環境で有効です。カスタム入力は即時反映されます。各アドレスの安定性は保証されません。プラグインやプロジェクトの更新時にエラーが発生した場合は、まずプロキシアドレスが正常に利用できるか確認してください。", "label": "GitHub プロキシアドレスを選択" }, + "pypiMirror": { + "title": "PyPI パッケージインデックス", + "subtitle": "AstrBot の Python 依存関係をインストールする際に使用する PyPI インデックスです。国内ミラーまたはカスタム URL を選択できます。変更は AstrBot の再起動後に反映されます。", + "label": "PyPI ソースを選択", + "custom": "カスタム", + "customPlaceholder": "https://example.com/pypi/simple", + "testConnection": "接続をテスト", + "testTip": { + "serial": "クリック:選択中の項目から 1 件ずつ順番にテスト", + "single": "Ctrl / ⌘ + クリック:選択中の項目のみテスト", + "parallel": "Shift + クリック:すべて並列にテスト" + }, + "unavailable": "利用不可", + "presets": { + "official": "公式", + "aliyun": "Alibaba Cloud", + "ustc": "中国科学技術大学", + "tsinghua": "清華大学", + "tencent": "Tencent Cloud" + } + }, "proxySelector": { - "title": "GitHub プロキシ", - "noProxy": "GitHub プロキシを使用しない", - "useProxy": "GitHub プロキシを使用", + "title": "GitHub ミラーを選択", + "noProxy": "GitHub ミラーを使用しない", "testConnection": "接続をテスト", - "available": "利用可能", + "testTip": { + "serial": "クリック:選択中の項目から 1 件ずつ順番にテスト", + "single": "Ctrl / ⌘ + クリック:選択中の項目のみテスト", + "parallel": "Shift + クリック:すべて並列にテスト" + }, "unavailable": "利用不可", - "custom": "カスタム" + "custom": "カスタム", + "customPlaceholder": "https://example.com" } }, "theme": { diff --git a/dashboard/src/i18n/locales/ru-RU/features/settings.json b/dashboard/src/i18n/locales/ru-RU/features/settings.json index 9b4587c1dcf..d68bd493498 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/settings.json +++ b/dashboard/src/i18n/locales/ru-RU/features/settings.json @@ -79,14 +79,39 @@ "subtitle": "Адрес для ускорения загрузки плагинов и обновлений AstrBot. Особенно актуально для пользователей из Китая. Все адреса предоставляются как есть, если обновление не удается — проверьте доступность выбранного зеркала.", "label": "Выбрать ускоритель GitHub" }, + "pypiMirror": { + "title": "Индекс пакетов PyPI", + "subtitle": "Индекс PyPI, который pip использует при установке Python-зависимостей AstrBot. Можно выбрать зеркало или указать свой адрес. Изменения вступят в силу после перезапуска AstrBot.", + "label": "Выбрать источник PyPI", + "custom": "Свой вариант", + "customPlaceholder": "https://example.com/pypi/simple", + "testConnection": "Проверить соединение", + "testTip": { + "serial": "Нажатие: проверять источники по одному, начиная с выбранного", + "single": "Ctrl / ⌘ + нажатие: проверить только выбранный источник", + "parallel": "Shift + нажатие: проверить все параллельно" + }, + "unavailable": "Недоступен", + "presets": { + "official": "Официальный", + "aliyun": "Alibaba Cloud", + "ustc": "USTC", + "tsinghua": "Tsinghua", + "tencent": "Tencent Cloud" + } + }, "proxySelector": { - "title": "Ускорение GitHub", - "noProxy": "Не использовать", - "useProxy": "Включить", + "title": "Выбрать зеркало GitHub", + "noProxy": "Не использовать зеркало GitHub", "testConnection": "Проверить соединение", - "available": "Доступен", + "testTip": { + "serial": "Нажатие: проверять источники по одному, начиная с выбранного", + "single": "Ctrl / ⌘ + нажатие: проверить только выбранный источник", + "parallel": "Shift + нажатие: проверить все параллельно" + }, "unavailable": "Недоступен", - "custom": "Свой вариант" + "custom": "Свой вариант", + "customPlaceholder": "https://example.com" } }, "theme": { diff --git a/dashboard/src/i18n/locales/zh-CN/features/settings.json b/dashboard/src/i18n/locales/zh-CN/features/settings.json index 8a1c3c31c23..f0b96458d38 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/settings.json +++ b/dashboard/src/i18n/locales/zh-CN/features/settings.json @@ -79,14 +79,39 @@ "subtitle": "设置下载插件或者更新 AstrBot 时所用的 GitHub 加速地址。这在中国大陆的网络环境有效。可以自定义,输入结果实时生效。所有地址均不保证稳定性,如果在更新插件/项目时出现报错,请首先检查加速地址是否能正常使用。", "label": "选择 GitHub 加速地址" }, + "pypiMirror": { + "title": "PyPI 软件仓库地址", + "subtitle": "安装 AstrBot 的 Python 依赖时使用的 PyPI 源。可选择国内镜像加速或自定义地址;修改后需重启 AstrBot 生效。", + "label": "选择 PyPI 源", + "custom": "自定义", + "customPlaceholder": "https://example.com/pypi/simple", + "testConnection": "测试连通性", + "testTip": { + "serial": "点击:从选中项开始逐个串行测试", + "single": "Ctrl / ⌘ + 点击:只测试当前选中项", + "parallel": "Shift + 点击:并行测试全部" + }, + "unavailable": "不可用", + "presets": { + "official": "官方", + "aliyun": "阿里云", + "ustc": "中科大", + "tsinghua": "清华大学", + "tencent": "腾讯云" + } + }, "proxySelector": { - "title": "GitHub 加速", - "noProxy": "不使用 GitHub 加速", - "useProxy": "使用 GitHub 加速", - "testConnection": "测试代理连通性", - "available": "可用", + "title": "选择 GitHub 镜像源", + "noProxy": "不使用 GitHub 镜像", + "testConnection": "测试连通性", + "testTip": { + "serial": "点击:从选中项开始逐个串行测试", + "single": "Ctrl / ⌘ + 点击:只测试当前选中项", + "parallel": "Shift + 点击:并行测试全部" + }, "unavailable": "不可用", - "custom": "自定义" + "custom": "自定义", + "customPlaceholder": "https://example.com" } }, "theme": { diff --git a/dashboard/src/views/Settings.vue b/dashboard/src/views/Settings.vue index 33bf04526d7..a76285ea6bd 100644 --- a/dashboard/src/views/Settings.vue +++ b/dashboard/src/views/Settings.vue @@ -154,6 +154,18 @@ +
+
+
+
{{ tm('network.pypiMirror.title') }}
+
{{ tm('network.pypiMirror.subtitle') }}
+
+
+ +
+
+
+
@@ -490,6 +502,7 @@ import { apiKeyApi, systemConfigApi } from '@/api/v1'; import AstrBotConfigV4 from '@/components/shared/AstrBotConfigV4.vue'; import WaitingForRestart from '@/components/shared/WaitingForRestart.vue'; import ProxySelector from '@/components/shared/ProxySelector.vue'; +import PypiMirrorSelector from '@/components/shared/PypiMirrorSelector.vue'; import BackupDialog from '@/components/shared/BackupDialog.vue'; import StorageCleanupPanel from '@/components/shared/StorageCleanupPanel.vue'; import LogExportButton from '@/components/shared/LogExportButton.vue'; @@ -741,7 +754,7 @@ const systemConfigGroups = computed(() => { return [ createGroup('runtime', ['timezone', 'callback_api_base']), - createGroup('network', ['http_proxy', 'no_proxy', 'pip_install_arg', 'pypi_index_url']), + createGroup('network', ['http_proxy', 'no_proxy', 'pip_install_arg']), createGroup('webuiSecurity', [ 'dashboard.trust_proxy_headers', 'dashboard.ssl.enable', diff --git a/openspec/openapi-v1.yaml b/openspec/openapi-v1.yaml index 4b77f1a7634..6f8192db685 100644 --- a/openspec/openapi-v1.yaml +++ b/openspec/openapi-v1.yaml @@ -4657,6 +4657,54 @@ paths: "200": $ref: "#/components/responses/Ok" + /api/v1/stats/pypi-index/test: + post: + tags: [Stats] + summary: Test a PyPI index (mirror) endpoint + operationId: testPypiIndexConnection + x-astrbot-scope: system + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/PypiIndexTestRequest" + responses: + "200": + $ref: "#/components/responses/Ok" + + /api/v1/stats/pypi-index/speed: + post: + tags: [Stats] + summary: Download a wheel from a PyPI index and measure the throughput + operationId: measurePypiIndexSpeed + x-astrbot-scope: system + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/PypiIndexTestRequest" + responses: + "200": + $ref: "#/components/responses/Ok" + + /api/v1/stats/ghproxy/speed: + post: + tags: [Stats] + summary: Download a repository file through a GitHub proxy and measure the throughput + operationId: measureGhproxySpeed + x-astrbot-scope: system + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/GhproxyTestRequest" + responses: + "200": + $ref: "#/components/responses/Ok" + /api/v1/changelogs: get: tags: [Stats] @@ -6792,6 +6840,14 @@ components: type: string additionalProperties: false + PypiIndexTestRequest: + type: object + required: [index_url] + properties: + index_url: + type: string + additionalProperties: false + TraceSettingsRequest: type: object properties: