From fe979bfdfe4d05ad42384771519d2d7856ebd8e9 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 12:30:36 +0800 Subject: [PATCH 01/28] feat(install): add multilingual setup and mainland China mirrors --- docs/en/deploy/astrbot/package.md | 58 +++++ docs/public/install.ps1 | 289 ++++++++++++++++++++- docs/public/install.sh | 403 ++++++++++++++++++++++++++---- docs/zh/deploy/astrbot/package.md | 58 +++++ 4 files changed, 752 insertions(+), 56 deletions(-) diff --git a/docs/en/deploy/astrbot/package.md b/docs/en/deploy/astrbot/package.md index c921f209d92..f90dfe989c8 100644 --- a/docs/en/deploy/astrbot/package.md +++ b/docs/en/deploy/astrbot/package.md @@ -18,6 +18,64 @@ AstrBot requires Python 3.12 or later. Use `--python 3.12` to ensure that `uv` c ## Install and Start +### Linux / macOS / WSL Installation Wizard + +Run with **Bash 3.2 or later**: + +```bash +curl -fsSL https://docs.astrbot.app/install.sh | bash +``` + +Choose Simplified Chinese, English, or Japanese, then a download source. The mainland China mirrors, official sources, and custom sources work as described in the Windows section below. Piped installations read menu input from the terminal. Without an available terminal, menus are skipped and the defaults are the system language and official sources. + +For unattended installation: + +```bash +curl -fsSL https://docs.astrbot.app/install.sh -o install.sh +bash install.sh --language en-US --download-source china --non-interactive +``` + +Use `--download-source custom --uv-download-base-url --pypi-index-url ` for custom sources. The directory must contain `uv-.tar.gz` and its matching `.sha256` file, using the official uv archive layout. Set `--python-install-mirror ` for a separate Python interpreter mirror, or use the existing `UV_PYTHON_INSTALL_MIRROR` environment variable. + +The script detects the macOS/Linux architecture and prefers static musl builds on Linux. Unavailable mirrors or checksum failures trigger a backup mirror and then the official installer. Downloads require `curl` or `wget`; mirror installation also requires `tar` and `sha256sum` or `shasum`. The uv directory is added to PATH for the current installation process, and mirror installations display the directory to add to PATH in new terminals. + +### Windows Installation Wizard + +Run the following in **PowerShell 7 or later**: + +```powershell +irm https://docs.astrbot.app/install.ps1 | iex +``` + +The wizard offers Simplified Chinese, English, and Japanese, followed by a download source selection: + +- **Mainland China accelerated download mirrors**: download uv from university mirrors listed by MirrorZ and install AstrBot and its Python package dependencies through `https://mirrors.cernet.edu.cn/pypi/web/simple/`. If a uv download or SHA256 verification fails, the installer tries the backup mirror and then the official installer. +- **Official sources**: use the official uv installer and PyPI. +- **Custom**: specify a uv ZIP download directory and a PyPI simple index. The uv directory must contain `uv--pc-windows-msvc.zip` and the matching `.sha256` file. + +In interactive mode, the system language determines the initial language selection. The Chinese interface defaults to the mainland China mirrors; English and Japanese default to official sources. Both selections can be changed. + +For unattended installation, download the script and pass explicit options: + +```powershell +Invoke-WebRequest https://docs.astrbot.app/install.ps1 -OutFile install.ps1 +.\install.ps1 -Language en-US -DownloadSource china -NonInteractive +``` + +`-Language` accepts `zh-CN`, `en-US`, or `ja-JP`; `-DownloadSource` accepts `china`, `official`, or `custom`. Unattended runs and redirected input skip all menus. Omitted options default to the system language and official sources. + +For custom sources, replace these example HTTPS URLs with your own: + +```powershell +.\install.ps1 -Language en-US -DownloadSource custom -NonInteractive ` + -UvDownloadBaseUrl 'https://mirror.example.com/uv/release' ` + -PypiIndexUrl 'https://mirror.example.com/pypi/simple/' +``` + +PyPI mirrors do not include Python interpreters. When Python 3.12 is missing, its download uses the existing uv configuration. Pass `-PythonInstallMirror ` to specify a Python download mirror compatible with uv, or set `UV_PYTHON_INSTALL_MIRROR` beforehand. The script does not change global pip settings; a Python mirror passed as an option applies only during this installation. + +### Manual Installation and Startup + ```bash uv tool install astrbot --python 3.12 astrbot diff --git a/docs/public/install.ps1 b/docs/public/install.ps1 index 0998d2f6a0a..412769325e9 100644 --- a/docs/public/install.ps1 +++ b/docs/public/install.ps1 @@ -1,10 +1,187 @@ -# deploy-cli.ps1 - Install astrbot with uv on Windows PowerShell. - #Requires -Version 7.0 +# Install AstrBot with uv on Windows PowerShell. +[CmdletBinding()] +param( + [ValidateSet('zh-CN', 'en-US', 'ja-JP')] + [string]$Language, + [ValidateSet('china', 'official', 'custom')] + [string]$DownloadSource, + [string]$UvDownloadBaseUrl, + [string]$PypiIndexUrl, + [string]$PythonInstallMirror, + [switch]$NonInteractive +) + $ErrorActionPreference = 'Stop' $UseColor = [string]::IsNullOrEmpty($env:NO_COLOR) -and [Console]::IsOutputRedirected -eq $false +$CanPrompt = -not $NonInteractive -and [Environment]::UserInteractive -and -not [Console]::IsInputRedirected + +$defaultLanguage = switch -Wildcard ((Get-UICulture).Name) { + 'zh*' { 'zh-CN' } + 'ja*' { 'ja-JP' } + default { 'en-US' } +} + +if (-not $Language) { + $Language = $defaultLanguage + if ($CanPrompt) { + $languages = @('zh-CN', 'en-US', 'ja-JP') + $defaultChoice = [array]::IndexOf($languages, $Language) + 1 + Write-Host "`n请选择语言 / Select language / 言語を選択してください" + Write-Host ' 1. 简体中文' + Write-Host ' 2. English' + Write-Host ' 3. 日本語' + do { + $choice = Read-Host "1-3 [$defaultChoice]" + } until ($choice -eq '' -or $choice -in @('1', '2', '3')) + if ($choice) { $Language = $languages[[int]$choice - 1] } + } +} + +$messages = @{ + 'zh-CN' = @{ + ChooseSource = '请选择下载源' + China = '中国大陆加速下载镜像源' + Official = '官方源' + Custom = '自定义' + Choice = '输入编号,按回车使用默认选项' + InvalidChoice = '请输入 1、2 或 3。' + UvUrl = 'uv ZIP 下载目录 URL(不含文件名)' + PypiUrl = 'PyPI simple 索引 URL' + CustomRequired = '自定义源需要同时提供 UvDownloadBaseUrl 和 PypiIndexUrl。' + InvalidUrl = '下载源必须是有效的 HTTPS URL:{0}' + WindowsOnly = '请在 Windows 上运行此 PowerShell 脚本;Linux/macOS 请使用 install.sh。' + Source = '下载源:{0}' + UvMirror = 'uv 下载目录:{0}' + Pypi = 'PyPI 索引:{0}' + PythonMirror = 'Python 下载镜像:{0}' + PythonDefault = 'Python 下载沿用 uv 的配置;如需加速,可通过 -PythonInstallMirror 指定镜像(PyPI 镜像不包含 Python 解释器)。' + InstallingUv = '未找到 uv,正在安装…' + TryingMirror = '正在尝试 uv 镜像:{0}' + MirrorFailed = 'uv 镜像下载失败:{0}' + OfficialFallback = '正在尝试 uv 官方安装器…' + UnsupportedArch = '不支持的 Windows 架构:{0}' + InvalidChecksum = 'uv 压缩包 SHA256 校验失败。' + MissingBinaries = 'uv 压缩包中缺少 uv.exe 或 uvx.exe。' + UvFailed = 'uv 安装失败。请参考 https://docs.astral.sh/uv/getting-started/installation/' + InstallingAstrBot = '正在使用 Python 3.12 安装 AstrBot…' + AstrBotFailed = 'AstrBot 安装失败(退出码 {0})。请检查上方的下载或依赖错误。' + Installed = 'AstrBot 安装完成。' + } + 'en-US' = @{ + ChooseSource = 'Select a download source' + China = 'Mainland China accelerated download mirrors' + Official = 'Official sources' + Custom = 'Custom' + Choice = 'Enter a number, or press Enter to use the default' + InvalidChoice = 'Please enter 1, 2, or 3.' + UvUrl = 'uv ZIP download directory URL (without the filename)' + PypiUrl = 'PyPI simple index URL' + CustomRequired = 'Custom sources require both UvDownloadBaseUrl and PypiIndexUrl.' + InvalidUrl = 'Download sources must be valid HTTPS URLs: {0}' + WindowsOnly = 'Run this PowerShell script on Windows; use install.sh on Linux/macOS.' + Source = 'Download source: {0}' + UvMirror = 'uv download directory: {0}' + Pypi = 'PyPI index: {0}' + PythonMirror = 'Python download mirror: {0}' + PythonDefault = 'Python downloads use the existing uv configuration. Set -PythonInstallMirror to use a mirror (PyPI mirrors do not include Python interpreters).' + InstallingUv = 'uv was not found. Installing uv...' + TryingMirror = 'Trying uv mirror: {0}' + MirrorFailed = 'uv mirror download failed: {0}' + OfficialFallback = 'Trying the official uv installer...' + UnsupportedArch = 'Unsupported Windows architecture: {0}' + InvalidChecksum = 'uv archive SHA256 verification failed.' + MissingBinaries = 'The uv archive is missing uv.exe or uvx.exe.' + UvFailed = 'Failed to install uv. See https://docs.astral.sh/uv/getting-started/installation/' + InstallingAstrBot = 'Installing AstrBot with Python 3.12...' + AstrBotFailed = 'AstrBot installation failed (exit code {0}). Check the download or dependency errors above.' + Installed = 'AstrBot has been installed.' + } + 'ja-JP' = @{ + ChooseSource = 'ダウンロード元を選択してください' + China = '中国本土向け高速ダウンロードミラー' + Official = '公式ソース' + Custom = 'カスタム' + Choice = '番号を入力してください。Enter で既定値を使用します' + InvalidChoice = '1、2、3 のいずれかを入力してください。' + UvUrl = 'uv ZIP のダウンロードディレクトリ URL(ファイル名を除く)' + PypiUrl = 'PyPI simple インデックス URL' + CustomRequired = 'カスタムソースには UvDownloadBaseUrl と PypiIndexUrl の両方が必要です。' + InvalidUrl = 'ダウンロード元には有効な HTTPS URL を指定してください:{0}' + WindowsOnly = 'この PowerShell スクリプトは Windows 用です。Linux/macOS では install.sh を使用してください。' + Source = 'ダウンロード元:{0}' + UvMirror = 'uv ダウンロードディレクトリ:{0}' + Pypi = 'PyPI インデックス:{0}' + PythonMirror = 'Python ダウンロードミラー:{0}' + PythonDefault = 'Python のダウンロードには既存の uv 設定を使用します。ミラーを使うには -PythonInstallMirror を指定してください(PyPI ミラーに Python 本体は含まれません)。' + InstallingUv = 'uv が見つかりません。インストールしています…' + TryingMirror = 'uv ミラーを試しています:{0}' + MirrorFailed = 'uv ミラーからのダウンロードに失敗しました:{0}' + OfficialFallback = 'uv の公式インストーラーを試しています…' + UnsupportedArch = '未対応の Windows アーキテクチャ:{0}' + InvalidChecksum = 'uv アーカイブの SHA256 検証に失敗しました。' + MissingBinaries = 'uv アーカイブに uv.exe または uvx.exe がありません。' + UvFailed = 'uv のインストールに失敗しました。https://docs.astral.sh/uv/getting-started/installation/ を参照してください。' + InstallingAstrBot = 'Python 3.12 で AstrBot をインストールしています…' + AstrBotFailed = 'AstrBot のインストールに失敗しました(終了コード {0})。上記のダウンロードまたは依存関係のエラーを確認してください。' + Installed = 'AstrBot のインストールが完了しました。' + } +} +$Text = $messages[$Language] + +if (-not $DownloadSource) { + $DownloadSource = 'official' + if ($CanPrompt) { + $defaultChoice = if ($Language -eq 'zh-CN') { '1' } else { '2' } + Write-Host "`n$($Text.ChooseSource)" + Write-Host " 1. $($Text.China)" + Write-Host " 2. $($Text.Official)" + Write-Host " 3. $($Text.Custom)" + do { + $choice = Read-Host "$($Text.Choice) [$defaultChoice]" + if ($choice -eq '') { $choice = $defaultChoice } + if ($choice -notin @('1', '2', '3')) { Write-Host $Text.InvalidChoice } + } until ($choice -in @('1', '2', '3')) + $DownloadSource = @('china', 'official', 'custom')[[int]$choice - 1] + } +} + +$uvMirrorUrls = @() +switch ($DownloadSource) { + 'china' { + # Use uv mirrors listed by MirrorZ; the release-directory layout is not GitHub's layout. + $uvMirrorUrls = @( + 'https://mirrors.ustc.edu.cn/github-release/astral-sh/uv/LatestRelease', + 'https://mirror.nyist.edu.cn/github-release/astral-sh/uv/LatestRelease' + ) + $PypiIndexUrl = 'https://mirrors.cernet.edu.cn/pypi/web/simple/' + } + 'official' { $PypiIndexUrl = 'https://pypi.org/simple/' } + 'custom' { + if ($CanPrompt) { + if (-not $UvDownloadBaseUrl) { $UvDownloadBaseUrl = Read-Host $Text.UvUrl } + if (-not $PypiIndexUrl) { $PypiIndexUrl = Read-Host $Text.PypiUrl } + } + if (-not $UvDownloadBaseUrl -or -not $PypiIndexUrl) { + throw $Text.CustomRequired + } + $uvMirrorUrls = @($UvDownloadBaseUrl.TrimEnd('/')) + } +} + +foreach ($url in @($uvMirrorUrls) + @($PypiIndexUrl, $PythonInstallMirror)) { + if (-not $url) { continue } + $parsedUrl = $null + if (-not [Uri]::TryCreate($url, [UriKind]::Absolute, [ref]$parsedUrl) -or + $parsedUrl.Scheme -ne 'https' -or -not $parsedUrl.Host -or $parsedUrl.UserInfo -or + $parsedUrl.Query -or $parsedUrl.Fragment) { + throw ($Text.InvalidUrl -f $url) + } +} + +if (-not $IsWindows) { throw $Text.WindowsOnly } function Write-Status { param( @@ -43,6 +220,12 @@ function Update-UvPath { if ($env:LOCALAPPDATA) { $candidateDirs += Join-Path $env:LOCALAPPDATA "uv" } + foreach ($dir in @($env:UV_INSTALL_DIR, $env:UV_UNMANAGED_INSTALL, $env:XDG_BIN_HOME)) { + if ($dir) { $candidateDirs += $dir } + } + if ($env:XDG_DATA_HOME) { + $candidateDirs += Join-Path $env:XDG_DATA_HOME '..\bin' + } foreach ($dir in $candidateDirs) { if ((Test-Path $dir) -and (($env:PATH -split ';') -notcontains $dir)) { @@ -52,22 +235,86 @@ function Update-UvPath { } function Install-Uv { - Info "uv was not found. Installing uv..." + Info $Text.InstallingUv - $tempScript = [System.IO.Path]::GetTempFileName() + ".ps1" + $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([guid]::NewGuid().ToString()) + $null = New-Item -ItemType Directory -Path $tempDir try { + if ($uvMirrorUrls.Count -gt 0) { + $arch = switch ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()) { + 'X64' { 'x86_64' } + 'X86' { 'i686' } + 'Arm64' { 'aarch64' } + default { throw ($Text.UnsupportedArch -f $_) } + } + $artifact = "uv-$arch-pc-windows-msvc.zip" + foreach ($baseUrl in $uvMirrorUrls) { + try { + Info ($Text.TryingMirror -f $baseUrl) + $archive = Join-Path $tempDir $artifact + $checksumFile = Join-Path $tempDir "$artifact.sha256" + Invoke-WebRequest -Uri "$baseUrl/$artifact" -OutFile $archive -TimeoutSec 60 + Invoke-WebRequest -Uri "$baseUrl/$artifact.sha256" -OutFile $checksumFile -TimeoutSec 30 + $expectedHash = ((Get-Content $checksumFile -Raw).Trim() -split '\s+')[0] + if ($expectedHash -notmatch '^[a-fA-F0-9]{64}$' -or + (Get-FileHash $archive -Algorithm SHA256).Hash -ne $expectedHash) { + throw $Text.InvalidChecksum + } + $unpacked = Join-Path $tempDir 'unpacked' + Expand-Archive -Path $archive -DestinationPath $unpacked -Force + if (-not (Test-Path (Join-Path $unpacked 'uv.exe')) -or + -not (Test-Path (Join-Path $unpacked 'uvx.exe'))) { + throw $Text.MissingBinaries + } + } catch { + Warn ($Text.MirrorFailed -f $_.Exception.Message) + Remove-Item (Join-Path $tempDir 'unpacked') -Recurse -Force -ErrorAction SilentlyContinue + continue + } + + # Respect uv's installation directory settings when using a mirror too. + $installDir = if ($env:UV_INSTALL_DIR) { $env:UV_INSTALL_DIR } + elseif ($env:UV_UNMANAGED_INSTALL) { $env:UV_UNMANAGED_INSTALL } + elseif ($env:XDG_BIN_HOME) { $env:XDG_BIN_HOME } + elseif ($env:XDG_DATA_HOME) { Join-Path $env:XDG_DATA_HOME '..\bin' } + else { Join-Path $env:USERPROFILE '.local\bin' } + $null = New-Item -ItemType Directory -Path $installDir -Force + Copy-Item (Join-Path $unpacked '*.exe') -Destination $installDir -Force + $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') + if (-not $env:UV_UNMANAGED_INSTALL -and -not $env:INSTALLER_NO_MODIFY_PATH -and + ($userPath -split ';') -notcontains $installDir) { + [Environment]::SetEnvironmentVariable('Path', "$installDir;$userPath", 'User') + } + Update-UvPath + return + } + } + + Info $Text.OfficialFallback + $tempScript = Join-Path $tempDir 'uv-install.ps1' Invoke-WebRequest -Uri "https://astral.sh/uv/install.ps1" -OutFile $tempScript -UseBasicParsing & $tempScript + if (-not $?) { throw $Text.UvFailed } Update-UvPath } catch { - Err "Failed to install uv." - Err "Please install uv manually: https://docs.astral.sh/uv/getting-started/installation/" + Err $_.Exception.Message + Err $Text.UvFailed exit 1 } finally { - Remove-Item $tempScript -ErrorAction SilentlyContinue + Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue } } +Info ($Text.Source -f $(switch ($DownloadSource) { + 'china' { $Text.China } + 'official' { $Text.Official } + 'custom' { $Text.Custom } +})) +foreach ($url in $uvMirrorUrls) { Info ($Text.UvMirror -f $url) } +Info ($Text.Pypi -f $PypiIndexUrl) + +# Recover an existing installation before downloading uv again. +Update-UvPath if (-not (Test-Command "uv")) { Install-Uv } @@ -75,12 +322,30 @@ if (-not (Test-Command "uv")) { Update-UvPath if (-not (Test-Command "uv")) { - Err "uv was not found after installation." - Err "Please install uv manually: https://docs.astral.sh/uv/getting-started/installation/" + Err $Text.UvFailed exit 1 } Ok (& uv --version) -Info "Installing AstrBot with Python 3.12..." -uv tool install --python 3.12 astrbot -Ok "AstrBot has been installed." +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +if (-not $PythonInstallMirror) { $PythonInstallMirror = $env:UV_PYTHON_INSTALL_MIRROR } +if ($PythonInstallMirror) { + Info ($Text.PythonMirror -f $PythonInstallMirror) +} else { + Info $Text.PythonDefault +} + +$previousPythonMirror = $env:UV_PYTHON_INSTALL_MIRROR +try { + if ($PythonInstallMirror) { $env:UV_PYTHON_INSTALL_MIRROR = $PythonInstallMirror } + Info $Text.InstallingAstrBot + uv tool install --python 3.12 astrbot --default-index $PypiIndexUrl + if ($LASTEXITCODE -ne 0) { + Err ($Text.AstrBotFailed -f $LASTEXITCODE) + exit $LASTEXITCODE + } + Ok $Text.Installed +} finally { + $env:UV_PYTHON_INSTALL_MIRROR = $previousPythonMirror +} diff --git a/docs/public/install.sh b/docs/public/install.sh index 16caa08e839..c677768cd67 100755 --- a/docs/public/install.sh +++ b/docs/public/install.sh @@ -1,68 +1,383 @@ #!/usr/bin/env bash -# deploy-cli.sh - Install astrbot with uv on Linux / macOS / WSL. +# Install AstrBot with uv on Linux / macOS / WSL. Compatible with Bash 3.2+. set -euo pipefail +language='' +download_source='' +uv_download_base_url='' +pypi_index_url='' +python_install_mirror='' +non_interactive=0 + +while [ "$#" -gt 0 ]; do + case "$1" in + --language|--download-source|--uv-download-base-url|--pypi-index-url|--python-install-mirror) + if [ "$#" -lt 2 ] || [[ "$2" == --* ]] || [ -z "$2" ]; then + printf 'Missing value for %s\n' "$1" >&2 + exit 2 + fi + case "$1" in + --language) language="$2" ;; + --download-source) download_source="$2" ;; + --uv-download-base-url) uv_download_base_url="$2" ;; + --pypi-index-url) pypi_index_url="$2" ;; + --python-install-mirror) python_install_mirror="$2" ;; + esac + shift 2 + ;; + --non-interactive) non_interactive=1; shift ;; + --help|-h) + cat <<'HELP' +Usage: bash install.sh [options] + --language zh-CN|en-US|ja-JP + --download-source china|official|custom + --uv-download-base-url HTTPS-URL Directory containing uv archives and .sha256 files + --pypi-index-url HTTPS-URL PyPI simple index for custom sources + --python-install-mirror HTTPS-URL Optional Python interpreter download mirror + --non-interactive Skip all menus (defaults: system language, official sources) +HELP + exit 0 + ;; + *) printf 'Unknown option: %s\n' "$1" >&2; exit 2 ;; + esac +done + if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then - RED='\033[0;31m' - GREEN='\033[0;32m' - YELLOW='\033[1;33m' - CYAN='\033[0;36m' - NC='\033[0m' + RED=$'\033[0;31m' + GREEN=$'\033[0;32m' + YELLOW=$'\033[1;33m' + CYAN=$'\033[0;36m' + NC=$'\033[0m' else - RED='' - GREEN='' - YELLOW='' - CYAN='' - NC='' + RED=''; GREEN=''; YELLOW=''; CYAN=''; NC='' fi -info() { echo -e "${CYAN}[INFO]${NC} $*"; } -warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } -ok() { echo -e "${GREEN}[OK]${NC} $*"; } -err() { echo -e "${RED}[ERROR]${NC} $*" >&2; } +info() { printf '%s[INFO]%s %s\n' "$CYAN" "$NC" "$*"; } +warn() { printf '%s[WARN]%s %s\n' "$YELLOW" "$NC" "$*"; } +ok() { printf '%s[OK]%s %s\n' "$GREEN" "$NC" "$*"; } +err() { printf '%s[ERROR]%s %s\n' "$RED" "$NC" "$*" >&2; } +has() { command -v "$1" >/dev/null 2>&1; } + +# Read menus from the terminal, never from the script stream in curl | bash. +can_prompt=0 +if [ "$non_interactive" -eq 0 ]; then + if [ -t 0 ]; then + exec 3<&0 + can_prompt=1 + elif { exec 3/dev/null; then + can_prompt=1 + fi +fi + +if [ -z "$language" ]; then + case "${LC_ALL:-${LC_MESSAGES:-${LANG:-en_US}}}" in + zh*) language='zh-CN'; default_choice=1 ;; + ja*) language='ja-JP'; default_choice=3 ;; + *) language='en-US'; default_choice=2 ;; + esac + if [ "$can_prompt" -eq 1 ]; then + printf '\n请选择语言 / Select language / 言語を選択してください\n 1. 简体中文\n 2. English\n 3. 日本語\n' >&2 + while :; do + printf '1-3 [%s]: ' "$default_choice" >&2 + IFS= read -r choice <&3 || exit 1 + case "${choice:-$default_choice}" in + 1) language='zh-CN'; break ;; + 2) language='en-US'; break ;; + 3) language='ja-JP'; break ;; + esac + done + fi +fi +case "$language" in + zh-CN|en-US|ja-JP) ;; + *) err "Unsupported language: $language"; exit 2 ;; +esac -has() { - command -v "$1" >/dev/null 2>&1 +message() { + local key="$1" format + shift + case "$language:$key" in + zh-CN:ChooseSource) format='请选择下载源' ;; + zh-CN:China) format='中国大陆加速下载镜像源' ;; + zh-CN:Official) format='官方源' ;; + zh-CN:Custom) format='自定义' ;; + zh-CN:Choice) format='输入编号,按回车使用默认选项' ;; + zh-CN:InvalidChoice) format='请输入 1、2 或 3。' ;; + zh-CN:UvUrl) format='uv tar.gz 下载目录 URL(不含文件名)' ;; + zh-CN:PypiUrl) format='PyPI simple 索引 URL' ;; + zh-CN:CustomRequired) format='自定义源需要同时提供 --uv-download-base-url 和 --pypi-index-url。' ;; + zh-CN:InvalidUrl) format='下载源必须是有效的 HTTPS URL:%s' ;; + zh-CN:Source) format='下载源:%s' ;; + zh-CN:UvMirror) format='uv 下载目录:%s' ;; + zh-CN:Pypi) format='PyPI 索引:%s' ;; + zh-CN:PythonMirror) format='Python 下载镜像:%s' ;; + zh-CN:PythonDefault) format='Python 下载沿用 uv 的配置;如需加速,可通过 --python-install-mirror 指定镜像(PyPI 镜像不包含 Python 解释器)。' ;; + zh-CN:InstallingUv) format='未找到 uv,正在安装…' ;; + zh-CN:TryingMirror) format='正在尝试 uv 镜像:%s' ;; + zh-CN:MirrorFailed) format='uv 镜像下载失败:%s' ;; + zh-CN:OfficialFallback) format='正在尝试 uv 官方安装器…' ;; + zh-CN:UnsupportedPlatform) format='当前平台无法使用预设 uv 镜像,将尝试官方安装器:%s' ;; + zh-CN:InvalidChecksum) format='uv 压缩包 SHA256 校验失败。' ;; + zh-CN:MissingBinaries) format='uv 压缩包中缺少 uv 或 uvx。' ;; + zh-CN:MissingTools) format='需要 curl 或 wget 下载文件;镜像安装还需要 tar 和 sha256sum 或 shasum。' ;; + zh-CN:PathHint) format='uv 安装目录:%s。请确保新终端的 PATH 中包含此目录。' ;; + zh-CN:UvFailed) format='uv 安装失败。请参考 https://docs.astral.sh/uv/getting-started/installation/' ;; + zh-CN:InstallingAstrBot) format='正在使用 Python 3.12 安装 AstrBot…' ;; + zh-CN:AstrBotFailed) format='AstrBot 安装失败(退出码 %s)。请检查上方的下载或依赖错误。' ;; + zh-CN:Installed) format='AstrBot 安装完成。' ;; + en-US:ChooseSource) format='Select a download source' ;; + en-US:China) format='Mainland China accelerated download mirrors' ;; + en-US:Official) format='Official sources' ;; + en-US:Custom) format='Custom' ;; + en-US:Choice) format='Enter a number, or press Enter to use the default' ;; + en-US:InvalidChoice) format='Please enter 1, 2, or 3.' ;; + en-US:UvUrl) format='uv tar.gz download directory URL (without the filename)' ;; + en-US:PypiUrl) format='PyPI simple index URL' ;; + en-US:CustomRequired) format='Custom sources require both --uv-download-base-url and --pypi-index-url.' ;; + en-US:InvalidUrl) format='Download sources must be valid HTTPS URLs: %s' ;; + en-US:Source) format='Download source: %s' ;; + en-US:UvMirror) format='uv download directory: %s' ;; + en-US:Pypi) format='PyPI index: %s' ;; + en-US:PythonMirror) format='Python download mirror: %s' ;; + en-US:PythonDefault) format='Python downloads use the existing uv configuration. Set --python-install-mirror to use a mirror (PyPI mirrors do not include Python interpreters).' ;; + en-US:InstallingUv) format='uv was not found. Installing uv...' ;; + en-US:TryingMirror) format='Trying uv mirror: %s' ;; + en-US:MirrorFailed) format='uv mirror download failed: %s' ;; + en-US:OfficialFallback) format='Trying the official uv installer...' ;; + en-US:UnsupportedPlatform) format='No preset uv mirror for this platform; trying the official installer: %s' ;; + en-US:InvalidChecksum) format='uv archive SHA256 verification failed.' ;; + en-US:MissingBinaries) format='The uv archive is missing uv or uvx.' ;; + en-US:MissingTools) format='curl or wget is required to download files; mirror installation also requires tar and sha256sum or shasum.' ;; + en-US:PathHint) format='uv installation directory: %s. Ensure it is on PATH in new terminals.' ;; + en-US:UvFailed) format='Failed to install uv. See https://docs.astral.sh/uv/getting-started/installation/' ;; + en-US:InstallingAstrBot) format='Installing AstrBot with Python 3.12...' ;; + en-US:AstrBotFailed) format='AstrBot installation failed (exit code %s). Check the download or dependency errors above.' ;; + en-US:Installed) format='AstrBot has been installed.' ;; + ja-JP:ChooseSource) format='ダウンロード元を選択してください' ;; + ja-JP:China) format='中国本土向け高速ダウンロードミラー' ;; + ja-JP:Official) format='公式ソース' ;; + ja-JP:Custom) format='カスタム' ;; + ja-JP:Choice) format='番号を入力してください。Enter で既定値を使用します' ;; + ja-JP:InvalidChoice) format='1、2、3 のいずれかを入力してください。' ;; + ja-JP:UvUrl) format='uv tar.gz のダウンロードディレクトリ URL(ファイル名を除く)' ;; + ja-JP:PypiUrl) format='PyPI simple インデックス URL' ;; + ja-JP:CustomRequired) format='カスタムソースには --uv-download-base-url と --pypi-index-url の両方が必要です。' ;; + ja-JP:InvalidUrl) format='ダウンロード元には有効な HTTPS URL を指定してください:%s' ;; + ja-JP:Source) format='ダウンロード元:%s' ;; + ja-JP:UvMirror) format='uv ダウンロードディレクトリ:%s' ;; + ja-JP:Pypi) format='PyPI インデックス:%s' ;; + ja-JP:PythonMirror) format='Python ダウンロードミラー:%s' ;; + ja-JP:PythonDefault) format='Python のダウンロードには既存の uv 設定を使用します。ミラーを使うには --python-install-mirror を指定してください(PyPI ミラーに Python 本体は含まれません)。' ;; + ja-JP:InstallingUv) format='uv が見つかりません。インストールしています…' ;; + ja-JP:TryingMirror) format='uv ミラーを試しています:%s' ;; + ja-JP:MirrorFailed) format='uv ミラーからのダウンロードに失敗しました:%s' ;; + ja-JP:OfficialFallback) format='uv の公式インストーラーを試しています…' ;; + ja-JP:UnsupportedPlatform) format='この環境に対応する uv ミラーがないため、公式インストーラーを試します:%s' ;; + ja-JP:InvalidChecksum) format='uv アーカイブの SHA256 検証に失敗しました。' ;; + ja-JP:MissingBinaries) format='uv アーカイブに uv または uvx がありません。' ;; + ja-JP:MissingTools) format='ダウンロードには curl または wget が必要です。ミラーからのインストールには tar と sha256sum または shasum も必要です。' ;; + ja-JP:PathHint) format='uv のインストール先:%s。新しいターミナルでも PATH に含まれていることを確認してください。' ;; + ja-JP:UvFailed) format='uv のインストールに失敗しました。https://docs.astral.sh/uv/getting-started/installation/ を参照してください。' ;; + ja-JP:InstallingAstrBot) format='Python 3.12 で AstrBot をインストールしています…' ;; + ja-JP:AstrBotFailed) format='AstrBot のインストールに失敗しました(終了コード %s)。上記のダウンロードまたは依存関係のエラーを確認してください。' ;; + ja-JP:Installed) format='AstrBot のインストールが完了しました。' ;; + *) printf 'Unknown message: %s\n' "$key" >&2; return 1 ;; + esac + printf "$format\n" "$@" } +if [ -z "$download_source" ]; then + download_source='official' + if [ "$can_prompt" -eq 1 ]; then + default_choice=2 + if [ "$language" = 'zh-CN' ]; then default_choice=1; fi + printf '\n%s\n 1. %s\n 2. %s\n 3. %s\n' \ + "$(message ChooseSource)" "$(message China)" "$(message Official)" "$(message Custom)" >&2 + while :; do + printf '%s [%s]: ' "$(message Choice)" "$default_choice" >&2 + IFS= read -r choice <&3 || exit 1 + case "${choice:-$default_choice}" in + 1) download_source='china'; break ;; + 2) download_source='official'; break ;; + 3) download_source='custom'; break ;; + *) warn "$(message InvalidChoice)" ;; + esac + done + fi +fi + +uv_mirror_urls=() +case "$download_source" in + china) + source_label="$(message China)" + uv_mirror_urls=( + 'https://mirrors.ustc.edu.cn/github-release/astral-sh/uv/LatestRelease' + 'https://mirror.nyist.edu.cn/github-release/astral-sh/uv/LatestRelease' + ) + pypi_index_url='https://mirrors.cernet.edu.cn/pypi/web/simple/' + ;; + official) + source_label="$(message Official)" + pypi_index_url='https://pypi.org/simple/' + ;; + custom) + source_label="$(message Custom)" + if [ "$can_prompt" -eq 1 ]; then + if [ -z "$uv_download_base_url" ]; then + printf '%s: ' "$(message UvUrl)" >&2 + IFS= read -r uv_download_base_url <&3 || exit 1 + fi + if [ -z "$pypi_index_url" ]; then + printf '%s: ' "$(message PypiUrl)" >&2 + IFS= read -r pypi_index_url <&3 || exit 1 + fi + fi + if [ -z "$uv_download_base_url" ] || [ -z "$pypi_index_url" ]; then + err "$(message CustomRequired)"; exit 2 + fi + uv_mirror_urls=("${uv_download_base_url%/}") + ;; + *) err "Unsupported download source: $download_source"; exit 2 ;; +esac + +# Bash 3.2 treats empty arrays as unset with nounset; use the guarded expansion. +for url in ${uv_mirror_urls[@]+"${uv_mirror_urls[@]}"} "$pypi_index_url" "$python_install_mirror"; do + [ -n "$url" ] || continue + if ! [[ "$url" =~ ^https://[^/?#[:space:]@]+(/[^?#[:space:]]*)?$ ]]; then + err "$(message InvalidUrl "$url")"; exit 2 + fi +done + +info "$(message Source "$source_label")" +for url in ${uv_mirror_urls[@]+"${uv_mirror_urls[@]}"}; do info "$(message UvMirror "$url")"; done +info "$(message Pypi "$pypi_index_url")" + refresh_uv_path() { - export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH" + local dir + for dir in "${UV_INSTALL_DIR:-}" "${UV_UNMANAGED_INSTALL:-}" "${XDG_BIN_HOME:-}" \ + "${XDG_DATA_HOME:+$XDG_DATA_HOME/../bin}" "$HOME/.local/bin" "$HOME/.cargo/bin"; do + if [ -n "$dir" ] && [ -d "$dir" ]; then export PATH="$dir:$PATH"; fi + done } -install_uv() { - info "uv was not found. Installing uv..." - +download() { if has curl; then - curl -fsSL https://astral.sh/uv/install.sh | sh - refresh_uv_path - return + curl -fsSL --connect-timeout 15 --max-time 90 -o "$2" -- "$1" + elif has wget; then + wget -q -T 30 -O "$2" -- "$1" + else + err "$(message MissingTools)" + return 1 fi +} + +install_temp_dir='' +trap 'if [ -n "$install_temp_dir" ]; then rm -rf -- "$install_temp_dir"; fi' EXIT + +install_uv() { + info "$(message InstallingUv)" + install_temp_dir="$(mktemp -d "${TMPDIR:-/tmp}/astrbot-install.XXXXXX")" + local platform machine target='' base_url artifact archive expected_hash actual_hash install_dir + platform="$(uname -s)" + machine="$(uname -m)" + case "$platform:$machine" in + Darwin:arm64|Darwin:aarch64) target='aarch64-apple-darwin' ;; + Darwin:x86_64) target='x86_64-apple-darwin' ;; + Linux:x86_64|Linux:amd64) target='x86_64-unknown-linux-musl' ;; + Linux:aarch64|Linux:arm64) target='aarch64-unknown-linux-musl' ;; + Linux:i?86) target='i686-unknown-linux-musl' ;; + Linux:armv7l|Linux:armv8l) target='armv7-unknown-linux-musleabihf' ;; + Linux:armv6l) target='arm-unknown-linux-musleabihf' ;; + Linux:ppc64le) target='powerpc64le-unknown-linux-gnu' ;; + Linux:s390x) target='s390x-unknown-linux-gnu' ;; + Linux:riscv64) target='riscv64gc-unknown-linux-musl' ;; + esac - if has wget; then - wget -qO- https://astral.sh/uv/install.sh | sh - refresh_uv_path - return + if [ -n "${uv_mirror_urls[*]:-}" ]; then + if [ -z "$target" ]; then + warn "$(message UnsupportedPlatform "$platform/$machine")" + elif ! has tar || { ! has sha256sum && ! has shasum; }; then + warn "$(message MissingTools)" + else + artifact="uv-$target.tar.gz" + archive="$install_temp_dir/$artifact" + for base_url in "${uv_mirror_urls[@]}"; do + info "$(message TryingMirror "$base_url")" + if ! download "$base_url/$artifact" "$archive" || \ + ! download "$base_url/$artifact.sha256" "$install_temp_dir/checksum"; then + warn "$(message MirrorFailed "$base_url")" + continue + fi + expected_hash='' + read -r expected_hash _ < "$install_temp_dir/checksum" || true + if has sha256sum; then + actual_hash="$(sha256sum "$archive")" + else + actual_hash="$(shasum -a 256 "$archive")" + fi + actual_hash="${actual_hash%% *}" + if ! [[ "$expected_hash" =~ ^[a-fA-F0-9]{64}$ ]] || \ + [ "$(printf '%s' "$expected_hash" | tr 'A-F' 'a-f')" != "$actual_hash" ]; then + warn "$(message InvalidChecksum)" + continue + fi + rm -rf -- "$install_temp_dir/unpacked" + mkdir -p "$install_temp_dir/unpacked" + if ! tar -xzf "$archive" -C "$install_temp_dir/unpacked" --strip-components=1; then + warn "$(message MirrorFailed "$base_url")" + continue + fi + if [ ! -f "$install_temp_dir/unpacked/uv" ] || [ ! -f "$install_temp_dir/unpacked/uvx" ]; then + warn "$(message MissingBinaries)" + continue + fi + install_dir="${UV_INSTALL_DIR:-${UV_UNMANAGED_INSTALL:-${XDG_BIN_HOME:-${XDG_DATA_HOME:+$XDG_DATA_HOME/../bin}}}}" + install_dir="${install_dir:-$HOME/.local/bin}" + mkdir -p "$install_dir" + cp "$install_temp_dir/unpacked/uv" "$install_temp_dir/unpacked/uvx" "$install_dir/" + chmod +x "$install_dir/uv" "$install_dir/uvx" + refresh_uv_path + info "$(message PathHint "$install_dir")" + return + done + fi fi - err "curl or wget is required to install uv." - exit 1 + info "$(message OfficialFallback)" + if ! download 'https://astral.sh/uv/install.sh' "$install_temp_dir/uv-install.sh" || \ + ! sh "$install_temp_dir/uv-install.sh" --pypi-index-url `。目录中需包含当前平台的 `uv-<平台>.tar.gz` 及同名 `.sha256` 文件;压缩包采用 uv 官方发布的目录结构。可通过 `--python-install-mirror ` 单独配置 Python 解释器镜像,也支持已有的 `UV_PYTHON_INSTALL_MIRROR`。 + +脚本自动匹配 macOS/Linux 架构,在 Linux 上优先使用静态 musl 构建;镜像不可用或校验失败时尝试备用镜像和官方安装器。需要 `curl` 或 `wget`;镜像安装还需要 `tar` 和 `sha256sum` 或 `shasum`。uv 安装目录会加入当前安装进程的 PATH,镜像安装后也会提示新终端需要使用的目录。 + +### Windows 安装向导 + +在 **PowerShell 7 或更高版本**中运行: + +```powershell +irm https://docs.astrbot.app/install.ps1 | iex +``` + +向导首先提供简体中文、English、日本語三种显示语言,然后选择下载源: + +- **中国大陆加速下载镜像源**:从 MirrorZ 收录的高校镜像下载 uv,并使用 `https://mirrors.cernet.edu.cn/pypi/web/simple/` 安装 AstrBot 及其 Python 包依赖。uv 镜像下载或 SHA256 校验失败时,依次尝试备用镜像和官方安装器。 +- **官方源**:使用 uv 官方安装器和官方 PyPI。 +- **自定义**:指定 uv ZIP 下载目录和 PyPI simple 索引。uv 目录需包含对应架构的 `uv-<架构>-pc-windows-msvc.zip` 及同名 `.sha256` 文件。 + +交互模式根据系统语言预选显示语言;中文界面默认选择中国大陆加速下载镜像源,英文和日文界面默认选择官方源,均可手动切换。 + +也可下载脚本后通过参数跳过交互: + +```powershell +Invoke-WebRequest https://docs.astrbot.app/install.ps1 -OutFile install.ps1 +.\install.ps1 -Language zh-CN -DownloadSource china -NonInteractive +``` + +`-Language` 支持 `zh-CN`、`en-US`、`ja-JP`;`-DownloadSource` 支持 `china`、`official`、`custom`。无人值守或输入被重定向时,不显示交互菜单;未指定语言时使用系统语言,未指定下载源时使用官方源。 + +自定义源示例(请替换为自己的 HTTPS 地址): + +```powershell +.\install.ps1 -Language zh-CN -DownloadSource custom -NonInteractive ` + -UvDownloadBaseUrl 'https://mirror.example.com/uv/release' ` + -PypiIndexUrl 'https://mirror.example.com/pypi/simple/' +``` + +PyPI 镜像不包含 Python 解释器。缺少 Python 3.12 时,解释器下载沿用 uv 的现有配置;可通过 `-PythonInstallMirror ` 单独指定兼容 uv 的 Python 下载镜像,或提前设置 `UV_PYTHON_INSTALL_MIRROR`。脚本不修改全局 pip 配置,参数指定的 Python 镜像只在本次安装期间生效。 + +### 手动安装并启动 + ```bash uv tool install astrbot --python 3.12 astrbot init # 只需要在第一次部署时执行,后续启动不需要执行 From 2df9b9d180d2db4002012e4e292d72043fcc5163 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 14:23:03 +0800 Subject: [PATCH 02/28] fix(install): support PowerShell 5.1 and Invoke-Expression --- docs/en/deploy/astrbot/package.md | 4 ++-- docs/public/install.ps1 | 26 ++++++++++++++++---------- docs/zh/deploy/astrbot/package.md | 4 ++-- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/docs/en/deploy/astrbot/package.md b/docs/en/deploy/astrbot/package.md index f90dfe989c8..b5f5276b0c8 100644 --- a/docs/en/deploy/astrbot/package.md +++ b/docs/en/deploy/astrbot/package.md @@ -41,7 +41,7 @@ The script detects the macOS/Linux architecture and prefers static musl builds o ### Windows Installation Wizard -Run the following in **PowerShell 7 or later**: +Run the following in **Windows PowerShell 5.1 or PowerShell 7+**: ```powershell irm https://docs.astrbot.app/install.ps1 | iex @@ -58,7 +58,7 @@ In interactive mode, the system language determines the initial language selecti For unattended installation, download the script and pass explicit options: ```powershell -Invoke-WebRequest https://docs.astrbot.app/install.ps1 -OutFile install.ps1 +Invoke-WebRequest https://docs.astrbot.app/install.ps1 -OutFile install.ps1 -UseBasicParsing .\install.ps1 -Language en-US -DownloadSource china -NonInteractive ``` diff --git a/docs/public/install.ps1 b/docs/public/install.ps1 index 412769325e9..8b9f3100977 100644 --- a/docs/public/install.ps1 +++ b/docs/public/install.ps1 @@ -1,11 +1,12 @@ -#Requires -Version 7.0 +#Requires -Version 5.1 # Install AstrBot with uv on Windows PowerShell. [CmdletBinding()] param( - [ValidateSet('zh-CN', 'en-US', 'ja-JP')] + # Invoke-Expression validates omitted string parameters as empty values. + [ValidateSet('', 'zh-CN', 'en-US', 'ja-JP')] [string]$Language, - [ValidateSet('china', 'official', 'custom')] + [ValidateSet('', 'china', 'official', 'custom')] [string]$DownloadSource, [string]$UvDownloadBaseUrl, [string]$PypiIndexUrl, @@ -15,6 +16,9 @@ param( $ErrorActionPreference = 'Stop' +# Windows PowerShell 5.1 may otherwise negotiate an obsolete TLS version. +[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + $UseColor = [string]::IsNullOrEmpty($env:NO_COLOR) -and [Console]::IsOutputRedirected -eq $false $CanPrompt = -not $NonInteractive -and [Environment]::UserInteractive -and -not [Console]::IsInputRedirected @@ -181,7 +185,7 @@ foreach ($url in @($uvMirrorUrls) + @($PypiIndexUrl, $PythonInstallMirror)) { } } -if (-not $IsWindows) { throw $Text.WindowsOnly } +if ($env:OS -ne 'Windows_NT') { throw $Text.WindowsOnly } function Write-Status { param( @@ -241,10 +245,12 @@ function Install-Uv { $null = New-Item -ItemType Directory -Path $tempDir try { if ($uvMirrorUrls.Count -gt 0) { - $arch = switch ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()) { - 'X64' { 'x86_64' } - 'X86' { 'i686' } - 'Arm64' { 'aarch64' } + # PROCESSOR_ARCHITEW6432 identifies the native OS when PowerShell is emulated. + $nativeArch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE } + $arch = switch ($nativeArch) { + 'AMD64' { 'x86_64' } + 'x86' { 'i686' } + 'ARM64' { 'aarch64' } default { throw ($Text.UnsupportedArch -f $_) } } $artifact = "uv-$arch-pc-windows-msvc.zip" @@ -253,8 +259,8 @@ function Install-Uv { Info ($Text.TryingMirror -f $baseUrl) $archive = Join-Path $tempDir $artifact $checksumFile = Join-Path $tempDir "$artifact.sha256" - Invoke-WebRequest -Uri "$baseUrl/$artifact" -OutFile $archive -TimeoutSec 60 - Invoke-WebRequest -Uri "$baseUrl/$artifact.sha256" -OutFile $checksumFile -TimeoutSec 30 + Invoke-WebRequest -Uri "$baseUrl/$artifact" -OutFile $archive -TimeoutSec 60 -UseBasicParsing + Invoke-WebRequest -Uri "$baseUrl/$artifact.sha256" -OutFile $checksumFile -TimeoutSec 30 -UseBasicParsing $expectedHash = ((Get-Content $checksumFile -Raw).Trim() -split '\s+')[0] if ($expectedHash -notmatch '^[a-fA-F0-9]{64}$' -or (Get-FileHash $archive -Algorithm SHA256).Hash -ne $expectedHash) { diff --git a/docs/zh/deploy/astrbot/package.md b/docs/zh/deploy/astrbot/package.md index 164cc9a7d43..53a3322998c 100644 --- a/docs/zh/deploy/astrbot/package.md +++ b/docs/zh/deploy/astrbot/package.md @@ -40,7 +40,7 @@ bash install.sh --language zh-CN --download-source china --non-interactive ### Windows 安装向导 -在 **PowerShell 7 或更高版本**中运行: +在 **Windows PowerShell 5.1 或 PowerShell 7+**中运行: ```powershell irm https://docs.astrbot.app/install.ps1 | iex @@ -57,7 +57,7 @@ irm https://docs.astrbot.app/install.ps1 | iex 也可下载脚本后通过参数跳过交互: ```powershell -Invoke-WebRequest https://docs.astrbot.app/install.ps1 -OutFile install.ps1 +Invoke-WebRequest https://docs.astrbot.app/install.ps1 -OutFile install.ps1 -UseBasicParsing .\install.ps1 -Language zh-CN -DownloadSource china -NonInteractive ``` From af4aa1c945194ce0f4fe3145227ed751746e0022 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 14:57:15 +0800 Subject: [PATCH 03/28] fix(install): support BOM-free PowerShell pipeline execution --- .github/workflows/install_script_test.yml | 37 ++++++++++++ docs/public/install.ps1 | 17 +++++- tests/install_script.Tests.ps1 | 73 +++++++++++++++++++++++ 3 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/install_script_test.yml create mode 100644 tests/install_script.Tests.ps1 diff --git a/.github/workflows/install_script_test.yml b/.github/workflows/install_script_test.yml new file mode 100644 index 00000000000..23ce3f407a4 --- /dev/null +++ b/.github/workflows/install_script_test.yml @@ -0,0 +1,37 @@ +name: Test Windows install script + +on: + push: + branches: + - master + paths: + - 'docs/public/install.ps1' + - 'tests/install_script.Tests.ps1' + - '.github/workflows/install_script_test.yml' + pull_request: + paths: + - 'docs/public/install.ps1' + - 'tests/install_script.Tests.ps1' + - '.github/workflows/install_script_test.yml' + workflow_dispatch: + +jobs: + test: + name: ${{ matrix.name }} + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + include: + - name: Windows PowerShell 5.1 + shell: powershell + - name: PowerShell 7 + shell: pwsh + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Test all supported invocation modes + shell: ${{ matrix.shell }} + run: .\tests\install_script.Tests.ps1 diff --git a/docs/public/install.ps1 b/docs/public/install.ps1 index 8b9f3100977..a32e53a83af 100644 --- a/docs/public/install.ps1 +++ b/docs/public/install.ps1 @@ -1,4 +1,4 @@ -#Requires -Version 5.1 +#Requires -Version 5.1 # Install AstrBot with uv on Windows PowerShell. [CmdletBinding()] @@ -44,6 +44,18 @@ if (-not $Language) { } } +$compressedMessages = [Convert]::FromBase64String( + 'H4sIAAAAAAAACq1Y204bWRZ9b6n/4YinRAqmk2jmIfOU0NPqjLonKASNNG+FfcA1Xa5i6uLARJFcVTjYYMYOCQaME24OOBjs9HR3uMSGf5njU6568i+MzqmLy3dmNC+Wqcs+e6+91l7bvPj6KwBG/saM/mli5AF4Qf4CYOTJzAwbZBnuO4bjppngTyMPwIgSBUgt4/gJrqaRVkD6L0i7QHoC6VWkfySf2pr58RCpG0g9ROoiUq+QuvXv2OHIHSfqj6woCuJ3DMvBkBtR33FeVleQlkRqGekxpB2S6PopDZ1EagkXfjbWN2joK/q506zmXnzz0gs9sTDHTokciTqxMPEYSGxkjoPAyVNfQrqOtArSLsDU0x+8tx7zUYZjQ86L3SfjuI7UElIrRj6Jly+Qegy+f/ZsYpIEAUhbM1JLuJxzK04jdRep60hd7EjORZMc4uJXo4BdeM+MK5IsRGgaWokAq10jfbcz0/EwDP4kKREXPW2fhinROrNILYPJ7x/e+93vgVHIm8VqT+RQTPPCTkWd2pUo+OvjCdAbf30JaQdIPyEIkn4fk/qb1QTS15G2R88u4cwq0tasrQJS081q0pe4JDMcx/KzDyVZfCTIdovksMCD+4G79wBSj4Bzi0DaxazSAEKNh1meIQHr56d4u2bkT3B+B2deI/W1Vdq0Yju9inEI56MOScbmpi+5Aa92tHeKl5S5OUGUYeihGAyTGEb+GFeu8HWetOQvLB8Snktet06R/opCqSL9oCPWeFgQJDgpKGIQ9uektmap58by+27m9cB9KuqpN2UeriC1QLW2TOHcRtobFNP+K9SfiQssP9sCrE3GvWZAD616Qu1WaCe60faBoZZ7Jdub52FZnpMejI2FhKAUYCRZZLiAFB5TomOzUJZZfnZUkhnStzHWhouRWYEfIzzEaa0RP+oG2K+e8bDA2m1qrB/j9Bl5Lf4BL2/3fO2PvAxFQndjYx+XczhWQNpavXbdeFt0097yQjuKaJXuSeTG9TericZvWv3yFdJ+sakDXnzzsllNophWP182i5t9pu0VAU+t1K/e4dNNK7tfv9boqUWvxY39S/N4dQg4dKA9hX9XWNEuwT/YvAFoz1cwFf1WeM5zAhN6xEhwSuQAUouAMOUxH4Lz9oVy/XzPyF4gNYWv4+ahitQjpG75D3Wk9oTnFuiJ6hsiwAnhORQnw5DjAE2ggvRjpG8Q+NSKJ0/aByfgDyyvzI9FmOCTSdIx8pjDkIAUBu1964uAI8Eb9S+FyynSqj5z2p5J38IZRuH8E7SfXxLL2tjHp6TFQIkCs3hKjKqLb0T6PunWa9dIfeW0ZNQ+w6nClvtg00vYim5Zegk4eRr5k3rtDVIrOFMiJ2spb/rYhPTtCJLE8rOPWJ4RWSj1M7oSUKIBOA+BR1egROedKymkah0Dzos/ZLR2DB/Pdl2h30Ux9R6Kqfcp9GTA5WgxK4O175tmHXNzuNk6KZEAL2mYkX+ER8f/PGRbM04PcL6IP70zK+uEAbi8aWQvcDlpHsTxVnHIWmat57Gerp+vmLUaLvxs/vrhZvtW49d9XF0fsGPZIY3LDL6OW3tfjE2yWhnriUZusbVaDVqfaBnGZabH4mQuHeNyrn6RHLI14X+uNKofcSruLUu7e9Zxyq5z0HZkJ9/YLuMarbFZTdTPV3GmZGSX6l8+48zq0M3HboutQuBfg+zOuJvQgCUHF46srVd4edeK7dgJ2c3yY9JnpfE/PnyHqZ+vGm8rRkolvXGHpPH+c+P94pClxaycWbGksfLR63bftcTIHxvJK5z4BJRos5pyWEuxGLRydLK7Z01te4bNzMG7haMOlwdm5QynNTOmg/9xj+ixKJhXb3D8Q6OaxekzUm8qibffm7VDmxHWl02zXLBiSWvvYvgq4E+3WU1YsRheumzsqi2bNytnxkHM2PlQP182sheN3KLTkUS2fvXO/C1rvd0yK5XBtu3JyrjMWPmYeajiTMrY+GykM/Wr7R7OjddSbc49wKAJxPmixy6yllxnzL2UcVrwu7a5mDPyJ83qtt+XzcqZo6OWNQ+1XwezcspIZG5isA5e/6LnKFHQyC1a8dVGrdysbuNDzcrHbCU2qymcrlixnHm91Ns5bdt0iWpbpTtmV3EqjjOeW5pHB9bSMt4q3sgbvXFWPz9tVC/xp9euNxqJrOuKvS3Q0+cw2zMrZzZzATVAGvn+AF/zT8pu/4L86NTkEP+yFQ/kMASCc5vg77QaioFAYJCBRWzQQw41wQy9+QDcwMNYQtsBFubSHUgURwlEFEkG0xDQh1ouJrWf5ncx97sbooeZOd8GOxkjBsNsFLo+FoUiO8MG6fBxKg70NTMPmhArwqAsiAv0vxq3nrNyWFBkivwMy0GeicDbg02tddH7JU+i+P3N3y7P0H5kWJ5j+BCgVwATDEIOiowMQ63s7EZKw6yt4/l26HuYm++SN30omjIMyooI2wN0Gtwk5GBQBkzrWLuRg359P2ckwAsymBEUPhQAPsyUqB+dTqtzhOBxupvDHoEpcztK9zmc/Q3IgqsicjKYhPD/6G/2j1wG8EpkGop3gCCCORFKErBvyAJQJEi5FXJm7VCb8x/psBrcgvOsDIJCCJJqbwcAFYYd120JkT+cg3wI8sEFACmNADMtRGFggNvZVzxti/YtMC3I4R5WR8jrt7pAP6N7qvBADrOS39WkoMjOyUDgXQb+gYLj+6Ep8MDneIHB1hZmJDANIe/NSJ/4+9mbC5bktQXOs5LsEC4o8DPsrCJS6AlT5N7W5nSVcafuLcrGiK1cEBIo71k+yCkh6A4FlvBhToQyFKXb/lHeZXHPwpAk4846VgIR+yHX5gTRdblWnJZWO8Z1uzy6jG6Cg4wEAaRsvXsH3KMUvu8fo21G1z1F7QOo23391cv/ALOh8zpfFwAA' +) +$messageInput = New-Object System.IO.MemoryStream(,$compressedMessages) +$messageGzip = New-Object System.IO.Compression.GZipStream( + $messageInput, + [System.IO.Compression.CompressionMode]::Decompress +) +$messageReader = New-Object System.IO.StreamReader($messageGzip, [Text.Encoding]::UTF8) +$messages = ConvertFrom-Json $messageReader.ReadToEnd() +$messageReader.Dispose() +<# $messages = @{ 'zh-CN' = @{ ChooseSource = '请选择下载源' @@ -133,7 +145,8 @@ $messages = @{ Installed = 'AstrBot のインストールが完了しました。' } } -$Text = $messages[$Language] +#> +$Text = $messages.$Language if (-not $DownloadSource) { $DownloadSource = 'official' diff --git a/tests/install_script.Tests.ps1 b/tests/install_script.Tests.ps1 new file mode 100644 index 00000000000..2283ec3f6f8 --- /dev/null +++ b/tests/install_script.Tests.ps1 @@ -0,0 +1,73 @@ +[CmdletBinding()] +param() + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +$scriptPath = (Resolve-Path (Join-Path $PSScriptRoot '..\docs\public\install.ps1')).Path +$scriptBytes = [System.IO.File]::ReadAllBytes($scriptPath) + +if ($scriptBytes.Length -ge 3 -and + $scriptBytes[0] -eq 0xEF -and + $scriptBytes[1] -eq 0xBB -and + $scriptBytes[2] -eq 0xBF) { + throw 'install.ps1 must be UTF-8 without BOM because Invoke-WebRequest preserves the BOM before Invoke-Expression.' +} + +$tokens = $null +$parseErrors = $null +$ast = [System.Management.Automation.Language.Parser]::ParseFile( + $scriptPath, + [ref]$tokens, + [ref]$parseErrors +) +if ($parseErrors.Count -ne 0) { + throw "install.ps1 has parser errors: $($parseErrors.Message -join '; ')" +} +if (-not $ast.ParamBlock) { + throw 'install.ps1 must retain its top-level param block.' +} + +$global:UvArguments = @() +function global:uv { + $global:UvArguments += ,@($args) + $global:LASTEXITCODE = 0 + if ($args.Count -eq 1 -and $args[0] -eq '--version') { + 'uv test-version' + } +} + +try { + & $scriptPath -Language en-US -DownloadSource official -NonInteractive + if ($LASTEXITCODE -ne 0) { + throw "Direct script execution failed with exit code $LASTEXITCODE." + } + + # Decode the raw bytes ourselves so this exercises the same parser path as + # `Invoke-WebRequest ... | Invoke-Expression`, including any leading BOM. + $downloadedScript = [System.Text.Encoding]::UTF8.GetString($scriptBytes) + $downloadedScript | Invoke-Expression + if ($LASTEXITCODE -ne 0) { + throw "Invoke-Expression execution failed with exit code $LASTEXITCODE." + } +} finally { + Remove-Item Function:\uv -ErrorAction SilentlyContinue +} + +$versionCalls = @($global:UvArguments | Where-Object { $_.Count -eq 1 -and $_[0] -eq '--version' }) +$installCalls = @($global:UvArguments | Where-Object { + $_.Count -eq 7 -and + $_[0] -eq 'tool' -and + $_[1] -eq 'install' -and + $_[2] -eq '--python' -and + $_[3] -eq '3.12' -and + $_[4] -eq 'astrbot' -and + $_[5] -eq '--default-index' -and + $_[6] -eq 'https://pypi.org/simple/' +}) + +if ($versionCalls.Count -ne 2 -or $installCalls.Count -ne 2) { + throw "Expected both execution modes to reach uv; got $($versionCalls.Count) version calls and $($installCalls.Count) install calls." +} + +Write-Host "install.ps1 passed direct-file and Invoke-Expression tests on $($PSVersionTable.PSEdition) $($PSVersionTable.PSVersion)." From f875a75dfda8e4b65f8cace342786e15ea2656b5 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 15:07:35 +0800 Subject: [PATCH 04/28] fix(install): configure UTF-8 console output --- docs/public/install.ps1 | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/public/install.ps1 b/docs/public/install.ps1 index a32e53a83af..29688418363 100644 --- a/docs/public/install.ps1 +++ b/docs/public/install.ps1 @@ -16,6 +16,16 @@ param( $ErrorActionPreference = 'Stop' +# Keep localized status messages readable in Windows PowerShell consoles whose +# legacy code page cannot represent CJK characters. +try { + $utf8 = New-Object System.Text.UTF8Encoding($false) + [Console]::InputEncoding = $utf8 + [Console]::OutputEncoding = $utf8 +} catch { + # Some hosts do not expose mutable console encodings; continue normally. +} + # Windows PowerShell 5.1 may otherwise negotiate an obsolete TLS version. [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 From a199bae6228bc91c2d18fa3d8f832233fa6338ec Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 15:16:00 +0800 Subject: [PATCH 05/28] fix(install): set Windows console code page internally --- docs/public/install.ps1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/public/install.ps1 b/docs/public/install.ps1 index 29688418363..918d0e62dc9 100644 --- a/docs/public/install.ps1 +++ b/docs/public/install.ps1 @@ -19,6 +19,11 @@ $ErrorActionPreference = 'Stop' # Keep localized status messages readable in Windows PowerShell consoles whose # legacy code page cannot represent CJK characters. try { + if ($Host.Name -eq 'ConsoleHost') { + # Windows PowerShell 5.1 may keep an OEM code page even when the + # managed output encoding is UTF-8. + $null = & chcp.com 65001 2>$null + } $utf8 = New-Object System.Text.UTF8Encoding($false) [Console]::InputEncoding = $utf8 [Console]::OutputEncoding = $utf8 From 4c84404d52fcd2ffac88a08abbb306f352af55d8 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 15:46:54 +0800 Subject: [PATCH 06/28] fix(install): keep IEX bootstrap menu ASCII-only --- docs/public/install.ps1 | 8 +++++--- tests/install_script.Tests.ps1 | 5 +++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/public/install.ps1 b/docs/public/install.ps1 index 918d0e62dc9..c73efcffca7 100644 --- a/docs/public/install.ps1 +++ b/docs/public/install.ps1 @@ -48,10 +48,12 @@ if (-not $Language) { if ($CanPrompt) { $languages = @('zh-CN', 'en-US', 'ja-JP') $defaultChoice = [array]::IndexOf($languages, $Language) + 1 - Write-Host "`n请选择语言 / Select language / 言語を選択してください" - Write-Host ' 1. 简体中文' + # Keep this bootstrap prompt ASCII-only: PowerShell 5.1 can decode + # source text from Invoke-WebRequest using the system code page. + Write-Host "`nSelect language" + Write-Host ' 1. Chinese (Simplified)' Write-Host ' 2. English' - Write-Host ' 3. 日本語' + Write-Host ' 3. Japanese' do { $choice = Read-Host "1-3 [$defaultChoice]" } until ($choice -eq '' -or $choice -in @('1', '2', '3')) diff --git a/tests/install_script.Tests.ps1 b/tests/install_script.Tests.ps1 index 2283ec3f6f8..79793463360 100644 --- a/tests/install_script.Tests.ps1 +++ b/tests/install_script.Tests.ps1 @@ -27,6 +27,11 @@ if ($parseErrors.Count -ne 0) { if (-not $ast.ParamBlock) { throw 'install.ps1 must retain its top-level param block.' } +if ($ast.Extent.Text -notmatch 'Write-Host "`nSelect language"' -or + $ast.Extent.Text -notmatch "Write-Host ' 1\. Chinese \(Simplified\)'" -or + $ast.Extent.Text -notmatch "Write-Host ' 3\. Japanese'") { + throw 'The Invoke-Expression bootstrap menu must remain ASCII-only.' +} $global:UvArguments = @() function global:uv { From 1097476279616d80b54c11f7dad0c963befa7a22 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 15:49:32 +0800 Subject: [PATCH 07/28] fix(install): default language selection to English --- docs/public/install.ps1 | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/public/install.ps1 b/docs/public/install.ps1 index c73efcffca7..242d1f95ced 100644 --- a/docs/public/install.ps1 +++ b/docs/public/install.ps1 @@ -37,11 +37,9 @@ try { $UseColor = [string]::IsNullOrEmpty($env:NO_COLOR) -and [Console]::IsOutputRedirected -eq $false $CanPrompt = -not $NonInteractive -and [Environment]::UserInteractive -and -not [Console]::IsInputRedirected -$defaultLanguage = switch -Wildcard ((Get-UICulture).Name) { - 'zh*' { 'zh-CN' } - 'ja*' { 'ja-JP' } - default { 'en-US' } -} +# Keep the first-run experience predictable across machines. Users can still +# choose Chinese or Japanese explicitly in the language menu. +$defaultLanguage = 'en-US' if (-not $Language) { $Language = $defaultLanguage From 05a92c71c00c91dc04e9c9a571144769762d43b8 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 15:50:42 +0800 Subject: [PATCH 08/28] test(install): cover language source defaults --- tests/install_script.Tests.ps1 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/install_script.Tests.ps1 b/tests/install_script.Tests.ps1 index 79793463360..d87c4e87ae6 100644 --- a/tests/install_script.Tests.ps1 +++ b/tests/install_script.Tests.ps1 @@ -29,8 +29,10 @@ if (-not $ast.ParamBlock) { } if ($ast.Extent.Text -notmatch 'Write-Host "`nSelect language"' -or $ast.Extent.Text -notmatch "Write-Host ' 1\. Chinese \(Simplified\)'" -or - $ast.Extent.Text -notmatch "Write-Host ' 3\. Japanese'") { - throw 'The Invoke-Expression bootstrap menu must remain ASCII-only.' + $ast.Extent.Text -notmatch "Write-Host ' 3\. Japanese'" -or + $ast.Extent.Text -notmatch "\$defaultLanguage = 'en-US'" -or + $ast.Extent.Text -notmatch "if \(\$Language -eq 'zh-CN'\) \{ '1' \} else \{ '2' \}") { + throw 'The bootstrap menu must remain ASCII-only, default to English, and select the source default by language.' } $global:UvArguments = @() From d39fa609cd026889b9dce2190192e81233c488ab Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 15:51:41 +0800 Subject: [PATCH 09/28] fix(test): escape PowerShell assertion patterns --- tests/install_script.Tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/install_script.Tests.ps1 b/tests/install_script.Tests.ps1 index d87c4e87ae6..f796e432106 100644 --- a/tests/install_script.Tests.ps1 +++ b/tests/install_script.Tests.ps1 @@ -30,8 +30,8 @@ if (-not $ast.ParamBlock) { if ($ast.Extent.Text -notmatch 'Write-Host "`nSelect language"' -or $ast.Extent.Text -notmatch "Write-Host ' 1\. Chinese \(Simplified\)'" -or $ast.Extent.Text -notmatch "Write-Host ' 3\. Japanese'" -or - $ast.Extent.Text -notmatch "\$defaultLanguage = 'en-US'" -or - $ast.Extent.Text -notmatch "if \(\$Language -eq 'zh-CN'\) \{ '1' \} else \{ '2' \}") { + $ast.Extent.Text -notmatch '\$defaultLanguage = ''en-US''' -or + $ast.Extent.Text -notmatch 'if \(\$Language -eq ''zh-CN''\) \{ ''1'' \} else \{ ''2'' \}') { throw 'The bootstrap menu must remain ASCII-only, default to English, and select the source default by language.' } From 975d6fbe80674aeb0733bbe0368f7a860c2a037d Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 16:09:35 +0800 Subject: [PATCH 10/28] fix(install): restore localized language menu --- docs/public/install.ps1 | 22 ++++++++++++++++------ tests/install_script.Tests.ps1 | 4 +--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/public/install.ps1 b/docs/public/install.ps1 index 242d1f95ced..3864a11905d 100644 --- a/docs/public/install.ps1 +++ b/docs/public/install.ps1 @@ -39,6 +39,18 @@ $CanPrompt = -not $NonInteractive -and [Environment]::UserInteractive -and -not # Keep the first-run experience predictable across machines. Users can still # choose Chinese or Japanese explicitly in the language menu. +$bootstrapCompressed = [Convert]::FromBase64String( + 'H4sIAAAAAAAACqtW8knMSy9NTE8NKMrPLShRslJ6sX77y4bOZ90rX6xf+2JFg4K+QnBqTmpyiUIOVKWCvsKLFQ0vVs173DTpZeOOZ13zHjdOf9y47HFj/+PGBY8bpz5ubFHSUXLOyMxLLU5VslJ6vq7hyd7JT3asfTatXUlHyTUvPSezOEPJCs7SUfJKLEiEqn42femzOWterJqnVAsAE4JdBJ4AAAA=' +) +$bootstrapInput = New-Object System.IO.MemoryStream(,$bootstrapCompressed) +$bootstrapGzip = New-Object System.IO.Compression.GZipStream( + $bootstrapInput, + [System.IO.Compression.CompressionMode]::Decompress +) +$bootstrapReader = New-Object System.IO.StreamReader($bootstrapGzip, [Text.Encoding]::UTF8) +$bootstrapText = ConvertFrom-Json $bootstrapReader.ReadToEnd() +$bootstrapReader.Dispose() + $defaultLanguage = 'en-US' if (-not $Language) { @@ -46,12 +58,10 @@ if (-not $Language) { if ($CanPrompt) { $languages = @('zh-CN', 'en-US', 'ja-JP') $defaultChoice = [array]::IndexOf($languages, $Language) + 1 - # Keep this bootstrap prompt ASCII-only: PowerShell 5.1 can decode - # source text from Invoke-WebRequest using the system code page. - Write-Host "`nSelect language" - Write-Host ' 1. Chinese (Simplified)' - Write-Host ' 2. English' - Write-Host ' 3. Japanese' + Write-Host "`n$($bootstrapText.LanguagePrompt)" + Write-Host " 1. $($bootstrapText.Chinese)" + Write-Host " 2. $($bootstrapText.English)" + Write-Host " 3. $($bootstrapText.Japanese)" do { $choice = Read-Host "1-3 [$defaultChoice]" } until ($choice -eq '' -or $choice -in @('1', '2', '3')) diff --git a/tests/install_script.Tests.ps1 b/tests/install_script.Tests.ps1 index f796e432106..a101f165335 100644 --- a/tests/install_script.Tests.ps1 +++ b/tests/install_script.Tests.ps1 @@ -27,9 +27,7 @@ if ($parseErrors.Count -ne 0) { if (-not $ast.ParamBlock) { throw 'install.ps1 must retain its top-level param block.' } -if ($ast.Extent.Text -notmatch 'Write-Host "`nSelect language"' -or - $ast.Extent.Text -notmatch "Write-Host ' 1\. Chinese \(Simplified\)'" -or - $ast.Extent.Text -notmatch "Write-Host ' 3\. Japanese'" -or +if ($ast.Extent.Text -notmatch '\$bootstrapCompressed = \[Convert\]::FromBase64String' -or $ast.Extent.Text -notmatch '\$defaultLanguage = ''en-US''' -or $ast.Extent.Text -notmatch 'if \(\$Language -eq ''zh-CN''\) \{ ''1'' \} else \{ ''2'' \}') { throw 'The bootstrap menu must remain ASCII-only, default to English, and select the source default by language.' From 819b282f6159914721872df3268834b2672ebb2a Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 16:10:57 +0800 Subject: [PATCH 11/28] fix(install): list English as first language --- docs/public/install.ps1 | 6 +++--- tests/install_script.Tests.ps1 | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/public/install.ps1 b/docs/public/install.ps1 index 3864a11905d..5b4c6eaec00 100644 --- a/docs/public/install.ps1 +++ b/docs/public/install.ps1 @@ -56,11 +56,11 @@ $defaultLanguage = 'en-US' if (-not $Language) { $Language = $defaultLanguage if ($CanPrompt) { - $languages = @('zh-CN', 'en-US', 'ja-JP') + $languages = @('en-US', 'zh-CN', 'ja-JP') $defaultChoice = [array]::IndexOf($languages, $Language) + 1 Write-Host "`n$($bootstrapText.LanguagePrompt)" - Write-Host " 1. $($bootstrapText.Chinese)" - Write-Host " 2. $($bootstrapText.English)" + Write-Host " 1. $($bootstrapText.English)" + Write-Host " 2. $($bootstrapText.Chinese)" Write-Host " 3. $($bootstrapText.Japanese)" do { $choice = Read-Host "1-3 [$defaultChoice]" diff --git a/tests/install_script.Tests.ps1 b/tests/install_script.Tests.ps1 index a101f165335..f12a4ade7a8 100644 --- a/tests/install_script.Tests.ps1 +++ b/tests/install_script.Tests.ps1 @@ -28,6 +28,7 @@ if (-not $ast.ParamBlock) { throw 'install.ps1 must retain its top-level param block.' } if ($ast.Extent.Text -notmatch '\$bootstrapCompressed = \[Convert\]::FromBase64String' -or + $ast.Extent.Text -notmatch '\$languages = @\(''en-US'', ''zh-CN'', ''ja-JP''\)' -or $ast.Extent.Text -notmatch '\$defaultLanguage = ''en-US''' -or $ast.Extent.Text -notmatch 'if \(\$Language -eq ''zh-CN''\) \{ ''1'' \} else \{ ''2'' \}') { throw 'The bootstrap menu must remain ASCII-only, default to English, and select the source default by language.' From 627ee9d1a4226f09723b90445befdfd0463cb294 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 16:15:02 +0800 Subject: [PATCH 12/28] fix(install): put English first in language prompt --- docs/public/install.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/public/install.ps1 b/docs/public/install.ps1 index 5b4c6eaec00..a2843e2dd5d 100644 --- a/docs/public/install.ps1 +++ b/docs/public/install.ps1 @@ -40,7 +40,7 @@ $CanPrompt = -not $NonInteractive -and [Environment]::UserInteractive -and -not # Keep the first-run experience predictable across machines. Users can still # choose Chinese or Japanese explicitly in the language menu. $bootstrapCompressed = [Convert]::FromBase64String( - 'H4sIAAAAAAAACqtW8knMSy9NTE8NKMrPLShRslJ6sX77y4bOZ90rX6xf+2JFg4K+QnBqTmpyiUIOVKWCvsKLFQ0vVs173DTpZeOOZ13zHjdOf9y47HFj/+PGBY8bpz5ubFHSUXLOyMxLLU5VslJ6vq7hyd7JT3asfTatXUlHyTUvPSezOEPJCs7SUfJKLEiEqn42femzOWterJqnVAsAE4JdBJ4AAAA=' + 'H4sIAAAAAAAACqtW8knMSy9NTE8NKMrPLShRslIKTs1JTS5RyIGKK+grvFi//WVD57PulS/Wr32xogEksqLhxap5j5smvWzc8axr3uPG6Y8blz1u7H/cuOBx49THjS1KOkrOGZl5qcWpSlZKz9c1PNk7+cmOtc+mtSvpKLnmpedkFmcoWcFZOkpeiQWJUNXPpi99NmfNi1XzlGoBBSle6p4AAAA=' ) $bootstrapInput = New-Object System.IO.MemoryStream(,$bootstrapCompressed) $bootstrapGzip = New-Object System.IO.Compression.GZipStream( From 1abe537e80efc75100857275631b9ff66839ba14 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 16:22:10 +0800 Subject: [PATCH 13/28] feat(install): add indented AstrBot installer header --- docs/public/install.ps1 | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/public/install.ps1 b/docs/public/install.ps1 index a2843e2dd5d..abf20eea474 100644 --- a/docs/public/install.ps1 +++ b/docs/public/install.ps1 @@ -40,7 +40,7 @@ $CanPrompt = -not $NonInteractive -and [Environment]::UserInteractive -and -not # Keep the first-run experience predictable across machines. Users can still # choose Chinese or Japanese explicitly in the language menu. $bootstrapCompressed = [Convert]::FromBase64String( - 'H4sIAAAAAAAACqtW8knMSy9NTE8NKMrPLShRslIKTs1JTS5RyIGKK+grvFi//WVD57PulS/Wr32xogEksqLhxap5j5smvWzc8axr3uPG6Y8blz1u7H/cuOBx49THjS1KOkrOGZl5qcWpSlZKz9c1PNk7+cmOtc+mtSvpKLnmpedkFmcoWcFZOkpeiQWJUNXPpi99NmfNi1XzlGoBBSle6p4AAAA=' + 'H4sIAAAAAAAACqtW8swrLknMyUktCsksyUlVslL6ML+nR8GxuKTIKb9EAS6rpKPkk5iXXpqYnhpQlJ9bUKJkpRScmpOaXKKQAxVX0Fd4sX77y4bOZ90rX6xf+2JFA0hkRcOLVfMeN0162bjjWde8x43THzcue9zY/7hxwePGqY8bW5R0lJwzMvNSi0F2P1/X8GTv5Cc71j6b1q6ko+Sal56TWZyhZAVn6Sh5JRYkQlU/m7702Zw1L1bNU6oFAAIYB+vIAAAA' ) $bootstrapInput = New-Object System.IO.MemoryStream(,$bootstrapCompressed) $bootstrapGzip = New-Object System.IO.Compression.GZipStream( @@ -58,12 +58,14 @@ if (-not $Language) { if ($CanPrompt) { $languages = @('en-US', 'zh-CN', 'ja-JP') $defaultChoice = [array]::IndexOf($languages, $Language) + 1 - Write-Host "`n$($bootstrapText.LanguagePrompt)" - Write-Host " 1. $($bootstrapText.English)" - Write-Host " 2. $($bootstrapText.Chinese)" - Write-Host " 3. $($bootstrapText.Japanese)" + Write-Host "`n $($bootstrapText.InstallerTitle)" -ForegroundColor Cyan + Write-Host ' -----------------------' -ForegroundColor DarkGray + Write-Host "`n $($bootstrapText.LanguagePrompt)" + Write-Host " 1. $($bootstrapText.English)" + Write-Host " 2. $($bootstrapText.Chinese)" + Write-Host " 3. $($bootstrapText.Japanese)" do { - $choice = Read-Host "1-3 [$defaultChoice]" + $choice = Read-Host " 1-3 [$defaultChoice]" } until ($choice -eq '' -or $choice -in @('1', '2', '3')) if ($choice) { $Language = $languages[[int]$choice - 1] } } From 8019ff131e59919ce05b75fc8d27c00400e0c97a Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 16:25:02 +0800 Subject: [PATCH 14/28] fix(install): apply indentation consistently --- docs/public/install.ps1 | 35 +++++++++++++++++----------------- tests/install_script.Tests.ps1 | 2 ++ 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/docs/public/install.ps1 b/docs/public/install.ps1 index abf20eea474..3085b824cb7 100644 --- a/docs/public/install.ps1 +++ b/docs/public/install.ps1 @@ -36,6 +36,7 @@ try { $UseColor = [string]::IsNullOrEmpty($env:NO_COLOR) -and [Console]::IsOutputRedirected -eq $false $CanPrompt = -not $NonInteractive -and [Environment]::UserInteractive -and -not [Console]::IsInputRedirected +$Indent = ' ' # Keep the first-run experience predictable across machines. Users can still # choose Chinese or Japanese explicitly in the language menu. @@ -58,14 +59,14 @@ if (-not $Language) { if ($CanPrompt) { $languages = @('en-US', 'zh-CN', 'ja-JP') $defaultChoice = [array]::IndexOf($languages, $Language) + 1 - Write-Host "`n $($bootstrapText.InstallerTitle)" -ForegroundColor Cyan - Write-Host ' -----------------------' -ForegroundColor DarkGray - Write-Host "`n $($bootstrapText.LanguagePrompt)" - Write-Host " 1. $($bootstrapText.English)" - Write-Host " 2. $($bootstrapText.Chinese)" - Write-Host " 3. $($bootstrapText.Japanese)" + Write-Host "`n$Indent$($bootstrapText.InstallerTitle)" -ForegroundColor Cyan + Write-Host "$Indent-----------------------" -ForegroundColor DarkGray + Write-Host "`n$Indent$($bootstrapText.LanguagePrompt)" + Write-Host "$Indent 1. $($bootstrapText.English)" + Write-Host "$Indent 2. $($bootstrapText.Chinese)" + Write-Host "$Indent 3. $($bootstrapText.Japanese)" do { - $choice = Read-Host " 1-3 [$defaultChoice]" + $choice = Read-Host "${Indent}1-3 [$defaultChoice]" } until ($choice -eq '' -or $choice -in @('1', '2', '3')) if ($choice) { $Language = $languages[[int]$choice - 1] } } @@ -179,14 +180,14 @@ if (-not $DownloadSource) { $DownloadSource = 'official' if ($CanPrompt) { $defaultChoice = if ($Language -eq 'zh-CN') { '1' } else { '2' } - Write-Host "`n$($Text.ChooseSource)" - Write-Host " 1. $($Text.China)" - Write-Host " 2. $($Text.Official)" - Write-Host " 3. $($Text.Custom)" + Write-Host "`n$Indent$($Text.ChooseSource)" + Write-Host "$Indent 1. $($Text.China)" + Write-Host "$Indent 2. $($Text.Official)" + Write-Host "$Indent 3. $($Text.Custom)" do { - $choice = Read-Host "$($Text.Choice) [$defaultChoice]" + $choice = Read-Host "$Indent$($Text.Choice) [$defaultChoice]" if ($choice -eq '') { $choice = $defaultChoice } - if ($choice -notin @('1', '2', '3')) { Write-Host $Text.InvalidChoice } + if ($choice -notin @('1', '2', '3')) { Write-Host "$Indent$($Text.InvalidChoice)" } } until ($choice -in @('1', '2', '3')) $DownloadSource = @('china', 'official', 'custom')[[int]$choice - 1] } @@ -205,8 +206,8 @@ switch ($DownloadSource) { 'official' { $PypiIndexUrl = 'https://pypi.org/simple/' } 'custom' { if ($CanPrompt) { - if (-not $UvDownloadBaseUrl) { $UvDownloadBaseUrl = Read-Host $Text.UvUrl } - if (-not $PypiIndexUrl) { $PypiIndexUrl = Read-Host $Text.PypiUrl } + if (-not $UvDownloadBaseUrl) { $UvDownloadBaseUrl = Read-Host "$Indent$($Text.UvUrl)" } + if (-not $PypiIndexUrl) { $PypiIndexUrl = Read-Host "$Indent$($Text.PypiUrl)" } } if (-not $UvDownloadBaseUrl -or -not $PypiIndexUrl) { throw $Text.CustomRequired @@ -235,9 +236,9 @@ function Write-Status { ) if ($UseColor) { - Write-Host "$Prefix $Message" -ForegroundColor $Color + Write-Host "$Indent$Prefix $Message" -ForegroundColor $Color } else { - Write-Host "$Prefix $Message" + Write-Host "$Indent$Prefix $Message" } } diff --git a/tests/install_script.Tests.ps1 b/tests/install_script.Tests.ps1 index f12a4ade7a8..5118d550f28 100644 --- a/tests/install_script.Tests.ps1 +++ b/tests/install_script.Tests.ps1 @@ -30,6 +30,8 @@ if (-not $ast.ParamBlock) { if ($ast.Extent.Text -notmatch '\$bootstrapCompressed = \[Convert\]::FromBase64String' -or $ast.Extent.Text -notmatch '\$languages = @\(''en-US'', ''zh-CN'', ''ja-JP''\)' -or $ast.Extent.Text -notmatch '\$defaultLanguage = ''en-US''' -or + $ast.Extent.Text -notmatch '\$Indent = '' ''' -or + $ast.Extent.Text -notmatch 'Write-Host "\$Indent\$Prefix \$Message"' -or $ast.Extent.Text -notmatch 'if \(\$Language -eq ''zh-CN''\) \{ ''1'' \} else \{ ''2'' \}') { throw 'The bootstrap menu must remain ASCII-only, default to English, and select the source default by language.' } From 85ae454cc1775a5e9fb02d09d9103a06a1a27d09 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 16:39:42 +0800 Subject: [PATCH 15/28] feat(install): show AstrBot startup commands --- docs/public/install.ps1 | 10 +++++++++- tests/install_script.Tests.ps1 | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/public/install.ps1 b/docs/public/install.ps1 index 3085b824cb7..48d661c3e69 100644 --- a/docs/public/install.ps1 +++ b/docs/public/install.ps1 @@ -41,7 +41,7 @@ $Indent = ' ' # Keep the first-run experience predictable across machines. Users can still # choose Chinese or Japanese explicitly in the language menu. $bootstrapCompressed = [Convert]::FromBase64String( - 'H4sIAAAAAAAACqtW8swrLknMyUktCsksyUlVslL6ML+nR8GxuKTIKb9EAS6rpKPkk5iXXpqYnhpQlJ9bUKJkpRScmpOaXKKQAxVX0Fd4sX77y4bOZ90rX6xf+2JFA0hkRcOLVfMeN0162bjjWde8x43THzcue9zY/7hxwePGqY8bW5R0lJwzMvNSi0F2P1/X8GTv5Cc71j6b1q6ko+Sal56TWZyhZAVn6Sh5JRYkQlU/m7702Zw1L1bNU6oFAAIYB+vIAAAA' + 'H4sIAAAAAAAACmWPzUrDQBSFX2W464D77FRcKCJCfYFQhySQPzJTEETIiCi1QUGpBbFps4i2adosG/DnZSZOpyt9BAmUNuDucu455+Ocw75DqGZZ2D8xqYVBhd9BGKJtQv0dl6L1FxQ41By9pen42Hdtj4IKDWzhJkXWSkdbSObzZdAWnbHMp3IUVMookGnELx+WrBC3EWc9zl45u+dsyFmXsytQYNcwHUwq9mIWlJ+PZTEVTzegwJ6jWyYxQF1fChxonrZyi14iXjKZRqDAET6jDYo9sslUkoL8loOogQlGTde2NeeUqHX7hi3ukrLoiH4i83n51RdZLNpvMg5/Pp7rgTo/izmb8ICV71W0Gjm8XnRTzibfs4GMw/9rq7KLP6djnXl1AQAA' ) $bootstrapInput = New-Object System.IO.MemoryStream(,$bootstrapCompressed) $bootstrapGzip = New-Object System.IO.Compression.GZipStream( @@ -393,6 +393,14 @@ try { exit $LASTEXITCODE } Ok $Text.Installed + $nextSteps = switch ($Language) { + 'zh-CN' { $bootstrapText.NextStepsChinese } + 'ja-JP' { $bootstrapText.NextStepsJapanese } + default { $bootstrapText.NextStepsEnglish } + } + Write-Host "`n$Indent$nextSteps" + Write-Host "$($Indent)$($Indent)astrbot init" -ForegroundColor Cyan + Write-Host "$($Indent)$($Indent)astrbot run" -ForegroundColor Cyan } finally { $env:UV_PYTHON_INSTALL_MIRROR = $previousPythonMirror } diff --git a/tests/install_script.Tests.ps1 b/tests/install_script.Tests.ps1 index 5118d550f28..2bcd9d6f144 100644 --- a/tests/install_script.Tests.ps1 +++ b/tests/install_script.Tests.ps1 @@ -32,6 +32,8 @@ if ($ast.Extent.Text -notmatch '\$bootstrapCompressed = \[Convert\]::FromBase64S $ast.Extent.Text -notmatch '\$defaultLanguage = ''en-US''' -or $ast.Extent.Text -notmatch '\$Indent = '' ''' -or $ast.Extent.Text -notmatch 'Write-Host "\$Indent\$Prefix \$Message"' -or + $ast.Extent.Text -notmatch 'Write-Host "\$\(\$Indent\)\$\(\$Indent\)astrbot init"' -or + $ast.Extent.Text -notmatch 'Write-Host "\$\(\$Indent\)\$\(\$Indent\)astrbot run"' -or $ast.Extent.Text -notmatch 'if \(\$Language -eq ''zh-CN''\) \{ ''1'' \} else \{ ''2'' \}') { throw 'The bootstrap menu must remain ASCII-only, default to English, and select the source default by language.' } From e131cdd3f803b9948d604412b5d12b285d262f05 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 16:42:22 +0800 Subject: [PATCH 16/28] docs(install): clarify AstrBot startup steps --- docs/public/install.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/public/install.ps1 b/docs/public/install.ps1 index 48d661c3e69..0e325fed60b 100644 --- a/docs/public/install.ps1 +++ b/docs/public/install.ps1 @@ -41,7 +41,7 @@ $Indent = ' ' # Keep the first-run experience predictable across machines. Users can still # choose Chinese or Japanese explicitly in the language menu. $bootstrapCompressed = [Convert]::FromBase64String( - 'H4sIAAAAAAAACmWPzUrDQBSFX2W464D77FRcKCJCfYFQhySQPzJTEETIiCi1QUGpBbFps4i2adosG/DnZSZOpyt9BAmUNuDucu455+Ocw75DqGZZ2D8xqYVBhd9BGKJtQv0dl6L1FxQ41By9pen42Hdtj4IKDWzhJkXWSkdbSObzZdAWnbHMp3IUVMookGnELx+WrBC3EWc9zl45u+dsyFmXsytQYNcwHUwq9mIWlJ+PZTEVTzegwJ6jWyYxQF1fChxonrZyi14iXjKZRqDAET6jDYo9sslUkoL8loOogQlGTde2NeeUqHX7hi3ukrLoiH4i83n51RdZLNpvMg5/Pp7rgTo/izmb8ICV71W0Gjm8XnRTzibfs4GMw/9rq7KLP6djnXl1AQAA' + 'H4sIAAAAAAAACmVQXUsCQRT9K8N9Xuh93yp6KCIC+wOLDSqsq+yOEESwE5HpDhmGCpEfC62aX/vYgtWfmZ1xfKqfEEumQm/3nnvuOYdzCYeWQwzTxPZZjpgYdPjuMoZ2HWLvFQhaX0GDY8PKlIwMPrUL+SIBHVLYxGmCzBWOdpAK35ZuRXqvKpyqoZsgQ1eNOvy6vqSRrHY4bXHa57TGaY/TBqc3oMF+NmdhJ/FezNz44zGOprJZBg0OrIyZc7KgrycNjoyisWLLViCfJ2rUAQ1O8AVJEVx0Nj8JpCG7ZCGSxQ5G6UI+b1jnjr5N33jL+yCOPNkORC2M50H82ZYTX1YGymfJGnmSleP5S0K4a4uBJ1hT1Jl4CEV1+NfX1/vTtvh21onP6Zi79FcrKaR3u2iMOB2LWVf57H8zidjVDw7uGtmhAQAA' ) $bootstrapInput = New-Object System.IO.MemoryStream(,$bootstrapCompressed) $bootstrapGzip = New-Object System.IO.Compression.GZipStream( From 99599d609bd8748aef110cb3ce7e3765f18a9152 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 17:21:53 +0800 Subject: [PATCH 17/28] feat(install): fall back from Python mirror --- docs/public/install.ps1 | 23 +++++++++++++-- tests/install_script.Tests.ps1 | 54 ++++++++++++++++++++++++---------- 2 files changed, 58 insertions(+), 19 deletions(-) diff --git a/docs/public/install.ps1 b/docs/public/install.ps1 index 0e325fed60b..48cda54586a 100644 --- a/docs/public/install.ps1 +++ b/docs/public/install.ps1 @@ -41,7 +41,7 @@ $Indent = ' ' # Keep the first-run experience predictable across machines. Users can still # choose Chinese or Japanese explicitly in the language menu. $bootstrapCompressed = [Convert]::FromBase64String( - 'H4sIAAAAAAAACmVQXUsCQRT9K8N9Xuh93yp6KCIC+wOLDSqsq+yOEESwE5HpDhmGCpEfC62aX/vYgtWfmZ1xfKqfEEumQm/3nnvuOYdzCYeWQwzTxPZZjpgYdPjuMoZ2HWLvFQhaX0GDY8PKlIwMPrUL+SIBHVLYxGmCzBWOdpAK35ZuRXqvKpyqoZsgQ1eNOvy6vqSRrHY4bXHa57TGaY/TBqc3oMF+NmdhJ/FezNz44zGOprJZBg0OrIyZc7KgrycNjoyisWLLViCfJ2rUAQ1O8AVJEVx0Nj8JpCG7ZCGSxQ5G6UI+b1jnjr5N33jL+yCOPNkORC2M50H82ZYTX1YGymfJGnmSleP5S0K4a4uBJ1hT1Jl4CEV1+NfX1/vTtvh21onP6Zi79FcrKaR3u2iMOB2LWVf57H8zidjVDw7uGtmhAQAA' + 'H4sIAAAAAAAACmWQ3W4SQRzFX2Uy1xu8xys1mmiMIakvsNKB3bjskt0htTEmO1AL7S7S1ABq+CiW5at82MREaqEP4+xMh6v6CGZaLCQkczFz5szvP+e8h89NB6uGgezXOjYQjMK/Td8HjxxsP7YwuL+FCnypmsmMmkQx20qlMYzCLWSgOAbGUgcPgJj8WrgH3OuLyUj0XKn0XDFo0Ozxgkz5YYOSKiUdSkqUnFBSpmQPKvCJppvIkbOvx244/xxOR7yShwp8aiYN3dFg9H6nwBdqWl26eTXgtaEYNKACX6F3eAujtLN6IyUF2BkTYA05CMStVEo1t53oun01m38KwqnH6wErTcLLILyq82GLH3RFy5fHqcf9fHjZloZCnXU95lfYsc+OJuyw97+vm9m3dfj6X4ctSs6oS+5YspCT/evygJIzNm6Klr/ZzB0stos1y3ymGsYbNf52lS52q4OUbtuWDRKqbqDth8BG2N7VzSTY0bEmcwMrkdDjumoAx8rYcRSJRDagqw6W0EW5xnKlcOqJ+Zy1z8XP4Gbm89Epq/VYIc+L31nhBxt/4ZUL/vtokS+KSfmP29ngrsVfgmmuSXN9mpvJ2O1zXq7exm7KlSXUJezjkM1KNDuXnuwFJV22XxT9zlpBe5RcUfJVDvzwD6o1noa/AgAA' ) $bootstrapInput = New-Object System.IO.MemoryStream(,$bootstrapCompressed) $bootstrapGzip = New-Object System.IO.Compression.GZipStream( @@ -177,7 +177,7 @@ $messages = @{ $Text = $messages.$Language if (-not $DownloadSource) { - $DownloadSource = 'official' + $DownloadSource = if ($Language -eq 'zh-CN') { 'china' } else { 'official' } if ($CanPrompt) { $defaultChoice = if ($Language -eq 'zh-CN') { '1' } else { '2' } Write-Host "`n$Indent$($Text.ChooseSource)" @@ -216,6 +216,14 @@ switch ($DownloadSource) { } } +if (-not $PythonInstallMirror) { + if ($env:UV_PYTHON_INSTALL_MIRROR) { + $PythonInstallMirror = $env:UV_PYTHON_INSTALL_MIRROR + } elseif ($DownloadSource -eq 'china') { + $PythonInstallMirror = 'https://mirrors.ustc.edu.cn/github-release/astral-sh/python-build-standalone/' + } +} + foreach ($url in @($uvMirrorUrls) + @($PypiIndexUrl, $PythonInstallMirror)) { if (-not $url) { continue } $parsedUrl = $null @@ -376,7 +384,6 @@ if (-not (Test-Command "uv")) { Ok (& uv --version) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -if (-not $PythonInstallMirror) { $PythonInstallMirror = $env:UV_PYTHON_INSTALL_MIRROR } if ($PythonInstallMirror) { Info ($Text.PythonMirror -f $PythonInstallMirror) } else { @@ -388,6 +395,16 @@ try { if ($PythonInstallMirror) { $env:UV_PYTHON_INSTALL_MIRROR = $PythonInstallMirror } Info $Text.InstallingAstrBot uv tool install --python 3.12 astrbot --default-index $PypiIndexUrl + if ($LASTEXITCODE -ne 0 -and $PythonInstallMirror) { + $pythonFallback = switch ($Language) { + 'zh-CN' { $bootstrapText.PythonFallbackChinese } + 'ja-JP' { $bootstrapText.PythonFallbackJapanese } + default { $bootstrapText.PythonFallbackEnglish } + } + Warn $pythonFallback + $env:UV_PYTHON_INSTALL_MIRROR = $null + uv tool install --python 3.12 astrbot --default-index $PypiIndexUrl + } if ($LASTEXITCODE -ne 0) { Err ($Text.AstrBotFailed -f $LASTEXITCODE) exit $LASTEXITCODE diff --git a/tests/install_script.Tests.ps1 b/tests/install_script.Tests.ps1 index 2bcd9d6f144..9e4f5fadcc0 100644 --- a/tests/install_script.Tests.ps1 +++ b/tests/install_script.Tests.ps1 @@ -39,14 +39,24 @@ if ($ast.Extent.Text -notmatch '\$bootstrapCompressed = \[Convert\]::FromBase64S } $global:UvArguments = @() +$global:UvPythonMirrors = @() +$global:FailNextToolInstall = $false function global:uv { $global:UvArguments += ,@($args) $global:LASTEXITCODE = 0 if ($args.Count -eq 1 -and $args[0] -eq '--version') { 'uv test-version' + } elseif ($args.Count -gt 1 -and $args[0] -eq 'tool' -and $args[1] -eq 'install') { + $global:UvPythonMirrors += [string]$env:UV_PYTHON_INSTALL_MIRROR + if ($global:FailNextToolInstall) { + $global:FailNextToolInstall = $false + $global:LASTEXITCODE = 1 + } } } +$previousPythonMirror = $env:UV_PYTHON_INSTALL_MIRROR +$env:UV_PYTHON_INSTALL_MIRROR = $null try { & $scriptPath -Language en-US -DownloadSource official -NonInteractive if ($LASTEXITCODE -ne 0) { @@ -60,24 +70,36 @@ try { if ($LASTEXITCODE -ne 0) { throw "Invoke-Expression execution failed with exit code $LASTEXITCODE." } + $versionCalls = @($global:UvArguments | Where-Object { $_.Count -eq 1 -and $_[0] -eq '--version' }) + $installCalls = @($global:UvArguments | Where-Object { + $_.Count -eq 7 -and + $_[0] -eq 'tool' -and + $_[1] -eq 'install' -and + $_[2] -eq '--python' -and + $_[3] -eq '3.12' -and + $_[4] -eq 'astrbot' -and + $_[5] -eq '--default-index' -and + $_[6] -eq 'https://pypi.org/simple/' + }) + if ($versionCalls.Count -ne 2 -or $installCalls.Count -ne 2) { + throw "Expected both execution modes to reach uv; got $($versionCalls.Count) version calls and $($installCalls.Count) install calls." + } + + $global:FailNextToolInstall = $true + & $scriptPath -Language zh-CN -DownloadSource china -NonInteractive + if ($LASTEXITCODE -ne 0) { + throw "China mirror fallback failed with exit code $LASTEXITCODE." + } + $fallbackMirrors = @($global:UvPythonMirrors | Select-Object -Last 2) + $ustcMirror = 'https://mirrors.ustc.edu.cn/github-release/astral-sh/python-build-standalone/' + if ($fallbackMirrors.Count -ne 2 -or + $fallbackMirrors[0] -ne $ustcMirror -or + $fallbackMirrors[1] -ne '') { + throw "Expected USTC then official Python sources; got: $($fallbackMirrors -join ', ')" + } } finally { + $env:UV_PYTHON_INSTALL_MIRROR = $previousPythonMirror Remove-Item Function:\uv -ErrorAction SilentlyContinue } -$versionCalls = @($global:UvArguments | Where-Object { $_.Count -eq 1 -and $_[0] -eq '--version' }) -$installCalls = @($global:UvArguments | Where-Object { - $_.Count -eq 7 -and - $_[0] -eq 'tool' -and - $_[1] -eq 'install' -and - $_[2] -eq '--python' -and - $_[3] -eq '3.12' -and - $_[4] -eq 'astrbot' -and - $_[5] -eq '--default-index' -and - $_[6] -eq 'https://pypi.org/simple/' -}) - -if ($versionCalls.Count -ne 2 -or $installCalls.Count -ne 2) { - throw "Expected both execution modes to reach uv; got $($versionCalls.Count) version calls and $($installCalls.Count) install calls." -} - Write-Host "install.ps1 passed direct-file and Invoke-Expression tests on $($PSVersionTable.PSEdition) $($PSVersionTable.PSVersion)." From f9bce8d045d6dcee1262b5b4f7686c4dfc6492a7 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 18:30:59 +0800 Subject: [PATCH 18/28] fix(install): keep Windows setup in one console --- docs/public/install.ps1 | 186 ++++++++++++++++++++++----------- tests/install_script.Tests.ps1 | 59 +++++++++++ 2 files changed, 186 insertions(+), 59 deletions(-) diff --git a/docs/public/install.ps1 b/docs/public/install.ps1 index 48cda54586a..eeb499a7883 100644 --- a/docs/public/install.ps1 +++ b/docs/public/install.ps1 @@ -198,8 +198,7 @@ switch ($DownloadSource) { 'china' { # Use uv mirrors listed by MirrorZ; the release-directory layout is not GitHub's layout. $uvMirrorUrls = @( - 'https://mirrors.ustc.edu.cn/github-release/astral-sh/uv/LatestRelease', - 'https://mirror.nyist.edu.cn/github-release/astral-sh/uv/LatestRelease' + 'https://mirrors.ustc.edu.cn/github-release/astral-sh/uv/LatestRelease' ) $PypiIndexUrl = 'https://mirrors.cernet.edu.cn/pypi/web/simple/' } @@ -261,6 +260,42 @@ function Test-Command { return $? } +function Test-ZipArchive { + param([string]$Path) + + if (-not (Test-Path -LiteralPath $Path) -or (Get-Item -LiteralPath $Path).Length -lt 4) { + return $false + } + + $stream = [System.IO.File]::OpenRead($Path) + try { + return $stream.ReadByte() -eq 0x50 -and + $stream.ReadByte() -eq 0x4B -and + $stream.ReadByte() -eq 0x03 -and + $stream.ReadByte() -eq 0x04 + } finally { + $stream.Dispose() + } +} + +function Invoke-UvToolInstall { + param([string[]]$ArgumentList) + + $uvCommand = Get-Command 'uv' -ErrorAction Stop + if ($uvCommand.CommandType -ne 'Application') { + # This path also makes the installer straightforward to test without + # downloading packages or changing the developer machine. + & $uvCommand @ArgumentList + return $LASTEXITCODE + } + + # PowerShell may otherwise give console programs their own window in some + # hosts. Explicitly keep uv and all of its installation output in this one. + $process = Start-Process -FilePath $uvCommand.Source -ArgumentList $ArgumentList ` + -NoNewWindow -Wait -PassThru + return $process.ExitCode +} + function Update-UvPath { $candidateDirs = @() @@ -293,64 +328,93 @@ function Install-Uv { $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([guid]::NewGuid().ToString()) $null = New-Item -ItemType Directory -Path $tempDir try { - if ($uvMirrorUrls.Count -gt 0) { - # PROCESSOR_ARCHITEW6432 identifies the native OS when PowerShell is emulated. - $nativeArch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE } - $arch = switch ($nativeArch) { - 'AMD64' { 'x86_64' } - 'x86' { 'i686' } - 'ARM64' { 'aarch64' } - default { throw ($Text.UnsupportedArch -f $_) } - } - $artifact = "uv-$arch-pc-windows-msvc.zip" - foreach ($baseUrl in $uvMirrorUrls) { - try { + # PROCESSOR_ARCHITEW6432 identifies the native OS when PowerShell is emulated. + $nativeArch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE } + $arch = switch ($nativeArch) { + 'AMD64' { 'x86_64' } + 'x86' { 'i686' } + 'ARM64' { 'aarch64' } + default { throw ($Text.UnsupportedArch -f $_) } + } + $artifact = "uv-$arch-pc-windows-msvc.zip" + $officialBaseUrl = 'https://github.com/astral-sh/uv/releases/latest/download' + foreach ($baseUrl in @($uvMirrorUrls) + $officialBaseUrl) { + try { + if ($baseUrl -eq $officialBaseUrl) { + Info "$($Text.Official): $officialBaseUrl" + } else { Info ($Text.TryingMirror -f $baseUrl) - $archive = Join-Path $tempDir $artifact - $checksumFile = Join-Path $tempDir "$artifact.sha256" - Invoke-WebRequest -Uri "$baseUrl/$artifact" -OutFile $archive -TimeoutSec 60 -UseBasicParsing - Invoke-WebRequest -Uri "$baseUrl/$artifact.sha256" -OutFile $checksumFile -TimeoutSec 30 -UseBasicParsing - $expectedHash = ((Get-Content $checksumFile -Raw).Trim() -split '\s+')[0] - if ($expectedHash -notmatch '^[a-fA-F0-9]{64}$' -or - (Get-FileHash $archive -Algorithm SHA256).Hash -ne $expectedHash) { - throw $Text.InvalidChecksum - } - $unpacked = Join-Path $tempDir 'unpacked' - Expand-Archive -Path $archive -DestinationPath $unpacked -Force - if (-not (Test-Path (Join-Path $unpacked 'uv.exe')) -or - -not (Test-Path (Join-Path $unpacked 'uvx.exe'))) { - throw $Text.MissingBinaries + } + $archive = Join-Path $tempDir $artifact + $checksumFile = Join-Path $tempDir "$artifact.sha256" + Invoke-WebRequest -Uri "$baseUrl/$artifact" -OutFile $archive -TimeoutSec 90 -UseBasicParsing + + # USTC protects larger release assets with a lightweight + # JavaScript cookie check. Reproduce that single documented + # browser step when its verification page is returned. + if (-not (Test-ZipArchive $archive) -and + ([Uri]$baseUrl).Host -eq 'mirrors.ustc.edu.cn') { + $challengePage = [System.IO.File]::ReadAllText($archive) + $addressMatch = [regex]::Match( + $challengePage, + 'document\.cookie\s*=\s*"addr=([0-9A-Fa-f:.]+);' + ) + if ($addressMatch.Success) { + # Windows PowerShell 5.1 silently drops a manually set + # Cookie header. A WebRequestSession works in both 5.1 + # and modern PowerShell. + $webSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession + $cookie = New-Object System.Net.Cookie( + 'addr', + $addressMatch.Groups[1].Value, + '/', + 'mirrors.ustc.edu.cn' + ) + $webSession.Cookies.Add($cookie) + Invoke-WebRequest -Uri "$baseUrl/$artifact" -OutFile $archive ` + -WebSession $webSession ` + -TimeoutSec 90 -UseBasicParsing } - } catch { - Warn ($Text.MirrorFailed -f $_.Exception.Message) - Remove-Item (Join-Path $tempDir 'unpacked') -Recurse -Force -ErrorAction SilentlyContinue - continue } - # Respect uv's installation directory settings when using a mirror too. - $installDir = if ($env:UV_INSTALL_DIR) { $env:UV_INSTALL_DIR } - elseif ($env:UV_UNMANAGED_INSTALL) { $env:UV_UNMANAGED_INSTALL } - elseif ($env:XDG_BIN_HOME) { $env:XDG_BIN_HOME } - elseif ($env:XDG_DATA_HOME) { Join-Path $env:XDG_DATA_HOME '..\bin' } - else { Join-Path $env:USERPROFILE '.local\bin' } - $null = New-Item -ItemType Directory -Path $installDir -Force - Copy-Item (Join-Path $unpacked '*.exe') -Destination $installDir -Force - $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') - if (-not $env:UV_UNMANAGED_INSTALL -and -not $env:INSTALLER_NO_MODIFY_PATH -and - ($userPath -split ';') -notcontains $installDir) { - [Environment]::SetEnvironmentVariable('Path', "$installDir;$userPath", 'User') + if (-not (Test-ZipArchive $archive)) { + throw $Text.InvalidChecksum + } + Invoke-WebRequest -Uri "$baseUrl/$artifact.sha256" -OutFile $checksumFile -TimeoutSec 30 -UseBasicParsing + $expectedHash = ((Get-Content $checksumFile -Raw).Trim() -split '\s+')[0] + if ($expectedHash -notmatch '^[a-fA-F0-9]{64}$' -or + (Get-FileHash $archive -Algorithm SHA256).Hash -ne $expectedHash) { + throw $Text.InvalidChecksum } - Update-UvPath - return + $unpacked = Join-Path $tempDir 'unpacked' + Expand-Archive -Path $archive -DestinationPath $unpacked -Force + if (-not (Test-Path (Join-Path $unpacked 'uv.exe')) -or + -not (Test-Path (Join-Path $unpacked 'uvx.exe'))) { + throw $Text.MissingBinaries + } + } catch { + Warn ($Text.MirrorFailed -f $_.Exception.Message) + Remove-Item (Join-Path $tempDir 'unpacked') -Recurse -Force -ErrorAction SilentlyContinue + continue } - } - Info $Text.OfficialFallback - $tempScript = Join-Path $tempDir 'uv-install.ps1' - Invoke-WebRequest -Uri "https://astral.sh/uv/install.ps1" -OutFile $tempScript -UseBasicParsing - & $tempScript - if (-not $?) { throw $Text.UvFailed } - Update-UvPath + # Respect uv's installation directory settings for every source. + $installDir = if ($env:UV_INSTALL_DIR) { $env:UV_INSTALL_DIR } + elseif ($env:UV_UNMANAGED_INSTALL) { $env:UV_UNMANAGED_INSTALL } + elseif ($env:XDG_BIN_HOME) { $env:XDG_BIN_HOME } + elseif ($env:XDG_DATA_HOME) { Join-Path $env:XDG_DATA_HOME '..\bin' } + else { Join-Path $env:USERPROFILE '.local\bin' } + $null = New-Item -ItemType Directory -Path $installDir -Force + Copy-Item (Join-Path $unpacked '*.exe') -Destination $installDir -Force + $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') + if (-not $env:UV_UNMANAGED_INSTALL -and -not $env:INSTALLER_NO_MODIFY_PATH -and + ($userPath -split ';') -notcontains $installDir) { + [Environment]::SetEnvironmentVariable('Path', "$installDir;$userPath", 'User') + } + Update-UvPath + return + } + throw $Text.UvFailed } catch { Err $_.Exception.Message Err $Text.UvFailed @@ -394,8 +458,10 @@ $previousPythonMirror = $env:UV_PYTHON_INSTALL_MIRROR try { if ($PythonInstallMirror) { $env:UV_PYTHON_INSTALL_MIRROR = $PythonInstallMirror } Info $Text.InstallingAstrBot - uv tool install --python 3.12 astrbot --default-index $PypiIndexUrl - if ($LASTEXITCODE -ne 0 -and $PythonInstallMirror) { + $toolInstallExitCode = Invoke-UvToolInstall -ArgumentList @( + 'tool', 'install', '--python', '3.12', 'astrbot', '--default-index', $PypiIndexUrl + ) + if ($toolInstallExitCode -ne 0 -and $PythonInstallMirror) { $pythonFallback = switch ($Language) { 'zh-CN' { $bootstrapText.PythonFallbackChinese } 'ja-JP' { $bootstrapText.PythonFallbackJapanese } @@ -403,11 +469,13 @@ try { } Warn $pythonFallback $env:UV_PYTHON_INSTALL_MIRROR = $null - uv tool install --python 3.12 astrbot --default-index $PypiIndexUrl + $toolInstallExitCode = Invoke-UvToolInstall -ArgumentList @( + 'tool', 'install', '--python', '3.12', 'astrbot', '--default-index', $PypiIndexUrl + ) } - if ($LASTEXITCODE -ne 0) { - Err ($Text.AstrBotFailed -f $LASTEXITCODE) - exit $LASTEXITCODE + if ($toolInstallExitCode -ne 0) { + Err ($Text.AstrBotFailed -f $toolInstallExitCode) + exit $toolInstallExitCode } Ok $Text.Installed $nextSteps = switch ($Language) { diff --git a/tests/install_script.Tests.ps1 b/tests/install_script.Tests.ps1 index 9e4f5fadcc0..954869777fe 100644 --- a/tests/install_script.Tests.ps1 +++ b/tests/install_script.Tests.ps1 @@ -37,6 +37,41 @@ if ($ast.Extent.Text -notmatch '\$bootstrapCompressed = \[Convert\]::FromBase64S $ast.Extent.Text -notmatch 'if \(\$Language -eq ''zh-CN''\) \{ ''1'' \} else \{ ''2'' \}') { throw 'The bootstrap menu must remain ASCII-only, default to English, and select the source default by language.' } +if ($ast.Extent.Text -match 'mirror\.nyist\.edu\.cn' -or + $ast.Extent.Text -match 'https://astral\.sh/uv/install\.ps1' -or + $ast.Extent.Text -notmatch 'https://github\.com/astral-sh/uv/releases/latest/download' -or + $ast.Extent.Text -notmatch 'Microsoft\.PowerShell\.Commands\.WebRequestSession' -or + $ast.Extent.Text -notmatch '-NoNewWindow -Wait -PassThru') { + throw 'uv must use the verified ZIP flow, support the USTC cookie challenge, and keep installation in the current window.' +} + +$zipFunctionAst = $ast.Find({ + param($node) + $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and + $node.Name -eq 'Test-ZipArchive' +}, $true) +if (-not $zipFunctionAst) { throw 'Test-ZipArchive is missing.' } +Invoke-Expression $zipFunctionAst.Extent.Text + +$archiveTestDir = Join-Path ([System.IO.Path]::GetTempPath()) ("astrbot-install-test-{0}" -f [guid]::NewGuid()) +$null = New-Item -ItemType Directory -Path $archiveTestDir +try { + $validArchive = Join-Path $archiveTestDir 'valid.zip' + $htmlResponse = Join-Path $archiveTestDir 'verification.html' + $shortFile = Join-Path $archiveTestDir 'short.bin' + [System.IO.File]::WriteAllBytes($validArchive, [byte[]](0x50, 0x4B, 0x03, 0x04)) + [System.IO.File]::WriteAllText($htmlResponse, 'Verifying') + [System.IO.File]::WriteAllBytes($shortFile, [byte[]](0x50, 0x4B, 0x03)) + if (-not (Test-ZipArchive $validArchive) -or + (Test-ZipArchive $htmlResponse) -or + (Test-ZipArchive $shortFile) -or + (Test-ZipArchive (Join-Path $archiveTestDir 'missing.zip'))) { + throw 'ZIP signature validation did not accept only a valid archive signature.' + } +} finally { + Remove-Item -LiteralPath $archiveTestDir -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item Function:\Test-ZipArchive -ErrorAction SilentlyContinue +} $global:UvArguments = @() $global:UvPythonMirrors = @() @@ -102,4 +137,28 @@ try { Remove-Item Function:\uv -ErrorAction SilentlyContinue } +$uvInstallFunctionAst = $ast.Find({ + param($node) + $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and + $node.Name -eq 'Invoke-UvToolInstall' +}, $true) +if (-not $uvInstallFunctionAst) { throw 'Invoke-UvToolInstall is missing.' } +Invoke-Expression $uvInstallFunctionAst.Extent.Text + +$applicationTestDir = Join-Path ([System.IO.Path]::GetTempPath()) ("astrbot-uv-process-test-{0}" -f [guid]::NewGuid()) +$null = New-Item -ItemType Directory -Path $applicationTestDir +$previousPath = $env:PATH +try { + Copy-Item -LiteralPath "$env:SystemRoot\System32\cmd.exe" -Destination (Join-Path $applicationTestDir 'uv.exe') + $env:PATH = "$applicationTestDir;$env:PATH" + $exitCode = Invoke-UvToolInstall -ArgumentList @('/d', '/c', 'exit', '0') + if ($exitCode -ne 0) { + throw "The no-new-window uv process path returned exit code $exitCode." + } +} finally { + $env:PATH = $previousPath + Remove-Item -LiteralPath $applicationTestDir -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item Function:\Invoke-UvToolInstall -ErrorAction SilentlyContinue +} + Write-Host "install.ps1 passed direct-file and Invoke-Expression tests on $($PSVersionTable.PSEdition) $($PSVersionTable.PSVersion)." From b68627b389cdfcda68bd0ed59decf050b230bcbf Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 18:38:06 +0800 Subject: [PATCH 19/28] ci: run install tests in fixed shells --- .github/workflows/install_script_test.yml | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/.github/workflows/install_script_test.yml b/.github/workflows/install_script_test.yml index 23ce3f407a4..bd3c8f7dea3 100644 --- a/.github/workflows/install_script_test.yml +++ b/.github/workflows/install_script_test.yml @@ -17,21 +17,17 @@ on: jobs: test: - name: ${{ matrix.name }} + name: Windows PowerShell compatibility runs-on: windows-latest - strategy: - fail-fast: false - matrix: - include: - - name: Windows PowerShell 5.1 - shell: powershell - - name: PowerShell 7 - shell: pwsh steps: - name: Checkout uses: actions/checkout@v7 - - name: Test all supported invocation modes - shell: ${{ matrix.shell }} + - name: Test Windows PowerShell 5.1 + shell: powershell + run: .\tests\install_script.Tests.ps1 + + - name: Test PowerShell 7 + shell: pwsh run: .\tests\install_script.Tests.ps1 From f0bcbd7ec240f3b8c04d26f9967e3432f952880c Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 19:28:24 +0800 Subject: [PATCH 20/28] fix(install): tolerate a removed current directory --- docs/public/install.ps1 | 20 ++++---------------- tests/install_script.Tests.ps1 | 14 +++++++++++++- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/public/install.ps1 b/docs/public/install.ps1 index eeb499a7883..3630986eddf 100644 --- a/docs/public/install.ps1 +++ b/docs/public/install.ps1 @@ -16,21 +16,6 @@ param( $ErrorActionPreference = 'Stop' -# Keep localized status messages readable in Windows PowerShell consoles whose -# legacy code page cannot represent CJK characters. -try { - if ($Host.Name -eq 'ConsoleHost') { - # Windows PowerShell 5.1 may keep an OEM code page even when the - # managed output encoding is UTF-8. - $null = & chcp.com 65001 2>$null - } - $utf8 = New-Object System.Text.UTF8Encoding($false) - [Console]::InputEncoding = $utf8 - [Console]::OutputEncoding = $utf8 -} catch { - # Some hosts do not expose mutable console encodings; continue normally. -} - # Windows PowerShell 5.1 may otherwise negotiate an obsolete TLS version. [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 @@ -291,8 +276,11 @@ function Invoke-UvToolInstall { # PowerShell may otherwise give console programs their own window in some # hosts. Explicitly keep uv and all of its installation output in this one. + # Do not inherit the current directory: it may have been removed while an + # existing PowerShell session was still located there. + $workingDirectory = [System.IO.Path]::GetTempPath() $process = Start-Process -FilePath $uvCommand.Source -ArgumentList $ArgumentList ` - -NoNewWindow -Wait -PassThru + -WorkingDirectory $workingDirectory -NoNewWindow -Wait -PassThru return $process.ExitCode } diff --git a/tests/install_script.Tests.ps1 b/tests/install_script.Tests.ps1 index 954869777fe..dacbb641e7c 100644 --- a/tests/install_script.Tests.ps1 +++ b/tests/install_script.Tests.ps1 @@ -41,7 +41,9 @@ if ($ast.Extent.Text -match 'mirror\.nyist\.edu\.cn' -or $ast.Extent.Text -match 'https://astral\.sh/uv/install\.ps1' -or $ast.Extent.Text -notmatch 'https://github\.com/astral-sh/uv/releases/latest/download' -or $ast.Extent.Text -notmatch 'Microsoft\.PowerShell\.Commands\.WebRequestSession' -or - $ast.Extent.Text -notmatch '-NoNewWindow -Wait -PassThru') { + $ast.Extent.Text -notmatch '-WorkingDirectory \$workingDirectory -NoNewWindow -Wait -PassThru' -or + $ast.Extent.Text -match 'chcp\.com' -or + $ast.Extent.Text -match '\[Console\]::(?:Input|Output)Encoding') { throw 'uv must use the verified ZIP flow, support the USTC cookie challenge, and keep installation in the current window.' } @@ -148,14 +150,24 @@ Invoke-Expression $uvInstallFunctionAst.Extent.Text $applicationTestDir = Join-Path ([System.IO.Path]::GetTempPath()) ("astrbot-uv-process-test-{0}" -f [guid]::NewGuid()) $null = New-Item -ItemType Directory -Path $applicationTestDir $previousPath = $env:PATH +$previousLocation = (Get-Location).Path try { Copy-Item -LiteralPath "$env:SystemRoot\System32\cmd.exe" -Destination (Join-Path $applicationTestDir 'uv.exe') $env:PATH = "$applicationTestDir;$env:PATH" + + # Reproduce a shell whose current directory was removed by an earlier + # cleanup. Start-Process fails with "The directory name is invalid" unless + # Invoke-UvToolInstall supplies a valid working directory explicitly. + $staleCurrentDirectory = Join-Path $applicationTestDir 'deleted-current-directory' + $null = New-Item -ItemType Directory -Path $staleCurrentDirectory + Set-Location -LiteralPath $staleCurrentDirectory + [System.IO.Directory]::Delete($staleCurrentDirectory) $exitCode = Invoke-UvToolInstall -ArgumentList @('/d', '/c', 'exit', '0') if ($exitCode -ne 0) { throw "The no-new-window uv process path returned exit code $exitCode." } } finally { + Set-Location -LiteralPath $previousLocation $env:PATH = $previousPath Remove-Item -LiteralPath $applicationTestDir -Recurse -Force -ErrorAction SilentlyContinue Remove-Item Function:\Invoke-UvToolInstall -ErrorAction SilentlyContinue From 666f8a0196f0bfddbb7da4c01ef77b4ba606d7b0 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 20:55:15 +0800 Subject: [PATCH 21/28] docs: simplify script deployment instructions --- README.md | 2 +- README_zh-TW.md | 2 +- README_zh.md | 2 +- docs/en/deploy/astrbot/package.md | 70 ++++++++---------------------- docs/zh/deploy/astrbot/launcher.md | 4 +- docs/zh/deploy/astrbot/package.md | 64 +++++++-------------------- docs/zh/others/diagnostics.md | 2 +- 7 files changed, 41 insertions(+), 105 deletions(-) diff --git a/README.md b/README.md index bfdc06b7aea..f6d5d01eca0 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ Visit [AstrBot Launcher](https://github.com/Raven95676/astrbot-launcher) to down **More deployment methods** -If you need panel-based management or deeper customization, see [BT-Panel Deployment](https://docs.astrbot.app/deploy/astrbot/btpanel.html) for BT Panel app-store setup, [1Panel Deployment](https://docs.astrbot.app/deploy/astrbot/1panel.html) for 1Panel app-market deployment, [CasaOS Deployment](https://docs.astrbot.app/deploy/astrbot/casaos.html) for NAS/home-server visual deployment, and [Manual Deployment](https://docs.astrbot.app/deploy/astrbot/cli.html) for fully custom source-based installation with `uv`. +If you need panel-based management or deeper customization, see [BT-Panel Deployment](https://docs.astrbot.app/deploy/astrbot/btpanel.html), [1Panel Deployment](https://docs.astrbot.app/deploy/astrbot/1panel.html), [CasaOS Deployment](https://docs.astrbot.app/deploy/astrbot/casaos.html), or [Source Deployment](https://docs.astrbot.app/deploy/astrbot/cli.html). ## ❤️ Sponsors diff --git a/README_zh-TW.md b/README_zh-TW.md index 6b6cd31e422..5794093b489 100644 --- a/README_zh-TW.md +++ b/README_zh-TW.md @@ -101,7 +101,7 @@ uv tool upgrade astrbot --python 3.12 **更多部署方式** -若你需要面板化或更高自訂程度的部署,可參考 [寶塔面板](https://docs.astrbot.app/deploy/astrbot/btpanel.html)(BT Panel 應用商店安裝)、[1Panel](https://docs.astrbot.app/deploy/astrbot/1panel.html)(1Panel 應用商店安裝)、[CasaOS](https://docs.astrbot.app/deploy/astrbot/casaos.html)(NAS / 家用伺服器可視化部署)與 [手動部署](https://docs.astrbot.app/deploy/astrbot/cli.html)(基於原始碼與 `uv` 的完整自訂安裝)。 +若你需要面板化或更高自訂程度的部署,可參考 [寶塔面板](https://docs.astrbot.app/deploy/astrbot/btpanel.html)、[1Panel](https://docs.astrbot.app/deploy/astrbot/1panel.html)、[CasaOS](https://docs.astrbot.app/deploy/astrbot/casaos.html) 或 [原始碼部署](https://docs.astrbot.app/deploy/astrbot/cli.html)。 ## ❤️ 贊助商 diff --git a/README_zh.md b/README_zh.md index b38385512dc..3422bd7b76b 100644 --- a/README_zh.md +++ b/README_zh.md @@ -102,7 +102,7 @@ uv tool upgrade astrbot --python 3.12 **更多部署方式** -若你需要面板化或更高自定义部署,可参考 [宝塔面板](https://docs.astrbot.app/deploy/astrbot/btpanel.html)(BT Panel 应用商店安装)、[1Panel](https://docs.astrbot.app/deploy/astrbot/1panel.html)(1Panel 应用商店安装)、[CasaOS](https://docs.astrbot.app/deploy/astrbot/casaos.html)(NAS / 家庭服务器可视化部署)和 [手动部署](https://docs.astrbot.app/deploy/astrbot/cli.html)(基于源码与 `uv` 的完整自定义安装)。 +若你需要面板化或更高自定义部署,可参考 [宝塔面板](https://docs.astrbot.app/deploy/astrbot/btpanel.html)、[1Panel](https://docs.astrbot.app/deploy/astrbot/1panel.html)、[CasaOS](https://docs.astrbot.app/deploy/astrbot/casaos.html) 和 [源码部署](https://docs.astrbot.app/deploy/astrbot/cli.html)。 ## ❤️ 赞助商 diff --git a/docs/en/deploy/astrbot/package.md b/docs/en/deploy/astrbot/package.md index b5f5276b0c8..cace154cb35 100644 --- a/docs/en/deploy/astrbot/package.md +++ b/docs/en/deploy/astrbot/package.md @@ -1,82 +1,50 @@ -# Package Manager Deployment (uv) +# Script Deployment -Use `uv` to install and run AstrBot quickly. +AstrBot provides installation scripts for Linux, macOS, WSL, and Windows. The scripts install `uv`, AstrBot, and Python 3.12. -## Before You Start +## Linux / macOS / WSL -If `uv` is not installed, install it first by following the official guide: - - -`uv` supports Linux, Windows, and macOS. - -## Important Notes - -> [!WARNING] -> AstrBot deployed via `uv` **does not support upgrading through the WebUI**. To update, run `uv tool upgrade astrbot --python 3.12` from the command line. - -AstrBot requires Python 3.12 or later. Use `--python 3.12` to ensure that `uv` creates the tool environment with Python 3.12; if Python downloads are enabled, `uv` will download Python 3.12 automatically when it is missing. - -## Install and Start - -### Linux / macOS / WSL Installation Wizard - -Run with **Bash 3.2 or later**: +Interactive installation: ```bash curl -fsSL https://docs.astrbot.app/install.sh | bash ``` -Choose Simplified Chinese, English, or Japanese, then a download source. The mainland China mirrors, official sources, and custom sources work as described in the Windows section below. Piped installations read menu input from the terminal. Without an available terminal, menus are skipped and the defaults are the system language and official sources. - -For unattended installation: +Unattended installation: ```bash curl -fsSL https://docs.astrbot.app/install.sh -o install.sh -bash install.sh --language en-US --download-source china --non-interactive +bash install.sh --language en-US --download-source official --non-interactive ``` -Use `--download-source custom --uv-download-base-url --pypi-index-url ` for custom sources. The directory must contain `uv-.tar.gz` and its matching `.sha256` file, using the official uv archive layout. Set `--python-install-mirror ` for a separate Python interpreter mirror, or use the existing `UV_PYTHON_INSTALL_MIRROR` environment variable. +`--download-source` accepts `china`, `official`, or `custom`. If omitted, the system language and official sources are used. -The script detects the macOS/Linux architecture and prefers static musl builds on Linux. Unavailable mirrors or checksum failures trigger a backup mirror and then the official installer. Downloads require `curl` or `wget`; mirror installation also requires `tar` and `sha256sum` or `shasum`. The uv directory is added to PATH for the current installation process, and mirror installations display the directory to add to PATH in new terminals. +## Windows -### Windows Installation Wizard +Run these commands in PowerShell 5.1 or PowerShell 7+. -Run the following in **Windows PowerShell 5.1 or PowerShell 7+**: +Interactive installation: ```powershell irm https://docs.astrbot.app/install.ps1 | iex ``` -The wizard offers Simplified Chinese, English, and Japanese, followed by a download source selection: - -- **Mainland China accelerated download mirrors**: download uv from university mirrors listed by MirrorZ and install AstrBot and its Python package dependencies through `https://mirrors.cernet.edu.cn/pypi/web/simple/`. If a uv download or SHA256 verification fails, the installer tries the backup mirror and then the official installer. -- **Official sources**: use the official uv installer and PyPI. -- **Custom**: specify a uv ZIP download directory and a PyPI simple index. The uv directory must contain `uv--pc-windows-msvc.zip` and the matching `.sha256` file. - -In interactive mode, the system language determines the initial language selection. The Chinese interface defaults to the mainland China mirrors; English and Japanese default to official sources. Both selections can be changed. - -For unattended installation, download the script and pass explicit options: +Unattended installation: ```powershell Invoke-WebRequest https://docs.astrbot.app/install.ps1 -OutFile install.ps1 -UseBasicParsing -.\install.ps1 -Language en-US -DownloadSource china -NonInteractive +.\install.ps1 -Language en-US -DownloadSource official -NonInteractive ``` -`-Language` accepts `zh-CN`, `en-US`, or `ja-JP`; `-DownloadSource` accepts `china`, `official`, or `custom`. Unattended runs and redirected input skip all menus. Omitted options default to the system language and official sources. - -For custom sources, replace these example HTTPS URLs with your own: +`-DownloadSource` accepts `china`, `official`, or `custom`. If omitted, the system language and official sources are used. -```powershell -.\install.ps1 -Language en-US -DownloadSource custom -NonInteractive ` - -UvDownloadBaseUrl 'https://mirror.example.com/uv/release' ` - -PypiIndexUrl 'https://mirror.example.com/pypi/simple/' -``` +## Start AstrBot -PyPI mirrors do not include Python interpreters. When Python 3.12 is missing, its download uses the existing uv configuration. Pass `-PythonInstallMirror ` to specify a Python download mirror compatible with uv, or set `UV_PYTHON_INSTALL_MIRROR` beforehand. The script does not change global pip settings; a Python mirror passed as an option applies only during this installation. - -### Manual Installation and Startup +Initialize the directory once, then start AstrBot: ```bash -uv tool install astrbot --python 3.12 -astrbot +astrbot init +astrbot run ``` + +> AstrBot installed by script does not support upgrades through the WebUI. Run `uv tool upgrade astrbot --python 3.12` to update it. diff --git a/docs/zh/deploy/astrbot/launcher.md b/docs/zh/deploy/astrbot/launcher.md index 588b3543d38..6251e403ca2 100644 --- a/docs/zh/deploy/astrbot/launcher.md +++ b/docs/zh/deploy/astrbot/launcher.md @@ -37,7 +37,7 @@ MacOS 用户下载安装好后,可能会遇到 "已损坏,无法打开" 的 ### 运行安装器 -> 视频和此处不一致,请参考此处!!!如果部署不了,请参阅其他两个部署方式:Docker 部署和 手动部署。 +> 视频和此处不一致,请参考此处!如果部署不了,请参阅 Docker 部署或源码部署。 解压后,打开文件夹, @@ -73,7 +73,7 @@ MacOS 用户下载安装好后,可能会遇到 "已损坏,无法打开" 的 > [!TIP] -> 如果部署不了,请参阅其他两个部署方式:Docker 部署和 手动部署。 +> 如果部署不了,请参阅 Docker 部署或源码部署。 ## 报错:Python is not installed diff --git a/docs/zh/deploy/astrbot/package.md b/docs/zh/deploy/astrbot/package.md index 53a3322998c..f664dd40805 100644 --- a/docs/zh/deploy/astrbot/package.md +++ b/docs/zh/deploy/astrbot/package.md @@ -1,82 +1,50 @@ -# 包管理器部署(uv) +# 脚本部署 -使用 `uv` 可以快速安装并启动 AstrBot。 +AstrBot 提供 Linux、macOS、WSL 和 Windows 安装脚本。脚本会安装 `uv`、AstrBot 及 Python 3.12。 -## 前置条件 +## Linux / macOS / WSL -如果尚未安装 `uv`,请先按照官方文档安装: - -`uv` 支持 Linux、Windows、macOS。 - -## 注意事项 - -> [!WARNING] -> 通过 `uv` 部署的 AstrBot **不支持在 WebUI 中进行版本升级**。如需更新,请在命令行中执行 `uv tool upgrade astrbot --python 3.12`。 - -AstrBot 需要 Python 3.12 或更高版本。使用 `--python 3.12` 可以确保 `uv` 使用 Python 3.12 创建 tool 环境;如果启用了 Python 自动下载,`uv` 会在缺少 Python 3.12 时自动下载。 - -## 安装并启动 - -### Linux / macOS / WSL 安装向导 - -使用 **Bash 3.2 或更高版本**运行: +交互式安装: ```bash curl -fsSL https://docs.astrbot.app/install.sh | bash ``` -向导同样先选择简体中文、English、日本語,再选择下载源;可选的中国大陆加速下载镜像源、官方源、自定义源与下方 Windows 向导一致。即使通过管道运行,菜单也会从终端读取输入;没有可用终端时跳过菜单,默认使用系统语言和官方源。 - -无人值守安装示例: +无人值守安装: ```bash curl -fsSL https://docs.astrbot.app/install.sh -o install.sh bash install.sh --language zh-CN --download-source china --non-interactive ``` -自定义源使用 `--download-source custom --uv-download-base-url --pypi-index-url `。目录中需包含当前平台的 `uv-<平台>.tar.gz` 及同名 `.sha256` 文件;压缩包采用 uv 官方发布的目录结构。可通过 `--python-install-mirror ` 单独配置 Python 解释器镜像,也支持已有的 `UV_PYTHON_INSTALL_MIRROR`。 +`--download-source` 可选 `china`、`official` 或 `custom`。省略参数时使用系统语言和官方源。 -脚本自动匹配 macOS/Linux 架构,在 Linux 上优先使用静态 musl 构建;镜像不可用或校验失败时尝试备用镜像和官方安装器。需要 `curl` 或 `wget`;镜像安装还需要 `tar` 和 `sha256sum` 或 `shasum`。uv 安装目录会加入当前安装进程的 PATH,镜像安装后也会提示新终端需要使用的目录。 +## Windows -### Windows 安装向导 +在 PowerShell 5.1 或 PowerShell 7+ 中运行: -在 **Windows PowerShell 5.1 或 PowerShell 7+**中运行: +交互式安装: ```powershell irm https://docs.astrbot.app/install.ps1 | iex ``` -向导首先提供简体中文、English、日本語三种显示语言,然后选择下载源: - -- **中国大陆加速下载镜像源**:从 MirrorZ 收录的高校镜像下载 uv,并使用 `https://mirrors.cernet.edu.cn/pypi/web/simple/` 安装 AstrBot 及其 Python 包依赖。uv 镜像下载或 SHA256 校验失败时,依次尝试备用镜像和官方安装器。 -- **官方源**:使用 uv 官方安装器和官方 PyPI。 -- **自定义**:指定 uv ZIP 下载目录和 PyPI simple 索引。uv 目录需包含对应架构的 `uv-<架构>-pc-windows-msvc.zip` 及同名 `.sha256` 文件。 - -交互模式根据系统语言预选显示语言;中文界面默认选择中国大陆加速下载镜像源,英文和日文界面默认选择官方源,均可手动切换。 - -也可下载脚本后通过参数跳过交互: +无人值守安装: ```powershell Invoke-WebRequest https://docs.astrbot.app/install.ps1 -OutFile install.ps1 -UseBasicParsing .\install.ps1 -Language zh-CN -DownloadSource china -NonInteractive ``` -`-Language` 支持 `zh-CN`、`en-US`、`ja-JP`;`-DownloadSource` 支持 `china`、`official`、`custom`。无人值守或输入被重定向时,不显示交互菜单;未指定语言时使用系统语言,未指定下载源时使用官方源。 - -自定义源示例(请替换为自己的 HTTPS 地址): +`-DownloadSource` 可选 `china`、`official` 或 `custom`;省略参数时使用系统语言和官方源。 -```powershell -.\install.ps1 -Language zh-CN -DownloadSource custom -NonInteractive ` - -UvDownloadBaseUrl 'https://mirror.example.com/uv/release' ` - -PypiIndexUrl 'https://mirror.example.com/pypi/simple/' -``` +## 启动 AstrBot -PyPI 镜像不包含 Python 解释器。缺少 Python 3.12 时,解释器下载沿用 uv 的现有配置;可通过 `-PythonInstallMirror ` 单独指定兼容 uv 的 Python 下载镜像,或提前设置 `UV_PYTHON_INSTALL_MIRROR`。脚本不修改全局 pip 配置,参数指定的 Python 镜像只在本次安装期间生效。 - -### 手动安装并启动 +首次使用时初始化目录,之后直接启动: ```bash -uv tool install astrbot --python 3.12 -astrbot init # 只需要在第一次部署时执行,后续启动不需要执行 +astrbot init astrbot run ``` + +> 通过脚本部署的 AstrBot 不支持在 WebUI 中升级。更新请执行 `uv tool upgrade astrbot --python 3.12`。 diff --git a/docs/zh/others/diagnostics.md b/docs/zh/others/diagnostics.md index 92082db63c4..67afcfe5561 100644 --- a/docs/zh/others/diagnostics.md +++ b/docs/zh/others/diagnostics.md @@ -74,7 +74,7 @@ data/logs/event_loop_watchdog.log 提交问题时,请尽量提供以下信息: - 问题发生的大致时间点和时区。 -- AstrBot 版本、部署方式(Docker、手动部署、桌面客户端等)、操作系统。 +- AstrBot 版本、部署方式(Docker、源码部署、桌面客户端等)、操作系统。 - 触发方式:启动、普通聊天、群聊、平台回调、定时任务、MCP 工具、插件功能等。 - 影响范围:所有会话、某个平台、某个群、某个用户,还是某个插件。 - `data/logs/astrbot.log` 中问题发生前后 1 到 3 分钟的日志。 From d7c66c8239cb31f4b60e8a08d09a73483d8ec5c7 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 21:03:56 +0800 Subject: [PATCH 22/28] fix(install): align Bash wizard with PowerShell flow --- .github/workflows/install_script_test.yml | 23 +- docs/en/deploy/astrbot/package.md | 6 +- docs/public/install.sh | 241 ++++++++----- docs/zh/deploy/astrbot/package.md | 6 +- tests/test_install_script.py | 394 ++++++++++++++++++++++ 5 files changed, 573 insertions(+), 97 deletions(-) create mode 100644 tests/test_install_script.py diff --git a/.github/workflows/install_script_test.yml b/.github/workflows/install_script_test.yml index bd3c8f7dea3..37d9468992c 100644 --- a/.github/workflows/install_script_test.yml +++ b/.github/workflows/install_script_test.yml @@ -1,4 +1,4 @@ -name: Test Windows install script +name: Test install scripts on: push: @@ -6,11 +6,15 @@ on: - master paths: - 'docs/public/install.ps1' + - 'docs/public/install.sh' + - 'tests/test_install_script.py' - 'tests/install_script.Tests.ps1' - '.github/workflows/install_script_test.yml' pull_request: paths: - 'docs/public/install.ps1' + - 'docs/public/install.sh' + - 'tests/test_install_script.py' - 'tests/install_script.Tests.ps1' - '.github/workflows/install_script_test.yml' workflow_dispatch: @@ -31,3 +35,20 @@ jobs: - name: Test PowerShell 7 shell: pwsh run: .\tests\install_script.Tests.ps1 + + shell: + name: Bash installer (${{ matrix.os }}) + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Test Bash installer + shell: bash + run: | + /bin/bash -n docs/public/install.sh + python3 tests/test_install_script.py diff --git a/docs/en/deploy/astrbot/package.md b/docs/en/deploy/astrbot/package.md index cace154cb35..1bc31ffe989 100644 --- a/docs/en/deploy/astrbot/package.md +++ b/docs/en/deploy/astrbot/package.md @@ -17,7 +17,7 @@ curl -fsSL https://docs.astrbot.app/install.sh -o install.sh bash install.sh --language en-US --download-source official --non-interactive ``` -`--download-source` accepts `china`, `official`, or `custom`. If omitted, the system language and official sources are used. +`--download-source` accepts `china`, `official`, or `custom`. The default language is English. If the source is omitted, Chinese uses the mainland China mirrors; English and Japanese use official sources. ## Windows @@ -36,7 +36,9 @@ Invoke-WebRequest https://docs.astrbot.app/install.ps1 -OutFile install.ps1 -Use .\install.ps1 -Language en-US -DownloadSource official -NonInteractive ``` -`-DownloadSource` accepts `china`, `official`, or `custom`. If omitted, the system language and official sources are used. +`-DownloadSource` accepts `china`, `official`, or `custom`. The default language is English. If the source is omitted, Chinese uses the mainland China mirrors; English and Japanese use official sources. + +The mainland China option also configures the USTC mirror for Python 3.12 and retries with the official Python source on failure. Existing Python mirror environment variables or explicit options take priority. ## Start AstrBot diff --git a/docs/public/install.sh b/docs/public/install.sh index c677768cd67..c58387367b0 100755 --- a/docs/public/install.sh +++ b/docs/public/install.sh @@ -35,7 +35,7 @@ Usage: bash install.sh [options] --uv-download-base-url HTTPS-URL Directory containing uv archives and .sha256 files --pypi-index-url HTTPS-URL PyPI simple index for custom sources --python-install-mirror HTTPS-URL Optional Python interpreter download mirror - --non-interactive Skip all menus (defaults: system language, official sources) + --non-interactive Skip all menus (defaults: English; source follows language) HELP exit 0 ;; @@ -49,14 +49,17 @@ if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then YELLOW=$'\033[1;33m' CYAN=$'\033[0;36m' NC=$'\033[0m' + GRAY=$'\033[0;90m' else - RED=''; GREEN=''; YELLOW=''; CYAN=''; NC='' + RED=''; GREEN=''; YELLOW=''; CYAN=''; NC=''; GRAY='' fi -info() { printf '%s[INFO]%s %s\n' "$CYAN" "$NC" "$*"; } -warn() { printf '%s[WARN]%s %s\n' "$YELLOW" "$NC" "$*"; } -ok() { printf '%s[OK]%s %s\n' "$GREEN" "$NC" "$*"; } -err() { printf '%s[ERROR]%s %s\n' "$RED" "$NC" "$*" >&2; } +indent=' ' + +info() { printf '%s%s[INFO] %s%s\n' "$indent" "$CYAN" "$*" "$NC"; } +warn() { printf '%s%s[WARN] %s%s\n' "$indent" "$YELLOW" "$*" "$NC"; } +ok() { printf '%s%s[OK] %s%s\n' "$indent" "$GREEN" "$*" "$NC"; } +err() { printf '%s%s[ERROR] %s%s\n' "$indent" "$RED" "$*" "$NC" >&2; } has() { command -v "$1" >/dev/null 2>&1; } # Read menus from the terminal, never from the script stream in curl | bash. @@ -71,19 +74,18 @@ if [ "$non_interactive" -eq 0 ]; then fi if [ -z "$language" ]; then - case "${LC_ALL:-${LC_MESSAGES:-${LANG:-en_US}}}" in - zh*) language='zh-CN'; default_choice=1 ;; - ja*) language='ja-JP'; default_choice=3 ;; - *) language='en-US'; default_choice=2 ;; - esac + # Match the PowerShell wizard regardless of the system locale. + language='en-US' + default_choice=1 if [ "$can_prompt" -eq 1 ]; then - printf '\n请选择语言 / Select language / 言語を選択してください\n 1. 简体中文\n 2. English\n 3. 日本語\n' >&2 + printf '\n%s%s🌌 AstrBot Installer%s\n%s%s-----------------------%s\n' "$indent" "$CYAN" "$NC" "$indent" "$GRAY" "$NC" >&2 + printf '\n%sSelect language / 请选择语言 / 言語を選択してください\n%s 1. English\n%s 2. 简体中文\n%s 3. 日本語\n' "$indent" "$indent" "$indent" "$indent" >&2 while :; do - printf '1-3 [%s]: ' "$default_choice" >&2 + printf '%s1-3 [%s]: ' "$indent" "$default_choice" >&2 IFS= read -r choice <&3 || exit 1 case "${choice:-$default_choice}" in - 1) language='zh-CN'; break ;; - 2) language='en-US'; break ;; + 1) language='en-US'; break ;; + 2) language='zh-CN'; break ;; 3) language='ja-JP'; break ;; esac done @@ -116,15 +118,16 @@ message() { zh-CN:InstallingUv) format='未找到 uv,正在安装…' ;; zh-CN:TryingMirror) format='正在尝试 uv 镜像:%s' ;; zh-CN:MirrorFailed) format='uv 镜像下载失败:%s' ;; - zh-CN:OfficialFallback) format='正在尝试 uv 官方安装器…' ;; - zh-CN:UnsupportedPlatform) format='当前平台无法使用预设 uv 镜像,将尝试官方安装器:%s' ;; + zh-CN:UnsupportedPlatform) format='不支持的平台或架构:%s' ;; zh-CN:InvalidChecksum) format='uv 压缩包 SHA256 校验失败。' ;; zh-CN:MissingBinaries) format='uv 压缩包中缺少 uv 或 uvx。' ;; - zh-CN:MissingTools) format='需要 curl 或 wget 下载文件;镜像安装还需要 tar 和 sha256sum 或 shasum。' ;; + zh-CN:MissingTools) format='需要 curl 或 wget 下载文件;安装压缩包还需要 tar 和 sha256sum 或 shasum。' ;; zh-CN:PathHint) format='uv 安装目录:%s。请确保新终端的 PATH 中包含此目录。' ;; zh-CN:UvFailed) format='uv 安装失败。请参考 https://docs.astral.sh/uv/getting-started/installation/' ;; zh-CN:InstallingAstrBot) format='正在使用 Python 3.12 安装 AstrBot…' ;; zh-CN:AstrBotFailed) format='AstrBot 安装失败(退出码 %s)。请检查上方的下载或依赖错误。' ;; + zh-CN:PythonFallback) format='Python 镜像下载失败,正在切换到官方源重试…' ;; + zh-CN:NextSteps) format='接下来可以依次执行以下指令来初始化和启动 AstrBot:' ;; zh-CN:Installed) format='AstrBot 安装完成。' ;; en-US:ChooseSource) format='Select a download source' ;; en-US:China) format='Mainland China accelerated download mirrors' ;; @@ -144,15 +147,16 @@ message() { en-US:InstallingUv) format='uv was not found. Installing uv...' ;; en-US:TryingMirror) format='Trying uv mirror: %s' ;; en-US:MirrorFailed) format='uv mirror download failed: %s' ;; - en-US:OfficialFallback) format='Trying the official uv installer...' ;; - en-US:UnsupportedPlatform) format='No preset uv mirror for this platform; trying the official installer: %s' ;; + en-US:UnsupportedPlatform) format='Unsupported platform or architecture: %s' ;; en-US:InvalidChecksum) format='uv archive SHA256 verification failed.' ;; en-US:MissingBinaries) format='The uv archive is missing uv or uvx.' ;; - en-US:MissingTools) format='curl or wget is required to download files; mirror installation also requires tar and sha256sum or shasum.' ;; + en-US:MissingTools) format='curl or wget is required to download files; archive installation also requires tar and sha256sum or shasum.' ;; en-US:PathHint) format='uv installation directory: %s. Ensure it is on PATH in new terminals.' ;; en-US:UvFailed) format='Failed to install uv. See https://docs.astral.sh/uv/getting-started/installation/' ;; en-US:InstallingAstrBot) format='Installing AstrBot with Python 3.12...' ;; en-US:AstrBotFailed) format='AstrBot installation failed (exit code %s). Check the download or dependency errors above.' ;; + en-US:PythonFallback) format='Python mirror failed; retrying with the official source...' ;; + en-US:NextSteps) format='Next, run these commands:' ;; en-US:Installed) format='AstrBot has been installed.' ;; ja-JP:ChooseSource) format='ダウンロード元を選択してください' ;; ja-JP:China) format='中国本土向け高速ダウンロードミラー' ;; @@ -172,15 +176,16 @@ message() { ja-JP:InstallingUv) format='uv が見つかりません。インストールしています…' ;; ja-JP:TryingMirror) format='uv ミラーを試しています:%s' ;; ja-JP:MirrorFailed) format='uv ミラーからのダウンロードに失敗しました:%s' ;; - ja-JP:OfficialFallback) format='uv の公式インストーラーを試しています…' ;; - ja-JP:UnsupportedPlatform) format='この環境に対応する uv ミラーがないため、公式インストーラーを試します:%s' ;; + ja-JP:UnsupportedPlatform) format='未対応のプラットフォームまたはアーキテクチャ:%s' ;; ja-JP:InvalidChecksum) format='uv アーカイブの SHA256 検証に失敗しました。' ;; ja-JP:MissingBinaries) format='uv アーカイブに uv または uvx がありません。' ;; - ja-JP:MissingTools) format='ダウンロードには curl または wget が必要です。ミラーからのインストールには tar と sha256sum または shasum も必要です。' ;; + ja-JP:MissingTools) format='ダウンロードには curl または wget が必要です。アーカイブのインストールには tar と sha256sum または shasum も必要です。' ;; ja-JP:PathHint) format='uv のインストール先:%s。新しいターミナルでも PATH に含まれていることを確認してください。' ;; ja-JP:UvFailed) format='uv のインストールに失敗しました。https://docs.astral.sh/uv/getting-started/installation/ を参照してください。' ;; ja-JP:InstallingAstrBot) format='Python 3.12 で AstrBot をインストールしています…' ;; ja-JP:AstrBotFailed) format='AstrBot のインストールに失敗しました(終了コード %s)。上記のダウンロードまたは依存関係のエラーを確認してください。' ;; + ja-JP:PythonFallback) format='Python ミラーに失敗したため、公式ソースで再試行しています…' ;; + ja-JP:NextSteps) format='次に、以下を順番に実行してください:' ;; ja-JP:Installed) format='AstrBot のインストールが完了しました。' ;; *) printf 'Unknown message: %s\n' "$key" >&2; return 1 ;; esac @@ -189,13 +194,15 @@ message() { if [ -z "$download_source" ]; then download_source='official' + if [ "$language" = 'zh-CN' ]; then download_source='china'; fi if [ "$can_prompt" -eq 1 ]; then default_choice=2 if [ "$language" = 'zh-CN' ]; then default_choice=1; fi - printf '\n%s\n 1. %s\n 2. %s\n 3. %s\n' \ - "$(message ChooseSource)" "$(message China)" "$(message Official)" "$(message Custom)" >&2 + printf '\n%s%s\n%s 1. %s\n%s 2. %s\n%s 3. %s\n' \ + "$indent" "$(message ChooseSource)" "$indent" "$(message China)" \ + "$indent" "$(message Official)" "$indent" "$(message Custom)" >&2 while :; do - printf '%s [%s]: ' "$(message Choice)" "$default_choice" >&2 + printf '%s%s [%s]: ' "$indent" "$(message Choice)" "$default_choice" >&2 IFS= read -r choice <&3 || exit 1 case "${choice:-$default_choice}" in 1) download_source='china'; break ;; @@ -213,7 +220,6 @@ case "$download_source" in source_label="$(message China)" uv_mirror_urls=( 'https://mirrors.ustc.edu.cn/github-release/astral-sh/uv/LatestRelease' - 'https://mirror.nyist.edu.cn/github-release/astral-sh/uv/LatestRelease' ) pypi_index_url='https://mirrors.cernet.edu.cn/pypi/web/simple/' ;; @@ -225,11 +231,11 @@ case "$download_source" in source_label="$(message Custom)" if [ "$can_prompt" -eq 1 ]; then if [ -z "$uv_download_base_url" ]; then - printf '%s: ' "$(message UvUrl)" >&2 + printf '%s%s: ' "$indent" "$(message UvUrl)" >&2 IFS= read -r uv_download_base_url <&3 || exit 1 fi if [ -z "$pypi_index_url" ]; then - printf '%s: ' "$(message PypiUrl)" >&2 + printf '%s%s: ' "$indent" "$(message PypiUrl)" >&2 IFS= read -r pypi_index_url <&3 || exit 1 fi fi @@ -241,6 +247,13 @@ case "$download_source" in *) err "Unsupported download source: $download_source"; exit 2 ;; esac +if [ -z "$python_install_mirror" ]; then + python_install_mirror="${UV_PYTHON_INSTALL_MIRROR:-}" + if [ -z "$python_install_mirror" ] && [ "$download_source" = 'china' ]; then + python_install_mirror='https://mirrors.ustc.edu.cn/github-release/astral-sh/python-build-standalone/' + fi +fi + # Bash 3.2 treats empty arrays as unset with nounset; use the guarded expansion. for url in ${uv_mirror_urls[@]+"${uv_mirror_urls[@]}"} "$pypi_index_url" "$python_install_mirror"; do [ -n "$url" ] || continue @@ -294,63 +307,93 @@ install_uv() { Linux:riscv64) target='riscv64gc-unknown-linux-musl' ;; esac - if [ -n "${uv_mirror_urls[*]:-}" ]; then - if [ -z "$target" ]; then - warn "$(message UnsupportedPlatform "$platform/$machine")" - elif ! has tar || { ! has sha256sum && ! has shasum; }; then - warn "$(message MissingTools)" + if [ -z "$target" ]; then + err "$(message UnsupportedPlatform "$platform/$machine")" + exit 1 + fi + if ! has tar || { ! has sha256sum && ! has shasum; }; then + err "$(message MissingTools)" + exit 1 + fi + + artifact="uv-$target.tar.gz" + archive="$install_temp_dir/$artifact" + local official_base_url='https://github.com/astral-sh/uv/releases/latest/download' + for base_url in ${uv_mirror_urls[@]+"${uv_mirror_urls[@]}"} "$official_base_url"; do + if [ "$base_url" = "$official_base_url" ]; then + info "$(message Official): $official_base_url" else - artifact="uv-$target.tar.gz" - archive="$install_temp_dir/$artifact" - for base_url in "${uv_mirror_urls[@]}"; do - info "$(message TryingMirror "$base_url")" - if ! download "$base_url/$artifact" "$archive" || \ - ! download "$base_url/$artifact.sha256" "$install_temp_dir/checksum"; then - warn "$(message MirrorFailed "$base_url")" - continue - fi - expected_hash='' - read -r expected_hash _ < "$install_temp_dir/checksum" || true - if has sha256sum; then - actual_hash="$(sha256sum "$archive")" + info "$(message TryingMirror "$base_url")" + fi + if ! download "$base_url/$artifact" "$archive"; then + warn "$(message MirrorFailed "$base_url")" + continue + fi + + # USTC may return a cookie verification page instead of a release archive. + # Parse only the address cookie; never execute JavaScript from the response. + local signature address cookie + signature="$(od -An -tx1 -N2 "$archive" | tr -d ' \n')" + if [ "$signature" != '1f8b' ] && [[ "$base_url" == https://mirrors.ustc.edu.cn/* ]]; then + address="$(head -c 65536 "$archive" | sed -n 's/.*document\.cookie[[:space:]]*=[[:space:]]*"addr=\([0-9A-Fa-f:.]*\);.*/\1/p' | head -n 1)" + if [ -n "$address" ]; then + cookie="addr=$address" + if has curl; then + if ! curl -fsSL --connect-timeout 15 --max-time 90 -b "$cookie" -o "$archive" -- "$base_url/$artifact"; then + warn "$(message MirrorFailed "$base_url")" + continue + fi else - actual_hash="$(shasum -a 256 "$archive")" - fi - actual_hash="${actual_hash%% *}" - if ! [[ "$expected_hash" =~ ^[a-fA-F0-9]{64}$ ]] || \ - [ "$(printf '%s' "$expected_hash" | tr 'A-F' 'a-f')" != "$actual_hash" ]; then - warn "$(message InvalidChecksum)" - continue - fi - rm -rf -- "$install_temp_dir/unpacked" - mkdir -p "$install_temp_dir/unpacked" - if ! tar -xzf "$archive" -C "$install_temp_dir/unpacked" --strip-components=1; then - warn "$(message MirrorFailed "$base_url")" - continue - fi - if [ ! -f "$install_temp_dir/unpacked/uv" ] || [ ! -f "$install_temp_dir/unpacked/uvx" ]; then - warn "$(message MissingBinaries)" - continue + if ! wget -q -T 30 --header="Cookie: $cookie" -O "$archive" -- "$base_url/$artifact"; then + warn "$(message MirrorFailed "$base_url")" + continue + fi fi - install_dir="${UV_INSTALL_DIR:-${UV_UNMANAGED_INSTALL:-${XDG_BIN_HOME:-${XDG_DATA_HOME:+$XDG_DATA_HOME/../bin}}}}" - install_dir="${install_dir:-$HOME/.local/bin}" - mkdir -p "$install_dir" - cp "$install_temp_dir/unpacked/uv" "$install_temp_dir/unpacked/uvx" "$install_dir/" - chmod +x "$install_dir/uv" "$install_dir/uvx" - refresh_uv_path - info "$(message PathHint "$install_dir")" - return - done + fi + signature="$(od -An -tx1 -N2 "$archive" | tr -d ' \n')" fi - fi - - info "$(message OfficialFallback)" - if ! download 'https://astral.sh/uv/install.sh' "$install_temp_dir/uv-install.sh" || \ - ! sh "$install_temp_dir/uv-install.sh" "$TEST_DIR/uv-args" +printf '%s\\n' "${UV_PYTHON_INSTALL_MIRROR:-}" >> "$TEST_DIR/python-mirrors" +if [ "${TEST_FAIL_ONCE:-0}" = 1 ] && [ ! -f "$TEST_DIR/failed-once" ]; then + touch "$TEST_DIR/failed-once" + exit 1 +fi +exit "${TEST_INSTALL_EXIT:-0}" +""" + +# BASH_ENV supplies only fixture commands; uv itself is a real fixture executable. +MOCK_ENV = r""" +command() { + if [ "$1" = -v ]; then + case "$2" in + uv) [ -f "$UV_UNMANAGED_INSTALL/uv" ]; return $? ;; + curl) if [ "${TEST_WGET:-0}" = 1 ]; then return 1; fi ;; + esac + fi + builtin command "$@" +} +uv() { "$UV_UNMANAGED_INSTALL/uv" "$@"; } +uname() { + case "$1" in + -s) printf '%s\n' "${TEST_PLATFORM:-Darwin}" ;; + -m) printf '%s\n' "${TEST_ARCH:-arm64}" ;; + *) /usr/bin/uname "$@" ;; + esac +} +mock_download() { + local url="$1" out="$2" cookie="$3" + printf '%s\n' "$url" >> "$TEST_DIR/requests" + if [ "${TEST_FAIL_ALL:-0}" = 1 ]; then return 22; fi + if [[ "$url" == https://mirrors.ustc.edu.cn/* ]]; then + if [ "${TEST_FAIL_PRIMARY:-0}" = 1 ]; then return 22; fi + if [ "${TEST_COOKIE:-0}" = 1 ] && [[ "$url" == *.tar.gz ]]; then + if [ "$cookie" != 'addr=192.0.2.1' ]; then + printf '' > "$out" + return + fi + printf '%s\n' "$cookie" > "$TEST_DIR/cookie" + fi + fi + case "$url" in + *.sha256) + if [ "${TEST_BAD_HASH:-0}" = 1 ] && [[ "$url" == https://mirrors.ustc.edu.cn/* ]]; then + printf '%064d fixture.tar.gz\n' 0 > "$out" + else cp "$TEST_DIR/checksum" "$out"; fi + ;; + *.tar.gz) cp "$TEST_DIR/fixture.tar.gz" "$out" ;; + *) printf 'Unexpected URL %s\n' "$url" >&2; return 1 ;; + esac +} +curl() { + local out='' url='' cookie='' + while [ "$#" -gt 0 ]; do + case "$1" in + -o) out="$2"; shift 2 ;; + -b) cookie="$2"; shift 2 ;; + --connect-timeout|--max-time) shift 2 ;; + --) shift; url="$1"; shift ;; + -*) shift ;; + *) url="$1"; shift ;; + esac + done + mock_download "$url" "$out" "$cookie" +} +wget() { + local out='' url='' cookie='' + while [ "$#" -gt 0 ]; do + case "$1" in + -O) out="$2"; shift 2 ;; + --header=*) cookie="${1#--header=Cookie: }"; shift ;; + -T|-t) shift 2 ;; + --) shift; url="$1"; shift ;; + -*) shift ;; + *) url="$1"; shift ;; + esac + done + mock_download "$url" "$out" "$cookie" +} +""" + + +@unittest.skipUnless(pty is not None, "Bash installer tests require a POSIX terminal") +class ShellInstallerTests(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory(prefix="astrbot-shell-test-") + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + (self.root / "fake-uv").write_bytes(FAKE_UV) + (self.root / "fake-uv").chmod(0o755) + (self.root / "env.sh").write_text(MOCK_ENV) + with tarfile.open(self.root / "fixture.tar.gz", "w:gz") as archive: + for name in ("uv", "uvx"): + member = tarfile.TarInfo(f"uv-fixture/{name}") + member.mode = 0o755 + member.size = len(FAKE_UV) + archive.addfile(member, io.BytesIO(FAKE_UV)) + checksum = hashlib.sha256( + (self.root / "fixture.tar.gz").read_bytes() + ).hexdigest() + (self.root / "checksum").write_text(f"{checksum} fixture.tar.gz\n") + self.env = os.environ.copy() + for key in ( + "UV_INSTALL_DIR", + "UV_PYTHON_INSTALL_MIRROR", + "XDG_BIN_HOME", + "XDG_DATA_HOME", + ): + self.env.pop(key, None) + self.env.update( + BASH_ENV=str(self.root / "env.sh"), + TEST_DIR=str(self.root), + UV_UNMANAGED_INSTALL=str(self.root / "bin"), + NO_COLOR="1", + TMPDIR=str(self.root), + ) + + def run_installer(self, args, *, existing=True, answers=None, piped=False): + if existing: + (self.root / "bin").mkdir(exist_ok=True) + shutil.copy2(self.root / "fake-uv", self.root / "bin/uv") + command = ["/bin/bash"] + (["-s", "--"] if piped else [str(SCRIPT)]) + args + if answers is None: + result = subprocess.run( + command, + input=SCRIPT.read_bytes() if piped else b"", + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + env=self.env, + timeout=15, + start_new_session=True, + ) + status, output = result.returncode, result.stdout.decode() + else: + # fork() attaches the child to a controlling terminal so /dev/tty + # still supplies menu input when stdin contains the script itself. + script_data = SCRIPT.read_bytes() if piped else b"" + if piped: + read_fd, write_fd = os.pipe() + pid, terminal = pty.fork() + if pid == 0: + if piped: + os.close(write_fd) + os.dup2(read_fd, 0) + os.close(read_fd) + os.execve(command[0], command, self.env) + if piped: + os.close(read_fd) + os.set_blocking(write_fd, False) + output_bytes = bytearray() + pending = answers.encode() + deadline = time.monotonic() + 15 + try: + while time.monotonic() < deadline: + readable, writable, _ = select.select( + [terminal], [write_fd] if script_data else [], [], 0.1 + ) + if writable: + written = os.write(write_fd, script_data) + script_data = script_data[written:] + if not script_data: + os.close(write_fd) + if readable: + try: + chunk = os.read(terminal, 65536) + except OSError: + break + if not chunk: + break + output_bytes.extend(chunk) + if pending and b"1-3 [1]:" in output_bytes: + os.write(terminal, pending) + pending = b"" + else: + os.kill(pid, 9) + self.fail("Interactive installer timed out") + finally: + os.close(terminal) + if script_data: + os.close(write_fd) + _, wait_status = os.waitpid(pid, 0) + status = os.WEXITSTATUS(wait_status) + output = output_bytes.decode().replace("\r\n", "\n") + self.assertEqual(list(self.root.glob("astrbot-install.*")), []) + return status, output + + def assert_install(self, index, mirrors): + self.assertEqual( + (self.root / "uv-args").read_text().splitlines(), + [ + "tool", + "install", + "--python", + "3.12", + "astrbot", + "--default-index", + index, + ], + ) + self.assertEqual( + (self.root / "python-mirrors").read_text().splitlines(), mirrors + ) + + def test_defaults_ignore_system_locale(self): + self.env["LANG"] = "zh_CN.UTF-8" + status, output = self.run_installer(["--non-interactive"]) + self.assertEqual(status, 0, output) + self.assert_install(OFFICIAL_INDEX, [""]) + self.assertIn(" [INFO] Download source: Official sources", output) + self.assertIn( + "\n Next, run these commands:\n astrbot init\n astrbot run\n", output + ) + + def test_chinese_defaults_to_all_china_mirrors(self): + status, output = self.run_installer( + ["--language", "zh-CN", "--non-interactive"] + ) + self.assertEqual(status, 0, output) + self.assert_install(CHINA_INDEX, [PYTHON_MIRROR]) + self.assertIn(" [INFO] 下载源:中国大陆加速下载镜像源", output) + + def test_explicit_official_source_overrides_chinese_default(self): + status, output = self.run_installer( + [ + "--language", + "zh-CN", + "--download-source", + "official", + "--non-interactive", + ] + ) + self.assertEqual(status, 0, output) + self.assert_install(OFFICIAL_INDEX, [""]) + + def test_explicit_python_mirror_overrides_environment(self): + self.env["UV_PYTHON_INSTALL_MIRROR"] = "https://env.example.com/python" + status, output = self.run_installer( + [ + "--download-source", + "china", + "--python-install-mirror", + "https://custom.example.com/python", + "--non-interactive", + ] + ) + self.assertEqual(status, 0, output) + self.assert_install(CHINA_INDEX, ["https://custom.example.com/python"]) + + def test_existing_python_mirror_overrides_china_default(self): + self.env["UV_PYTHON_INSTALL_MIRROR"] = "https://env.example.com/python" + status, output = self.run_installer( + ["--language", "zh-CN", "--non-interactive"] + ) + self.assertEqual(status, 0, output) + self.assert_install(CHINA_INDEX, ["https://env.example.com/python"]) + + def test_python_mirror_fallback_preserves_pypi(self): + self.env["TEST_FAIL_ONCE"] = "1" + status, output = self.run_installer( + ["--language", "zh-CN", "--non-interactive"] + ) + self.assertEqual(status, 0, output) + self.assert_install(CHINA_INDEX, [PYTHON_MIRROR, ""]) + self.assertIn("正在切换到官方源重试", output) + + def test_install_failure_propagates_after_retry(self): + self.env["TEST_INSTALL_EXIT"] = "9" + status, output = self.run_installer( + ["--language", "zh-CN", "--non-interactive"] + ) + self.assertEqual(status, 9, output) + self.assertNotIn("AstrBot 安装完成", output) + self.assertNotIn(" astrbot init", output) + self.assert_install(CHINA_INDEX, [PYTHON_MIRROR, ""]) + + def test_english_menu_and_empty_defaults_when_piped(self): + status, output = self.run_installer([], answers="\n\n", piped=True) + self.assertEqual(status, 0, output) + self.assertIn("\n 🌌 AstrBot Installer\n -----------------------", output) + self.assertIn(" 1. English\n 2. 简体中文\n 3. 日本語", output) + self.assertIn(" 1-3 [1]:", output) + self.assertIn(" 1. Mainland China accelerated download mirrors", output) + self.assertIn( + " Enter a number, or press Enter to use the default [2]:", output + ) + self.assert_install(OFFICIAL_INDEX, [""]) + + def test_chinese_menu_default(self): + status, output = self.run_installer([], answers="2\n\n", piped=True) + self.assertEqual(status, 0, output) + self.assertIn(" 输入编号,按回车使用默认选项 [1]:", output) + self.assert_install(CHINA_INDEX, [PYTHON_MIRROR]) + + def test_japanese_custom_menu(self): + status, output = self.run_installer( + [], + answers="3\n0\n3\nhttps://uv.example.com/releases\nhttps://pypi.example.com/simple/\n", + piped=True, + ) + self.assertEqual(status, 0, output) + self.assertIn( + " 番号を入力してください。Enter で既定値を使用します [2]:", output + ) + self.assertIn(" 次に、以下を順番に実行してください:", output) + self.assert_install("https://pypi.example.com/simple/", [""]) + + def test_official_archive_is_verified_without_installer_script(self): + status, output = self.run_installer(["--non-interactive"], existing=False) + self.assertEqual(status, 0, output) + requests = (self.root / "requests").read_text().splitlines() + base = "https://github.com/astral-sh/uv/releases/latest/download/uv-aarch64-apple-darwin.tar.gz" + self.assertEqual(requests, [base, base + ".sha256"]) + + def test_linux_archive_selection(self): + self.env.update(TEST_PLATFORM="Linux", TEST_ARCH="x86_64") + status, output = self.run_installer(["--non-interactive"], existing=False) + self.assertEqual(status, 0, output) + self.assertIn( + "uv-x86_64-unknown-linux-musl.tar.gz", (self.root / "requests").read_text() + ) + + def test_primary_outage_uses_official_archive(self): + self.env["TEST_FAIL_PRIMARY"] = "1" + status, output = self.run_installer( + ["--language", "zh-CN", "--non-interactive"], existing=False + ) + self.assertEqual(status, 0, output) + self.assertEqual(len((self.root / "requests").read_text().splitlines()), 3) + + def test_checksum_mismatch_uses_official_archive(self): + self.env["TEST_BAD_HASH"] = "1" + status, output = self.run_installer( + ["--language", "zh-CN", "--non-interactive"], existing=False + ) + self.assertEqual(status, 0, output) + self.assertIn("SHA256", output) + self.assertEqual(len((self.root / "requests").read_text().splitlines()), 4) + + def test_cookie_challenge_curl(self): + self.env["TEST_COOKIE"] = "1" + status, output = self.run_installer( + ["--download-source", "china", "--non-interactive"], existing=False + ) + self.assertEqual(status, 0, output) + self.assertEqual((self.root / "cookie").read_text(), "addr=192.0.2.1\n") + self.assertEqual(len((self.root / "requests").read_text().splitlines()), 3) + + def test_cookie_challenge_wget(self): + self.env.update(TEST_COOKIE="1", TEST_WGET="1") + status, output = self.run_installer( + ["--download-source", "china", "--non-interactive"], existing=False + ) + self.assertEqual(status, 0, output) + self.assertEqual((self.root / "cookie").read_text(), "addr=192.0.2.1\n") + + def test_all_sources_fail_without_false_success(self): + self.env["TEST_FAIL_ALL"] = "1" + status, output = self.run_installer(["--non-interactive"], existing=False) + self.assertEqual(status, 1, output) + self.assertNotIn("AstrBot has been installed", output) + self.assertFalse((self.root / "uv-args").exists()) + + +if __name__ == "__main__": + unittest.main(verbosity=2) From 23cb404fc8a785bcdc242364efffac99b0431fb2 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 21:43:25 +0800 Subject: [PATCH 23/28] fix(install): refresh shell before manual startup --- docs/en/deploy/astrbot/package.md | 17 +++++++++++++++-- docs/public/install.ps1 | 21 ++++++++++++++++----- docs/public/install.sh | 19 ++++++++++++++----- docs/zh/deploy/astrbot/package.md | 17 +++++++++++++++-- tests/test_install_script.py | 14 ++++++++++++-- 5 files changed, 72 insertions(+), 16 deletions(-) diff --git a/docs/en/deploy/astrbot/package.md b/docs/en/deploy/astrbot/package.md index 1bc31ffe989..2e3e03e342e 100644 --- a/docs/en/deploy/astrbot/package.md +++ b/docs/en/deploy/astrbot/package.md @@ -40,9 +40,22 @@ Invoke-WebRequest https://docs.astrbot.app/install.ps1 -OutFile install.ps1 -Use The mainland China option also configures the USTC mirror for Python 3.12 and retries with the official Python source on failure. Existing Python mirror environment variables or explicit options take priority. -## Start AstrBot +## After Installation -Initialize the directory once, then start AstrBot: +The installer runs `uv tool update-shell`. Reload the environment for your shell: + +```bash +# Bash +source ~/.bashrc +# Zsh +source ~/.zshrc +# Fish +source ~/.config/fish/config.fish +# PowerShell +. $PROFILE +``` + +Then run these commands manually: ```bash astrbot init diff --git a/docs/public/install.ps1 b/docs/public/install.ps1 index 3630986eddf..5ef2881bf29 100644 --- a/docs/public/install.ps1 +++ b/docs/public/install.ps1 @@ -466,12 +466,23 @@ try { exit $toolInstallExitCode } Ok $Text.Installed - $nextSteps = switch ($Language) { - 'zh-CN' { $bootstrapText.NextStepsChinese } - 'ja-JP' { $bootstrapText.NextStepsJapanese } - default { $bootstrapText.NextStepsEnglish } + try { & uv tool update-shell *> $null } catch { } + $reloadShell = switch ($Language) { + 'zh-CN' { '请根据你使用的终端重新加载环境:' } + 'ja-JP' { '使用するシェルに合わせて環境を再読み込みしてください:' } + default { 'Reload the environment for your shell:' } } - Write-Host "`n$Indent$nextSteps" + $runCommands = switch ($Language) { + 'zh-CN' { '重新加载后,请手动运行:' } + 'ja-JP' { 'その後、以下のコマンドを手動で実行してください:' } + default { 'Then run these commands manually:' } + } + Write-Host "`n$Indent$reloadShell" + Write-Host "$($Indent) Bash: source ~/.bashrc" + Write-Host "$($Indent) Zsh: source ~/.zshrc" + Write-Host "$($Indent) Fish: source ~/.config/fish/config.fish" + Write-Host "$($Indent) PowerShell: . `$PROFILE" + Write-Host "`n$Indent$runCommands" Write-Host "$($Indent)$($Indent)astrbot init" -ForegroundColor Cyan Write-Host "$($Indent)$($Indent)astrbot run" -ForegroundColor Cyan } finally { diff --git a/docs/public/install.sh b/docs/public/install.sh index c58387367b0..700fcd7fe9e 100755 --- a/docs/public/install.sh +++ b/docs/public/install.sh @@ -127,7 +127,8 @@ message() { zh-CN:InstallingAstrBot) format='正在使用 Python 3.12 安装 AstrBot…' ;; zh-CN:AstrBotFailed) format='AstrBot 安装失败(退出码 %s)。请检查上方的下载或依赖错误。' ;; zh-CN:PythonFallback) format='Python 镜像下载失败,正在切换到官方源重试…' ;; - zh-CN:NextSteps) format='接下来可以依次执行以下指令来初始化和启动 AstrBot:' ;; + zh-CN:ReloadShell) format='请根据你使用的终端重新加载环境:' ;; + zh-CN:RunCommands) format='重新加载后,请手动运行:' ;; zh-CN:Installed) format='AstrBot 安装完成。' ;; en-US:ChooseSource) format='Select a download source' ;; en-US:China) format='Mainland China accelerated download mirrors' ;; @@ -156,7 +157,8 @@ message() { en-US:InstallingAstrBot) format='Installing AstrBot with Python 3.12...' ;; en-US:AstrBotFailed) format='AstrBot installation failed (exit code %s). Check the download or dependency errors above.' ;; en-US:PythonFallback) format='Python mirror failed; retrying with the official source...' ;; - en-US:NextSteps) format='Next, run these commands:' ;; + en-US:ReloadShell) format='Reload the environment for your shell:' ;; + en-US:RunCommands) format='Then run these commands manually:' ;; en-US:Installed) format='AstrBot has been installed.' ;; ja-JP:ChooseSource) format='ダウンロード元を選択してください' ;; ja-JP:China) format='中国本土向け高速ダウンロードミラー' ;; @@ -185,7 +187,8 @@ message() { ja-JP:InstallingAstrBot) format='Python 3.12 で AstrBot をインストールしています…' ;; ja-JP:AstrBotFailed) format='AstrBot のインストールに失敗しました(終了コード %s)。上記のダウンロードまたは依存関係のエラーを確認してください。' ;; ja-JP:PythonFallback) format='Python ミラーに失敗したため、公式ソースで再試行しています…' ;; - ja-JP:NextSteps) format='次に、以下を順番に実行してください:' ;; + ja-JP:ReloadShell) format='使用するシェルに合わせて環境を再読み込みしてください:' ;; + ja-JP:RunCommands) format='その後、以下のコマンドを手動で実行してください:' ;; ja-JP:Installed) format='AstrBot のインストールが完了しました。' ;; *) printf 'Unknown message: %s\n' "$key" >&2; return 1 ;; esac @@ -436,5 +439,11 @@ if [ "$tool_install_exit_code" -ne 0 ]; then exit "$tool_install_exit_code" fi ok "$(message Installed)" -printf '\n%s%s\n%s%sastrbot init%s\n%s%sastrbot run%s\n' \ - "$indent" "$(message NextSteps)" "$indent$indent" "$CYAN" "$NC" "$indent$indent" "$CYAN" "$NC" +uv tool update-shell >/dev/null 2>&1 || true +printf '\n%s%s\n' "$indent" "$(message ReloadShell)" +printf '%s Bash: source ~/.bashrc\n' "$indent" +printf '%s Zsh: source ~/.zshrc\n' "$indent" +printf '%s Fish: source ~/.config/fish/config.fish\n' "$indent" +printf '%s PowerShell: . $PROFILE\n' "$indent" +printf '\n%s%s\n%s astrbot init\n%s astrbot run\n' \ + "$indent" "$(message RunCommands)" "$indent" "$indent" diff --git a/docs/zh/deploy/astrbot/package.md b/docs/zh/deploy/astrbot/package.md index 7eed5097177..8aa78f9940d 100644 --- a/docs/zh/deploy/astrbot/package.md +++ b/docs/zh/deploy/astrbot/package.md @@ -40,9 +40,22 @@ Invoke-WebRequest https://docs.astrbot.app/install.ps1 -OutFile install.ps1 -Use 选择中国大陆源时,脚本也会为 Python 3.12 配置 USTC 镜像,失败后使用官方 Python 源重试。已有的 Python 镜像环境变量或显式参数优先。 -## 启动 AstrBot +## 安装完成后 -首次使用时初始化目录,之后直接启动: +安装脚本会自动执行 `uv tool update-shell`。请根据你使用的终端重新加载环境: + +```bash +# Bash +source ~/.bashrc +# Zsh +source ~/.zshrc +# Fish +source ~/.config/fish/config.fish +# PowerShell +. $PROFILE +``` + +重新加载后,手动运行: ```bash astrbot init diff --git a/tests/test_install_script.py b/tests/test_install_script.py index 2a26cc3d7c1..006b83dace2 100644 --- a/tests/test_install_script.py +++ b/tests/test_install_script.py @@ -26,6 +26,7 @@ FAKE_UV = b"""#!/bin/bash if [ "$1" = --version ]; then printf 'uv fixture\\n'; exit 0; fi +if [ "$1" = tool ] && [ "$2" = update-shell ]; then touch "$TEST_DIR/uv-update-shell"; exit 0; fi printf '%s\\n' "$@" > "$TEST_DIR/uv-args" printf '%s\\n' "${UV_PYTHON_INSTALL_MIRROR:-}" >> "$TEST_DIR/python-mirrors" if [ "${TEST_FAIL_ONCE:-0}" = 1 ] && [ ! -f "$TEST_DIR/failed-once" ]; then @@ -235,9 +236,18 @@ def test_defaults_ignore_system_locale(self): status, output = self.run_installer(["--non-interactive"]) self.assertEqual(status, 0, output) self.assert_install(OFFICIAL_INDEX, [""]) + self.assertTrue((self.root / "uv-update-shell").exists()) self.assertIn(" [INFO] Download source: Official sources", output) self.assertIn( - "\n Next, run these commands:\n astrbot init\n astrbot run\n", output + "\n Reload the environment for your shell:\n" + " Bash: source ~/.bashrc\n" + " Zsh: source ~/.zshrc\n" + " Fish: source ~/.config/fish/config.fish\n" + " PowerShell: . $PROFILE\n\n" + " Then run these commands manually:\n" + " astrbot init\n" + " astrbot run\n", + output, ) def test_chinese_defaults_to_all_china_mirrors(self): @@ -330,7 +340,7 @@ def test_japanese_custom_menu(self): self.assertIn( " 番号を入力してください。Enter で既定値を使用します [2]:", output ) - self.assertIn(" 次に、以下を順番に実行してください:", output) + self.assertIn(" 使用するシェルに合わせて環境を再読み込みしてください:", output) self.assert_install("https://pypi.example.com/simple/", [""]) def test_official_archive_is_verified_without_installer_script(self): From f9c95d3e2b8007be488badd0fc768fa66ece493d Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 21:56:45 +0800 Subject: [PATCH 24/28] docs: rename deployment sidebar entries --- docs/.vitepress/config.mjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index b7d494a4a69..245521ad2a0 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -43,7 +43,7 @@ export default defineConfig({ base: "/deploy", collapsed: false, items: [ - { text: "包管理器部署", link: "/astrbot/package" }, + { text: "脚本部署", link: "/astrbot/package" }, { text: "雨云一键云部署", link: "/astrbot/rainyun" }, { text: "桌面客户端部署", link: "/astrbot/desktop" }, { text: "启动器一键部署", link: "/astrbot/launcher" }, @@ -51,7 +51,7 @@ export default defineConfig({ { text: "Kubernetes 部署", link: "/astrbot/kubernetes" }, { text: "宝塔面板部署", link: "/astrbot/btpanel" }, { text: "1Panel 部署", link: "/astrbot/1panel" }, - { text: "手动部署", link: "/astrbot/cli" }, + { text: "源码部署", link: "/astrbot/cli" }, { text: "其他部署方式", link: "/astrbot/other-deployments", @@ -313,14 +313,14 @@ export default defineConfig({ base: "/en/deploy", collapsed: false, items: [ - { text: "Package Manager", link: "/astrbot/package" }, + { text: "Script Deployment", link: "/astrbot/package" }, { text: "Desktop Client", link: "/astrbot/desktop" }, { text: "One-click Launcher", link: "/astrbot/launcher" }, { text: "Docker", link: "/astrbot/docker" }, { text: "Kubernetes", link: "/astrbot/kubernetes" }, { text: "BT Panel", link: "/astrbot/btpanel" }, { text: "1Panel", link: "/astrbot/1panel" }, - { text: "Manual", link: "/astrbot/cli" }, + { text: "Source Deployment", link: "/astrbot/cli" }, { text: "Other Deployments", link: "/astrbot/other-deployments", From 7a2b5dffd9f666284836e1682a55ec39f6773596 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 22:14:49 +0800 Subject: [PATCH 25/28] fix: restore PowerShell 5.1 installer compatibility --- docs/public/install.ps1 | 237 ++++++++++++++++----------------- tests/install_script.Tests.ps1 | 7 +- tests/test_install_script.py | 8 +- 3 files changed, 124 insertions(+), 128 deletions(-) diff --git a/docs/public/install.ps1 b/docs/public/install.ps1 index 5ef2881bf29..6e65377e474 100644 --- a/docs/public/install.ps1 +++ b/docs/public/install.ps1 @@ -25,17 +25,23 @@ $Indent = ' ' # Keep the first-run experience predictable across machines. Users can still # choose Chinese or Japanese explicitly in the language menu. -$bootstrapCompressed = [Convert]::FromBase64String( - 'H4sIAAAAAAAACmWQ3W4SQRzFX2Uy1xu8xys1mmiMIakvsNKB3bjskt0htTEmO1AL7S7S1ABq+CiW5at82MREaqEP4+xMh6v6CGZaLCQkczFz5szvP+e8h89NB6uGgezXOjYQjMK/Td8HjxxsP7YwuL+FCnypmsmMmkQx20qlMYzCLWSgOAbGUgcPgJj8WrgH3OuLyUj0XKn0XDFo0Ozxgkz5YYOSKiUdSkqUnFBSpmQPKvCJppvIkbOvx244/xxOR7yShwp8aiYN3dFg9H6nwBdqWl26eTXgtaEYNKACX6F3eAujtLN6IyUF2BkTYA05CMStVEo1t53oun01m38KwqnH6wErTcLLILyq82GLH3RFy5fHqcf9fHjZloZCnXU95lfYsc+OJuyw97+vm9m3dfj6X4ctSs6oS+5YspCT/evygJIzNm6Klr/ZzB0stos1y3ymGsYbNf52lS52q4OUbtuWDRKqbqDth8BG2N7VzSTY0bEmcwMrkdDjumoAx8rYcRSJRDagqw6W0EW5xnKlcOqJ+Zy1z8XP4Gbm89Epq/VYIc+L31nhBxt/4ZUL/vtokS+KSfmP29ngrsVfgmmuSXN9mpvJ2O1zXq7exm7KlSXUJezjkM1KNDuXnuwFJV22XxT9zlpBe5RcUfJVDvzwD6o1noa/AgAA' -) -$bootstrapInput = New-Object System.IO.MemoryStream(,$bootstrapCompressed) -$bootstrapGzip = New-Object System.IO.Compression.GZipStream( - $bootstrapInput, - [System.IO.Compression.CompressionMode]::Decompress -) -$bootstrapReader = New-Object System.IO.StreamReader($bootstrapGzip, [Text.Encoding]::UTF8) -$bootstrapText = ConvertFrom-Json $bootstrapReader.ReadToEnd() -$bootstrapReader.Dispose() +# Use JSON Unicode escapes so Windows PowerShell 5.1 can read this file +# with any system encoding, while downloaded UTF-8 scripts also work without a BOM. +$bootstrapText = ConvertFrom-Json @' +{ + "InstallerTitle": "\ud83c\udf0c AstrBot Installer", + "LanguagePrompt": "Select language / \u8bf7\u9009\u62e9\u8bed\u8a00 / \u8a00\u8a9e\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044", + "Chinese": "\u7b80\u4f53\u4e2d\u6587", + "English": "English", + "Japanese": "\u65e5\u672c\u8a9e", + "NextStepsEnglish": "Next, run these commands:", + "NextStepsChinese": "\u63a5\u4e0b\u6765\u53ef\u4ee5\u4f9d\u6b21\u6267\u884c\u4ee5\u4e0b\u6307\u4ee4\u6765\u521d\u59cb\u5316\u548c\u542f\u52a8 AstrBot\uff1a", + "NextStepsJapanese": "\u6b21\u306b\u3001\u4ee5\u4e0b\u3092\u9806\u756a\u306b\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\uff1a", + "PythonFallbackEnglish": "Python mirror failed; retrying with the official source...", + "PythonFallbackChinese": "Python \u955c\u50cf\u4e0b\u8f7d\u5931\u8d25\uff0c\u6b63\u5728\u5207\u6362\u5230\u5b98\u65b9\u6e90\u91cd\u8bd5\u2026", + "PythonFallbackJapanese": "Python \u30df\u30e9\u30fc\u306b\u5931\u6557\u3057\u305f\u305f\u3081\u3001\u516c\u5f0f\u30bd\u30fc\u30b9\u3067\u518d\u8a66\u884c\u3057\u3066\u3044\u307e\u3059\u2026" +} +'@ $defaultLanguage = 'en-US' @@ -57,108 +63,103 @@ if (-not $Language) { } } -$compressedMessages = [Convert]::FromBase64String( - 'H4sIAAAAAAAACq1Y204bWRZ9b6n/4YinRAqmk2jmIfOU0NPqjLonKASNNG+FfcA1Xa5i6uLARJFcVTjYYMYOCQaME24OOBjs9HR3uMSGf5njU6568i+MzqmLy3dmNC+Wqcs+e6+91l7bvPj6KwBG/saM/mli5AF4Qf4CYOTJzAwbZBnuO4bjppngTyMPwIgSBUgt4/gJrqaRVkD6L0i7QHoC6VWkfySf2pr58RCpG0g9ROoiUq+QuvXv2OHIHSfqj6woCuJ3DMvBkBtR33FeVleQlkRqGekxpB2S6PopDZ1EagkXfjbWN2joK/q506zmXnzz0gs9sTDHTokciTqxMPEYSGxkjoPAyVNfQrqOtArSLsDU0x+8tx7zUYZjQ86L3SfjuI7UElIrRj6Jly+Qegy+f/ZsYpIEAUhbM1JLuJxzK04jdRep60hd7EjORZMc4uJXo4BdeM+MK5IsRGgaWokAq10jfbcz0/EwDP4kKREXPW2fhinROrNILYPJ7x/e+93vgVHIm8VqT+RQTPPCTkWd2pUo+OvjCdAbf30JaQdIPyEIkn4fk/qb1QTS15G2R88u4cwq0tasrQJS081q0pe4JDMcx/KzDyVZfCTIdovksMCD+4G79wBSj4Bzi0DaxazSAEKNh1meIQHr56d4u2bkT3B+B2deI/W1Vdq0Yju9inEI56MOScbmpi+5Aa92tHeKl5S5OUGUYeihGAyTGEb+GFeu8HWetOQvLB8Snktet06R/opCqSL9oCPWeFgQJDgpKGIQ9uektmap58by+27m9cB9KuqpN2UeriC1QLW2TOHcRtobFNP+K9SfiQssP9sCrE3GvWZAD616Qu1WaCe60faBoZZ7Jdub52FZnpMejI2FhKAUYCRZZLiAFB5TomOzUJZZfnZUkhnStzHWhouRWYEfIzzEaa0RP+oG2K+e8bDA2m1qrB/j9Bl5Lf4BL2/3fO2PvAxFQndjYx+XczhWQNpavXbdeFt0097yQjuKaJXuSeTG9TericZvWv3yFdJ+sakDXnzzsllNophWP182i5t9pu0VAU+t1K/e4dNNK7tfv9boqUWvxY39S/N4dQg4dKA9hX9XWNEuwT/YvAFoz1cwFf1WeM5zAhN6xEhwSuQAUouAMOUxH4Lz9oVy/XzPyF4gNYWv4+ahitQjpG75D3Wk9oTnFuiJ6hsiwAnhORQnw5DjAE2ggvRjpG8Q+NSKJ0/aByfgDyyvzI9FmOCTSdIx8pjDkIAUBu1964uAI8Eb9S+FyynSqj5z2p5J38IZRuH8E7SfXxLL2tjHp6TFQIkCs3hKjKqLb0T6PunWa9dIfeW0ZNQ+w6nClvtg00vYim5Zegk4eRr5k3rtDVIrOFMiJ2spb/rYhPTtCJLE8rOPWJ4RWSj1M7oSUKIBOA+BR1egROedKymkah0Dzos/ZLR2DB/Pdl2h30Ux9R6Kqfcp9GTA5WgxK4O175tmHXNzuNk6KZEAL2mYkX+ER8f/PGRbM04PcL6IP70zK+uEAbi8aWQvcDlpHsTxVnHIWmat57Gerp+vmLUaLvxs/vrhZvtW49d9XF0fsGPZIY3LDL6OW3tfjE2yWhnriUZusbVaDVqfaBnGZabH4mQuHeNyrn6RHLI14X+uNKofcSruLUu7e9Zxyq5z0HZkJ9/YLuMarbFZTdTPV3GmZGSX6l8+48zq0M3HboutQuBfg+zOuJvQgCUHF46srVd4edeK7dgJ2c3yY9JnpfE/PnyHqZ+vGm8rRkolvXGHpPH+c+P94pClxaycWbGksfLR63bftcTIHxvJK5z4BJRos5pyWEuxGLRydLK7Z01te4bNzMG7haMOlwdm5QynNTOmg/9xj+ixKJhXb3D8Q6OaxekzUm8qibffm7VDmxHWl02zXLBiSWvvYvgq4E+3WU1YsRheumzsqi2bNytnxkHM2PlQP182sheN3KLTkUS2fvXO/C1rvd0yK5XBtu3JyrjMWPmYeajiTMrY+GykM/Wr7R7OjddSbc49wKAJxPmixy6yllxnzL2UcVrwu7a5mDPyJ83qtt+XzcqZo6OWNQ+1XwezcspIZG5isA5e/6LnKFHQyC1a8dVGrdysbuNDzcrHbCU2qymcrlixnHm91Ns5bdt0iWpbpTtmV3EqjjOeW5pHB9bSMt4q3sgbvXFWPz9tVC/xp9euNxqJrOuKvS3Q0+cw2zMrZzZzATVAGvn+AF/zT8pu/4L86NTkEP+yFQ/kMASCc5vg77QaioFAYJCBRWzQQw41wQy9+QDcwMNYQtsBFubSHUgURwlEFEkG0xDQh1ouJrWf5ncx97sbooeZOd8GOxkjBsNsFLo+FoUiO8MG6fBxKg70NTMPmhArwqAsiAv0vxq3nrNyWFBkivwMy0GeicDbg02tddH7JU+i+P3N3y7P0H5kWJ5j+BCgVwATDEIOiowMQ63s7EZKw6yt4/l26HuYm++SN30omjIMyooI2wN0Gtwk5GBQBkzrWLuRg359P2ckwAsymBEUPhQAPsyUqB+dTqtzhOBxupvDHoEpcztK9zmc/Q3IgqsicjKYhPD/6G/2j1wG8EpkGop3gCCCORFKErBvyAJQJEi5FXJm7VCb8x/psBrcgvOsDIJCCJJqbwcAFYYd120JkT+cg3wI8sEFACmNADMtRGFggNvZVzxti/YtMC3I4R5WR8jrt7pAP6N7qvBADrOS39WkoMjOyUDgXQb+gYLj+6Ep8MDneIHB1hZmJDANIe/NSJ/4+9mbC5bktQXOs5LsEC4o8DPsrCJS6AlT5N7W5nSVcafuLcrGiK1cEBIo71k+yCkh6A4FlvBhToQyFKXb/lHeZXHPwpAk4846VgIR+yHX5gTRdblWnJZWO8Z1uzy6jG6Cg4wEAaRsvXsH3KMUvu8fo21G1z1F7QOo23391cv/ALOh8zpfFwAA' -) -$messageInput = New-Object System.IO.MemoryStream(,$compressedMessages) -$messageGzip = New-Object System.IO.Compression.GZipStream( - $messageInput, - [System.IO.Compression.CompressionMode]::Decompress -) -$messageReader = New-Object System.IO.StreamReader($messageGzip, [Text.Encoding]::UTF8) -$messages = ConvertFrom-Json $messageReader.ReadToEnd() -$messageReader.Dispose() -<# -$messages = @{ - 'zh-CN' = @{ - ChooseSource = '请选择下载源' - China = '中国大陆加速下载镜像源' - Official = '官方源' - Custom = '自定义' - Choice = '输入编号,按回车使用默认选项' - InvalidChoice = '请输入 1、2 或 3。' - UvUrl = 'uv ZIP 下载目录 URL(不含文件名)' - PypiUrl = 'PyPI simple 索引 URL' - CustomRequired = '自定义源需要同时提供 UvDownloadBaseUrl 和 PypiIndexUrl。' - InvalidUrl = '下载源必须是有效的 HTTPS URL:{0}' - WindowsOnly = '请在 Windows 上运行此 PowerShell 脚本;Linux/macOS 请使用 install.sh。' - Source = '下载源:{0}' - UvMirror = 'uv 下载目录:{0}' - Pypi = 'PyPI 索引:{0}' - PythonMirror = 'Python 下载镜像:{0}' - PythonDefault = 'Python 下载沿用 uv 的配置;如需加速,可通过 -PythonInstallMirror 指定镜像(PyPI 镜像不包含 Python 解释器)。' - InstallingUv = '未找到 uv,正在安装…' - TryingMirror = '正在尝试 uv 镜像:{0}' - MirrorFailed = 'uv 镜像下载失败:{0}' - OfficialFallback = '正在尝试 uv 官方安装器…' - UnsupportedArch = '不支持的 Windows 架构:{0}' - InvalidChecksum = 'uv 压缩包 SHA256 校验失败。' - MissingBinaries = 'uv 压缩包中缺少 uv.exe 或 uvx.exe。' - UvFailed = 'uv 安装失败。请参考 https://docs.astral.sh/uv/getting-started/installation/' - InstallingAstrBot = '正在使用 Python 3.12 安装 AstrBot…' - AstrBotFailed = 'AstrBot 安装失败(退出码 {0})。请检查上方的下载或依赖错误。' - Installed = 'AstrBot 安装完成。' - } - 'en-US' = @{ - ChooseSource = 'Select a download source' - China = 'Mainland China accelerated download mirrors' - Official = 'Official sources' - Custom = 'Custom' - Choice = 'Enter a number, or press Enter to use the default' - InvalidChoice = 'Please enter 1, 2, or 3.' - UvUrl = 'uv ZIP download directory URL (without the filename)' - PypiUrl = 'PyPI simple index URL' - CustomRequired = 'Custom sources require both UvDownloadBaseUrl and PypiIndexUrl.' - InvalidUrl = 'Download sources must be valid HTTPS URLs: {0}' - WindowsOnly = 'Run this PowerShell script on Windows; use install.sh on Linux/macOS.' - Source = 'Download source: {0}' - UvMirror = 'uv download directory: {0}' - Pypi = 'PyPI index: {0}' - PythonMirror = 'Python download mirror: {0}' - PythonDefault = 'Python downloads use the existing uv configuration. Set -PythonInstallMirror to use a mirror (PyPI mirrors do not include Python interpreters).' - InstallingUv = 'uv was not found. Installing uv...' - TryingMirror = 'Trying uv mirror: {0}' - MirrorFailed = 'uv mirror download failed: {0}' - OfficialFallback = 'Trying the official uv installer...' - UnsupportedArch = 'Unsupported Windows architecture: {0}' - InvalidChecksum = 'uv archive SHA256 verification failed.' - MissingBinaries = 'The uv archive is missing uv.exe or uvx.exe.' - UvFailed = 'Failed to install uv. See https://docs.astral.sh/uv/getting-started/installation/' - InstallingAstrBot = 'Installing AstrBot with Python 3.12...' - AstrBotFailed = 'AstrBot installation failed (exit code {0}). Check the download or dependency errors above.' - Installed = 'AstrBot has been installed.' - } - 'ja-JP' = @{ - ChooseSource = 'ダウンロード元を選択してください' - China = '中国本土向け高速ダウンロードミラー' - Official = '公式ソース' - Custom = 'カスタム' - Choice = '番号を入力してください。Enter で既定値を使用します' - InvalidChoice = '1、2、3 のいずれかを入力してください。' - UvUrl = 'uv ZIP のダウンロードディレクトリ URL(ファイル名を除く)' - PypiUrl = 'PyPI simple インデックス URL' - CustomRequired = 'カスタムソースには UvDownloadBaseUrl と PypiIndexUrl の両方が必要です。' - InvalidUrl = 'ダウンロード元には有効な HTTPS URL を指定してください:{0}' - WindowsOnly = 'この PowerShell スクリプトは Windows 用です。Linux/macOS では install.sh を使用してください。' - Source = 'ダウンロード元:{0}' - UvMirror = 'uv ダウンロードディレクトリ:{0}' - Pypi = 'PyPI インデックス:{0}' - PythonMirror = 'Python ダウンロードミラー:{0}' - PythonDefault = 'Python のダウンロードには既存の uv 設定を使用します。ミラーを使うには -PythonInstallMirror を指定してください(PyPI ミラーに Python 本体は含まれません)。' - InstallingUv = 'uv が見つかりません。インストールしています…' - TryingMirror = 'uv ミラーを試しています:{0}' - MirrorFailed = 'uv ミラーからのダウンロードに失敗しました:{0}' - OfficialFallback = 'uv の公式インストーラーを試しています…' - UnsupportedArch = '未対応の Windows アーキテクチャ:{0}' - InvalidChecksum = 'uv アーカイブの SHA256 検証に失敗しました。' - MissingBinaries = 'uv アーカイブに uv.exe または uvx.exe がありません。' - UvFailed = 'uv のインストールに失敗しました。https://docs.astral.sh/uv/getting-started/installation/ を参照してください。' - InstallingAstrBot = 'Python 3.12 で AstrBot をインストールしています…' - AstrBotFailed = 'AstrBot のインストールに失敗しました(終了コード {0})。上記のダウンロードまたは依存関係のエラーを確認してください。' - Installed = 'AstrBot のインストールが完了しました。' +$messages = ConvertFrom-Json @' +{ + "zh-CN": { + "ChooseSource": "\u8bf7\u9009\u62e9\u4e0b\u8f7d\u6e90", + "China": "\u4e2d\u56fd\u5927\u9646\u52a0\u901f\u4e0b\u8f7d\u955c\u50cf\u6e90", + "Official": "\u5b98\u65b9\u6e90", + "Custom": "\u81ea\u5b9a\u4e49", + "Choice": "\u8f93\u5165\u7f16\u53f7\uff0c\u6309\u56de\u8f66\u4f7f\u7528\u9ed8\u8ba4\u9009\u9879", + "InvalidChoice": "\u8bf7\u8f93\u5165 1\u30012 \u6216 3\u3002", + "UvUrl": "uv ZIP \u4e0b\u8f7d\u76ee\u5f55 URL\uff08\u4e0d\u542b\u6587\u4ef6\u540d\uff09", + "PypiUrl": "PyPI simple \u7d22\u5f15 URL", + "CustomRequired": "\u81ea\u5b9a\u4e49\u6e90\u9700\u8981\u540c\u65f6\u63d0\u4f9b UvDownloadBaseUrl \u548c PypiIndexUrl\u3002", + "InvalidUrl": "\u4e0b\u8f7d\u6e90\u5fc5\u987b\u662f\u6709\u6548\u7684 HTTPS URL\uff1a{0}", + "WindowsOnly": "\u8bf7\u5728 Windows \u4e0a\u8fd0\u884c\u6b64 PowerShell \u811a\u672c\uff1bLinux/macOS \u8bf7\u4f7f\u7528 install.sh\u3002", + "Source": "\u4e0b\u8f7d\u6e90\uff1a{0}", + "UvMirror": "uv \u4e0b\u8f7d\u76ee\u5f55\uff1a{0}", + "Pypi": "PyPI \u7d22\u5f15\uff1a{0}", + "PythonMirror": "Python \u4e0b\u8f7d\u955c\u50cf\uff1a{0}", + "PythonDefault": "Python \u4e0b\u8f7d\u6cbf\u7528 uv \u7684\u914d\u7f6e\uff1b\u5982\u9700\u52a0\u901f\uff0c\u53ef\u901a\u8fc7 -PythonInstallMirror \u6307\u5b9a\u955c\u50cf\uff08PyPI \u955c\u50cf\u4e0d\u5305\u542b Python \u89e3\u91ca\u5668\uff09\u3002", + "InstallingUv": "\u672a\u627e\u5230 uv\uff0c\u6b63\u5728\u5b89\u88c5\u2026", + "TryingMirror": "\u6b63\u5728\u5c1d\u8bd5 uv \u955c\u50cf\uff1a{0}", + "MirrorFailed": "uv \u955c\u50cf\u4e0b\u8f7d\u5931\u8d25\uff1a{0}", + "OfficialFallback": "\u6b63\u5728\u5c1d\u8bd5 uv \u5b98\u65b9\u5b89\u88c5\u5668\u2026", + "UnsupportedArch": "\u4e0d\u652f\u6301\u7684 Windows \u67b6\u7784\uff1a{0}", + "InvalidChecksum": "uv \u538b\u7f29\u5305 SHA256 \u6821\u9a8c\u5931\u8d25\u3002", + "MissingBinaries": "uv \u538b\u7f29\u5305\u4e2d\u7f3a\u5c11 uv.exe \u6216 uvx.exe\u3002", + "UvFailed": "uv \u5b89\u88c5\u5931\u8d25\u3002\u8bf7\u53c2\u8003 https://docs.astral.sh/uv/getting-started/installation/", + "InstallingAstrBot": "\u6b63\u5728\u4f7f\u7528 Python 3.12 \u5b89\u88c5 AstrBot\u2026", + "AstrBotFailed": "AstrBot \u5b89\u88c5\u5931\u8d25\uff08\u9000\u51fa\u7801 {0}\uff09\u3002\u8bf7\u68c0\u67e5\u4e0a\u65b9\u7684\u4e0b\u8f7d\u6216\u4f9d\u8d56\u9519\u8bef\u3002", + "Installed": "AstrBot \u5b89\u88c5\u5b8c\u6210\u3002", + "ReloadShell": "\u8bf7\u6839\u636e\u4f60\u4f7f\u7528\u7684\u7ec8\u7aef\u91cd\u65b0\u52a0\u8f7d\u73af\u5883\uff1a", + "RunCommands": "\u91cd\u65b0\u52a0\u8f7d\u540e\uff0c\u8bf7\u624b\u52a8\u8fd0\u884c\uff1a" + }, + "en-US": { + "ChooseSource": "Select a download source", + "China": "Mainland China accelerated download mirrors", + "Official": "Official sources", + "Custom": "Custom", + "Choice": "Enter a number, or press Enter to use the default", + "InvalidChoice": "Please enter 1, 2, or 3.", + "UvUrl": "uv ZIP download directory URL (without the filename)", + "PypiUrl": "PyPI simple index URL", + "CustomRequired": "Custom sources require both UvDownloadBaseUrl and PypiIndexUrl.", + "InvalidUrl": "Download sources must be valid HTTPS URLs: {0}", + "WindowsOnly": "Run this PowerShell script on Windows; use install.sh on Linux/macOS.", + "Source": "Download source: {0}", + "UvMirror": "uv download directory: {0}", + "Pypi": "PyPI index: {0}", + "PythonMirror": "Python download mirror: {0}", + "PythonDefault": "Python downloads use the existing uv configuration. Set -PythonInstallMirror to use a mirror (PyPI mirrors do not include Python interpreters).", + "InstallingUv": "uv was not found. Installing uv...", + "TryingMirror": "Trying uv mirror: {0}", + "MirrorFailed": "uv mirror download failed: {0}", + "OfficialFallback": "Trying the official uv installer...", + "UnsupportedArch": "Unsupported Windows architecture: {0}", + "InvalidChecksum": "uv archive SHA256 verification failed.", + "MissingBinaries": "The uv archive is missing uv.exe or uvx.exe.", + "UvFailed": "Failed to install uv. See https://docs.astral.sh/uv/getting-started/installation/", + "InstallingAstrBot": "Installing AstrBot with Python 3.12...", + "AstrBotFailed": "AstrBot installation failed (exit code {0}). Check the download or dependency errors above.", + "Installed": "AstrBot has been installed.", + "ReloadShell": "Reload the environment for your shell:", + "RunCommands": "Then run these commands manually:" + }, + "ja-JP": { + "ChooseSource": "\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u5143\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044", + "China": "\u4e2d\u56fd\u672c\u571f\u5411\u3051\u9ad8\u901f\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u30df\u30e9\u30fc", + "Official": "\u516c\u5f0f\u30bd\u30fc\u30b9", + "Custom": "\u30ab\u30b9\u30bf\u30e0", + "Choice": "\u756a\u53f7\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002Enter \u3067\u65e2\u5b9a\u5024\u3092\u4f7f\u7528\u3057\u307e\u3059", + "InvalidChoice": "1\u30012\u30013 \u306e\u3044\u305a\u308c\u304b\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "UvUrl": "uv ZIP \u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u30c7\u30a3\u30ec\u30af\u30c8\u30ea URL\uff08\u30d5\u30a1\u30a4\u30eb\u540d\u3092\u9664\u304f\uff09", + "PypiUrl": "PyPI simple \u30a4\u30f3\u30c7\u30c3\u30af\u30b9 URL", + "CustomRequired": "\u30ab\u30b9\u30bf\u30e0\u30bd\u30fc\u30b9\u306b\u306f UvDownloadBaseUrl \u3068 PypiIndexUrl \u306e\u4e21\u65b9\u304c\u5fc5\u8981\u3067\u3059\u3002", + "InvalidUrl": "\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u5143\u306b\u306f\u6709\u52b9\u306a HTTPS URL \u3092\u6307\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\uff1a{0}", + "WindowsOnly": "\u3053\u306e PowerShell \u30b9\u30af\u30ea\u30d7\u30c8\u306f Windows \u7528\u3067\u3059\u3002Linux/macOS \u3067\u306f install.sh \u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "Source": "\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u5143\uff1a{0}", + "UvMirror": "uv \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\uff1a{0}", + "Pypi": "PyPI \u30a4\u30f3\u30c7\u30c3\u30af\u30b9\uff1a{0}", + "PythonMirror": "Python \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u30df\u30e9\u30fc\uff1a{0}", + "PythonDefault": "Python \u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u306b\u306f\u65e2\u5b58\u306e uv \u8a2d\u5b9a\u3092\u4f7f\u7528\u3057\u307e\u3059\u3002\u30df\u30e9\u30fc\u3092\u4f7f\u3046\u306b\u306f -PythonInstallMirror \u3092\u6307\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\uff08PyPI \u30df\u30e9\u30fc\u306b Python \u672c\u4f53\u306f\u542b\u307e\u308c\u307e\u305b\u3093\uff09\u3002", + "InstallingUv": "uv \u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3057\u3066\u3044\u307e\u3059\u2026", + "TryingMirror": "uv \u30df\u30e9\u30fc\u3092\u8a66\u3057\u3066\u3044\u307e\u3059\uff1a{0}", + "MirrorFailed": "uv \u30df\u30e9\u30fc\u304b\u3089\u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u306b\u5931\u6557\u3057\u307e\u3057\u305f\uff1a{0}", + "OfficialFallback": "uv \u306e\u516c\u5f0f\u30a4\u30f3\u30b9\u30c8\u30fc\u30e9\u30fc\u3092\u8a66\u3057\u3066\u3044\u307e\u3059\u2026", + "UnsupportedArch": "\u672a\u5bfe\u5fdc\u306e Windows \u30a2\u30fc\u30ad\u30c6\u30af\u30c1\u30e3\uff1a{0}", + "InvalidChecksum": "uv \u30a2\u30fc\u30ab\u30a4\u30d6\u306e SHA256 \u691c\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", + "MissingBinaries": "uv \u30a2\u30fc\u30ab\u30a4\u30d6\u306b uv.exe \u307e\u305f\u306f uvx.exe \u304c\u3042\u308a\u307e\u305b\u3093\u3002", + "UvFailed": "uv \u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002https://docs.astral.sh/uv/getting-started/installation/ \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "InstallingAstrBot": "Python 3.12 \u3067 AstrBot \u3092\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3057\u3066\u3044\u307e\u3059\u2026", + "AstrBotFailed": "AstrBot \u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306b\u5931\u6557\u3057\u307e\u3057\u305f\uff08\u7d42\u4e86\u30b3\u30fc\u30c9 {0}\uff09\u3002\u4e0a\u8a18\u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u307e\u305f\u306f\u4f9d\u5b58\u95a2\u4fc2\u306e\u30a8\u30e9\u30fc\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "Installed": "AstrBot \u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u304c\u5b8c\u4e86\u3057\u307e\u3057\u305f\u3002", + "ReloadShell": "\u4f7f\u7528\u3059\u308b\u30b7\u30a7\u30eb\u306b\u5408\u308f\u305b\u3066\u74b0\u5883\u3092\u518d\u8aad\u307f\u8fbc\u307f\u3057\u3066\u304f\u3060\u3055\u3044\uff1a", + "RunCommands": "\u305d\u306e\u5f8c\u3001\u4ee5\u4e0b\u306e\u30b3\u30de\u30f3\u30c9\u3092\u624b\u52d5\u3067\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\uff1a" } } -#> +'@ $Text = $messages.$Language if (-not $DownloadSource) { @@ -467,22 +468,12 @@ try { } Ok $Text.Installed try { & uv tool update-shell *> $null } catch { } - $reloadShell = switch ($Language) { - 'zh-CN' { '请根据你使用的终端重新加载环境:' } - 'ja-JP' { '使用するシェルに合わせて環境を再読み込みしてください:' } - default { 'Reload the environment for your shell:' } - } - $runCommands = switch ($Language) { - 'zh-CN' { '重新加载后,请手动运行:' } - 'ja-JP' { 'その後、以下のコマンドを手動で実行してください:' } - default { 'Then run these commands manually:' } - } - Write-Host "`n$Indent$reloadShell" + Write-Host "`n$Indent$($Text.ReloadShell)" Write-Host "$($Indent) Bash: source ~/.bashrc" Write-Host "$($Indent) Zsh: source ~/.zshrc" Write-Host "$($Indent) Fish: source ~/.config/fish/config.fish" Write-Host "$($Indent) PowerShell: . `$PROFILE" - Write-Host "`n$Indent$runCommands" + Write-Host "`n$Indent$($Text.RunCommands)" Write-Host "$($Indent)$($Indent)astrbot init" -ForegroundColor Cyan Write-Host "$($Indent)$($Indent)astrbot run" -ForegroundColor Cyan } finally { diff --git a/tests/install_script.Tests.ps1 b/tests/install_script.Tests.ps1 index dacbb641e7c..d6c61efb806 100644 --- a/tests/install_script.Tests.ps1 +++ b/tests/install_script.Tests.ps1 @@ -14,6 +14,11 @@ if ($scriptBytes.Length -ge 3 -and throw 'install.ps1 must be UTF-8 without BOM because Invoke-WebRequest preserves the BOM before Invoke-Expression.' } +# Windows PowerShell 5.1 reads BOM-less files using the system ANSI code page. +if (@($scriptBytes | Where-Object { $_ -gt 0x7F }).Count -ne 0) { + throw 'install.ps1 must use ASCII source with JSON Unicode escapes for localized messages.' +} + $tokens = $null $parseErrors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile( @@ -27,7 +32,7 @@ if ($parseErrors.Count -ne 0) { if (-not $ast.ParamBlock) { throw 'install.ps1 must retain its top-level param block.' } -if ($ast.Extent.Text -notmatch '\$bootstrapCompressed = \[Convert\]::FromBase64String' -or +if ($ast.Extent.Text -notmatch '\$bootstrapText = ConvertFrom-Json @''' -or $ast.Extent.Text -notmatch '\$languages = @\(''en-US'', ''zh-CN'', ''ja-JP''\)' -or $ast.Extent.Text -notmatch '\$defaultLanguage = ''en-US''' -or $ast.Extent.Text -notmatch '\$Indent = '' ''' -or diff --git a/tests/test_install_script.py b/tests/test_install_script.py index 006b83dace2..34901a829aa 100644 --- a/tests/test_install_script.py +++ b/tests/test_install_script.py @@ -240,10 +240,10 @@ def test_defaults_ignore_system_locale(self): self.assertIn(" [INFO] Download source: Official sources", output) self.assertIn( "\n Reload the environment for your shell:\n" - " Bash: source ~/.bashrc\n" - " Zsh: source ~/.zshrc\n" - " Fish: source ~/.config/fish/config.fish\n" - " PowerShell: . $PROFILE\n\n" + " Bash: source ~/.bashrc\n" + " Zsh: source ~/.zshrc\n" + " Fish: source ~/.config/fish/config.fish\n" + " PowerShell: . $PROFILE\n\n" " Then run these commands manually:\n" " astrbot init\n" " astrbot run\n", From b05a46f9bd476eacce38416cacfc3ce3f65512c0 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 23:06:14 +0800 Subject: [PATCH 26/28] test: isolate reboot thread mock from asyncio executor --- tests/unit/test_core_lifecycle.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_core_lifecycle.py b/tests/unit/test_core_lifecycle.py index 058f869fd61..f187677e164 100644 --- a/tests/unit/test_core_lifecycle.py +++ b/tests/unit/test_core_lifecycle.py @@ -895,8 +895,10 @@ async def test_restart_terminates_managers_and_starts_thread( lifecycle.astrbot_updater = MagicMock() - with patch("astrbot.core.core_lifecycle.threading.Thread") as mock_thread: - await lifecycle.restart() + # Replace only this module's reference; patching threading.Thread also + # disables the executor worker used by asyncio.to_thread during shutdown. + with patch("astrbot.core.core_lifecycle.threading") as mock_threading: + await asyncio.wait_for(lifecycle.restart(), timeout=5) # Verify managers were terminated lifecycle.provider_manager.terminate.assert_awaited_once() @@ -904,8 +906,8 @@ async def test_restart_terminates_managers_and_starts_thread( lifecycle.kb_manager.terminate.assert_awaited_once() # Verify thread was started - mock_thread.assert_called_once() - mock_thread.return_value.start.assert_called_once() + mock_threading.Thread.assert_called_once() + mock_threading.Thread.return_value.start.assert_called_once() class TestAstrBotCoreLifecycleLoadPipelineScheduler: From 2a0a7305017132061fc13629048055b7ed1ba123 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 23:26:07 +0800 Subject: [PATCH 27/28] test: shorten long prompt IDs for Windows --- tests/unit/test_trace_store.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_trace_store.py b/tests/unit/test_trace_store.py index 1e38fa37368..ac37b06967f 100644 --- a/tests/unit/test_trace_store.py +++ b/tests/unit/test_trace_store.py @@ -416,7 +416,12 @@ def test_database_pages_filters_details_and_restart(persisted, capture): restarted.close() -@pytest.mark.parametrize("prompt", ["x" * 8192, "字" * 20000, "x" * 131072]) +# Keep node IDs below Windows' PYTEST_CURRENT_TEST environment variable limit. +@pytest.mark.parametrize( + "prompt", + ["x" * 8192, "字" * 20000, "x" * 131072], + ids=["ascii-8k", "unicode-20k", "ascii-128k"], +) def test_long_prompt_is_preserved_in_spans_events_and_database( persisted, capture, prompt ): From 13d18bb40cfe55941eacedd655136f9a04053b03 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 19 Oct 2026 23:51:00 +0800 Subject: [PATCH 28/28] fix: keep completed traces closed when update timestamps collide --- astrbot/core/utils/trace_store.py | 3 ++- tests/unit/test_trace_store.py | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/astrbot/core/utils/trace_store.py b/astrbot/core/utils/trace_store.py index 63eb2bfec00..9a0be6e9489 100644 --- a/astrbot/core/utils/trace_store.py +++ b/astrbot/core/utils/trace_store.py @@ -257,7 +257,8 @@ def _write(self, connection: sqlite3.Connection, batch: list[dict]) -> None: has_models=MAX(traces.has_models, excluded.has_models), has_plugins=MAX(traces.has_plugins, excluded.has_plugins), search=excluded.search, data=excluded.data, size_bytes=excluded.size_bytes - WHERE excluded.updated_ns >= traces.updated_ns""", + WHERE excluded.updated_ns >= traces.updated_ns + AND (traces.state = 'running' OR excluded.state != 'running')""", ( trace_id, int(summary["start_time_unix_nano"]), diff --git a/tests/unit/test_trace_store.py b/tests/unit/test_trace_store.py index ac37b06967f..0af2579ecd0 100644 --- a/tests/unit/test_trace_store.py +++ b/tests/unit/test_trace_store.py @@ -535,11 +535,18 @@ def held_write(connection, batch): assert persisted._pending_bytes == 0 -def test_stale_updates_do_not_reopen_completed_trace(persisted, capture): +@pytest.mark.parametrize("same_timestamp", [False, True]) +def test_stale_updates_do_not_reopen_completed_trace( + persisted, capture, same_timestamp +): trace = TraceSpan("message", message_outline="Literal % _") trace.record("event_received") trace.record("pipeline_end", status="success") for frame in reversed(capture): + # Wall-clock timestamps can collide on Windows even for distinct updates. + if same_timestamp: + frame["time_unix_nano"] = capture[-1]["time_unix_nano"] + frame["trace"]["updated_time_unix_nano"] = frame["time_unix_nano"] assert persisted.append(frame) assert persisted.flush() detail = persisted.get_trace(trace.trace_id)