Files
Qwen3.5-9B-ToolHub-Enhanced…/install.win.ps1
2026-03-11 16:49:00 +08:00

439 lines
15 KiB
PowerShell
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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