Скрипт не находил приложение, если оно стоит как пакет MSIX в C:\Program Files\WindowsApps\Claude_<версия>_x64__pzs8sxrjxfjjc. - Поиск через Get-AppxPackage с запасным путём по папке Packages. - Запуск пакетного приложения по shell:AppsFolder\<PackageFamilyName>!Claude, запасные варианты: explorer.exe, alias в WindowsApps, протокол claude://. - Сторонний режим у обеих сборок в %LOCALAPPDATA%\Claude-3p (по коду приложения 1.44121.2, в манифесте MSIX папка исключена из виртуализации). - Флаг разработчика кладётся во все родные папки: %APPDATA%\Claude, LocalCache\Roaming\Claude у MSIX, %LOCALAPPDATA%\Claude. - Честная проверка перезапуска и в setup.sh для Windows.
612 lines
31 KiB
Bash
612 lines
31 KiB
Bash
#!/bin/bash
|
||
# =============================================================
|
||
# Claude на чужих мозгах — настройщик (Мак и Windows, один файл)
|
||
#
|
||
# Запуск (Мак, Терминал):
|
||
# curl -fsSL {URL}/setup.sh | bash -s ollama только Ollama
|
||
# curl -fsSL {URL}/setup.sh | bash -s zai только Z.ai
|
||
# curl -fsSL {URL}/setup.sh | bash -s both обе конфигурации
|
||
# curl -fsSL {URL}/setup.sh | bash спросит, что ставить
|
||
# curl -fsSL {URL}/setup.sh | bash -s restore вернуть родного Claude
|
||
#
|
||
# Что делает: находит УЖЕ УСТАНОВЛЕННЫЕ приложения, кладёт конфиги
|
||
# в приложение Claude и честно его перезапускает.
|
||
# НИЧЕГО НЕ СКАЧИВАЕТ И НЕ УСТАНАВЛИВАЕТ: Claude и Ollama ставятся
|
||
# руками по ссылкам из гайда. Трогает только приложение Claude,
|
||
# терминальный Claude Code не затрагивается.
|
||
#
|
||
# Флаг --no-restart: не перезапускать приложение.
|
||
# =============================================================
|
||
set -euo pipefail
|
||
|
||
# ---------- ОС и пути ----------
|
||
case "$(uname -s)" in
|
||
Darwin) OS=mac ;;
|
||
MINGW*|MSYS*|CYGWIN*) OS=win ;;
|
||
*) echo "Эта система не поддерживается (нужен Мак или Windows с Git Bash)."; exit 1 ;;
|
||
esac
|
||
|
||
# ---------- Обнаружение приложения (Windows) ----------
|
||
# Сборок две, и ведут они себя по-разному.
|
||
# · Классическая (Squirrel): %LOCALAPPDATA%\AnthropicClaude\app-<версия>\claude.exe,
|
||
# родные данные в %APPDATA%\Claude.
|
||
# · MSIX (Microsoft Store и установщик с claude.com): C:\Program Files\WindowsApps\
|
||
# Claude_<версия>_x64__pzs8sxrjxfjjc\app\Claude.exe. Папка закрыта системой, exe
|
||
# оттуда напрямую не запускается, приложение поднимают по идентификатору пакета,
|
||
# а родные данные Windows подменяет на
|
||
# %LOCALAPPDATA%\Packages\Claude_pzs8sxrjxfjjc\LocalCache\Roaming\Claude.
|
||
# Сторонний режим у ОБЕИХ сборок живёт в %LOCALAPPDATA%\Claude-3p: так прописано
|
||
# в коде приложения, а в манифесте MSIX эта папка исключена из подмены.
|
||
WIN_KIND=""; WIN_AUMID=""; WIN_PFN=""; WIN_EXE=""; WIN_VER=""
|
||
WIN_PROBE='
|
||
$ErrorActionPreference = "SilentlyContinue"
|
||
$exe = @("$env:LOCALAPPDATA\AnthropicClaude", "$env:LOCALAPPDATA\Programs") | Where-Object { Test-Path $_ } |
|
||
ForEach-Object { Get-ChildItem $_ -Recurse -Filter "claude.exe" -ErrorAction SilentlyContinue } | Select-Object -First 1
|
||
$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*" | 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 { }
|
||
$pfn = ""; $aumid = ""; $ver = ""
|
||
if ($pkg) {
|
||
$pfn = "$($pkg.PackageFamilyName)"; $ver = "$($pkg.Version)"; $id = "Claude"
|
||
try { $ids = @((Get-AppxPackageManifest $pkg).Package.Applications.Application.Id); if ($ids.Count -gt 0 -and $ids[0]) { $id = "$($ids[0])" } } catch { }
|
||
$aumid = "$pfn!$id"
|
||
} else {
|
||
$d = Get-ChildItem (Join-Path $env:LOCALAPPDATA "Packages") -Directory -Filter "Claude_*" | Select-Object -First 1
|
||
if ($d) { $pfn = $d.Name; $aumid = "$pfn!Claude" }
|
||
}
|
||
$kind = ""
|
||
if ($pfn) { $launched = Test-Path (Join-Path $env:LOCALAPPDATA "Packages\$pfn\LocalCache\Roaming\Claude"); if ($launched -or -not $exe) { $kind = "msix" } }
|
||
if (-not $kind -and $exe) { $kind = "classic" }
|
||
$exePath = ""; if ($exe) { $exePath = $exe.FullName }
|
||
Write-Output "$kind|$aumid|$pfn|$exePath|$ver"
|
||
'
|
||
win_probe() {
|
||
local out
|
||
out="$(powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "$WIN_PROBE" 2>/dev/null | tr -d '\r' | tail -n 1)" || out=""
|
||
IFS='|' read -r WIN_KIND WIN_AUMID WIN_PFN WIN_EXE WIN_VER <<EOF
|
||
$out
|
||
EOF
|
||
}
|
||
|
||
# ---------- Пути ----------
|
||
# P1_DIRS: родные папки приложения, туда идёт только флаг разработчика и чистка
|
||
# кэшей. Их несколько, потому что настоящая зависит от сборки, а лишний флаг
|
||
# ничему не вредит. C3P: папка стороннего режима, туда идут конфиги.
|
||
if [ "$OS" = "mac" ]; then
|
||
C3P="$HOME/Library/Application Support/Claude-3p"
|
||
P1_DIRS=("$HOME/Library/Application Support/Claude")
|
||
else
|
||
win_probe
|
||
WLA="$(cygpath -u "$LOCALAPPDATA")"
|
||
WAP="$(cygpath -u "$APPDATA")"
|
||
C3P="$WLA/Claude-3p"
|
||
P1_DIRS=()
|
||
if [ "$WIN_KIND" = "msix" ]; then P1_DIRS+=("$WLA/Packages/$WIN_PFN/LocalCache/Roaming/Claude"); fi
|
||
for d in "$WAP/Claude" "$WLA/Claude" "$WLA/Claude-Data"; do
|
||
if [ -d "$d" ] || [ "${#P1_DIRS[@]}" -eq 0 ]; then P1_DIRS+=("$d"); fi
|
||
done
|
||
fi
|
||
LIB="$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"
|
||
|
||
MODE=install
|
||
BRAIN="${AK:-}"
|
||
NO_RESTART=false
|
||
for a in "$@"; do
|
||
case "$a" in
|
||
restore) MODE=restore ;;
|
||
ollama|zai|both) BRAIN="$a" ;;
|
||
--no-restart) NO_RESTART=true ;;
|
||
esac
|
||
done
|
||
[ "$BRAIN" = "restore" ] && { MODE=restore; BRAIN=""; }
|
||
|
||
say() { printf '\n\033[1m%s\033[0m\n' "$*"; }
|
||
ok() { printf ' \033[32m[ok]\033[0m %s\n' "$*"; }
|
||
warn() { printf ' \033[33m[!]\033[0m %s\n' "$*"; }
|
||
die() { printf ' \033[31m[стоп]\033[0m %s\n' "$*"; exit 1; }
|
||
|
||
# ---------- JSON-хелпер ----------
|
||
PY=""
|
||
if command -v python3 >/dev/null 2>&1; then PY=python3
|
||
elif command -v python >/dev/null 2>&1; then PY=python
|
||
fi
|
||
have_ps() { command -v powershell.exe >/dev/null 2>&1; }
|
||
[ -n "$PY" ] || have_ps || die "Нужен python3 (Мак) или powershell (Windows) для работы с JSON."
|
||
|
||
# ---------- Флаг разработчика ----------
|
||
# ВАЖНО. Приложение читает developer_settings.json из ТЕКУЩЕЙ папки данных,
|
||
# а она переключается на -3p только когда сторонний режим уже взлетел.
|
||
# Конфиги при этом всегда читаются из -3p. Замкнутый круг: если положить
|
||
# флаг только в -3p, до переключения приложение его не увидит и пункта
|
||
# Developer не будет. Поэтому пишем в ОБЕ папки.
|
||
write_dev_flag() {
|
||
local d
|
||
for d in "${P1_DIRS[@]}" "$C3P"; do
|
||
mkdir -p "$d"
|
||
printf '{"allowDevTools": true}\n' > "$d/developer_settings.json"
|
||
done
|
||
ok "Режим разработчика включён во всех папках приложения."
|
||
}
|
||
|
||
# ---------- deploymentMode ----------
|
||
# Ключ "deploymentMode": "1p" намертво запрещает сторонний режим.
|
||
clear_deployment_pin() {
|
||
local f="$C3P/claude_desktop_config.json"
|
||
[ -f "$f" ] || return 0
|
||
[ -n "$PY" ] || return 0
|
||
"$PY" - "$f" <<'PY'
|
||
import json, sys
|
||
p = sys.argv[1]
|
||
try:
|
||
d = json.load(open(p, encoding="utf-8"))
|
||
except Exception:
|
||
sys.exit(0)
|
||
if isinstance(d, dict) and d.get("deploymentMode") == "1p":
|
||
d.pop("deploymentMode", None)
|
||
json.dump(d, open(p, "w", encoding="utf-8"), indent=2, ensure_ascii=False)
|
||
print("снят")
|
||
PY
|
||
}
|
||
|
||
# ---------- Чистка BOM ----------
|
||
# Приложение падает на BOM: в логе видно
|
||
# [managedConfig] Failed to read config library meta: Unexpected token
|
||
# и тогда ВСЯ библиотека конфигов не читается. BOM приезжает из Блокнота на
|
||
# Windows (он по умолчанию сохраняет UTF-8 именно так) и из старых скриптов.
|
||
repair_json_bom() {
|
||
[ -n "$PY" ] || return 0
|
||
"$PY" - "$LIB" "${P1_DIRS[@]}" "$C3P" <<'PY'
|
||
import os, sys
|
||
lib = sys.argv[1]
|
||
dirs = sys.argv[2:]
|
||
targets = []
|
||
if os.path.isdir(lib):
|
||
targets += [os.path.join(lib, f) for f in os.listdir(lib) if f.endswith(".json")]
|
||
for d in dirs:
|
||
for n in ("developer_settings.json", "claude_desktop_config.json"):
|
||
p = os.path.join(d, n)
|
||
if os.path.isfile(p): targets.append(p)
|
||
fixed = 0
|
||
for p in targets:
|
||
try:
|
||
b = open(p, "rb").read()
|
||
if b.startswith(b"\xef\xbb\xbf"):
|
||
open(p, "wb").write(b[3:]); fixed += 1
|
||
except Exception:
|
||
pass
|
||
if fixed: print(fixed)
|
||
PY
|
||
}
|
||
|
||
# merge_meta <applied: ollama|zai|default> <routes: ollama|zai|both|none>
|
||
merge_meta() {
|
||
local applied="$1" routes="$2"
|
||
if [ -n "$PY" ]; then
|
||
"$PY" - "$LIB" "$ID_OLLAMA" "$ID_ZAI" "$ID_DEFAULT" "$applied" "$routes" <<'PY'
|
||
import json, os, sys
|
||
lib, id_o, id_z, id_d, applied, routes = sys.argv[1:7]
|
||
p = os.path.join(lib, "_meta.json")
|
||
try:
|
||
meta = json.load(open(p, encoding="utf-8"))
|
||
if not isinstance(meta, dict): meta = {}
|
||
except Exception:
|
||
meta = {}
|
||
ent = meta.get("entries") or []
|
||
def ensure(i, n):
|
||
e = next((x for x in ent if x.get("id") == i), None)
|
||
if e: e["name"] = n
|
||
else: ent.append({"id": i, "name": n})
|
||
# Default обязан существовать как пустой конфиг: это точка возврата.
|
||
dflt = next((x for x in ent if x.get("name") == "Default"), None)
|
||
if dflt is None:
|
||
open(os.path.join(lib, id_d + ".json"), "w", encoding="utf-8").write("{}")
|
||
ensure(id_d, "Default")
|
||
dflt_id = id_d
|
||
else:
|
||
dflt_id = dflt["id"]
|
||
# Старые записи с нашими именами, но чужими id (от ручной настройки или прошлых
|
||
# версий) переименовываем, иначе в панели будут два одинаковых пункта.
|
||
ours = {id_o: "ollama-cloud", id_z: "zai-glm"}
|
||
for x in ent:
|
||
if x.get("name") in ours.values() and x.get("id") not in ours:
|
||
x["name"] = x["name"] + " (старый)"
|
||
if routes in ("ollama", "both"): ensure(id_o, "ollama-cloud")
|
||
if routes in ("zai", "both"): ensure(id_z, "zai-glm")
|
||
# Выкидываем записи, у которых файла на диске нет: они ломают выбор в панели.
|
||
ent = [x for x in ent if os.path.exists(os.path.join(lib, x.get("id", "") + ".json"))]
|
||
meta["entries"] = ent
|
||
meta["appliedId"] = {"ollama": id_o, "zai": id_z}.get(applied, dflt_id)
|
||
json.dump(meta, open(p, "w", encoding="utf-8"), indent=2, ensure_ascii=False)
|
||
PY
|
||
else
|
||
MERGE_LIB="$(cygpath -w "$LIB")" MERGE_APPLIED="$applied" MERGE_ROUTES="$routes" \
|
||
MERGE_IDO="$ID_OLLAMA" MERGE_IDZ="$ID_ZAI" MERGE_IDD="$ID_DEFAULT" \
|
||
powershell.exe -NoProfile -Command '
|
||
$lib=$env:MERGE_LIB; $p=Join-Path $lib "_meta.json"
|
||
if (Test-Path $p) { $meta=Get-Content $p -Raw -Encoding UTF8 | ConvertFrom-Json }
|
||
else { $meta=[pscustomobject]@{appliedId="";entries=@()} }
|
||
function Ensure($id,$name){
|
||
$e=$script:meta.entries | Where-Object { $_.id -eq $id }
|
||
if($e){$e.name=$name}
|
||
else{$script:meta.entries=@($script:meta.entries)+[pscustomobject]@{id=$id;name=$name}}
|
||
}
|
||
if(-not ($meta.entries | Where-Object {$_.name -eq "Default"})){
|
||
"{}" | Set-Content (Join-Path $lib ($env:MERGE_IDD+".json")) -Encoding UTF8
|
||
Ensure $env:MERGE_IDD "Default"
|
||
}
|
||
if($env:MERGE_ROUTES -eq "ollama" -or $env:MERGE_ROUTES -eq "both"){ Ensure $env:MERGE_IDO "ollama-cloud" }
|
||
if($env:MERGE_ROUTES -eq "zai" -or $env:MERGE_ROUTES -eq "both"){ Ensure $env:MERGE_IDZ "zai-glm" }
|
||
$meta.entries = @($meta.entries | Where-Object { Test-Path (Join-Path $lib ($_.id+".json")) })
|
||
$def=($meta.entries | Where-Object {$_.name -eq "Default"} | Select-Object -First 1).id
|
||
switch($env:MERGE_APPLIED){
|
||
"ollama" { $meta.appliedId=$env:MERGE_IDO }
|
||
"zai" { $meta.appliedId=$env:MERGE_IDZ }
|
||
default { $meta.appliedId=$def }
|
||
}
|
||
$meta | ConvertTo-Json -Depth 5 | Set-Content $p -Encoding UTF8
|
||
'
|
||
fi
|
||
}
|
||
|
||
# Проверяем, что применённый конфиг действительно включает сторонний режим.
|
||
check_applied() {
|
||
[ -n "$PY" ] || return 0
|
||
"$PY" - "$LIB" <<'PY'
|
||
import json, os, sys
|
||
lib = sys.argv[1]
|
||
try:
|
||
meta = json.load(open(os.path.join(lib, "_meta.json"), encoding="utf-8"))
|
||
applied = meta.get("appliedId", "")
|
||
cfg = json.load(open(os.path.join(lib, applied + ".json"), encoding="utf-8"))
|
||
except Exception as e:
|
||
print("НЕТ: не смог прочитать конфиг (%s)" % e); sys.exit(0)
|
||
name = next((x.get("name") for x in meta.get("entries", []) if x.get("id") == applied), applied)
|
||
print("ДА: %s" % name if cfg.get("inferenceProvider") else "НЕТ: применён пустой конфиг «%s»" % name)
|
||
PY
|
||
}
|
||
|
||
# ---------- Чистка кэшей ----------
|
||
# Повторяет пункт меню Developer -> Clear Cache and Restart. В коде приложения он
|
||
# чистит ТОЛЬКО кэши: HTTP, скомпилированный JS, шейдеры, service workers, DNS.
|
||
# Ни cookies, ни Local Storage там нет, поэтому вход в аккаунт, чаты, MCP,
|
||
# скиллы и наши конфиги остаются на месте. Делать это можно лишь при закрытом
|
||
# приложении, иначе файлы заняты.
|
||
# ВАЖНО: не путать с соседним пунктом Reset App Data, тот сносит папку целиком.
|
||
clear_caches() {
|
||
local d
|
||
for d in "${P1_DIRS[@]}" "$C3P"; do
|
||
[ -d "$d" ] || continue
|
||
rm -rf "$d/Cache" "$d/Code Cache" "$d/GPUCache" \
|
||
"$d/DawnGraphiteCache" "$d/DawnWebGPUCache" "$d/Service Worker" 2>/dev/null || true
|
||
done
|
||
ok "Кэши очищены (чаты, входы и конфиги не тронуты)."
|
||
}
|
||
|
||
# ---------- Обнаружение приложения (Мак) ----------
|
||
# pgrep тут ненадёжен: под песочницей он не видит процесс, хотя тот живой.
|
||
# Единственный путь приложения, который нам нужен, это главный бинарь в
|
||
# /Applications. Рядом крутится встроенный агент по похожему пути внутри
|
||
# папки данных, и его трогать нельзя, поэтому сравниваем строго целиком.
|
||
CLAUDE_BIN="/Applications/Claude.app/Contents/MacOS/Claude"
|
||
|
||
claude_pids() { ps -Ao pid=,comm= | awk -v b="$CLAUDE_BIN" '$2==b {print $1}'; }
|
||
|
||
claude_running() {
|
||
case "$(osascript -e 'application "Claude" is running' 2>/dev/null)" in
|
||
true) return 0 ;;
|
||
esac
|
||
[ -n "$(claude_pids)" ]
|
||
}
|
||
|
||
# ---------- Честный перезапуск ----------
|
||
# open -a на живом процессе только активирует окно: приложение не завершается
|
||
# при закрытии окна и настройки перечитывает лишь при настоящем старте.
|
||
restart_claude() {
|
||
if [ "$NO_RESTART" = "true" ]; then warn "Перезапуск пропущен (--no-restart). Настройки применятся после Cmd+Q и запуска."; return 0; fi
|
||
say "Перезапускаю приложение Claude..."
|
||
if [ "$OS" = "mac" ]; then
|
||
if claude_running; then
|
||
osascript -e 'tell application "Claude" to quit' >/dev/null 2>&1 || true
|
||
i=0; while claude_running && [ "$i" -lt 12 ]; do sleep 1; i=$((i+1)); done
|
||
if claude_running; then
|
||
for pid in $(claude_pids); do kill "$pid" 2>/dev/null || true; done
|
||
sleep 3
|
||
fi
|
||
if claude_running; then
|
||
warn "Claude не закрылся сам (обычно это запрет на автоматизацию:"
|
||
echo " Системные настройки -> Конфиденциальность и безопасность -> Автоматизация)."
|
||
echo " Конфиги уже на месте. Закрой Claude вручную через Cmd+Q и открой заново."
|
||
return 0
|
||
fi
|
||
fi
|
||
clear_caches
|
||
open -a "Claude" >/dev/null 2>&1 || { warn "Не смог открыть Claude, открой вручную."; return 0; }
|
||
# Проверяем, что процесс реально поднялся: рапортовать успех вслепую нельзя.
|
||
j=0; while ! claude_running && [ "$j" -lt 10 ]; do sleep 1; j=$((j+1)); done
|
||
if ! claude_running; then
|
||
warn "Claude не поднялся автоматически. Конфиги на месте, открой приложение вручную."
|
||
return 0
|
||
fi
|
||
else
|
||
powershell.exe -NoProfile -Command '
|
||
Get-Process -Name "Claude*" -ErrorAction SilentlyContinue | Stop-Process -Force
|
||
Start-Sleep 3' 2>/dev/null || true
|
||
clear_caches
|
||
if [ "$WIN_KIND" = "msix" ]; then
|
||
# Пакетное приложение запускают по идентификатору, а не по пути к exe.
|
||
AK_AUMID="$WIN_AUMID" powershell.exe -NoProfile -Command '
|
||
$t = "shell:AppsFolder\" + $env:AK_AUMID
|
||
try { Start-Process $t -ErrorAction Stop; exit 0 } catch { }
|
||
try { Start-Process explorer.exe $t -ErrorAction Stop; exit 0 } catch { }
|
||
$a = Join-Path $env:LOCALAPPDATA "Microsoft\WindowsApps\Claude.exe"
|
||
if (Test-Path $a) { try { Start-Process $a -ErrorAction Stop; exit 0 } catch { } }
|
||
try { Start-Process "claude://" -ErrorAction Stop; exit 0 } catch { }
|
||
exit 1
|
||
' || { warn "Не смог запустить Claude, открой его вручную из меню Пуск."; return 0; }
|
||
elif [ -n "$WIN_EXE" ]; then
|
||
AK_EXE="$WIN_EXE" powershell.exe -NoProfile -Command 'Start-Process $env:AK_EXE' \
|
||
|| { warn "Не смог перезапустить Claude, закрой и открой его вручную."; return 0; }
|
||
else
|
||
warn "Не нашёл приложение Claude, открой его вручную."; return 0
|
||
fi
|
||
# Проверяем, что процесс реально поднялся: рапортовать успех вслепую нельзя.
|
||
j=0
|
||
while [ "$j" -lt 12 ]; do
|
||
if powershell.exe -NoProfile -Command 'if (Get-Process -Name "Claude*" -ErrorAction SilentlyContinue) { exit 0 } else { exit 1 }' 2>/dev/null; then break; fi
|
||
sleep 1; j=$((j+1))
|
||
done
|
||
if [ "$j" -ge 12 ]; then
|
||
warn "Claude не поднялся автоматически. Конфиги на месте, открой приложение вручную."
|
||
return 0
|
||
fi
|
||
fi
|
||
ok "Claude перезапущен."
|
||
}
|
||
|
||
# ---------- TLS-фикс песочницы (только Мак, для вкладки Code) ----------
|
||
apply_tls_fix() {
|
||
[ "$OS" = "mac" ] || return 0
|
||
[ -n "$PY" ] || return 0
|
||
local sj="$HOME/.claude/settings.json"
|
||
mkdir -p "$HOME/.claude"
|
||
[ -f "$sj" ] || echo '{}' > "$sj"
|
||
"$PY" - "$sj" <<'PY'
|
||
import json, sys
|
||
p = sys.argv[1]
|
||
try:
|
||
d = json.load(open(p, encoding="utf-8"))
|
||
if not isinstance(d, dict): d = {}
|
||
except Exception:
|
||
d = {}
|
||
sb = d.get("sandbox")
|
||
if not isinstance(sb, dict): sb = {}
|
||
if sb.get("enableWeakerNetworkIsolation") is not True:
|
||
sb["enableWeakerNetworkIsolation"] = True
|
||
d["sandbox"] = sb
|
||
json.dump(d, open(p, "w", encoding="utf-8"), indent=2, ensure_ascii=False)
|
||
print("добавлен")
|
||
else:
|
||
print("уже был")
|
||
PY
|
||
}
|
||
|
||
# ---------- ОТКАТ ----------
|
||
if [ "$MODE" = "restore" ]; then
|
||
say "Откат на родного Claude..."
|
||
if [ ! -f "$LIB/_meta.json" ]; then ok "Конфигов нет, откатывать нечего."; exit 0; fi
|
||
merge_meta default none
|
||
repair_json_bom >/dev/null 2>&1 || true
|
||
ok "Применён пустой профиль Default."
|
||
echo " Если приложение всё равно открывается на чужом мозге: меню аккаунта внизу слева -> Sign out,"
|
||
echo " затем на экране входа выбери обычный вход по почте или Google."
|
||
write_dev_flag
|
||
restart_claude
|
||
echo
|
||
echo "Если что-то осталось сломанным, полный сброс (сотрёт чаты стороннего режима):"
|
||
echo " rm -rf \"$C3P\""
|
||
exit 0
|
||
fi
|
||
|
||
# ---------- 1. Приложение Claude ----------
|
||
say "[1/4] Ищу приложение Claude..."
|
||
CLAUDE_OK=false
|
||
if [ "$OS" = "mac" ] && [ -d "/Applications/Claude.app" ]; then CLAUDE_OK=true; fi
|
||
if [ "$OS" = "win" ] && [ -n "$WIN_KIND" ]; then CLAUDE_OK=true; fi
|
||
if [ "$CLAUDE_OK" = "true" ]; then
|
||
case "$WIN_KIND" in
|
||
msix) ok "Claude на месте: сборка MSIX (Microsoft Store или установщик с claude.com)${WIN_VER:+, версия $WIN_VER}." ;;
|
||
classic) ok "Claude на месте: классическая сборка." ;;
|
||
*) ok "Claude на месте." ;;
|
||
esac
|
||
ok "Конфиги стороннего режима: $C3P"
|
||
else
|
||
warn "Приложение Claude не найдено, а без него настраивать нечего."
|
||
[ "$OS" = "win" ] && echo " Смотрел классическую сборку в AppData\\Local и пакеты Microsoft Store."
|
||
echo
|
||
echo " Поставь его руками, это одна минута:"
|
||
echo " 1. Открой https://claude.com/download и скачай версию для своей системы."
|
||
echo " 2. Установи и запусти приложение хотя бы один раз."
|
||
echo " 3. Вернись сюда и запусти эту же команду снова."
|
||
echo
|
||
echo " Логиниться в аккаунт Anthropic не обязательно."
|
||
exit 1
|
||
fi
|
||
|
||
# ---------- 2. Выбор мозга ----------
|
||
if [ -z "$BRAIN" ]; then
|
||
say "[2/4] Какой мозг подключаем?"
|
||
echo " 1) Ollama бесплатный старт, программа Ollama должна быть установлена и запущена"
|
||
echo " 2) Z.ai по подписке от \$18, локально ничего не нужно, только ключ"
|
||
echo " 3) оба положу обе конфигурации, переключаться будешь в приложении"
|
||
printf " Твой выбор [1/2/3, по умолчанию 3]: "
|
||
choice=""
|
||
[ -r /dev/tty ] && { read -r choice < /dev/tty || choice=""; }
|
||
case "${choice:-3}" in
|
||
1) BRAIN=ollama ;;
|
||
2) BRAIN=zai ;;
|
||
*) BRAIN=both ;;
|
||
esac
|
||
else
|
||
say "[2/4] Режим: $BRAIN"
|
||
fi
|
||
case "$BRAIN" in
|
||
ollama) ok "Ставлю только Ollama." ;;
|
||
zai) ok "Ставлю только Z.ai." ;;
|
||
both) ok "Ставлю обе конфигурации." ;;
|
||
*) warn "Непонятный режим «$BRAIN», беру оба."; BRAIN=both ;;
|
||
esac
|
||
|
||
# ---------- 3. Ollama и псевдонимы ----------
|
||
HAVE_OLLAMA=false
|
||
if [ "$BRAIN" = "ollama" ] || [ "$BRAIN" = "both" ]; then
|
||
say "[3/4] Проверяю программу Ollama..."
|
||
OLLAMA_BIN=""
|
||
if command -v ollama >/dev/null 2>&1; then OLLAMA_BIN="ollama"
|
||
elif [ "$OS" = "mac" ] && [ -x "/Applications/Ollama.app/Contents/Resources/ollama" ]; then OLLAMA_BIN="/Applications/Ollama.app/Contents/Resources/ollama"
|
||
fi
|
||
|
||
if [ -z "$OLLAMA_BIN" ]; then
|
||
warn "Ollama не найдена."
|
||
echo
|
||
echo " Поставь её руками:"
|
||
echo " 1. Открой https://ollama.com/download и скачай версию для своей системы."
|
||
echo " 2. Установи, запусти, войди в аккаунт (иконка ламы -> Sign in)."
|
||
echo " 3. Вернись сюда и запусти команду снова."
|
||
echo
|
||
if [ "$BRAIN" = "ollama" ]; then
|
||
echo " Или подключи Z.ai, ему Ollama не нужна: та же команда со словом zai вместо ollama."
|
||
exit 1
|
||
fi
|
||
warn "Пропускаю Ollama, кладу только конфиг Z.ai."
|
||
BRAIN=zai
|
||
else
|
||
ok "Ollama на месте."
|
||
[ "$OS" = "mac" ] && { open -a "Ollama" >/dev/null 2>&1 || true; }
|
||
HAVE_OLLAMA=true
|
||
say " Создаю псевдонимы облачных моделей (нужен вход в аккаунт Ollama)..."
|
||
# Приложение пускает только точные идентификаторы моделей Claude,
|
||
# поэтому облачная модель получает второе, «правильное» имя.
|
||
make_alias() {
|
||
"$OLLAMA_BIN" pull "$1" >/dev/null 2>&1 || {
|
||
if [ "${3:-}" = "optional" ]; then
|
||
warn "$1 недоступна на твоём тарифе Ollama, пропускаю (нужен Pro или Max)."
|
||
return 0
|
||
fi
|
||
warn "Не получилось скачать $1. Обычно это нет входа в аккаунт Ollama."
|
||
echo " Открой окно Ollama (иконка ламы) -> Sign in, затем запусти команду снова."
|
||
exit 1
|
||
}
|
||
"$OLLAMA_BIN" cp "$1" "$2" >/dev/null
|
||
ok "$2 -> $1"
|
||
}
|
||
make_alias "glm-5.3:cloud" "claude-sonnet-4-6"
|
||
make_alias "kimi-k3:cloud" "claude-opus-4-6"
|
||
make_alias "glm-5.3-flash:cloud" "claude-haiku-4-5"
|
||
make_alias "qwen3.5:397b-cloud" "claude-sonnet-4-5" optional
|
||
make_alias "deepseek-v4-pro:0813-cloud" "claude-opus-4-1" optional
|
||
make_alias "gpt-oss:120b-cloud" "claude-haiku-3-5" optional
|
||
make_alias "minimax-m3:cloud" "claude-3-7-sonnet" optional
|
||
make_alias "nemotron-3-super:cloud" "claude-3-5-haiku" optional
|
||
fi
|
||
else
|
||
say "[3/4] Маршрут Z.ai, Ollama не нужна."
|
||
fi
|
||
|
||
# ---------- 4. Конфиги ----------
|
||
say "[4/4] Кладу конфиги в приложение Claude..."
|
||
mkdir -p "$LIB"
|
||
write_dev_flag
|
||
pin="$(clear_deployment_pin || true)"
|
||
[ -n "$pin" ] && ok "Снят запрет на сторонний режим (deploymentMode)."
|
||
|
||
if [ "$BRAIN" = "ollama" ] || [ "$BRAIN" = "both" ]; then
|
||
cat > "$LIB/$ID_OLLAMA.json" <<'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 }
|
||
]
|
||
}
|
||
JSON
|
||
ok "Конфиг ollama-cloud на месте."
|
||
fi
|
||
|
||
if [ "$BRAIN" = "zai" ] || [ "$BRAIN" = "both" ]; then
|
||
if [ -f "$LIB/$ID_ZAI.json" ] && ! grep -q "$ZAI_PLACEHOLDER" "$LIB/$ID_ZAI.json"; then
|
||
ok "Конфиг zai-glm уже настроен, ключ не трогаю."
|
||
else
|
||
cat > "$LIB/$ID_ZAI.json" <<JSON
|
||
{
|
||
"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 }
|
||
]
|
||
}
|
||
JSON
|
||
ok "Конфиг zai-glm на месте, ждёт твой ключ."
|
||
fi
|
||
fi
|
||
|
||
# Активной делаем ту конфигурацию, которая реально готова работать.
|
||
if [ "$HAVE_OLLAMA" = "true" ]; then merge_meta ollama "$BRAIN"; else merge_meta zai "$BRAIN"; fi
|
||
nbom="$(repair_json_bom || true)"
|
||
[ -n "$nbom" ] && ok "Убран BOM у $nbom файлов конфигурации (приложение их не читало)."
|
||
|
||
if [ "$OS" = "mac" ]; then
|
||
ok "TLS-фикс песочницы для вкладки Code: $(apply_tls_fix)"
|
||
fi
|
||
|
||
# ---------- Самопроверка ----------
|
||
say "Проверяю, что получилось..."
|
||
res="$(check_applied || true)"
|
||
case "$res" in
|
||
ДА:*) ok "Сторонний режим включится: ${res#ДА: }" ;;
|
||
НЕТ:*) warn "${res#НЕТ: }"
|
||
echo " Открой Developer -> Configure Third-Party Inference и выбери конфигурацию вручную." ;;
|
||
esac
|
||
|
||
restart_claude
|
||
|
||
# ---------- Что дальше ----------
|
||
echo
|
||
echo "Что дальше:"
|
||
if [ "$HAVE_OLLAMA" = "true" ]; then
|
||
echo " · Открой новый чат и кликни по имени модели рядом с полем ввода:"
|
||
echo " там GLM-5.3 и GLM-5.3 Flash, а на платном тарифе Ollama ещё и Kimi K3."
|
||
echo " · Программа Ollama должна быть запущена: закрыл её, мост отвалился."
|
||
fi
|
||
if [ "$BRAIN" = "zai" ] || [ "$BRAIN" = "both" ]; then
|
||
echo " · Z.ai: возьми ключ на z.ai/manage-apikey/apikey-list, затем в приложении"
|
||
echo " Developer -> Configure Third-Party Inference -> конфигурация zai-glm ->"
|
||
echo " вставь ключ в Gateway API key -> Apply Changes -> перезапусти Claude."
|
||
fi
|
||
echo " · Нет пункта Developer? Полностью выйди из Claude (Cmd+Q) и открой заново."
|
||
echo " · Вернуть родного Claude: та же команда со словом restore."
|