install.ps1
Rama main · réplica de lectura del repositorio original.
# Configuration-only bootstrap. Service lifecycle and database migration are manual.
[CmdletBinding()]
param(
[string]$InstallDir = "",
[string]$Version = "4.0.0"
)
$ErrorActionPreference = "Stop"
$Repository = "YuanYeYouTao/Yuki"
# This source installer targets the matching future Yuki release artifacts.
# Published releases retain their own installers and immutable image names.
$BotImage = "ghcr.io/yuanyeyoutao/yuki"
function Fail([string]$Message) {
throw $Message
}
if ($Version -notmatch '^\d+\.\d+\.\d+$') {
Fail "Version must use X.Y.Z."
}
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
Fail "Docker is not installed."
}
& docker compose version *> $null
if ($LASTEXITCODE -ne 0) {
Fail "The Docker Compose CLI plugin is not available."
}
& docker info *> $null
if ($LASTEXITCODE -ne 0) {
Fail "Docker Engine is not running."
}
$Architecture = (& docker info --format '{{.Architecture}}').Trim()
$DockerOs = (& docker info --format '{{.OSType}}').Trim()
if ($DockerOs -ne 'linux') {
Fail "Docker Desktop must be running Linux containers."
}
$Platform = switch ($Architecture) {
{ $_ -in @('amd64', 'x86_64') } { 'linux/amd64' }
{ $_ -in @('arm64', 'aarch64') } { 'linux/arm64' }
default { Fail "Yuki $Version supports linux/amd64 and linux/arm64; Docker reports $Architecture." }
}
if (-not $InstallDir) {
if ((Test-Path -LiteralPath "docker-compose.yml") -and (Test-Path -LiteralPath ".env.example")) {
$InstallDir = (Get-Location).Path
} else {
$InstallDir = Join-Path (Get-Location).Path "yuki"
}
}
$InstallDir = [System.IO.Path]::GetFullPath($InstallDir)
[System.IO.Directory]::CreateDirectory($InstallDir) | Out-Null
$WriteProbe = Join-Path $InstallDir (".yuki-write-test-" + [guid]::NewGuid())
try {
[System.IO.File]::WriteAllText($WriteProbe, "write-test")
} catch {
Fail "Installation directory is not writable."
} finally {
if (Test-Path -LiteralPath $WriteProbe) {
Remove-Item -LiteralPath $WriteProbe -Force
}
}
$ComposePath = Join-Path $InstallDir "docker-compose.yml"
$EnvTemplatePath = Join-Path $InstallDir ".env.example"
$Existing = (Test-Path -LiteralPath $ComposePath) -and (Test-Path -LiteralPath $EnvTemplatePath)
if (-not $Existing -and (Get-ChildItem -LiteralPath $InstallDir -Force | Select-Object -First 1)) {
Fail "Installation directory is not empty and is not a Yuki deployment."
}
$TemporaryRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath())
$Temporary = Join-Path $TemporaryRoot ("yuki-setup-" + [guid]::NewGuid())
[System.IO.Directory]::CreateDirectory($Temporary) | Out-Null
try {
if (-not $Existing) {
$Base = "https://github.com/$Repository/releases/download/v$Version"
$ArchiveName = "yuki-$Version-deploy.zip"
$Archive = Join-Path $Temporary $ArchiveName
$Checksums = Join-Path $Temporary "SHA256SUMS"
Invoke-WebRequest -Uri "$Base/$ArchiveName" -OutFile $Archive
Invoke-WebRequest -Uri "$Base/SHA256SUMS" -OutFile $Checksums
$ChecksumLine = Get-Content -LiteralPath $Checksums | Where-Object {
$_ -match "^[0-9a-fA-F]{64}\s+$([regex]::Escape($ArchiveName))$"
} | Select-Object -First 1
if (-not $ChecksumLine) {
Fail "Release checksum does not list $ArchiveName."
}
$Expected = ($ChecksumLine -split '\s+')[0].ToLowerInvariant()
$Actual = (Get-FileHash -LiteralPath $Archive -Algorithm SHA256).Hash.ToLowerInvariant()
if ($Expected -ne $Actual) {
Fail "Release archive checksum mismatch."
}
Expand-Archive -LiteralPath $Archive -DestinationPath $Temporary
$Source = Join-Path $Temporary "yuki-$Version-deploy"
if (-not (Test-Path -LiteralPath $Source -PathType Container)) {
Fail "Release archive layout is invalid."
}
if (-not $Existing) {
Copy-Item -Path (Join-Path $Source '*') -Destination $InstallDir -Recurse -Force
Get-ChildItem -LiteralPath $Source -Force | Where-Object Name -Like '.*' | ForEach-Object {
Copy-Item -LiteralPath $_.FullName -Destination $InstallDir -Recurse -Force
}
}
}
$Image = "${BotImage}:$Version"
Write-Host "Pulling $Image for $Platform" -ForegroundColor Cyan
& docker pull --platform $Platform $Image
if ($LASTEXITCODE -ne 0) { Fail "Unable to pull $Image." }
& docker run --rm -it `
--platform $Platform `
--entrypoint yuki `
--volume "${InstallDir}:/deploy" `
--workdir /deploy `
$Image setup --deployment-root /deploy
if ($LASTEXITCODE -ne 0) { Fail "Guided setup did not complete." }
if ($env:OS -eq "Windows_NT") {
$Identity = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$PrivateTargets = @(
@{
Path = (Join-Path $InstallDir ".env")
Permission = "${Identity}:(F)"
Recurse = $false
},
@{
Path = (Join-Path $InstallDir ".yuki\backups")
Permission = "${Identity}:(OI)(CI)F"
Recurse = $true
},
@{
Path = (Join-Path $InstallDir "character")
Permission = "${Identity}:(OI)(CI)F"
Recurse = $true
}
)
foreach ($Target in $PrivateTargets) {
if (Test-Path -LiteralPath $Target.Path) {
$AclArguments = @($Target.Path, "/inheritance:r", "/grant:r", $Target.Permission)
if ($Target.Recurse) { $AclArguments += @("/T", "/C") }
& icacls @AclArguments *> $null
if ($LASTEXITCODE -ne 0) { Fail "Unable to restrict configuration ACLs." }
}
}
}
Write-Host "Configuration saved. No services were stopped or started and no database was upgraded." -ForegroundColor Green
Write-Host "Review $InstallDir/Yuki-$Version-Upgrade.md before starting or upgrading the deployment."
Write-Host "Upgrade guide: https://github.com/$Repository/blob/v$Version/docs/upgrade-$Version.md"
Write-Host "Path file tools and terminals require the Linux Docker host Manager; Compose does not install it."
Write-Host "After preparing the persistent-environment prerequisites, explicitly run on that Linux host:"
Write-Host "sudo sh ./deploy/sandbox/install-manager.sh --deployment-root /absolute/deployment/path --bot-container BOT_CONTAINER"
Write-Host "Use the Linux host deployment root; a Windows client path is not the Docker host path."
Write-Host "Then check: docker compose exec --user 10001:10001 bot yuki setup environment-check"
} finally {
$ResolvedTemporary = [System.IO.Path]::GetFullPath($Temporary)
if ([System.IO.Path]::GetDirectoryName($ResolvedTemporary).TrimEnd([System.IO.Path]::DirectorySeparatorChar) -ne $TemporaryRoot.TrimEnd([System.IO.Path]::DirectorySeparatorChar)) {
Fail "Unexpected temporary directory; cleanup refused."
}
if (Test-Path -LiteralPath $ResolvedTemporary) {
Remove-Item -LiteralPath $ResolvedTemporary -Recurse -Force
}
}