This commit is contained in:
2026-03-11 16:49:00 +08:00
commit 52d7d14795
53 changed files with 4991 additions and 0 deletions

438
install.win.ps1 Normal file
View File

@@ -0,0 +1,438 @@
param()
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$RootDir = (Resolve-Path $ScriptDir).Path
$EnvConfig = Join-Path $RootDir 'env_config.ps1'
if (Test-Path $EnvConfig) {
. $EnvConfig
Import-EnvFile -Path (Join-Path $RootDir '.env')
}
$VenvDir = Join-Path $RootDir '.venv-qwen35'
$VenvPython = Join-Path $VenvDir 'Scripts\python.exe'
$LlamaDir = Join-Path $RootDir '.tmp\llama_win_cuda'
$ModelRelativeDir = '.tmp\models\crossrepo\lmstudio-community__Qwen3.5-9B-GGUF'
$DefaultGgufRelativePath = Join-Path $ModelRelativeDir 'Qwen3.5-9B-Q4_K_M.gguf'
$DefaultMmprojRelativePath = Join-Path $ModelRelativeDir 'mmproj-Qwen3.5-9B-BF16.gguf'
$GgufPath = Resolve-ManagedPath -BaseDir $RootDir -Value $env:MODEL_PATH -DefaultRelativePath $DefaultGgufRelativePath
$MmprojPath = Resolve-ManagedPath -BaseDir $RootDir -Value $env:MMPROJ_PATH -DefaultRelativePath $DefaultMmprojRelativePath
$DefaultGgufUrl = 'https://huggingface.co/lmstudio-community/Qwen3.5-9B-GGUF/resolve/main/Qwen3.5-9B-Q4_K_M.gguf'
$DefaultMmprojUrl = 'https://huggingface.co/lmstudio-community/Qwen3.5-9B-GGUF/resolve/main/mmproj-Qwen3.5-9B-BF16.gguf'
$LlamaReleaseApiUrl = 'https://api.github.com/repos/ggml-org/llama.cpp/releases/latest'
$LlamaReleasePageUrl = 'https://github.com/ggml-org/llama.cpp/releases/latest'
$LlamaReleaseDownloadPrefix = 'https://github.com/ggml-org/llama.cpp/releases/latest/download/'
$PreferredCudaBinAssetRegexes = @(
'^llama-.*-bin-win-cuda-12\.4-x64\.zip$',
'^llama-.*-bin-win-cuda-13\.1-x64\.zip$',
'^llama-.*-bin-win-cuda-.*-x64\.zip$'
)
$PreferredCudaRuntimeAssetRegexes = @(
'^cudart-llama-bin-win-cuda-12\.4-x64\.zip$',
'^cudart-llama-bin-win-cuda-13\.1-x64\.zip$',
'^cudart-llama-bin-win-cuda-.*-x64\.zip$'
)
function Write-Step {
param([string]$Message)
Write-Host "[install] $Message"
}
function New-PythonCandidate {
param(
[string]$Label,
[string]$Command,
[string[]]$Args = @()
)
return [PSCustomObject]@{
Label = $Label
Command = $Command
Args = $Args
}
}
function Get-PythonCandidates {
$candidates = @()
if ($env:PYTHON_BIN) {
$candidates += New-PythonCandidate -Label "PYTHON_BIN=$($env:PYTHON_BIN)" -Command $env:PYTHON_BIN
}
$candidates += New-PythonCandidate -Label 'py -3' -Command 'py' -Args @('-3')
$candidates += New-PythonCandidate -Label 'python' -Command 'python'
$candidates += New-PythonCandidate -Label 'python3' -Command 'python3'
return $candidates
}
function Test-PythonCandidate {
param([object]$PythonSpec)
$probeCode = 'import sys, venv; raise SystemExit(0 if sys.version_info >= (3, 10) else 3)'
try {
& $PythonSpec.Command @($PythonSpec.Args + @('-c', $probeCode)) *> $null
} catch {
Write-Step "跳过 Python 候选 $($PythonSpec.Label): $($_.Exception.Message)"
return $false
}
if ($LASTEXITCODE -eq 0) {
return $true
}
if ($LASTEXITCODE -eq 3) {
Write-Step "跳过 Python 候选 $($PythonSpec.Label): Python 版本低于 3.10"
return $false
}
Write-Step "跳过 Python 候选 $($PythonSpec.Label): 解释器不可用或缺少 venv 模块exit code: $LASTEXITCODE"
return $false
}
function Resolve-PythonSpec {
foreach ($candidate in Get-PythonCandidates) {
if (Test-PythonCandidate -PythonSpec $candidate) {
Write-Step "使用 Python: $($candidate.Label)"
return $candidate
}
}
throw '未找到可用 Python请安装 Python 3.10+ 并确保 venv 模块可用。'
}
function Invoke-CommandChecked {
param(
[string]$Command,
[string[]]$CommandArgs,
[string]$Action,
[string]$DisplayName = $Command
)
try {
& $Command @CommandArgs
} catch {
throw "$Action 失败。命令: $DisplayName。错误: $($_.Exception.Message)"
}
if ($LASTEXITCODE -ne 0) {
throw "$Action 失败。命令: $DisplayName。exit code: $LASTEXITCODE"
}
}
function Invoke-Python {
param(
[object]$PythonSpec,
[string[]]$PythonArgs,
[string]$Action
)
Invoke-CommandChecked -Command $PythonSpec.Command -CommandArgs ($PythonSpec.Args + $PythonArgs) -Action $Action -DisplayName $PythonSpec.Label
}
function Test-VenvPython {
param([string]$Path)
if (-not (Test-Path $Path)) {
return $false
}
try {
& $Path '-c' 'import sys' *> $null
} catch {
return $false
}
return $LASTEXITCODE -eq 0
}
function Ensure-Dir {
param([string]$Path)
if (-not (Test-Path $Path)) {
New-Item -Path $Path -ItemType Directory -Force | Out-Null
}
}
function Resolve-CurlPath {
$curl = Get-Command curl.exe -ErrorAction SilentlyContinue
if (-not $curl) {
throw '未找到 curl.exe无法执行带进度显示的下载。'
}
return $curl.Source
}
function Download-File {
param(
[string]$Url,
[string]$OutFile
)
Write-Step "下载: $Url"
$targetDir = Split-Path -Parent $OutFile
if (-not [string]::IsNullOrWhiteSpace($targetDir)) {
Ensure-Dir $targetDir
}
$tempFile = "$OutFile.part"
$curlPath = Resolve-CurlPath
$curlArgs = @(
'--fail',
'--location',
'--retry', '5',
'--retry-delay', '2',
'--output', $tempFile
)
if (Test-Path $tempFile) {
Write-Step '检测到未完成下载,继续传输'
$curlArgs += @('--continue-at', '-')
}
$curlArgs += $Url
try {
& $curlPath @curlArgs
} catch {
throw "下载失败。命令: curl.exe。错误: $($_.Exception.Message)"
}
if ($LASTEXITCODE -ne 0) {
throw "下载失败。命令: curl.exe。exit code: $LASTEXITCODE"
}
if (Test-Path $OutFile) {
Remove-Item -Path $OutFile -Force -ErrorAction SilentlyContinue
}
Move-Item -Path $tempFile -Destination $OutFile -Force
}
function Verify-Sha256 {
param(
[string]$Path,
[string]$Expected
)
if ([string]::IsNullOrWhiteSpace($Expected)) {
return
}
$actual = (Get-FileHash -Path $Path -Algorithm SHA256).Hash.ToLowerInvariant()
$exp = $Expected.ToLowerInvariant()
if ($actual -ne $exp) {
throw "SHA256 校验失败: $Path"
}
}
function Get-LlamaReleaseAssetsFromApi {
try {
$release = Invoke-RestMethod -Uri $LlamaReleaseApiUrl -Method Get
return @($release.assets | ForEach-Object {
[PSCustomObject]@{
Name = [string]$_.name
Url = [string]$_.browser_download_url
}
})
} catch {
Write-Step "GitHub API 不可用,改用页面解析。原因: $($_.Exception.Message)"
return @()
}
}
function Get-LlamaReleaseAssetsFromHtml {
try {
$response = Invoke-WebRequest -Uri $LlamaReleasePageUrl -UseBasicParsing
} catch {
throw "获取 llama.cpp release 页面失败: $($_.Exception.Message)"
}
$content = [string]$response.Content
$regex = '(?:cudart-)?llama-[^"''<> ]*bin-win-cuda-[0-9.]+-x64\.zip'
$matches = [regex]::Matches($content, $regex, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
$seen = @{}
$assets = @()
foreach ($match in $matches) {
$name = [string]$match.Value
$key = $name.ToLowerInvariant()
if ($seen.ContainsKey($key)) {
continue
}
$seen[$key] = $true
$assets += [PSCustomObject]@{
Name = $name
Url = "$LlamaReleaseDownloadPrefix$name"
}
}
return $assets
}
function Select-LlamaAsset {
param(
[object[]]$Assets,
[string[]]$Regexes
)
foreach ($regex in $Regexes) {
$candidate = $Assets | Where-Object { $_.Name -match $regex } | Select-Object -First 1
if ($candidate) {
return $candidate
}
}
return $null
}
function Resolve-LlamaCudaAssets {
if ($env:LLAMA_WIN_CUDA_URL) {
$binName = Split-Path -Path $env:LLAMA_WIN_CUDA_URL -Leaf
$runtimeUrl = if ($env:LLAMA_WIN_CUDART_URL) { [string]$env:LLAMA_WIN_CUDART_URL } else { '' }
$runtimeName = if ([string]::IsNullOrWhiteSpace($runtimeUrl)) { '' } else { (Split-Path -Path $runtimeUrl -Leaf) }
Write-Step "使用自定义 llama.cpp 主包: $binName"
if (-not [string]::IsNullOrWhiteSpace($runtimeName)) {
Write-Step "使用自定义 CUDA 运行时包: $runtimeName"
}
return @{
BinUrl = [string]$env:LLAMA_WIN_CUDA_URL
RuntimeUrl = $runtimeUrl
}
}
$assets = Get-LlamaReleaseAssetsFromApi
if ($assets.Count -eq 0) {
$assets = Get-LlamaReleaseAssetsFromHtml
}
if ($assets.Count -eq 0) {
throw '自动解析 llama.cpp CUDA 资源失败,未读取到任何 win-cuda 包。'
}
$bin = Select-LlamaAsset -Assets $assets -Regexes $PreferredCudaBinAssetRegexes
if (-not $bin) {
$preview = (@($assets | Select-Object -ExpandProperty Name | Select-Object -First 12)) -join ', '
throw "自动解析失败:未找到完整 CUDA 主包。可用资源: $preview"
}
$runtime = Select-LlamaAsset -Assets $assets -Regexes $PreferredCudaRuntimeAssetRegexes
Write-Step "使用 llama.cpp 主包: $($bin.Name)"
if ($runtime) {
Write-Step "可选 CUDA 运行时包: $($runtime.Name)"
}
return @{
BinUrl = [string]$bin.Url
RuntimeUrl = if ($runtime) { [string]$runtime.Url } else { '' }
}
}
function Get-LlamaRuntimeStatus {
param([string]$BaseDir)
$missing = @()
$llamaExe = Test-Path (Join-Path $BaseDir 'llama-server.exe')
if (-not $llamaExe) {
$missing += 'llama-server.exe'
}
$cudaBackendDll = @(Get-ChildItem -Path $BaseDir -Filter 'ggml-cuda*.dll' -File -ErrorAction SilentlyContinue | Select-Object -First 1)
if ($cudaBackendDll.Count -eq 0) {
$missing += 'ggml-cuda*.dll'
}
$cudartDll = @(Get-ChildItem -Path $BaseDir -Filter 'cudart64_*.dll' -File -ErrorAction SilentlyContinue | Select-Object -First 1)
if ($cudartDll.Count -eq 0) {
$missing += 'cudart64_*.dll'
}
$cublasDll = @(Get-ChildItem -Path $BaseDir -Filter 'cublas64_*.dll' -File -ErrorAction SilentlyContinue | Select-Object -First 1)
if ($cublasDll.Count -eq 0) {
$missing += 'cublas64_*.dll'
}
return @{
Ready = ($missing.Count -eq 0)
Missing = $missing
}
}
function Clear-LlamaRuntimeDirectory {
if (-not (Test-Path $LlamaDir)) {
Ensure-Dir $LlamaDir
return
}
try {
Get-ChildItem -Path $LlamaDir -Force -ErrorAction Stop | Remove-Item -Recurse -Force -ErrorAction Stop
} catch {
throw "清理 CUDA 运行时目录失败,请先停止服务后重试。目录: $LlamaDir。错误: $($_.Exception.Message)"
}
}
function Ensure-PythonEnv {
$python = Resolve-PythonSpec
$venvExists = Test-Path $VenvDir
$venvReady = Test-VenvPython -Path $VenvPython
if ($venvExists -and -not $venvReady) {
Write-Step "检测到不完整或非 Windows 虚拟环境,重建: $VenvDir"
Remove-Item -Path $VenvDir -Recurse -Force -ErrorAction SilentlyContinue
if (Test-Path $VenvDir) {
Write-Step '目录无法直接删除,尝试 venv --clear 重建'
Invoke-Python -PythonSpec $python -PythonArgs @('-m', 'venv', '--clear', $VenvDir) -Action '清空并重建虚拟环境'
}
}
if (-not (Test-Path $VenvDir)) {
Write-Step "创建虚拟环境: $VenvDir"
Invoke-Python -PythonSpec $python -PythonArgs @('-m', 'venv', $VenvDir) -Action '创建虚拟环境'
}
if (-not (Test-VenvPython -Path $VenvPython)) {
throw "虚拟环境未就绪: $VenvPython。请检查上面的 Python 或权限报错。"
}
Write-Step '安装 Python 依赖'
Invoke-CommandChecked -Command $VenvPython -CommandArgs @('-m', 'pip', 'install', '--upgrade', 'pip', 'wheel') -Action '升级 pip 和 wheel'
Invoke-CommandChecked -Command $VenvPython -CommandArgs @('-m', 'pip', 'install', '-r', (Join-Path $RootDir 'requirements.txt')) -Action '安装 requirements.txt 依赖'
}
function Ensure-LlamaRuntime {
Ensure-Dir $LlamaDir
$status = Get-LlamaRuntimeStatus -BaseDir $LlamaDir
if ($status.Ready) {
Write-Step '检测到完整 CUDA 运行时,跳过下载'
return
}
Write-Step '检测到不完整 CUDA 运行时,清理后重装'
Clear-LlamaRuntimeDirectory
$assets = Resolve-LlamaCudaAssets
$binZipPath = Join-Path $LlamaDir 'llama-win-cuda-bin.zip'
Download-File -Url $assets.BinUrl -OutFile $binZipPath
Write-Step '解压 llama.cpp CUDA 主包'
Expand-Archive -Path $binZipPath -DestinationPath $LlamaDir -Force
$foundServer = Get-ChildItem -Path $LlamaDir -Filter 'llama-server.exe' -Recurse -File | Select-Object -First 1
if (-not $foundServer) {
throw 'llama-server.exe 下载或解压失败,未在主包中找到可执行文件。'
}
$srcDir = Split-Path -Parent $foundServer.FullName
$srcDirResolved = (Resolve-Path $srcDir).Path
$llamaDirResolved = (Resolve-Path $LlamaDir).Path
if ($srcDirResolved -ne $llamaDirResolved) {
Copy-Item -Path (Join-Path $srcDir '*') -Destination $LlamaDir -Recurse -Force
}
$status = Get-LlamaRuntimeStatus -BaseDir $LlamaDir
$needRuntime = ($status.Missing | Where-Object { $_ -match '^cudart64_|^cublas64_' }).Count -gt 0
if ($needRuntime -and -not [string]::IsNullOrWhiteSpace([string]$assets.RuntimeUrl)) {
$runtimeZipPath = Join-Path $LlamaDir 'llama-win-cuda-runtime.zip'
Download-File -Url $assets.RuntimeUrl -OutFile $runtimeZipPath
Write-Step '解压 CUDA 运行时补充包'
Expand-Archive -Path $runtimeZipPath -DestinationPath $LlamaDir -Force
}
$status = Get-LlamaRuntimeStatus -BaseDir $LlamaDir
if (-not $status.Ready) {
$missingText = ($status.Missing -join ', ')
throw "CUDA 运行时不完整,缺失: $missingText"
}
}
function Ensure-ModelFiles {
Ensure-Dir (Split-Path -Parent $GgufPath)
Ensure-Dir (Split-Path -Parent $MmprojPath)
$ggufUrl = if ($env:MODEL_GGUF_URL) { $env:MODEL_GGUF_URL } else { $DefaultGgufUrl }
$mmprojUrl = if ($env:MODEL_MMPROJ_URL) { $env:MODEL_MMPROJ_URL } else { $DefaultMmprojUrl }
Write-Step "主模型路径: $GgufPath"
Write-Step "视觉模型路径: $MmprojPath"
if (-not (Test-Path $GgufPath)) {
Download-File -Url $ggufUrl -OutFile $GgufPath
} else {
Write-Step '检测到现有 9B 主模型,跳过下载'
}
if (-not (Test-Path $MmprojPath)) {
Download-File -Url $mmprojUrl -OutFile $MmprojPath
} else {
Write-Step '检测到现有 mmproj跳过下载'
}
Verify-Sha256 -Path $GgufPath -Expected $env:MODEL_GGUF_SHA256
Verify-Sha256 -Path $MmprojPath -Expected $env:MODEL_MMPROJ_SHA256
}
function Main {
Ensure-PythonEnv
Ensure-LlamaRuntime
Ensure-ModelFiles
Write-Step '安装完成'
Write-Step '启动命令: .\\start_8080_toolhub_stack.cmd start'
Write-Step '停止命令: .\\start_8080_toolhub_stack.cmd stop'
}
Main