Files
giteaMyFirstvdsRoot cd80888018 Windows: ollama pull ронял скрипт в PowerShell 5.1
При $ErrorActionPreference = Stop любая строка stderr внешней программы,
перенаправленная в $null, становится ошибкой NativeCommandError. Ollama
пишет прогресс pulling manifest в stderr, скрипт падал на первой модели.

Внешние команды теперь идут через Invoke-Quiet: локальная мягкая политика
ошибок, наружу только код возврата. Ошибка ollama cp тоже не молчит.
2026-09-11 20:53:23 +03:00

496 lines
27 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.
# =============================================================
# Claude на чужих мозгах — настройщик (Windows, PowerShell)
#
# Запуск:
# $env:AK='ollama'; irm {URL}/setup.ps1 | iex только Ollama
# $env:AK='zai'; irm {URL}/setup.ps1 | iex только Z.ai
# $env:AK='both'; irm {URL}/setup.ps1 | iex обе конфигурации
# irm {URL}/setup.ps1 | iex спросит, что ставить
# $env:AK='restore'; irm {URL}/setup.ps1 | iex вернуть родного Claude
#
# Что делает: находит УЖЕ УСТАНОВЛЕННЫЕ приложения, кладёт конфиги
# в приложение Claude и перезапускает его. Ничего не скачивает
# и не устанавливает. Терминальный Claude Code не затрагивается.
#
# Понимает обе сборки приложения Claude для Windows:
# · классическую: %LOCALAPPDATA%\AnthropicClaude\app-<версия>\claude.exe;
# · MSIX (Microsoft Store и установщик с claude.com):
# C:\Program Files\WindowsApps\Claude_<версия>_x64__pzs8sxrjxfjjc\app\Claude.exe.
# Сторонний режим у обеих живёт в %LOCALAPPDATA%\Claude-3p (так в коде приложения),
# родная папка данных зависит от сборки, см. Get-ClaudeInstall.
#
# ВНИМАНИЕ при правке: сохранять в UTF-8 БЕЗ BOM. Gitea отдаёт файл
# с заголовком charset=utf-8, irm декодирует кириллицу правильно,
# а BOM в конвейере irm | iex становится лишней командой и роняет разбор.
# =============================================================
# Никакого param(): скрипт запускается через `irm ... | iex`, а там param
# обязан быть самым первым выражением и всё равно не принимает аргументы.
# Режим передаётся переменной окружения AK.
$Mode = ""
if ($env:AK) { $Mode = $env:AK; $env:AK = $null }
$ErrorActionPreference = "Stop"
$ID_OLLAMA = "a11a0000-0000-4000-8000-0000000000aa"
$ID_ZAI = "a11a0000-0000-4000-8000-0000000000bb"
$ID_DEFAULT = "a11a0000-0000-4000-8000-0000000000dd"
$ZAI_PLACEHOLDER = "ВСТАВЬ_СЮДА_СВОЙ_КЛЮЧ_ZAI"
function Say($t) { Write-Host "`n$t" -ForegroundColor White }
function Ok($t) { Write-Host " [ok] $t" -ForegroundColor Green }
function Warn($t) { Write-Host " [!] $t" -ForegroundColor Yellow }
# JSON приложение читает строго: BOM его ломает, поэтому пишем UTF-8 без BOM.
function Write-Utf8($path, $text) {
$enc = New-Object System.Text.UTF8Encoding($false)
[IO.File]::WriteAllText($path, $text, $enc)
}
function Read-Json($path) {
if (-not (Test-Path $path)) { return $null }
try { Get-Content $path -Raw -Encoding UTF8 | ConvertFrom-Json } catch { $null }
}
# Windows PowerShell 5.1 при $ErrorActionPreference = "Stop" превращает любую
# строку stderr внешней программы в ошибку, если поток перенаправлен в $null.
# Ollama пишет прогресс "pulling manifest" именно в stderr, и первая же строка
# роняла скрипт. Поэтому внешние команды зовём через эту обёртку: внутри неё
# своя, мягкая политика ошибок, наружу уходит только код возврата.
function Invoke-Quiet {
$ErrorActionPreference = "SilentlyContinue"
$exe = $args[0]
$rest = @()
if ($args.Count -gt 1) { $rest = @($args[1..($args.Count - 1)]) }
& $exe @rest 2>&1 | Out-Null
return $LASTEXITCODE
}
# ---------- Где приложение и где его данные ----------
# Сборок две, и ведут они себя по-разному.
# · Классическая (Squirrel): %LOCALAPPDATA%\AnthropicClaude\app-<версия>\claude.exe,
# родные данные в %APPDATA%\Claude, логи в %LOCALAPPDATA%\Claude\logs.
# · MSIX (Microsoft Store и установщик с claude.com):
# C:\Program Files\WindowsApps\Claude_<версия>_x64__pzs8sxrjxfjjc\app\Claude.exe.
# Папка WindowsApps закрыта системой, exe оттуда напрямую не запускается, а всё,
# что приложение пишет в %APPDATA%, Windows подменяет на
# %LOCALAPPDATA%\Packages\Claude_pzs8sxrjxfjjc\LocalCache\Roaming\Claude.
# Сторонний режим у ОБЕИХ сборок живёт в %LOCALAPPDATA%\Claude-3p: так прописано
# в коде приложения, а в манифесте MSIX эта папка исключена из подмены.
function Find-ClaudeExe {
@("$env:LOCALAPPDATA\AnthropicClaude", "$env:LOCALAPPDATA\Programs") |
Where-Object { Test-Path $_ } |
ForEach-Object { Get-ChildItem $_ -Recurse -Filter "claude.exe" -ErrorAction SilentlyContinue } |
Select-Object -First 1
}
function Find-ClaudeMsix {
$pkg = $null
try {
if (-not (Get-Command Get-AppxPackage -ErrorAction SilentlyContinue) -and $PSVersionTable.PSVersion.Major -ge 7) {
Import-Module Appx -UseWindowsPowerShell -ErrorAction SilentlyContinue
}
$all = @(Get-AppxPackage -Name "*Claude*" -ErrorAction SilentlyContinue |
Where-Object { $_.Name -eq "Claude" -or $_.Name -like "*ClaudeDesktop*" -or $_.Publisher -like "*Anthropic*" })
$pkg = $all | Where-Object { $_.Name -eq "Claude" } | Select-Object -First 1
if (-not $pkg) { $pkg = $all | Select-Object -First 1 }
} catch { $pkg = $null }
if ($pkg) {
$appId = "Claude"
try {
$ids = @((Get-AppxPackageManifest $pkg).Package.Applications.Application.Id)
if ($ids.Count -gt 0 -and $ids[0]) { $appId = "$($ids[0])" }
} catch { }
return [pscustomobject]@{ Family = "$($pkg.PackageFamilyName)"; Aumid = "$($pkg.PackageFamilyName)!$appId"; Version = "$($pkg.Version)" }
}
# Запасной путь: Get-AppxPackage бывает недоступен (PowerShell 7 без модуля Appx),
# но папка данных пакета остаётся на месте после первого запуска приложения.
$d = Get-ChildItem (Join-Path $env:LOCALAPPDATA "Packages") -Directory -Filter "Claude_*" -ErrorAction SilentlyContinue |
Select-Object -First 1
if ($d) { return [pscustomobject]@{ Family = $d.Name; Aumid = "$($d.Name)!Claude"; Version = "" } }
return $null
}
function Get-ClaudeInstall {
$msix = Find-ClaudeMsix
$exe = Find-ClaudeExe
if ($msix) {
$home1p = Join-Path $env:LOCALAPPDATA "Packages\$($msix.Family)\LocalCache\Roaming\Claude"
$launched = Test-Path $home1p
# Если рядом осталась и классическая сборка, верим той, которой пользовались.
if ($launched -or -not $exe) {
$label = "сборка MSIX (Microsoft Store или установщик с claude.com)"
if ($msix.Version) { $label += ", версия $($msix.Version)" }
return [pscustomobject]@{ Kind = "msix"; Label = $label; Aumid = $msix.Aumid; Exe = $null; Home = $home1p; Launched = $launched }
}
}
if ($exe) {
$home1p = Join-Path $env:APPDATA "Claude"
return [pscustomobject]@{ Kind = "classic"; Label = "классическая сборка"; Aumid = $null; Exe = $exe.FullName; Home = $home1p; Launched = (Test-Path $home1p) }
}
return $null
}
$INST = Get-ClaudeInstall
# Сторонний режим: конфиги, библиотека, флаг разработчика для стороннего режима.
$C3P = Join-Path $env:LOCALAPPDATA "Claude-3p"
$LIB = Join-Path $C3P "configLibrary"
# Родные папки: сюда кладём только флаг разработчика и чистим кэши. Их несколько,
# потому что настоящая зависит от сборки, а лишний флаг ничему не вредит.
$P1_DIRS = @()
if ($INST) { $P1_DIRS += $INST.Home }
foreach ($d in @((Join-Path $env:APPDATA "Claude"), (Join-Path $env:LOCALAPPDATA "Claude"), (Join-Path $env:LOCALAPPDATA "Claude-Data"))) {
if ($P1_DIRS -notcontains $d -and ((Test-Path $d) -or $P1_DIRS.Count -eq 0)) { $P1_DIRS += $d }
}
if ($P1_DIRS.Count -eq 0) { $P1_DIRS += (Join-Path $env:APPDATA "Claude") }
$ALL_DIRS = @($P1_DIRS) + @($C3P)
# Пакетное приложение запускают по идентификатору, а не по пути к exe.
function Start-Claude {
if ($INST.Kind -eq "msix") {
$target = "shell:AppsFolder\$($INST.Aumid)"
try { Start-Process $target -ErrorAction Stop; return } catch { }
try { Start-Process explorer.exe $target -ErrorAction Stop; return } catch { }
$alias = Join-Path $env:LOCALAPPDATA "Microsoft\WindowsApps\Claude.exe"
if (Test-Path $alias) { try { Start-Process $alias -ErrorAction Stop; return } catch { } }
try { Start-Process "claude://" -ErrorAction Stop } catch { }
return
}
if ($INST.Exe) { Start-Process $INST.Exe }
}
# ВАЖНО. Приложение читает developer_settings.json из ТЕКУЩЕЙ папки данных,
# а она переключается на -3p только когда сторонний режим уже взлетел.
# Кладём флаг и в родные папки, и в -3p, иначе пункт Developer не появится.
function Write-DevFlag {
foreach ($d in $ALL_DIRS) {
New-Item -ItemType Directory -Force -Path $d | Out-Null
Write-Utf8 (Join-Path $d "developer_settings.json") '{"allowDevTools": true}'
}
Ok "Режим разработчика включён во всех папках приложения."
}
# "deploymentMode": "1p" намертво запрещает сторонний режим.
function Clear-DeploymentPin {
$f = Join-Path $C3P "claude_desktop_config.json"
$cfg = Read-Json $f
if ($cfg -and $cfg.PSObject.Properties.Name -contains "deploymentMode" -and $cfg.deploymentMode -eq "1p") {
$cfg.PSObject.Properties.Remove("deploymentMode")
Write-Utf8 $f ($cfg | ConvertTo-Json -Depth 5)
Ok "Снят запрет на сторонний режим (deploymentMode)."
}
}
# Приложение падает на BOM: в логе видно
# [managedConfig] Failed to read config library meta: Unexpected token 'п»ї'
# и тогда ВСЯ библиотека конфигов не читается. BOM берётся из старых версий
# скрипта и из Блокнота, который по умолчанию сохраняет UTF-8 именно так.
# Поэтому чистим BOM у всех json приложения, а не только у своих.
function Repair-JsonBom {
$files = @()
if (Test-Path $LIB) { $files += Get-ChildItem $LIB -Filter *.json -ErrorAction SilentlyContinue }
foreach ($d in $ALL_DIRS) {
foreach ($n in @("developer_settings.json", "claude_desktop_config.json")) {
$f = Join-Path $d $n
if (Test-Path $f) { $files += Get-Item $f }
}
}
$fixed = 0
foreach ($f in $files) {
try {
$b = [IO.File]::ReadAllBytes($f.FullName)
if ($b.Length -ge 3 -and $b[0] -eq 239 -and $b[1] -eq 187 -and $b[2] -eq 191) {
[IO.File]::WriteAllBytes($f.FullName, $b[3..($b.Length - 1)])
$fixed++
}
} catch { }
}
if ($fixed -gt 0) { Ok "Убран BOM у $fixed файлов конфигурации (приложение их не читало)." }
}
function Merge-Meta($applied, $routes) {
$p = Join-Path $LIB "_meta.json"
$old = Read-Json $p
# Собираем список записей заново: мутировать объект из ConvertFrom-Json
# ненадёжно, свойства может не быть вовсе.
$entries = [System.Collections.ArrayList]::new()
if ($old -and $old.entries) {
foreach ($e in @($old.entries)) {
if ($e.id) { [void]$entries.Add([pscustomobject]@{ id = "$($e.id)"; name = "$($e.name)" }) }
}
}
function Set-Entry($list, $id, $name) {
$e = $list | Where-Object { $_.id -eq $id } | Select-Object -First 1
if ($e) { $e.name = $name } else { [void]$list.Add([pscustomobject]@{ id = $id; name = $name }) }
}
if (-not ($entries | Where-Object { $_.name -eq "Default" })) {
Write-Utf8 (Join-Path $LIB "$ID_DEFAULT.json") "{}"
Set-Entry $entries $ID_DEFAULT "Default"
}
# Старые записи с нашими именами, но чужими id переименовываем, иначе в панели два одинаковых пункта.
foreach ($e in $entries) {
if (($e.name -eq "ollama-cloud" -or $e.name -eq "zai-glm") -and $e.id -ne $ID_OLLAMA -and $e.id -ne $ID_ZAI) {
$e.name = $e.name + " (старый)"
}
}
if ($routes -eq "ollama" -or $routes -eq "both") { Set-Entry $entries $ID_OLLAMA "ollama-cloud" }
if ($routes -eq "zai" -or $routes -eq "both") { Set-Entry $entries $ID_ZAI "zai-glm" }
# Записи без файла на диске ломают выбор в панели.
$kept = @($entries | Where-Object { Test-Path (Join-Path $LIB ("$($_.id).json")) })
$def = ($kept | Where-Object { $_.name -eq "Default" } | Select-Object -First 1).id
$appliedId = switch ($applied) {
"ollama" { $ID_OLLAMA }
"zai" { $ID_ZAI }
default { $def }
}
$meta = [pscustomobject]@{ appliedId = $appliedId; entries = $kept }
Write-Utf8 $p ($meta | ConvertTo-Json -Depth 5)
}
function Check-Applied {
$meta = Read-Json (Join-Path $LIB "_meta.json")
if (-not $meta) { return "НЕТ: нет _meta.json" }
$cfg = Read-Json (Join-Path $LIB "$($meta.appliedId).json")
$name = ($meta.entries | Where-Object { $_.id -eq $meta.appliedId } | Select-Object -First 1).name
if (-not $name) { $name = $meta.appliedId }
if ($cfg -and $cfg.inferenceProvider) { return "ДА: $name" }
return "НЕТ: применён пустой конфиг «$name»"
}
# Повторяет пункт меню Developer -> Clear Cache and Restart. В коде приложения он
# чистит ТОЛЬКО кэши: HTTP, скомпилированный JS, шейдеры, service workers, DNS.
# Ни cookies, ни Local Storage там нет, поэтому вход в аккаунт, чаты, MCP,
# скиллы и наши конфиги остаются на месте. Делать только при закрытом приложении.
# ВАЖНО: не путать с соседним пунктом Reset App Data, тот сносит папку целиком.
function Clear-Caches {
foreach ($d in $ALL_DIRS) {
if (-not (Test-Path $d)) { continue }
foreach ($c in @("Cache", "Code Cache", "GPUCache", "DawnGraphiteCache", "DawnWebGPUCache", "Service Worker")) {
Remove-Item (Join-Path $d $c) -Recurse -Force -ErrorAction SilentlyContinue
}
}
Ok "Кэши очищены (чаты, входы и конфиги не тронуты)."
}
function Restart-Claude {
Say "Перезапускаю приложение Claude..."
Get-Process -Name "Claude*" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
$i = 0
while ((Get-Process -Name "Claude*" -ErrorAction SilentlyContinue) -and $i -lt 12) { Start-Sleep 1; $i++ }
Clear-Caches
if (-not $INST) { Warn "Не нашёл приложение Claude, открой его вручную."; return }
Start-Claude
# Проверяем, что процесс реально поднялся: рапортовать успех вслепую нельзя.
# Пакетной сборке на старт нужно больше времени.
$j = 0
while (-not (Get-Process -Name "Claude*" -ErrorAction SilentlyContinue) -and $j -lt 12) { Start-Sleep 1; $j++ }
if (Get-Process -Name "Claude*" -ErrorAction SilentlyContinue) {
Ok "Claude перезапущен."
} else {
Warn "Claude не поднялся автоматически. Конфиги на месте, открой приложение вручную."
}
}
# ---------- ОТКАТ ----------
if ($Mode -eq "restore") {
Say "Откат на родного Claude..."
if (-not (Test-Path (Join-Path $LIB "_meta.json"))) { Ok "Конфигов нет, откатывать нечего."; return }
Merge-Meta "default" "none"
Repair-JsonBom
Ok "Применён пустой профиль Default."
Write-Host " Если приложение всё равно открывается на чужом мозге: меню аккаунта внизу слева -> Sign out,"
Write-Host " затем на экране входа выбери обычный вход по почте или Google."
Write-DevFlag
Restart-Claude
Write-Host "`nЕсли что-то осталось сломанным, полный сброс (сотрёт чаты стороннего режима):"
Write-Host " удали папку $C3P"
return
}
# ---------- 1. Приложение Claude ----------
Say "[1/4] Ищу приложение Claude..."
if ($INST) {
Ok "Claude на месте: $($INST.Label)."
Ok "Конфиги стороннего режима: $C3P"
if (-not $INST.Launched) {
Warn "Похоже, приложение ещё ни разу не запускалось. Это не страшно, но если после"
Write-Host " запуска пункта Developer не будет, запусти эту же команду ещё раз."
}
} else {
Warn "Приложение Claude не найдено, а без него настраивать нечего."
Write-Host " Смотрел классическую сборку в AppData\Local и пакеты Microsoft Store."
Write-Host ""
Write-Host " Поставь его руками, это одна минута:"
Write-Host " 1. Открой https://claude.com/download и скачай версию для Windows."
Write-Host " 2. Установи и запусти приложение хотя бы один раз."
Write-Host " 3. Вернись сюда и запусти эту же команду снова."
Write-Host ""
Write-Host " Логиниться в аккаунт Anthropic не обязательно."
return
}
# ---------- 2. Выбор мозга ----------
$brain = $Mode
if ($brain -notin @("ollama", "zai", "both")) {
Say "[2/4] Какой мозг подключаем?"
Write-Host " 1) Ollama бесплатный старт, программа Ollama должна быть установлена и запущена"
Write-Host " 2) Z.ai по подписке от `$18, локально ничего не нужно, только ключ"
Write-Host " 3) оба положу обе конфигурации, переключаться будешь в приложении"
$choice = Read-Host " Твой выбор [1/2/3, по умолчанию 3]"
switch ($choice) {
"1" { $brain = "ollama" }
"2" { $brain = "zai" }
default { $brain = "both" }
}
} else {
Say "[2/4] Режим: $brain"
}
switch ($brain) {
"ollama" { Ok "Ставлю только Ollama." }
"zai" { Ok "Ставлю только Z.ai." }
"both" { Ok "Ставлю обе конфигурации." }
}
# ---------- 3. Ollama и псевдонимы ----------
$haveOllama = $false
if ($brain -eq "ollama" -or $brain -eq "both") {
Say "[3/4] Проверяю программу Ollama..."
if (Get-Command ollama -ErrorAction SilentlyContinue) {
Ok "Ollama на месте."
$haveOllama = $true
Say " Создаю псевдонимы облачных моделей (нужен вход в аккаунт Ollama)..."
# Приложение пускает только точные идентификаторы моделей Claude,
# поэтому облачная модель получает второе, «правильное» имя.
# Третий элемент "optional": модель платная, её отсутствие не валит установку.
$pairs = @(
@("glm-5.3:cloud", "claude-sonnet-4-6", ""),
@("kimi-k3:cloud", "claude-opus-4-6", ""),
@("glm-5.3-flash:cloud", "claude-haiku-4-5", ""),
@("qwen3.5:397b-cloud", "claude-sonnet-4-5", "optional"),
@("deepseek-v4-pro:0813-cloud", "claude-opus-4-1", "optional"),
@("gpt-oss:120b-cloud", "claude-haiku-3-5", "optional"),
@("minimax-m3:cloud", "claude-3-7-sonnet", "optional"),
@("nemotron-3-super:cloud", "claude-3-5-haiku", "optional")
)
foreach ($p in $pairs) {
$rc = Invoke-Quiet ollama pull $p[0]
if ($rc -ne 0) {
if ($p[2] -eq "optional") {
Warn "$($p[0]) недоступна на твоём тарифе Ollama, пропускаю (нужен Pro или Max)."
continue
}
Warn "Не получилось скачать $($p[0]). Обычно это нет входа в аккаунт Ollama."
Write-Host " Открой окно Ollama (иконка ламы) -> Sign in, затем запусти команду снова."
return
}
$rc = Invoke-Quiet ollama cp $p[0] $p[1]
if ($rc -ne 0) { Warn "Не смог создать псевдоним $($p[1]) для $($p[0])."; continue }
Ok "$($p[1]) -> $($p[0])"
}
} else {
Warn "Ollama не найдена."
Write-Host ""
Write-Host " Поставь её руками:"
Write-Host " 1. Открой https://ollama.com/download и скачай версию для Windows."
Write-Host " 2. Установи, запусти, войди в аккаунт (иконка ламы -> Sign in)."
Write-Host " 3. Вернись сюда и запусти команду снова."
Write-Host ""
if ($brain -eq "ollama") {
Write-Host " Или подключи Z.ai, ему Ollama не нужна: `$env:AK='zai' вместо 'ollama'."
return
}
Warn "Пропускаю Ollama, кладу только конфиг Z.ai."
$brain = "zai"
}
} else {
Say "[3/4] Маршрут Z.ai, Ollama не нужна."
}
# ---------- 4. Конфиги ----------
Say "[4/4] Кладу конфиги в приложение Claude..."
New-Item -ItemType Directory -Force -Path $LIB | Out-Null
Write-DevFlag
Clear-DeploymentPin
if ($brain -eq "ollama" -or $brain -eq "both") {
Write-Utf8 (Join-Path $LIB "$ID_OLLAMA.json") @'
{
"inferenceProvider": "gateway",
"inferenceGatewayBaseUrl": "http://127.0.0.1:11434",
"inferenceGatewayApiKey": "ollama",
"inferenceGatewayAuthScheme": "bearer",
"inferenceCredentialKind": "static",
"modelDiscoveryEnabled": false,
"inferenceModels": [
{ "name": "claude-sonnet-4-6", "labelOverride": "GLM-5.3 (Ollama)", "anthropicFamilyTier": "sonnet", "isFamilyDefault": true },
{ "name": "claude-opus-4-6", "labelOverride": "Kimi K3 (Ollama)", "anthropicFamilyTier": "opus", "isFamilyDefault": true },
{ "name": "claude-haiku-4-5", "labelOverride": "GLM-5.3 Flash (Ollama)", "anthropicFamilyTier": "haiku", "isFamilyDefault": true },
{ "name": "claude-sonnet-4-5", "labelOverride": "Qwen3.5 397B (Ollama)", "anthropicFamilyTier": "sonnet", "isFamilyDefault": false },
{ "name": "claude-opus-4-1", "labelOverride": "DeepSeek V4 Pro (Ollama)", "anthropicFamilyTier": "opus", "isFamilyDefault": false },
{ "name": "claude-haiku-3-5", "labelOverride": "GPT-OSS 120B (Ollama)", "anthropicFamilyTier": "haiku", "isFamilyDefault": false },
{ "name": "claude-3-7-sonnet", "labelOverride": "MiniMax M3 (Ollama)", "anthropicFamilyTier": "sonnet", "isFamilyDefault": false },
{ "name": "claude-3-5-haiku", "labelOverride": "Nemotron 3 Super (Ollama)", "anthropicFamilyTier": "haiku", "isFamilyDefault": false }
]
}
'@
Ok "Конфиг ollama-cloud на месте."
}
if ($brain -eq "zai" -or $brain -eq "both") {
$zaiPath = Join-Path $LIB "$ID_ZAI.json"
$zaiHasKey = (Test-Path $zaiPath) -and -not ((Get-Content $zaiPath -Raw -Encoding UTF8) -match $ZAI_PLACEHOLDER)
if ($zaiHasKey) {
Ok "Конфиг zai-glm уже настроен, ключ не трогаю."
} else {
Write-Utf8 $zaiPath @"
{
"inferenceProvider": "gateway",
"inferenceGatewayBaseUrl": "https://api.z.ai/api/anthropic",
"inferenceGatewayApiKey": "$ZAI_PLACEHOLDER",
"inferenceGatewayAuthScheme": "bearer",
"inferenceCredentialKind": "static",
"modelDiscoveryEnabled": false,
"inferenceModels": [
{ "name": "claude-sonnet-4-6", "labelOverride": "GLM-5.3 (Z.ai Coding Plan)", "anthropicFamilyTier": "sonnet", "isFamilyDefault": true },
{ "name": "claude-haiku-4-5", "labelOverride": "GLM-5.3 Flash (Z.ai)", "anthropicFamilyTier": "haiku", "isFamilyDefault": true }
]
}
"@
Ok "Конфиг zai-glm на месте, ждёт твой ключ."
}
}
if ($haveOllama) { Merge-Meta "ollama" $brain } else { Merge-Meta "zai" $brain }
Repair-JsonBom
# ---------- Самопроверка ----------
Say "Проверяю, что получилось..."
$res = Check-Applied
if ($res.StartsWith("ДА:")) {
Ok "Сторонний режим включится: $($res.Substring(4))"
} else {
Warn $res.Substring(5)
Write-Host " Открой Developer -> Configure Third-Party Inference и выбери конфигурацию вручную."
}
Restart-Claude
# ---------- Что дальше ----------
Write-Host ""
Write-Host "Что дальше:"
if ($haveOllama) {
Write-Host " · Открой новый чат и кликни по имени модели рядом с полем ввода:"
Write-Host " там GLM-5.3 и GLM-5.3 Flash, а на платном тарифе Ollama ещё и Kimi K3."
Write-Host " · Программа Ollama должна быть запущена: закрыл её, мост отвалился."
}
if ($brain -eq "zai" -or $brain -eq "both") {
Write-Host " · Z.ai: возьми ключ на z.ai/manage-apikey/apikey-list, затем в приложении"
Write-Host " Developer -> Configure Third-Party Inference -> конфигурация zai-glm ->"
Write-Host " вставь ключ в Gateway API key -> Apply Changes -> перезапусти Claude."
}
Write-Host " · Нет пункта Developer? Полностью закрой Claude и открой заново."
Write-Host " · Вернуть родного Claude: `$env:AK='restore' и та же команда."