feat(chatgpt): добавлен отдельный запуск через прокси
- Зачем: - приложение должно явно работать через прокси и сохранять настройку после обновлений. - Что: - добавлены отдельный ярлык ChatGPT Proxy и запускатель на основе proxy-client. - исключено смешение прямого и проксированного режимов существующего процесса. - установка, документация и автоматические проверки дополнены новым Stow-пакетом. - Проверка: - bash tests/test_chatgpt_proxy_launcher.sh. - bash tests/test_proxy_launchers.sh. - bash tests/test_install.sh.
This commit is contained in:
1 parent
039cf29b95
commit
cb600bcaa8
8 files changed
+462
-3
No files matched your search
@@ -54,6 +54,7 @@ Chezmoi использует отдельный source dir `chezmoi/` внутр
|
|||||||
| `wezterm` | Общий для Linux и Windows конфиг: одинаковые оформление и горячие клавиши |
|
| `wezterm` | Общий для Linux и Windows конфиг: одинаковые оформление и горячие клавиши |
|
||||||
| `kitty` | `kitty.conf` и тема; `~/.config/kitty` остаётся настоящим каталогом, чтобы файлы kitty не попадали в репозиторий |
|
| `kitty` | `kitty.conf` и тема; `~/.config/kitty` остаётся настоящим каталогом, чтобы файлы kitty не попадали в репозиторий |
|
||||||
| `vscode` | `server-env-setup` для VS Code Server в WSL: секреты + авто-прокси |
|
| `vscode` | `server-env-setup` для VS Code Server в WSL: секреты + авто-прокси |
|
||||||
|
| `chatgpt` | Отдельный ярлык `ChatGPT Proxy` с запретом прямого запуска при ошибке |
|
||||||
|
|
||||||
`install.sh` дополнительно настраивает opencode (симлинки, MCP-конфиг), claude (симлинкает `settings.local.json` из репозитория) и подключает bash_custom.
|
`install.sh` дополнительно настраивает opencode (симлинки, MCP-конфиг), claude (симлинкает `settings.local.json` из репозитория) и подключает bash_custom.
|
||||||
|
|
||||||
@@ -128,6 +129,15 @@ PowerShell и VS Code: одного общего файла у Windows и WSL н
|
|||||||
поправь и копии. Расхождение ловит `tests/test_proxy_env_sync.py` — раньше
|
поправь и копии. Расхождение ловит `tests/test_proxy_env_sync.py` — раньше
|
||||||
копии разъезжались, и один и тот же хост то шёл мимо прокси, то нет.
|
копии разъезжались, и один и тот же хост то шёл мимо прокси, то нет.
|
||||||
|
|
||||||
|
На Linux Stow-пакет `chatgpt` добавляет отдельный пользовательский ярлык
|
||||||
|
`ChatGPT Proxy`. Штатный `ChatGPT` остаётся прямым. Прокси-ярлык получает
|
||||||
|
активные настройки от `proxy-client`, передаёт Chromium явные параметры прокси
|
||||||
|
и сохраняет proxy-переменные для дочерних процессов. Повторный запуск поднимает
|
||||||
|
окно уже работающего проксированного экземпляра. Если ChatGPT открыт напрямую
|
||||||
|
или прокси недоступен, новый режим не подмешивается: GNOME показывает
|
||||||
|
уведомление с инструкцией. Файлы Debian-пакета не меняются, поэтому настройка
|
||||||
|
переживает его обновление.
|
||||||
|
|
||||||
### Git baseline
|
### Git baseline
|
||||||
|
|
||||||
На всех платформах фиксируются настройки для предсказуемого поведения:
|
На всех платформах фиксируются настройки для предсказуемого поведения:
|
||||||
@@ -197,6 +207,8 @@ PowerShell и VS Code: одного общего файла у Windows и WSL н
|
|||||||
| `~/.local/bin/start-claude-proxy.ps1` | Скрипт запуска Claude Desktop для Windows с переменными прокси и параметрами Electron |
|
| `~/.local/bin/start-claude-proxy.ps1` | Скрипт запуска Claude Desktop для Windows с переменными прокси и параметрами Electron |
|
||||||
| `~/.local/bin/start-claude-proxy.vbs` | Скрытый запуск PowerShell-скрипта без привязки Claude к видимому окну терминала |
|
| `~/.local/bin/start-claude-proxy.vbs` | Скрытый запуск PowerShell-скрипта без привязки Claude к видимому окну терминала |
|
||||||
| `~/.local/bin/start-codex-proxy.ps1` | Запуск Codex для Windows через системный идентификатор; прокси Electron задаётся параметрами, а `codex.exe` читает `~/.codex/.env` |
|
| `~/.local/bin/start-codex-proxy.ps1` | Запуск Codex для Windows через системный идентификатор; прокси Electron задаётся параметрами, а `codex.exe` читает `~/.codex/.env` |
|
||||||
|
| `~/.local/bin/start-chatgpt-proxy` | Запуск Linux-приложения ChatGPT через активную настройку `proxy-client` |
|
||||||
|
| `~/.local/share/applications/chatgpt-proxy.desktop` | Отдельный Linux-ярлык `ChatGPT Proxy`, не подменяющий штатный `ChatGPT` |
|
||||||
| `~/.config/powershell/profile.extra.ps1` | Локальные дополнения к pwsh-профилю |
|
| `~/.config/powershell/profile.extra.ps1` | Локальные дополнения к pwsh-профилю |
|
||||||
| `~/.config/powershell/profile.d/*.ps1` | Фрагменты, загружаемые по алфавиту |
|
| `~/.config/powershell/profile.d/*.ps1` | Фрагменты, загружаемые по алфавиту |
|
||||||
| `~/.claude/settings.json` | Управляется Claude Code: `model`, `effortLevel`, «always allow» permissions |
|
| `~/.claude/settings.json` | Управляется Claude Code: `model`, `effortLevel`, «always allow» permissions |
|
||||||
|
|||||||
Executable
+134
@@ -0,0 +1,134 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CHATGPT_BIN="${CHATGPT_BIN:-/usr/bin/chatgpt}"
|
||||||
|
CHATGPT_PROCESS_NAME="${CHATGPT_PROCESS_NAME:-ChatGPT}"
|
||||||
|
PROC_ROOT="${PROC_ROOT:-/proc}"
|
||||||
|
PROXY_CLIENT_SHELL="${PROXY_CLIENT_SHELL:-${XDG_CONFIG_HOME:-$HOME/.config}/proxy-client/shell.bash}"
|
||||||
|
|
||||||
|
fail_with_notification() {
|
||||||
|
local message="$1"
|
||||||
|
printf 'Ошибка: %s\n' "$message" >&2
|
||||||
|
if command -v notify-send >/dev/null 2>&1; then
|
||||||
|
notify-send \
|
||||||
|
--app-name='ChatGPT Proxy' \
|
||||||
|
--icon=chatgpt \
|
||||||
|
'ChatGPT Proxy' \
|
||||||
|
"$message" \
|
||||||
|
>/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
chromium_bypass_list() {
|
||||||
|
local source_list="$1" item result=''
|
||||||
|
local -a items=()
|
||||||
|
IFS=',' read -r -a items <<< "$source_list"
|
||||||
|
|
||||||
|
for item in "${items[@]}"; do
|
||||||
|
item="${item#"${item%%[![:space:]]*}"}"
|
||||||
|
item="${item%"${item##*[![:space:]]}"}"
|
||||||
|
[ -n "$item" ] || continue
|
||||||
|
case "$item" in
|
||||||
|
.*) item="*$item" ;;
|
||||||
|
esac
|
||||||
|
if [ -n "$result" ]; then
|
||||||
|
result+=';'
|
||||||
|
fi
|
||||||
|
result+="$item"
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '%s\n' "$result"
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_existing_chatgpt() {
|
||||||
|
local pgrep_output pgrep_status pid cmdline_text
|
||||||
|
local is_main has_proxy_server has_proxy_bypass
|
||||||
|
|
||||||
|
if pgrep_output="$(pgrep -x "$CHATGPT_PROCESS_NAME" 2>/dev/null)"; then
|
||||||
|
pgrep_status=0
|
||||||
|
else
|
||||||
|
pgrep_status=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$pgrep_status" in
|
||||||
|
0) ;;
|
||||||
|
1)
|
||||||
|
printf '%s\n' none
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
while IFS= read -r pid; do
|
||||||
|
[ -n "$pid" ] || continue
|
||||||
|
[ -r "$PROC_ROOT/$pid/cmdline" ] || continue
|
||||||
|
is_main=1
|
||||||
|
has_proxy_server=0
|
||||||
|
has_proxy_bypass=0
|
||||||
|
|
||||||
|
cmdline_text="$(tr '\0' ' ' < "$PROC_ROOT/$pid/cmdline")"
|
||||||
|
case " $cmdline_text " in
|
||||||
|
*" --type="*) is_main=0 ;;
|
||||||
|
esac
|
||||||
|
case " $cmdline_text " in
|
||||||
|
*" --proxy-server=$HTTP_PROXY "*) has_proxy_server=1 ;;
|
||||||
|
esac
|
||||||
|
case " $cmdline_text " in
|
||||||
|
*" --proxy-bypass-list=$proxy_bypass_list "*) has_proxy_bypass=1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[ "$is_main" -eq 1 ] || continue
|
||||||
|
if [ "$has_proxy_server" -eq 1 ] && [ "$has_proxy_bypass" -eq 1 ]; then
|
||||||
|
printf '%s\n' proxied
|
||||||
|
else
|
||||||
|
printf '%s\n' direct
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
done <<< "$pgrep_output"
|
||||||
|
|
||||||
|
printf '%s\n' none
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ ! -x "$CHATGPT_BIN" ]; then
|
||||||
|
fail_with_notification "ChatGPT не найден: $CHATGPT_BIN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -r "$PROXY_CLIENT_SHELL" ]; then
|
||||||
|
fail_with_notification "Модуль proxy-client не найден: $PROXY_CLIENT_SHELL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${PROXY_CLIENT_BIN:-}" ] && [ -x "$HOME/.local/bin/proxy-client" ]; then
|
||||||
|
PROXY_CLIENT_BIN="$HOME/.local/bin/proxy-client"
|
||||||
|
export PROXY_CLIENT_BIN
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$PROXY_CLIENT_SHELL"
|
||||||
|
|
||||||
|
if ! proxy_on >/dev/null; then
|
||||||
|
fail_with_notification 'Прокси недоступен. Проверьте его командой: proxy-client status'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${HTTP_PROXY:-}" ] || [ -z "${NO_PROXY:-}" ]; then
|
||||||
|
fail_with_notification 'proxy-client вернул неполные настройки прокси.'
|
||||||
|
fi
|
||||||
|
|
||||||
|
proxy_bypass_list="$(chromium_bypass_list "$NO_PROXY")"
|
||||||
|
|
||||||
|
if ! command -v pgrep >/dev/null 2>&1; then
|
||||||
|
fail_with_notification 'Не удалось проверить запущенные процессы: команда pgrep не найдена.'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! existing_chatgpt="$(detect_existing_chatgpt)"; then
|
||||||
|
fail_with_notification 'Не удалось проверить, запущен ли ChatGPT.'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$existing_chatgpt" = direct ]; then
|
||||||
|
fail_with_notification 'ChatGPT уже работает без прокси. В системном лотке выберите «Quit ChatGPT», затем снова откройте «ChatGPT Proxy». Кнопка «Перезапустить» сохраняет прямой режим.'
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$CHATGPT_BIN" \
|
||||||
|
"--proxy-server=$HTTP_PROXY" \
|
||||||
|
"--proxy-bypass-list=$proxy_bypass_list" \
|
||||||
|
"$@"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Name=ChatGPT Proxy
|
||||||
|
Comment=ChatGPT через настроенный proxy-client
|
||||||
|
GenericName=AI assistant
|
||||||
|
Exec=start-chatgpt-proxy %U
|
||||||
|
TryExec=start-chatgpt-proxy
|
||||||
|
Icon=chatgpt
|
||||||
|
Type=Application
|
||||||
|
Terminal=false
|
||||||
|
StartupNotify=true
|
||||||
|
Categories=Utility;
|
||||||
|
Keywords=OpenAI;ChatGPT;Proxy;
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Отдельный запуск ChatGPT через прокси
|
||||||
|
|
||||||
|
## Проблема
|
||||||
|
|
||||||
|
Linux-приложение ChatGPT не применяет обычные proxy-переменные ко всему
|
||||||
|
Chromium-трафику и при штатном запуске выходит в интернет напрямую. Правка
|
||||||
|
файлов Debian-пакета недолговечна: обновление приложения их заменит.
|
||||||
|
|
||||||
|
## Цели
|
||||||
|
|
||||||
|
- Дать отдельный и явно названный ярлык `ChatGPT Proxy`.
|
||||||
|
- Использовать активные настройки и проверку доступности `proxy-client`.
|
||||||
|
- Не допускать тихого запуска напрямую при ошибке прокси.
|
||||||
|
- Сохранить штатный ярлык `ChatGPT` для прямого запуска.
|
||||||
|
- Переживать обновления Debian-пакета ChatGPT.
|
||||||
|
|
||||||
|
## Не входит в задачу
|
||||||
|
|
||||||
|
- Изменение системного прокси GNOME.
|
||||||
|
- Перенаправление трафика других приложений.
|
||||||
|
- Изменение файлов в `/usr/lib/chatgpt` и `/usr/share/applications`.
|
||||||
|
- Автоматический запуск или переключение прокси.
|
||||||
|
|
||||||
|
## Рассмотренные варианты
|
||||||
|
|
||||||
|
- Подменить штатный пользовательский ярлык. Удобно для обязательного прокси,
|
||||||
|
но стирает различие между прямым и проксированным запуском.
|
||||||
|
- Добавить отдельный пользовательский ярлык. Оба режима видны явно, а файлы
|
||||||
|
приложения остаются нетронутыми.
|
||||||
|
- Включить системный прокси GNOME. Решение переживает обновления, но меняет
|
||||||
|
поведение всех приложений рабочего стола.
|
||||||
|
- Перенаправлять трафик на сетевом уровне. Это даёт более жёсткую изоляцию, но
|
||||||
|
заметно усложняет личную конфигурацию.
|
||||||
|
- Оставить всё как есть. Тогда пользователь должен каждый раз помнить о
|
||||||
|
прямом соединении ChatGPT.
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
В dotfiles хранится отдельный пользовательский ярлык `ChatGPT Proxy` и его
|
||||||
|
запускатель. Штатный `ChatGPT` остаётся прямым аналогично паре `codex` и
|
||||||
|
`pcodex`.
|
||||||
|
|
||||||
|
Запускатель получает окружение от `proxy-client`, а для сетевого слоя Chromium
|
||||||
|
добавляет явные параметры сервера и списка обхода. Если прокси недоступен,
|
||||||
|
ChatGPT не запускается, а GNOME показывает уведомление с причиной и командой
|
||||||
|
для диагностики.
|
||||||
|
|
||||||
|
Если ChatGPT уже работает через тот же прокси, повторный запуск передаёт ему
|
||||||
|
запрос: Electron поднимает существующее окно. Если работает прямой экземпляр
|
||||||
|
или экземпляр с другими настройками прокси, запуск отклоняется. Уведомление
|
||||||
|
предлагает полностью завершить ChatGPT через системный лоток. Встроенный
|
||||||
|
перезапуск для обновления сохраняет режим, в котором приложение было открыто.
|
||||||
|
|
||||||
|
Ярлык и запускатель устанавливаются в пользовательские каталоги через Stow.
|
||||||
|
Они не заменяют одноимённые файлы Debian-пакета, поэтому обновление ChatGPT их
|
||||||
|
не затрагивает.
|
||||||
|
|
||||||
|
## Отклонённые решения
|
||||||
|
|
||||||
|
- Подмена штатного ярлыка отклонена: отдельное имя лучше показывает выбранный
|
||||||
|
режим и не скрывает возможность прямого запуска.
|
||||||
|
- Системный прокси отклонён из-за слишком широкой области действия.
|
||||||
|
- Сетевое перенаправление отложено как избыточное для одного приложения.
|
||||||
|
- Переход на прямое соединение при ошибке запрещён: имя `ChatGPT Proxy` должно
|
||||||
|
быть гарантией, а не пожеланием.
|
||||||
|
|
||||||
|
## Риски и защита
|
||||||
|
|
||||||
|
- Уже работающий прямой процесс не примет новые параметры. Запускатель отличает
|
||||||
|
его от экземпляра с теми же параметрами прокси и отказывает только при
|
||||||
|
конфликте режимов.
|
||||||
|
- Одних proxy-переменных может быть недостаточно для Chromium. Запускатель
|
||||||
|
передаёт параметры прокси явно и одновременно сохраняет окружение для
|
||||||
|
дочерних процессов.
|
||||||
|
- Ошибка графического запуска может остаться незаметной. Причина выводится в
|
||||||
|
stderr и дублируется уведомлением GNOME.
|
||||||
|
- Формат списка обхода отличается у `NO_PROXY` и Chromium. Запускатель явно
|
||||||
|
преобразует список перед запуском приложения.
|
||||||
|
|
||||||
|
## Проверка
|
||||||
|
|
||||||
|
- При доступном прокси ChatGPT получает proxy-переменные и параметры Chromium.
|
||||||
|
- Адреса из `NO_PROXY` попадают в список обхода Chromium.
|
||||||
|
- При недоступном прокси приложение не запускается и появляется уведомление.
|
||||||
|
- При повторном прокси-запуске поднимается окно уже работающего проксированного
|
||||||
|
экземпляра.
|
||||||
|
- При работающем прямом экземпляре появляется уведомление с инструкцией
|
||||||
|
завершить его через системный лоток.
|
||||||
|
- Штатный системный ярлык не меняется.
|
||||||
|
- Повторное применение dotfiles восстанавливает пользовательский ярлык после
|
||||||
|
обновления Debian-пакета.
|
||||||
+9
-1
@@ -370,7 +370,7 @@ if ! command -v "$STOW_BIN" &>/dev/null; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PACKAGES=(bash tmux vim wezterm kitty vscode agents codex gemini proxy-client)
|
PACKAGES=(bash tmux vim wezterm kitty vscode agents codex gemini proxy-client chatgpt)
|
||||||
AVAILABLE_PACKAGES=()
|
AVAILABLE_PACKAGES=()
|
||||||
|
|
||||||
info "Применяю stow пакеты..."
|
info "Применяю stow пакеты..."
|
||||||
@@ -392,6 +392,14 @@ if [ -d "$HOME/.local" ] && [ ! -L "$HOME/.local" ] \
|
|||||||
&& [ ! -L "$HOME/.local/bin" ]; then
|
&& [ ! -L "$HOME/.local/bin" ]; then
|
||||||
mkdir -p "$HOME/.local/bin"
|
mkdir -p "$HOME/.local/bin"
|
||||||
fi
|
fi
|
||||||
|
if [ -d "$HOME/.local" ] && [ ! -L "$HOME/.local" ] \
|
||||||
|
&& [ ! -L "$HOME/.local/share" ]; then
|
||||||
|
mkdir -p "$HOME/.local/share"
|
||||||
|
fi
|
||||||
|
if [ -d "$HOME/.local/share" ] && [ ! -L "$HOME/.local/share" ] \
|
||||||
|
&& [ ! -L "$HOME/.local/share/applications" ]; then
|
||||||
|
mkdir -p "$HOME/.local/share/applications"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ ! -L "$HOME/.config" ]; then
|
if [ ! -L "$HOME/.config" ]; then
|
||||||
mkdir -p "$HOME/.config"
|
mkdir -p "$HOME/.config"
|
||||||
|
|||||||
Executable
+193
@@ -0,0 +1,193 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
LAUNCHER="$REPO_ROOT/chatgpt/.local/bin/start-chatgpt-proxy"
|
||||||
|
DESKTOP_FILE="$REPO_ROOT/chatgpt/.local/share/applications/chatgpt-proxy.desktop"
|
||||||
|
SHELL_MODULE="$REPO_ROOT/proxy-client/.config/proxy-client/shell.bash"
|
||||||
|
TEST_ROOT="$(mktemp -d /tmp/chatgpt-proxy-launcher-test.XXXXXX)"
|
||||||
|
trap 'rm -rf "$TEST_ROOT"' EXIT
|
||||||
|
|
||||||
|
pass_count=0
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "FAIL: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
ok() {
|
||||||
|
pass_count=$((pass_count + 1))
|
||||||
|
echo "ok - $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_contains() {
|
||||||
|
local file="$1" expected="$2" message="$3"
|
||||||
|
grep -Fq -- "$expected" "$file" || fail "$message"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_not_exists() {
|
||||||
|
local path="$1" message="$2"
|
||||||
|
[ ! -e "$path" ] || fail "$message"
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_fixture() {
|
||||||
|
local fixture="$1"
|
||||||
|
mkdir -p "$fixture/bin" "$fixture/home" "$fixture/proc" "$fixture/results"
|
||||||
|
|
||||||
|
cat > "$fixture/bin/pgrep" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
if [ "${FAKE_PGREP_STATUS:-1}" = 0 ]; then
|
||||||
|
printf '%s\n' "${FAKE_PGREP_PIDS:-4242}"
|
||||||
|
fi
|
||||||
|
exit "${FAKE_PGREP_STATUS:-1}"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$fixture/bin/notify-send" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf '<%s>\n' "$@" >> "$RESULTS_DIR/notifications"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$fixture/bin/proxy-client" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf 'called\n' >> "$RESULTS_DIR/proxy-calls"
|
||||||
|
if [ "${FAKE_PROXY_FAIL:-0}" = 1 ]; then
|
||||||
|
echo 'тестовый прокси недоступен' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
[ "${1:-}" = env ] || exit 99
|
||||||
|
cat <<'ENV'
|
||||||
|
HTTP_PROXY=http://proxy.test:1234
|
||||||
|
HTTPS_PROXY=http://proxy.test:1234
|
||||||
|
ALL_PROXY=socks5://proxy.test:1234
|
||||||
|
NO_PROXY=proxy.test,localhost,127.0.0.1,::1,[::1],.example.test
|
||||||
|
http_proxy=http://proxy.test:1234
|
||||||
|
https_proxy=http://proxy.test:1234
|
||||||
|
all_proxy=socks5://proxy.test:1234
|
||||||
|
no_proxy=proxy.test,localhost,127.0.0.1,::1,[::1],.example.test
|
||||||
|
ENV
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$fixture/bin/chatgpt" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf '<%s>\n' "$@" > "$RESULTS_DIR/chatgpt-args"
|
||||||
|
printf 'HTTP_PROXY=%s\nALL_PROXY=%s\nNO_PROXY=%s\n' \
|
||||||
|
"$HTTP_PROXY" "$ALL_PROXY" "$NO_PROXY" > "$RESULTS_DIR/chatgpt-env"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x "$fixture/bin/pgrep" "$fixture/bin/notify-send" \
|
||||||
|
"$fixture/bin/proxy-client" "$fixture/bin/chatgpt"
|
||||||
|
}
|
||||||
|
|
||||||
|
write_process() {
|
||||||
|
local fixture="$1" pid="$2"
|
||||||
|
shift 2
|
||||||
|
mkdir -p "$fixture/proc/$pid"
|
||||||
|
printf '%s\0' "$@" > "$fixture/proc/$pid/cmdline"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_launcher() {
|
||||||
|
local fixture="$1"
|
||||||
|
shift
|
||||||
|
HOME="$fixture/home" \
|
||||||
|
PATH="$fixture/bin:/usr/bin:/bin" \
|
||||||
|
RESULTS_DIR="$fixture/results" \
|
||||||
|
PROXY_CLIENT_BIN="$fixture/bin/proxy-client" \
|
||||||
|
PROXY_CLIENT_SHELL="$SHELL_MODULE" \
|
||||||
|
CHATGPT_BIN="$fixture/bin/chatgpt" \
|
||||||
|
PROC_ROOT="$fixture/proc" \
|
||||||
|
"$LAUNCHER" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Успешный запуск передаёт и окружение, и явные параметры Chromium.
|
||||||
|
fixture="$TEST_ROOT/success"
|
||||||
|
setup_fixture "$fixture"
|
||||||
|
FAKE_PGREP_STATUS=1 run_launcher "$fixture" 'chatgpt://example/path'
|
||||||
|
assert_contains "$fixture/results/chatgpt-args" \
|
||||||
|
'<--proxy-server=http://proxy.test:1234>' \
|
||||||
|
'не передан сервер Chromium'
|
||||||
|
assert_contains "$fixture/results/chatgpt-args" \
|
||||||
|
'<--proxy-bypass-list=proxy.test;localhost;127.0.0.1;::1;[::1];*.example.test>' \
|
||||||
|
'неверно преобразован список обхода Chromium'
|
||||||
|
assert_contains "$fixture/results/chatgpt-args" '<chatgpt://example/path>' \
|
||||||
|
'пользовательский аргумент потерян'
|
||||||
|
assert_contains "$fixture/results/chatgpt-env" \
|
||||||
|
'ALL_PROXY=socks5://proxy.test:1234' \
|
||||||
|
'proxy-окружение не унаследовано'
|
||||||
|
assert_not_exists "$fixture/results/notifications" \
|
||||||
|
'успешный запуск показал ошибку'
|
||||||
|
ok 'успешный запуск применяет proxy-client к Chromium и окружению'
|
||||||
|
|
||||||
|
# Уже работающий через тот же прокси экземпляр получает повторный запрос:
|
||||||
|
# Electron поднимает его существующее окно вместо ошибки «уже запущен».
|
||||||
|
fixture="$TEST_ROOT/running-proxied"
|
||||||
|
setup_fixture "$fixture"
|
||||||
|
write_process "$fixture" 4242 \
|
||||||
|
'/usr/lib/chatgpt/ChatGPT --proxy-server=http://proxy.test:1234 --proxy-bypass-list=proxy.test;localhost;127.0.0.1;::1;[::1];*.example.test'
|
||||||
|
FAKE_PGREP_STATUS=0 run_launcher "$fixture" 'chatgpt://existing'
|
||||||
|
assert_contains "$fixture/results/chatgpt-args" '<chatgpt://existing>' \
|
||||||
|
'повторный запрос не передан прокси-экземпляру'
|
||||||
|
assert_not_exists "$fixture/results/notifications" \
|
||||||
|
'работающий прокси-экземпляр ошибочно объявлен конфликтом'
|
||||||
|
ok 'повторный прокси-запуск открывает существующий экземпляр'
|
||||||
|
|
||||||
|
# Прямой экземпляр блокируется с точной инструкцией: встроенный Restart
|
||||||
|
# сохранит прямой режим, поэтому приложение нужно завершить через системный лоток.
|
||||||
|
fixture="$TEST_ROOT/running-direct"
|
||||||
|
setup_fixture "$fixture"
|
||||||
|
write_process "$fixture" 4242 /usr/lib/chatgpt/ChatGPT
|
||||||
|
set +e
|
||||||
|
FAKE_PGREP_STATUS=0 run_launcher "$fixture" >/dev/null 2>&1
|
||||||
|
status=$?
|
||||||
|
set -e
|
||||||
|
[ "$status" -eq 1 ] || fail "ожидался код 1 при прямом ChatGPT, получен $status"
|
||||||
|
assert_not_exists "$fixture/results/chatgpt-args" \
|
||||||
|
'прямой ChatGPT получил повторный запуск'
|
||||||
|
assert_contains "$fixture/results/notifications" 'без прокси' \
|
||||||
|
'уведомление не различает прямой экземпляр'
|
||||||
|
assert_contains "$fixture/results/notifications" 'системном лотке' \
|
||||||
|
'нет инструкции для полного завершения прямого экземпляра'
|
||||||
|
ok 'прямой экземпляр блокируется с точной инструкцией'
|
||||||
|
|
||||||
|
# Недоступный прокси не приводит к прямому запуску.
|
||||||
|
fixture="$TEST_ROOT/unavailable"
|
||||||
|
setup_fixture "$fixture"
|
||||||
|
set +e
|
||||||
|
FAKE_PGREP_STATUS=1 FAKE_PROXY_FAIL=1 run_launcher "$fixture" >/dev/null 2>&1
|
||||||
|
status=$?
|
||||||
|
set -e
|
||||||
|
[ "$status" -eq 1 ] || fail "ожидался код 1 при ошибке прокси, получен $status"
|
||||||
|
assert_not_exists "$fixture/results/chatgpt-args" \
|
||||||
|
'ChatGPT запущен напрямую после ошибки прокси'
|
||||||
|
assert_contains "$fixture/results/notifications" 'proxy-client status' \
|
||||||
|
'нет диагностической подсказки при ошибке прокси'
|
||||||
|
ok 'ошибка прокси запрещает прямой запуск'
|
||||||
|
|
||||||
|
# Ярлык остаётся отдельным и не перехватывает штатные обработчики ChatGPT.
|
||||||
|
assert_contains "$DESKTOP_FILE" 'Name=ChatGPT Proxy' \
|
||||||
|
'неверное имя ярлыка'
|
||||||
|
assert_contains "$DESKTOP_FILE" 'Exec=start-chatgpt-proxy %U' \
|
||||||
|
'ярлык не вызывает управляемый запускатель'
|
||||||
|
if grep -q '^MimeType=' "$DESKTOP_FILE"; then
|
||||||
|
fail 'прокси-ярлык не должен перехватывать штатные MIME-обработчики'
|
||||||
|
fi
|
||||||
|
[ "$(basename "$DESKTOP_FILE")" = chatgpt-proxy.desktop ] \
|
||||||
|
|| fail 'прокси-ярлык подменяет системный chatgpt.desktop'
|
||||||
|
ok 'ChatGPT Proxy остаётся отдельным пользовательским ярлыком'
|
||||||
|
|
||||||
|
if command -v desktop-file-validate >/dev/null 2>&1; then
|
||||||
|
desktop-file-validate "$DESKTOP_FILE" >/dev/null \
|
||||||
|
|| fail 'desktop-file-validate нашёл ошибку'
|
||||||
|
ok 'desktop-файл корректен'
|
||||||
|
else
|
||||||
|
echo 'skip - desktop-file-validate не установлен'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v shellcheck >/dev/null 2>&1; then
|
||||||
|
shellcheck -s bash "$LAUNCHER" "$0" \
|
||||||
|
|| fail 'shellcheck нашёл проблемы'
|
||||||
|
ok 'shellcheck чист'
|
||||||
|
else
|
||||||
|
echo 'skip - shellcheck не установлен'
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "PASS: $pass_count проверок пройдено"
|
||||||
+10
-2
@@ -36,9 +36,9 @@ setup_fixture() {
|
|||||||
# дерева. Локальные данные инструментов внутри пакетов (vscode/.vscode-server,
|
# дерева. Локальные данные инструментов внутри пакетов (vscode/.vscode-server,
|
||||||
# zed/.config/opencode и т.п.) весят гигабайты, переполняют /tmp и ломают
|
# zed/.config/opencode и т.п.) весят гигабайты, переполняют /tmp и ломают
|
||||||
# stow конфликтами, поэтому в тестовое дерево не попадает.
|
# stow конфликтами, поэтому в тестовое дерево не попадает.
|
||||||
git -C "$REPO_ROOT" ls-files -z --cached -- \
|
git -C "$REPO_ROOT" ls-files -z --cached --others --exclude-standard -- \
|
||||||
install.sh bash tmux vim zed kitty wezterm vscode agents opencode codex gemini \
|
install.sh bash tmux vim zed kitty wezterm vscode agents opencode codex gemini \
|
||||||
claude proxy-client scripts \
|
claude proxy-client chatgpt scripts \
|
||||||
| tar -C "$REPO_ROOT" --null -T - -cf - \
|
| tar -C "$REPO_ROOT" --null -T - -cf - \
|
||||||
| tar -xf - -C "$repo_root"
|
| tar -xf - -C "$repo_root"
|
||||||
|
|
||||||
@@ -102,6 +102,14 @@ test_fresh_install_uses_home_target() {
|
|||||||
[ -d "$home_dir/.local/bin" ] || fail "Expected ~/.local/bin directory"
|
[ -d "$home_dir/.local/bin" ] || fail "Expected ~/.local/bin directory"
|
||||||
[ ! -L "$home_dir/.local/bin" ] || fail "Expected ~/.local/bin to stay a real directory"
|
[ ! -L "$home_dir/.local/bin" ] || fail "Expected ~/.local/bin to stay a real directory"
|
||||||
[ -x "$home_dir/.local/bin/proxy-client" ] || fail "Expected executable proxy-client CLI in HOME"
|
[ -x "$home_dir/.local/bin/proxy-client" ] || fail "Expected executable proxy-client CLI in HOME"
|
||||||
|
[ -x "$home_dir/.local/bin/start-chatgpt-proxy" ] || fail "Expected ChatGPT proxy launcher in HOME"
|
||||||
|
[ -f "$home_dir/.local/share/applications/chatgpt-proxy.desktop" ] \
|
||||||
|
|| fail "Expected ChatGPT Proxy desktop entry in HOME"
|
||||||
|
[ -d "$home_dir/.local/share" ] || fail "Expected ~/.local/share directory"
|
||||||
|
[ ! -L "$home_dir/.local/share" ] || fail "Expected ~/.local/share to stay a real directory"
|
||||||
|
[ -d "$home_dir/.local/share/applications" ] || fail "Expected applications directory"
|
||||||
|
[ ! -L "$home_dir/.local/share/applications" ] \
|
||||||
|
|| fail "Expected applications directory to stay real"
|
||||||
[ -d "$home_dir/.config/proxy-client" ] || fail "Expected proxy-client config directory"
|
[ -d "$home_dir/.config/proxy-client" ] || fail "Expected proxy-client config directory"
|
||||||
[ ! -L "$home_dir/.config/proxy-client" ] || fail "Expected proxy-client config to stay outside the repository"
|
[ ! -L "$home_dir/.config/proxy-client" ] || fail "Expected proxy-client config to stay outside the repository"
|
||||||
[ -f "$home_dir/.config/proxy-client/shell.bash" ] || fail "Expected proxy-client shell module"
|
[ -f "$home_dir/.config/proxy-client/shell.bash" ] || fail "Expected proxy-client shell module"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export PYTHONDONTWRITEBYTECODE=1
|
|||||||
|
|
||||||
python3 -m unittest discover -s tests
|
python3 -m unittest discover -s tests
|
||||||
bash tests/test_proxy_launchers.sh
|
bash tests/test_proxy_launchers.sh
|
||||||
|
bash tests/test_chatgpt_proxy_launcher.sh
|
||||||
bash tests/test_proxy_client_install.sh
|
bash tests/test_proxy_client_install.sh
|
||||||
bash tests/test_install.sh
|
bash tests/test_install.sh
|
||||||
uv run --python 3.8 --no-project python -m unittest discover -s tests
|
uv run --python 3.8 --no-project python -m unittest discover -s tests
|
||||||
Reference in new issue
Block a user