README обёртки приведён в соответствие с кодом: - раздел «Автообновление»: раскладка каталога `UPDATES_DIR`, публикация релиза (`make desktop-release`, `make desktop-release-upload HOST=…`), зачем нужен `latest.json`, проверка через curl и лог обёртки; отдельно отмечено, что владельцу внешнего прокси добавлять ничего не нужно; - раздел «Уведомления и переход в канал»: реализованное поведение по активации приложения и честное ограничение (ОС не сообщает, что активация вызвана кликом по уведомлению); - раздел «Ресурсы и холодный старт»: замеры 708–740 мс и 252 МБ RSS, как повторить (`scripts/desktop-perf.py`); - «Что осталось оператору»: точные команды для `.dmg` (`make desktop-build` из обычного терминала), подписи Developer ID и нотаризации (`APPLE_*`, `stapler validate`, `spctl`), сборки и подписи Windows/Linux, требования к ключу обновлений; отдельно — поведение на Wayland (нативная тряска окна не работает, остаётся CSS).
desktop — desktop-обёртка glchat (Tauri 2)
Тот же веб-клиент из web/, обёрнутый в Tauri 2 (docs/client-tauri.md). Отдельного
UI-кода нет: страница инстанса загружается в системный webview, а обёртка даёт
платформенную интеграцию — трей, нативные уведомления, глобальный push-to-talk,
автозапуск, deep links и автообновление.
desktop/
└── src-tauri/ # Rust-проект обёртки
├── src/ # окно, трей, шорткаты, уведомления, deep links, обновления
├── capabilities/ # allowlist IPC: локальная страница настроек и домен инстанса
├── ui/ # локальная страница настроек обёртки (tauri://localhost)
├── icons/ # иконки приложения (сгенерированы из web/public/icons)
└── tauri.conf.json # конфигурация сборки и плагинов
Сборка и запуск
make desktop-check # cargo fmt --check, clippy, тесты (кэш в .cache/)
make desktop-build # бандлы текущей ОС (macOS: .app и .dmg)
make desktop-build-app # только .app + артефакты обновления, без .dmg
open desktop/src-tauri/target/release/bundle/macos/glchat.app
Требуется Rust 1.77+ и Node 22 (Tauri CLI ставится через npx). Кэши cargo и
npm держатся в .cache/ — как у остальных инструментов проекта.
make desktop-build-app (tauri build --bundles app) нужен там, где .dmg
собрать нельзя: bundle_dmg.sh монтирует образ через hdiutil и управляет
Finder через AppleScript, поэтому из песочницы агента и из headless-окружения он
падает (см. «Что не сделано»).
Сборка релиза с артефактами автообновления использует ключ подписи:
# ключ и пароль не коммитятся: приватный ключ живёт в CI-секретах,
# локально — в build/desktop-keys/ (каталог build/ в .gitignore)
npx @tauri-apps/cli@^2 signer generate -w build/desktop-keys/glchat-updater.key -p '…'
make desktop-build
Публичный ключ уже вписан в src-tauri/tauri.conf.json и в UPDATER_PUBKEY
(src/updates.rs). Без локального ключа make desktop-build собирает бандл без
артефактов обновления, чтобы сборка не падала у тех, у кого ключа нет.
Аргументы командной строки
| Аргумент | Что делает |
|---|---|
--instance <url> |
адрес инстанса вместо сохранённого (--instance=https://… тоже работает) |
--minimized |
запуск свёрнутым в трей (используется автозапуском) |
--settings |
сразу открыть страницу настроек обёртки |
--autostart on|off |
включить или выключить автозапуск при установке скриптом |
Настройки
Хранятся в settings.json каталога конфигурации приложения
(~/Library/Application Support/su.mhspx.glchat/ на macOS):
| Ключ | Смысл |
|---|---|
instanceUrl |
адрес клиента, по умолчанию https://gl.mhspx.su |
closeToTray |
закрытие окна сворачивает приложение в трей |
autostart |
запуск при входе в систему (--minimized) |
notifications |
показывать системные уведомления |
pttShortcut, muteShortcut, deafenShortcut |
глобальные комбинации (пусто — выключено) |
Меняются из меню трея и со страницы настроек обёртки (трей → «Настройки обёртки…»). Сессия и токены живут только в cookie webview: обёртка их не хранит.
Deep links
Схема glchat:// (регистрируется бандлом):
glchat://invite/<code>→ страница приглашения;glchat://guild/<guildId>/channel/<channelId>→ сервер и комната.
Ссылка, пришедшая до загрузки клиента (холодный старт), ждёт в очереди и
доставляется после desktop_ready.
Автообновление
Обёртка проверяет обновления сама: фоновая проверка через 30 секунд после старта
и вручную — трей → «Проверить обновления…». Адрес манифеста —
GET {instance}/updates/{target}/{arch}/{current_version} (target:
darwin/windows/linux, arch: aarch64/x86_64; см. src/updates.rs).
Манифест отдаёт сам инстанс (internal/server/updates.go), отдельная статика в
Caddy не нужна: и встроенный Caddy профиля, и внешний прокси стенда проксируют
весь домен на приложение, поэтому /updates/... доходит до него без правок
чужих конфигов.
Раскладка каталога (UPDATES_DIR; на стенде /opt/glchat/updates, в контейнере
смонтирован как /app/updates:ro):
/opt/glchat/updates/
└── darwin/aarch64/
├── glchat.app.tar.gz # артефакт релиза (создаёт tauri build)
├── glchat.app.tar.gz.sig # подпись ed25519 (формат minisign)
├── 0.1.0.json # манифест версии
└── latest.json # манифест последнего релиза — его и отдаёт инстанс
Публикация релиза:
make desktop-build # собрать бандл (.app/.dmg) и артефакты обновления
sudo make desktop-release # разложить в /opt/glchat/updates + создать манифест
make desktop-release-upload HOST=user@server # то же, но по SSH (sudo спросит пароль в терминале)
make desktop-release ARGS='--dry-run' # посмотреть план, ничего не записывая
Скрипт scripts/desktop-release.sh берёт версию из tauri.conf.json, сам
определяет target/arch по имени артефакта, копирует артефакт и .sig, собирает
манифест (version, notes, pub_date, platforms.<target>-<arch>.signature/url)
и кладёт его и как <version>.json, и как latest.json. Несколько платформ
одной версии (macOS + Windows + Linux) складываются в один манифест: запускайте
скрипт по разу на артефакт.
latest.json нужен потому, что обёртка приходит со своей установленной
версией: «новее ли релиз» решает клиент сравнением semver
(tauri-plugin-updater), поэтому на запрос любой версии сервер отдаёт
последний манифест, а если есть манифест конкретной версии — его. Без этого
обновление не получил бы никто, кроме тех, у кого уже стоит последняя версия.
Владельцу прокси добавлять ничего не нужно: /updates/... уходит в
catch-all handle и доходит до приложения (так же, как статика клиента).
Вариант «раздавать обновления статикой мимо приложения» требует правила
rewrite на latest.json с подстановкой target/arch в Caddy — лишняя
работа без выгоды, поэтому не рекомендуется.
Проверка (стенд):
curl -s https://gl.mhspx.su/updates/darwin/aarch64/0.1.0 | jq .
# в логе обёртки (~/Library/Logs/su.mhspx.glchat/glchat.log) —
# «обновлений нет» (манифест разобран) вместо
# «проверка обновлений не удалась: … 404»
Порядок обновления: проверка → уведомление в интерфейсе → скачивание → подтверждение в диалоге (во время звонка перезапуск без согласия не делается) → подпись проверяется до установки, downgrade отклоняется.
Уведомления и переход в канал
Решение «показывать ли уведомление» принимает веб-клиент (упоминания и личные сообщения, окно неактивно или комната не открыта), обёртка показывает системное уведомление и обновляет счётчик.
Клик по уведомлению: у tauri-plugin-notification на desktop нет колбэка
действия, поэтому реализовано лучшее доступное поведение — уведомление,
показанное при неактивном окне, запоминается, и когда приложение
активируется в течение 10 секунд (окно получило фокус или macOS прислала
Reopen), окно поднимается и открывается комната из уведомления
(glchat://guild/<id>/channel/<id>, для личной беседы glchat://dm/<id>).
Ограничение (честно): ОС не сообщает, что активация вызвана именно кликом по
уведомлению, поэтому переход сработает и при обычном возврате в приложение в
течение этих 10 секунд. Клик по уведомлению старше 10 секунд только поднимает
окно. Полноценный колбэк требует своего моста к UNUserNotificationCenter
(UNUserNotificationCenterDelegate) — отдельная задача.
Ресурсы и холодный старт
Замеры собраны скриптом scripts/desktop-perf.py (macOS arm64, Mac на M-серии,
инстанс gl.mhspx.su через внешний прокси; подробности — docs/PERF.md):
| Метрика | Значение | Бюджет (docs/client-tauri.md §1) |
|---|---|---|
Холодный старт (open → процесс → строка «загружена страница») |
708–740 мс (три прогона) | ≤ 2 с |
| Из них: запуск процесса | 45–74 мс | — |
| Из них: загрузка страницы клиента | 637–682 мс | — |
| RAM, клиент открыт и простаивает (RSS: обёртка + 4 процесса WebKit) | 252 МБ (117 + 135) | ≤ 350 МБ |
RAM, окно свёрнуто в трей (--minimized) |
251 МБ | ≤ 350 МБ |
| Физический след (Activity Monitor) | 103 МБ | — |
Наблюдения:
- свёрнутое в трей окно памяти не освобождает: webview с загруженным клиентом продолжает жить (так работают WKWebView и трей-режим) — экономия была бы только при выгрузке страницы, а это потеря мгновенного возврата в клиент;
- первый запуск сразу после сборки бандла (холодный кэш файловой системы и страницы) дал 1,26 с — тоже в бюджете;
- почти вся память — WebKit (страница клиента), сама обёртка занимает ~117 МБ.
Повторить замер:
make desktop-build-app # свежий .app (+ артефакты обновления)
python3 scripts/desktop-perf.py --runs 3 # 3 холодных старта + простой в трее
Что не сделано (открытые пункты Фазы 6)
- Автообновление end-to-end не проверено на реальном релизе: манифест отдаётся и разбирается (проверено на стенде), но установка «поверх старой версии» требует публикации версии с бо́льшим номером — это действие оператора (см. «Публикация релиза» выше).
- Аватары в уведомлениях (thumb/буква) и группировка — сейчас иконка приложения.
- Захват экрана и микрофон: разрешения macOS выдаёт webview, отдельных экранов-инструкций у обёртки нет.
- Developer ID и нотаризация macOS, EV/Trusted Signing для Windows — только в CI с платными сертификатами (ниже — точные команды).
.dmg: собирается только из обычного терминала (Finder/AppleScript).- Windows/Linux-бандлы: конфигурация кросс-платформенная, но собирались и проверялись только macOS-arm64.
Что осталось оператору (точные команды)
macOS: .dmg
bundle_dmg.sh монтирует образ (hdiutil) и управляет Finder через AppleScript,
поэтому из песочницы агента и из headless-сессии он падает. Из обычного
терминала на macOS:
cd /Volumes/Samsung/projects/glchat
make desktop-build # .app + .dmg + артефакты обновления
ls -lh desktop/src-tauri/target/release/bundle/dmg/
Артефакты без .dmg (для автообновления) собираются где угодно:
make desktop-build-app.
macOS: подпись Developer ID и нотаризация
Нужны сертификат Developer ID Application в связке ключей и пароль приложения (appleid.apple.com → App-Specific Passwords):
security find-identity -v -p codesigning # проверить наличие сертификата
export APPLE_SIGNING_IDENTITY="Developer ID Application: … (TEAMID)"
export APPLE_ID="…@example.com"
export APPLE_PASSWORD="app-specific-password"
export APPLE_TEAM_ID="TEAMID"
make desktop-build # Tauri подпишет и отправит на нотаризацию
xcrun stapler validate desktop/src-tauri/target/release/bundle/macos/glchat.app
spctl --assess --type execute --verbose=4 desktop/src-tauri/target/release/bundle/macos/glchat.app
Переменные читает сам Tauri (APPLE_*), в репозиторий они не попадают.
Проверка «не подписано»: codesign -dv --verbose=4 <app> покажет Signature=adhoc.
Windows (x64): MSI/NSIS + подпись
Сборка идёт на Windows-машине или в CI (windows-latest), Rust + Node 22:
npx @tauri-apps/cli@^2 build --bundles nsis,msi
# подпись (EV/Trusted Signing или signtool с сертификатом):
signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 `
/f cert.pfx /p $env:CERT_PASSWORD `
desktop\src-tauri\target\release\bundle\nsis\glchat_0.1.0_x64-setup.exe
Артефакты обновления (*.nsis.zip/*.msi + .sig) публикуются тем же
скриптом: bash scripts/desktop-release.sh --artifact <файл> --target windows --arch x86_64.
Linux (AppImage + deb)
Сборка на Ubuntu 24.04/26.04 (нужны libwebkit2gtk-4.1-dev, librsvg2-dev,
patchelf, libssl-dev, build-essential):
sudo apt-get install -y libwebkit2gtk-4.1-dev librsvg2-dev patchelf libssl-dev build-essential
npx @tauri-apps/cli@^2 build --bundles appimage,deb
bash scripts/desktop-release.sh --artifact desktop/src-tauri/target/release/bundle/appimage/*.AppImage.tar.gz \
--target linux --arch x86_64
На Wayland нативная тряска окна при несохранённых изменениях не работает
(композитор запрещает set_position) — остаётся CSS-тряска контента; это
ожидаемое поведение, а не дефект.
Ключ подписи обновлений
Приватный ключ — только в CI-секретах (TAURI_SIGNING_PRIVATE_KEY,
TAURI_SIGNING_PRIVATE_KEY_PASSWORD); локально — build/desktop-keys/
(каталог в .gitignore). Публичный ключ уже вшит в сборку и меняется только
вместе с ключом: старые сборки отвергнут манифест, подписанный новым ключом.