Files
claude-brains/setup.ps1
T

337 lines
17 KiB
PowerShell
Raw 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 не затрагивается.
#
# ВНИМАНИЕ при правке: сохранять в 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"
$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 }
# 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 }
}
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»"
}
# Повторяет пункт меню Developer -> Clear Cache and Restart. В коде приложения он
# чистит ТОЛЬКО кэши: HTTP, скомпилированный JS, шейдеры, service workers, DNS.
# Ни cookies, ни Local Storage там нет, поэтому вход в аккаунт, чаты, MCP,
# скиллы и наши конфиги остаются на месте. Делать только при закрытом приложении.
# ВАЖНО: не путать с соседним пунктом Reset App Data, тот сносит папку целиком.
function Clear-Caches {
foreach ($d in @($C1P, $C3P)) {
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
$exe = Find-ClaudeExe
if (-not $exe) { Warn "Не нашёл claude.exe, открой Claude вручную."; return }
Start-Process $exe.FullName
# Проверяем, что процесс реально поднялся: рапортовать успех вслепую нельзя.
$j = 0
while (-not (Get-Process -Name "Claude*" -ErrorAction SilentlyContinue) -and $j -lt 8) { 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"
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 (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. Выбор мозга ----------
$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", ""),
@("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/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://localhost:11434",
"inferenceGatewayApiKey": "ollama",
"inferenceGatewayAuthScheme": "bearer",
"inferenceCredentialKind": "static",
"modelDiscoveryEnabled": false,
"inferenceModels": [
{ "name": "claude-sonnet-4-6", "labelOverride": "GLM-5.3 (Ollama Cloud)", "anthropicFamilyTier": "sonnet", "isFamilyDefault": true },
{ "name": "claude-opus-4-6", "labelOverride": "Kimi K3 (Ollama Cloud)", "anthropicFamilyTier": "opus", "isFamilyDefault": true },
{ "name": "claude-haiku-4-5", "labelOverride": "GLM-5.3 Flash (Ollama Cloud)", "anthropicFamilyTier": "haiku", "isFamilyDefault": true }
]
}
'@
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 }
# ---------- Самопроверка ----------
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' и та же команда."