From 21657dcfc2b4c54cd0bc53879b0c85a346d1c772 Mon Sep 17 00:00:00 2001 From: giteaMyFirstvdsRoot Date: Tue, 1 Sep 2026 15:50:16 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20dev-=D1=84=D0=BB=D0=B0=D0=B3=20=D0=B2=20?= =?UTF-8?q?=D0=BE=D0=B1=D0=B5=20=D0=BF=D0=B0=D0=BF=D0=BA=D0=B8,=20=D1=87?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=BD=D1=8B=D0=B9=20=D1=80=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D1=80=D1=82,=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BA=D0=B0=20applied,=20=D0=B0=D1=80=D0=B3=D1=83=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.ps1 | 359 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 241 insertions(+), 118 deletions(-) diff --git a/setup.ps1 b/setup.ps1 index 4cf642b..0a20a7d 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -1,129 +1,239 @@ # ============================================================= -# Claude на чужих мозгах — установщик (Windows) -# Что делает: проверяет Claude и Ollama, создаёт псевдонимы -# моделей, кладёт два конфига (Ollama Cloud + Z.ai) в приложение -# Claude и перезапускает его. -# Запуск одной командой (Windows, PowerShell): -# irm {URL}/setup.ps1 | iex -# Откат: -# $env:SETUP_MODE='restore'; irm {URL}/setup.ps1 | iex +# 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 не затрагивается. +# +# ВНИМАНИЕ при правке файла: сохранять в UTF-8 С BOM, иначе +# PowerShell 5.1 читает кириллицу как ANSI и падает на разборе. # ============================================================= -param([string]$Mode = "install") -if ($env:SETUP_MODE) { $Mode = $env:SETUP_MODE; $env:SETUP_MODE = $null } +param([string]$Mode = "") +if ($env:AK) { $Mode = $env:AK; $env:AK = $null } $ErrorActionPreference = "Stop" -$C3P = Join-Path $env:LOCALAPPDATA "Claude-3p" +$C1P = Join-Path $env:LOCALAPPDATA "Claude" +$C3P = "$C1P-3p" $LIB = Join-Path $C3P "configLibrary" $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 } -function Read-Meta { - $metaPath = Join-Path $LIB "_meta.json" - if (Test-Path $metaPath) { Get-Content $metaPath -Raw | ConvertFrom-Json } - else { [pscustomobject]@{ appliedId = ""; entries = @() } } +# JSON приложение читает строго: BOM его ломает, поэтому пишем UTF-8 без BOM. +function Write-Utf8($path, $text) { + $enc = New-Object System.Text.UTF8Encoding($false) + [IO.File]::WriteAllText($path, $text, $enc) } -function Write-Meta($meta) { - $meta | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $LIB "_meta.json") -Encoding UTF8 +function Read-Json($path) { + if (-not (Test-Path $path)) { return $null } + try { Get-Content $path -Raw -Encoding UTF8 | ConvertFrom-Json } catch { $null } } -function Ensure-Entry($meta, $id, $name) { - $e = $meta.entries | Where-Object { $_.id -eq $id } - if ($e) { $e.name = $name } - else { $meta.entries = @($meta.entries) + [pscustomobject]@{ id = $id; name = $name } } - return $meta + +function Find-ClaudeExe { + @("$env:LOCALAPPDATA\AnthropicClaude", "$env:LOCALAPPDATA\Programs") | + ForEach-Object { Get-ChildItem $_ -Recurse -Filter "claude.exe" -ErrorAction SilentlyContinue } | + Select-Object -First 1 } + +# ВАЖНО. Приложение читает developer_settings.json из ТЕКУЩЕЙ папки данных, +# а она переключается на -3p только когда сторонний режим уже взлетел. +# Кладём флаг в ОБЕ папки, иначе пункт Developer не появится. +function Write-DevFlag { + foreach ($d in @($C1P, $C3P)) { + 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)." + } +} + +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" + } + 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»" +} + function Restart-Claude { Say "Перезапускаю приложение Claude..." - Get-Process -Name "Claude*" -ErrorAction SilentlyContinue | Stop-Process -Force - Start-Sleep 3 - Start-Process "claude://" -ErrorAction SilentlyContinue - if (-not $?) { - $exe = @("$env:LOCALAPPDATA\AnthropicClaude","$env:LOCALAPPDATA\Programs") | ForEach-Object { Get-ChildItem $_ -Recurse -Filter "claude.exe" -ErrorAction SilentlyContinue } | Select-Object -First 1 - if ($exe) { Start-Process $exe.FullName } else { Warn "Открой Claude вручную." } - } - Ok "Готово. Открой выбор модели в чате: там GLM-5.3 и Flash, а на платном тарифе Ollama ещё и Kimi K3." + 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++ } + $exe = Find-ClaudeExe + if ($exe) { Start-Process $exe.FullName; Ok "Claude перезапущен." } + else { Warn "Не нашёл claude.exe, открой Claude вручную." } } # ---------- ОТКАТ ---------- if ($Mode -eq "restore") { - Say "Откат на родной Claude..." - if (-not (Test-Path (Join-Path $LIB "_meta.json"))) { Ok "Конфигов нет, откатывать нечего."; exit } - $meta = Read-Meta - $def = $meta.entries | Where-Object { $_.name -eq "Default" } | Select-Object -First 1 - if (-not $def) { - "{}" | Set-Content (Join-Path $LIB "$ID_DEFAULT.json") -Encoding UTF8 - $meta = Ensure-Entry $meta $ID_DEFAULT "Default" - $def = $meta.entries | Where-Object { $_.id -eq $ID_DEFAULT } - } - $meta.appliedId = $def.id - Write-Meta $meta + Say "Откат на родного Claude..." + if (-not (Test-Path (Join-Path $LIB "_meta.json"))) { Ok "Конфигов нет, откатывать нечего."; return } + Merge-Meta "default" "none" Ok "Применён пустой конфиг Default: приложение вернётся к родному входу." + Write-DevFlag Restart-Claude Write-Host "`nЕсли что-то осталось сломанным, полный сброс (сотрёт чаты стороннего режима):" - Write-Host " удали папки $env:LOCALAPPDATA\Claude-3p и $env:LOCALAPPDATA\Claude Nest-3p" - exit + Write-Host " удали папку $C3P" + return } -# ---------- 1. Claude ---------- -Say "[1/5] Проверяю приложение Claude..." -$claudeExe = @("$env:LOCALAPPDATA\AnthropicClaude","$env:LOCALAPPDATA\Programs") | ForEach-Object { Get-ChildItem $_ -Recurse -Filter "claude.exe" -ErrorAction SilentlyContinue } | Select-Object -First 1 -if ($claudeExe) { Ok "Claude установлен." } -else { - Warn "Claude не найден. Открываю страницу загрузки..." - Start-Process "https://claude.com/download" - Write-Host " Установи Claude, запусти его один раз, войди в аккаунт, затем запусти скрипт снова." - exit +# ---------- 1. Приложение Claude ---------- +Say "[1/4] Ищу приложение Claude..." +if (Find-ClaudeExe) { + Ok "Claude на месте." +} else { + Warn "Приложение Claude не найдено, а без него настраивать нечего." + 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. Ollama ---------- -Say "[2/5] Проверяю программу Ollama..." -$haveOllama = [bool](Get-Command ollama -ErrorAction SilentlyContinue) -if ($haveOllama) { Ok "Ollama установлена." } -else { - $ans = Read-Host " Ollama не найдена. Скачать? Нужна для маршрута Ollama Cloud [Y/n]" - if ($ans -ne "n" -and $ans -ne "N") { - Start-Process "https://ollama.com/download/OllamaSetup.exe" - Write-Host " Установи Ollama, запусти, войди в аккаунт (Sign in), затем запусти скрипт снова." - exit - } else { Warn "Пропускаю Ollama: поставлю только конфиг Z.ai." } -} - -# ---------- 3. Псевдонимы ---------- -if ($haveOllama) { - Say "[3/5] Создаю псевдонимы облачных моделей (нужен вход в аккаунт Ollama)..." - # третий элемент "optional": модель платная (Kimi K3), её отсутствие не валит установку - $pairs = @( - @("glm-5.3:cloud", "claude-sonnet-4-6", ""), - @("glm-5.3-flash:cloud", "claude-haiku-4-5", ""), - @("kimi-k3:cloud", "claude-opus-4-6", "optional") - ) - foreach ($p in $pairs) { - & ollama pull $p[0] 2>$null - if ($LASTEXITCODE -ne 0) { - if ($p[2] -eq "optional") { - Warn "$($p[0]) недоступна на твоём тарифе Ollama, пропускаю (нужен Pro или Max)." - continue - } - Warn "Не получилось скачать $($p[0]). Обычно это нет входа в аккаунт Ollama, реже недоступность модели на твоём тарифе." - Write-Host " Открой окно Ollama -> Settings -> Sign in, затем запусти скрипт снова." - exit 1 - } - & ollama cp $p[0] $p[1] | Out-Null - Ok "$($p[1]) -> $($p[0])" +# ---------- 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 "[3/5] Ollama нет, псевдонимы пропускаю." } +} 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", ""), + @("glm-5.3-flash:cloud", "claude-haiku-4-5", ""), + @("kimi-k3:cloud", "claude-opus-4-6", "optional") + ) + foreach ($p in $pairs) { + & ollama pull $p[0] 2>$null | Out-Null + if ($LASTEXITCODE -ne 0) { + if ($p[2] -eq "optional") { + Warn "$($p[0]) недоступна на твоём тарифе Ollama, пропускаю (нужен Pro или Max)." + continue + } + Warn "Не получилось скачать $($p[0]). Обычно это нет входа в аккаунт Ollama." + Write-Host " Открой окно Ollama (иконка ламы) -> Sign in, затем запусти команду снова." + return + } + & ollama cp $p[0] $p[1] | Out-Null + 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/5] Кладу конфиги в приложение Claude..." +Say "[4/4] Кладу конфиги в приложение Claude..." New-Item -ItemType Directory -Force -Path $LIB | Out-Null -$devSettings = Join-Path $C3P "developer_settings.json" -if (-not (Test-Path $devSettings)) { '{"allowDevTools": true}' | Set-Content $devSettings -Encoding UTF8 } +Write-DevFlag +Clear-DeploymentPin -@' +if ($brain -eq "ollama" -or $brain -eq "both") { + Write-Utf8 (Join-Path $LIB "$ID_OLLAMA.json") @' { "inferenceProvider": "gateway", "inferenceGatewayBaseUrl": "http://localhost:11434", @@ -137,47 +247,60 @@ if (-not (Test-Path $devSettings)) { '{"allowDevTools": true}' | Set-Content $de { "name": "claude-haiku-4-5", "labelOverride": "GLM-5.3 Flash (Ollama Cloud)", "anthropicFamilyTier": "haiku", "isFamilyDefault": true } ] } -'@ | Set-Content (Join-Path $LIB "$ID_OLLAMA.json") -Encoding UTF8 +'@ + Ok "Конфиг ollama-cloud на месте." +} -# Не затираем уже вставленный ключ: перезаписываем только заглушку или отсутствующий файл -$zaiPath = Join-Path $LIB "$ID_ZAI.json" -$zaiHasKey = (Test-Path $zaiPath) -and -not ((Get-Content $zaiPath -Raw -Encoding UTF8) -match "ВСТАВЬ_СЮДА_СВОЙ_КЛЮЧ_ZAI") -if ($zaiHasKey) { Ok "Конфиг zai-glm уже настроен, ключ не трогаю." } -else { -@' +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", + "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, vision)", "anthropicFamilyTier": "haiku", "isFamilyDefault": true } + { "name": "claude-haiku-4-5", "labelOverride": "GLM-5.3 Flash (Z.ai)", "anthropicFamilyTier": "haiku", "isFamilyDefault": true } ] } -'@ | Set-Content $zaiPath -Encoding UTF8 +"@ + Ok "Конфиг zai-glm на месте, ждёт твой ключ." + } } -$meta = Read-Meta -if (-not ($meta.entries | Where-Object { $_.name -eq "Default" })) { - "{}" | Set-Content (Join-Path $LIB "$ID_DEFAULT.json") -Encoding UTF8 - $meta = Ensure-Entry $meta $ID_DEFAULT "Default" +if ($haveOllama) { Merge-Meta "ollama" $brain } else { Merge-Meta "zai" $brain } + +# ---------- Самопроверка ---------- +Say "Проверяю, что получилось..." +$res = Check-Applied +if ($res.StartsWith("ДА:")) { + Ok "Сторонний режим включится: $($res.Substring(4))" +} else { + Warn $res.Substring(5) + Write-Host " Открой Developer -> Configure Third-Party Inference и выбери конфигурацию вручную." } -$meta = Ensure-Entry $meta $ID_OLLAMA "ollama-cloud" -$meta = Ensure-Entry $meta $ID_ZAI "zai-glm" -if ($haveOllama) { $meta.appliedId = $ID_OLLAMA } -elseif (-not $meta.appliedId) { $meta.appliedId = $ID_DEFAULT } -Write-Meta $meta -Ok "Конфиги на месте." -# ---------- 5. Рестарт ---------- -if ($Mode -eq "no-restart") { Warn "Перезапуск Claude пропущен." } else { Restart-Claude } +Restart-Claude +# ---------- Что дальше ---------- Write-Host "" Write-Host "Что дальше:" -Write-Host " · Модель выбирается в чате, рядом с полем ввода." -Write-Host " · Конфиг Z.ai лежит рядом (панель Developer -> Configure Third-Party Inference):" -Write-Host " выбери zai-glm, вставь свой ключ в Gateway API key, Apply Changes, перезапуск." -Write-Host " · Вернуть родной Claude: powershell -File setup-windows.ps1 restore" +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' и та же команда."