feat(desktop): обёртка Tauri 2 вокруг веб-клиента
Первый инкремент Фазы 6 (docs/client-tauri.md): окно с тем же веб-клиентом (по умолчанию https://gl.mhspx.su, адрес настраивается), трей с меню и индикатором непрочитанных, нативные уведомления, глобальный push-to-talk и переключение микрофона/deafen, автозапуск, single-instance, сохранение геометрии окна, deep links glchat://…, автообновление по подписанному манифесту. Связь страницы с обёрткой — через платформенный адаптер (web/src/lib/platform.ts) и минимальный типизированный набор команд; в веб-клиенте появились системные уведомления об упоминаниях и личных сообщениях и переключатель в настройках. В web/src/stores/gateway.ts вместе с вызовом уведомления лежат правки по дефектам realtime (READY и сессии) из параллельной волны: файл коммитится целиком по договорённости с координатором.
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
//! Автообновление (docs/client-tauri.md §4).
|
||||
//!
|
||||
//! Манифест обновлений отдаёт сам инстанс: `GET /updates/{target}/{arch}/{current_version}`
|
||||
//! (статика через Caddy). Артефакты подписаны ключом ed25519, подпись
|
||||
//! проверяется до установки — публичный ключ задаётся в `UPDATER_PUBKEY`.
|
||||
//!
|
||||
//! Пока ключ не заведён (сборка без релизного контура), проверка обновлений
|
||||
//! честно сообщает, что канал не настроен, вместо «суррогатного» обновления.
|
||||
|
||||
use tauri::{AppHandle, Manager, Runtime};
|
||||
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
use crate::bridge::{self, WebMessage};
|
||||
use crate::instance;
|
||||
use crate::state::AppState;
|
||||
|
||||
/// Публичный ключ подписи обновлений (ed25519, формат minisign).
|
||||
///
|
||||
/// Приватный ключ в репозиторий не попадает: он живёт в CI-секретах
|
||||
/// (`TAURI_SIGNING_PRIVATE_KEY`), а локально — в `build/desktop-keys/`
|
||||
/// (каталог `build/` в .gitignore). Подпись обязательна: манифест без неё
|
||||
/// плагин отклоняет, downgrade запрещён (docs/client-tauri.md §4, §6).
|
||||
pub const UPDATER_PUBKEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDhCQkQ1NjkwN0EyQzZBOEEKUldTS2FpeDZrRmE5aXhqSThPWXA3am5YMnB5dmVWZWFEcVY4NGI3cG8yOTU1MjdtMW9sR09BNGIK";
|
||||
|
||||
/// Путь манифеста обновлений на инстансе.
|
||||
const UPDATE_PATH: &str = "/updates/{{target}}/{{arch}}/{{current_version}}";
|
||||
|
||||
/// Проверяет обновления. `interactive` — вызов из меню трея или настроек,
|
||||
/// тогда пользователь видит результат в диалоге; фоновая проверка молчит.
|
||||
pub async fn check<R: Runtime>(app: AppHandle<R>, interactive: bool) {
|
||||
if UPDATER_PUBKEY.is_empty() {
|
||||
log::info!("автообновление не настроено: в сборке нет публичного ключа подписи");
|
||||
if interactive {
|
||||
app.dialog()
|
||||
.message(
|
||||
"В этой сборке нет публичного ключа подписи обновлений, поэтому проверка \
|
||||
недоступна. Ключ и манифест обновлений появятся вместе с релизным контуром \
|
||||
(docs/client-tauri.md §4).",
|
||||
)
|
||||
.title("Обновления не настроены")
|
||||
.buttons(MessageDialogButtons::Ok)
|
||||
.show(|_| {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let settings = app.state::<AppState>().settings();
|
||||
let endpoint = match instance::normalize(&settings.instance_url) {
|
||||
Ok(url) => format!("{}{UPDATE_PATH}", instance::display(&url)),
|
||||
Err(error) => {
|
||||
log::warn!("не удалось определить адрес инстанса для обновлений: {error}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let Ok(endpoint) = url::Url::parse(&endpoint) else {
|
||||
log::warn!("некорректный адрес манифеста обновлений: {endpoint}");
|
||||
return;
|
||||
};
|
||||
|
||||
let updater = match app
|
||||
.updater_builder()
|
||||
.endpoints(vec![endpoint])
|
||||
.and_then(|builder| builder.pubkey(UPDATER_PUBKEY).build())
|
||||
{
|
||||
Ok(updater) => updater,
|
||||
Err(error) => {
|
||||
log::warn!("не удалось собрать клиент обновлений: {error}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match updater.check().await {
|
||||
Ok(Some(update)) => {
|
||||
let version = update.version.clone();
|
||||
log::info!("доступно обновление {version}");
|
||||
// Обновление показываем в интерфейсе, установку спрашиваем отдельно:
|
||||
// во время звонка перезапуск приложения недопустим без согласия.
|
||||
bridge::send(&app, &WebMessage::UpdateAvailable { version: version.clone() });
|
||||
prompt_install(app, update, version).await;
|
||||
}
|
||||
Ok(None) => {
|
||||
log::info!("обновлений нет");
|
||||
if interactive {
|
||||
app.dialog()
|
||||
.message("Установлена последняя версия.")
|
||||
.title("Обновления")
|
||||
.buttons(MessageDialogButtons::Ok)
|
||||
.show(|_| {});
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
log::warn!("проверка обновлений не удалась: {error}");
|
||||
if interactive {
|
||||
let text = format!("Не удалось проверить обновления: {error}");
|
||||
app.dialog()
|
||||
.message(text)
|
||||
.title("Обновления")
|
||||
.buttons(MessageDialogButtons::Ok)
|
||||
.show(|_| {});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Спрашивает согласие и устанавливает обновление.
|
||||
async fn prompt_install<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
update: tauri_plugin_updater::Update,
|
||||
version: String,
|
||||
) {
|
||||
let handle = app.clone();
|
||||
let question = format!(
|
||||
"Доступна версия {version}. Скачать и установить сейчас? Приложение будет перезапущено, \
|
||||
активный голосовой звонок прервётся."
|
||||
);
|
||||
let (tx, rx) = std::sync::mpsc::channel::<bool>();
|
||||
app.dialog()
|
||||
.message(question)
|
||||
.title("Обновление glchat")
|
||||
.buttons(MessageDialogButtons::YesNo)
|
||||
.show(move |confirmed| {
|
||||
let _ = tx.send(confirmed);
|
||||
});
|
||||
|
||||
// Диалог асинхронный: ждём ответ, не блокируя основной поток.
|
||||
let confirmed = tauri::async_runtime::spawn_blocking(move || rx.recv().unwrap_or(false))
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
if !confirmed {
|
||||
return;
|
||||
}
|
||||
|
||||
match update
|
||||
.download_and_install(|_chunk, _total| {}, || {})
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
log::info!("обновление {version} установлено, перезапуск");
|
||||
handle.restart();
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("установка обновления не удалась: {error}");
|
||||
app.dialog()
|
||||
.message(format!(
|
||||
"Обновление не установлено: {error}. Скачать установщик вручную можно на \
|
||||
странице инстанса."
|
||||
))
|
||||
.title("Обновление glchat")
|
||||
.buttons(MessageDialogButtons::Ok)
|
||||
.show(|_| {});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user