-
Журнал
+
+
Журнал
+
+ {(["wizard", "proxy"] as const).map((value) => (
+
+ ))}
+
+
- Последние 256 КиБ текущего файла Wizard.log. {paused ? "Обновление приостановлено." : "Обновляется раз в секунду."}
+ Последние 256 КиБ текущего файла {fileName}. {paused ? "Автообновление приостановлено." : "Обновляется раз в секунду."}
{error && {error}}
{text === undefined ? "Загрузка…" : text || "Журнал пока пуст."}
diff --git a/crates/wizard-gui/wizard-ui/src/settings/SettingsPage.tsx b/crates/wizard-gui/wizard-ui/src/settings/SettingsPage.tsx
index 1202753..56443cc 100644
--- a/crates/wizard-gui/wizard-ui/src/settings/SettingsPage.tsx
+++ b/crates/wizard-gui/wizard-ui/src/settings/SettingsPage.tsx
@@ -7,6 +7,7 @@ import { Switch, SwitchField, SwitchGroup } from "../components/catalyst/switch"
import { Text } from "../components/catalyst/text";
import { ErrorText } from "../components/ErrorText";
import { type ApplicationSetting, setApplicationSetting, useApplicationSettings } from "./application_settings";
+import { setSystemProxy, useSystemProxy } from "./system_proxy";
export function SettingsPage() {
const { settings, error: loadError } = useApplicationSettings();
@@ -62,6 +63,60 @@ export function SettingsPage() {
>
)}
+
>
);
}
+
+function SystemProxySettings() {
+ const { state, error: loadError } = useSystemProxy();
+ const inFlight = useRef(false);
+ const [submitting, setSubmitting] = useState(false);
+ const [commandError, setCommandError] = useState();
+ const hasOurs = state?.status === "on" || state?.status === "partial";
+ const known = hasOurs || state?.status === "off";
+ const busy = submitting || state?.busy === true;
+ const disabled = busy || Boolean(loadError) || !known || (!hasOurs && state?.conflict === true);
+
+ async function change(enabled: boolean) {
+ if (inFlight.current) return;
+ inFlight.current = true;
+ setSubmitting(true);
+ setCommandError(undefined);
+ try {
+ await setSystemProxy(enabled);
+ } catch (cause: unknown) {
+ setCommandError(typeof cause === "string" ? cause : "Не удалось изменить системный прокси");
+ } finally {
+ inFlight.current = false;
+ setSubmitting(false);
+ }
+ }
+
+ return (
+
+
+
+ Wizard будет проксировать сайты OpenAI и Claude, остальной интернет будет работать как раньше.
+ { void change(enabled); }} />
+
+
+ {busy && Применение системных настроек… На macOS может потребоваться авторизация.}
+ {(!state || (state.status === "unknown" && !state.read_error)) && !loadError && Определение состояния…}
+ {state?.status === "unsupported" && Системный прокси пока доступен только на macOS и Windows.}
+ {state?.status === "unavailable" && Нет поддерживаемых сетевых сервисов.}
+ {state?.status === "partial" && (
+ Частично включён: настройки Wizard действуют не для всех протоколов или сетевых сервисов либо конфликтуют с другими настройками. Выключение уберёт только настройки Wizard.
+ )}
+ {state?.conflict && Обнаружены другие настройки прокси, PAC или автоопределение. Wizard не перезаписывает их; сначала отключите их в настройках ОС.}
+ {known && !state?.listener_running && (
+ Локальный прокси Claude Desktop остановлен. Включение разрешено, но приложения с системным прокси могут потерять доступ к сети.
+ )}
+ {hasOurs && После выхода из Wizard приложения с системным прокси могут остаться без сети до запуска локального прокси или отключения этой настройки.}
+ {loadError && {loadError}}
+ {state?.read_error && {state.read_error}}
+ {(state?.operation_error || commandError) && {state?.operation_error || commandError}}
+
+
+ );
+}
diff --git a/crates/wizard-gui/wizard-ui/src/settings/system_proxy.ts b/crates/wizard-gui/wizard-ui/src/settings/system_proxy.ts
new file mode 100644
index 0000000..c39c73a
--- /dev/null
+++ b/crates/wizard-gui/wizard-ui/src/settings/system_proxy.ts
@@ -0,0 +1,48 @@
+import { invoke } from "@tauri-apps/api/core";
+import { useEffect, useState } from "react";
+
+type SystemProxyState = {
+ status: "unknown" | "off" | "on" | "partial" | "unavailable" | "unsupported";
+ busy: boolean;
+ conflict: boolean;
+ listener_running: boolean;
+ read_error: string | null;
+ operation_error: string | null;
+};
+type SystemProxySnapshot = SystemProxyState & { revision: number };
+
+// Only observes the shared backend state. No saved flag, automatic mutation or
+// UI-owned process state; tray actions remain visible after reopening the window.
+export function useSystemProxy() {
+ const [state, setState] = useState();
+ const [error, setError] = useState();
+
+ useEffect(() => {
+ let stopped = false;
+ async function observe() {
+ let lastRevision: number | null = null;
+ while (!stopped) {
+ try {
+ const next: SystemProxySnapshot = await invoke("get_system_proxy_state", { lastRevision });
+ if (stopped) return;
+ setState(next);
+ setError(undefined);
+ lastRevision = next.revision;
+ } catch {
+ if (stopped) return;
+ setError("Не удалось получить состояние системного прокси");
+ lastRevision = null;
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ }
+ }
+ }
+ void observe();
+ return () => { stopped = true; };
+ }, []);
+
+ return { state, error };
+}
+
+export function setSystemProxy(enabled: boolean): Promise {
+ return invoke("set_system_proxy", { enabled });
+}
diff --git a/crates/wizard-proxy-daemon/Cargo.toml b/crates/wizard-proxy-daemon/Cargo.toml
new file mode 100644
index 0000000..aa85288
--- /dev/null
+++ b/crates/wizard-proxy-daemon/Cargo.toml
@@ -0,0 +1,32 @@
+[package]
+name = "wizard-proxy-daemon"
+version.workspace = true
+authors.workspace = true
+license.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+publish.workspace = true
+
+[lints]
+workspace = true
+
+[dependencies]
+chrono.workspace = true
+file-rotate.workspace = true
+log.workspace = true
+tokio = { workspace = true, features = ["rt", "macros", "signal"] }
+wizard-proxy.workspace = true
+wizard-settings.workspace = true
+
+[target.'cfg(unix)'.dependencies]
+libc.workspace = true
+
+[target.'cfg(windows)'.dependencies]
+tempfile.workspace = true
+windows-sys = { workspace = true, features = ["Win32_Foundation", "Win32_System_SystemInformation", "Win32_System_Threading"] }
+
+[target.'cfg(target_os = "macos")'.dependencies]
+block2.workspace = true
+objc2.workspace = true
+objc2-foundation.workspace = true
+objc2-service-management.workspace = true
diff --git a/crates/wizard-proxy-daemon/src/lib.rs b/crates/wizard-proxy-daemon/src/lib.rs
new file mode 100644
index 0000000..1afbbb4
--- /dev/null
+++ b/crates/wizard-proxy-daemon/src/lib.rs
@@ -0,0 +1,396 @@
+use core::time::Duration;
+use std::fs::{self, File, OpenOptions, TryLockError};
+use std::io;
+use std::process::ExitCode;
+use tokio::sync::Mutex;
+use wizard_proxy::diagnostics;
+
+mod logging;
+
+#[cfg(unix)]
+mod limits;
+#[cfg(unix)]
+mod unix;
+
+#[cfg(target_os = "macos")]
+mod macos;
+#[cfg(target_os = "macos")]
+use macos as platform;
+
+#[cfg(windows)]
+mod windows;
+#[cfg(windows)]
+use windows as platform;
+
+#[cfg(any(windows, test))]
+mod windows_task;
+
+#[cfg(target_os = "linux")]
+mod linux;
+#[cfg(target_os = "linux")]
+use linux as platform;
+
+/// Prepare the service once at GUI startup, before synchronizing settings.
+/// In release builds, a running daemon with the same workspace crate version is
+/// left untouched. Debug builds ignore version matches to pick up local rebuilds.
+/// Otherwise a valid Status or refused connection triggers platform registration.
+/// Other HTTP errors leave registration unchanged.
+/// Await this operation to completion: cancellation can leave native registration
+/// running after the synchronization lock is released.
+pub async fn create_daemon() -> io::Result<()> {
+ let _guard = SYNC_LOCK.lock().await;
+ match wizard_proxy::control::status().await {
+ Ok(status) if !cfg!(debug_assertions) && status.version == env!("CARGO_PKG_VERSION") => {
+ log::info!("proxy daemon version matches; keeping current service");
+ return Ok(());
+ }
+ Ok(_) => log::info!("preparing proxy service registration: version differs or debug build"),
+ Err(error) if error.kind() == io::ErrorKind::ConnectionRefused => {
+ log::info!("preparing proxy service registration: daemon is not listening");
+ }
+ Err(error) => {
+ diagnostics::error("proxy version lookup before registration", &error);
+ return Err(error);
+ }
+ }
+
+ let result = platform::create_daemon().await;
+ match &result {
+ Ok(()) => log::info!("proxy service registration ready"),
+ Err(error) => diagnostics::error("proxy service registration", error),
+ }
+ result
+}
+
+static SYNC_LOCK: Mutex<()> = Mutex::const_new(());
+
+/// Exclusive access to service lifecycle operations while replacing the application.
+/// Keep this guard until restart, or consume it to recover after an installation error.
+/// Unlike CLI uninstall, this is used inside a GUI holding the shared GUI lifetime lock.
+#[must_use = "keep the guard alive throughout installation and until application restart"]
+pub struct UpdateGuard {
+ _lock: tokio::sync::MutexGuard<'static, ()>,
+ restore_service: bool,
+}
+
+/// Stop and unregister the service after downloading and verifying an update.
+/// Failure must abort installation. Await completion: native work may outlive cancellation.
+pub async fn prepare_update() -> io::Result {
+ prepare_update_with(platform::uninstall()).await
+}
+
+async fn prepare_update_with(
+ uninstall: impl Future