diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock
index b632e51f..decb4e28 100644
--- a/desktop/src-tauri/Cargo.lock
+++ b/desktop/src-tauri/Cargo.lock
@@ -486,6 +486,7 @@ version = "0.2.7"
dependencies = [
"anyhow",
"portable-pty",
+ "reqwest",
"serde",
"serde_json",
"tauri",
@@ -535,6 +536,16 @@ dependencies = [
"version_check",
]
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
[[package]]
name = "core-foundation"
version = "0.10.1"
@@ -558,7 +569,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
dependencies = [
"bitflags 2.11.1",
- "core-foundation",
+ "core-foundation 0.10.1",
"core-graphics-types",
"foreign-types",
"libc",
@@ -571,7 +582,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
"bitflags 2.11.1",
- "core-foundation",
+ "core-foundation 0.10.1",
"libc",
]
@@ -1698,9 +1709,11 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"socket2",
+ "system-configuration",
"tokio",
"tower-service",
"tracing",
+ "windows-registry",
]
[[package]]
@@ -3392,7 +3405,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
dependencies = [
- "core-foundation",
+ "core-foundation 0.10.1",
"core-foundation-sys",
"jni 0.22.4",
"log",
@@ -3512,7 +3525,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags 2.11.1",
- "core-foundation",
+ "core-foundation 0.10.1",
"core-foundation-sys",
"libc",
"security-framework-sys",
@@ -4053,6 +4066,27 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "system-configuration"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
+dependencies = [
+ "bitflags 2.11.1",
+ "core-foundation 0.9.4",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
[[package]]
name = "system-deps"
version = "6.2.2"
@@ -4074,7 +4108,7 @@ checksum = "9103edf55f2da3c82aea4c7fab7c4241032bfeea0e71fa557d98e00e7ce7cc20"
dependencies = [
"bitflags 2.11.1",
"block2",
- "core-foundation",
+ "core-foundation 0.10.1",
"core-graphics",
"crossbeam-channel",
"dispatch2",
@@ -5487,6 +5521,17 @@ dependencies = [
"windows-link 0.1.3",
]
+[[package]]
+name = "windows-registry"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
+dependencies = [
+ "windows-link 0.2.1",
+ "windows-result 0.4.1",
+ "windows-strings 0.5.1",
+]
+
[[package]]
name = "windows-result"
version = "0.3.4"
diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml
index dfb533c0..22e55098 100644
--- a/desktop/src-tauri/Cargo.toml
+++ b/desktop/src-tauri/Cargo.toml
@@ -24,3 +24,4 @@ anyhow = "1.0.102"
portable-pty = "0.9.0"
tauri-plugin-notification = "2"
tauri-plugin-single-instance = "2"
+reqwest = { version = "0.13", default-features = false, features = ["system-proxy"] }
diff --git a/desktop/src-tauri/tauri-config.test.ts b/desktop/src-tauri/tauri-config.test.ts
index ff32f223..21fbe15b 100644
--- a/desktop/src-tauri/tauri-config.test.ts
+++ b/desktop/src-tauri/tauri-config.test.ts
@@ -23,4 +23,11 @@ describe('tauri security config', () => {
expect(csp).toContain('http://127.0.0.1:*')
expect(csp).toContain('http://localhost:*')
})
+
+ it('enables OS proxy discovery for updater downloads', () => {
+ const cargoToml = readFileSync(join(currentDir, 'Cargo.toml'), 'utf8')
+
+ expect(cargoToml).toContain('reqwest = { version = "0.13"')
+ expect(cargoToml).toContain('features = ["system-proxy"]')
+ })
})
diff --git a/desktop/src/__tests__/generalSettings.test.tsx b/desktop/src/__tests__/generalSettings.test.tsx
index 3cde1548..2cba509e 100644
--- a/desktop/src/__tests__/generalSettings.test.tsx
+++ b/desktop/src/__tests__/generalSettings.test.tsx
@@ -8,7 +8,7 @@ import { useUIStore } from '../stores/uiStore'
import { useUpdateStore } from '../stores/updateStore'
import type { SavedProvider } from '../types/provider'
import type { ProviderPreset } from '../types/providerPreset'
-import type { ThemeMode } from '../types/settings'
+import type { ThemeMode, UpdateProxySettings } from '../types/settings'
const MOCK_DELETE_PROVIDER = vi.fn()
const MOCK_GET_SETTINGS = vi.fn()
@@ -799,6 +799,13 @@ describe('Settings > Providers tab', () => {
describe('Settings > About tab', () => {
beforeEach(() => {
useUIStore.setState({ pendingSettingsTab: 'about' })
+ useSettingsStore.setState({
+ locale: 'en',
+ updateProxy: { mode: 'system', url: '' },
+ setUpdateProxy: vi.fn().mockImplementation(async (next: UpdateProxySettings) => {
+ useSettingsStore.setState({ updateProxy: next })
+ }),
+ })
useUpdateStore.setState({
status: 'available',
availableVersion: '0.1.5',
@@ -846,4 +853,57 @@ describe('Settings > About tab', () => {
expect(await screen.findByText('Downloading update... 1.5 KB downloaded')).toBeInTheDocument()
expect(screen.queryByText('Downloading update... 0%')).not.toBeInTheDocument()
})
+
+ it('saves a manual update proxy from the advanced update controls', async () => {
+ render()
+
+ fireEvent.click(screen.getByRole('button', { name: /Advanced update proxy/i }))
+ expect(screen.getByRole('button', { name: /System proxy/i })).toHaveAttribute('aria-pressed', 'true')
+ expect(screen.getByText('This only affects app update checks and downloads.')).toBeInTheDocument()
+
+ fireEvent.click(screen.getByRole('button', { name: /Manual proxy/i }))
+ const proxyInput = screen.getByLabelText('Proxy URL')
+ const saveButton = screen.getByRole('button', { name: 'Save' })
+
+ expect(screen.getByText('Enter a proxy URL.')).toBeInTheDocument()
+ expect(saveButton).toBeDisabled()
+
+ fireEvent.change(proxyInput, { target: { value: 'socks5://127.0.0.1:7890' } })
+ expect(screen.getByText('Enter an HTTP or HTTPS proxy URL.')).toBeInTheDocument()
+ expect(saveButton).toBeDisabled()
+
+ fireEvent.change(proxyInput, { target: { value: ' http://127.0.0.1:7890 ' } })
+ expect(screen.getByText('HTTP and HTTPS proxy URLs are supported, for example http://127.0.0.1:7890.')).toBeInTheDocument()
+
+ await act(async () => {
+ fireEvent.click(saveButton)
+ })
+
+ expect(useSettingsStore.getState().setUpdateProxy).toHaveBeenCalledWith({
+ mode: 'manual',
+ url: 'http://127.0.0.1:7890',
+ })
+ })
+
+ it('can switch update proxy settings back to system mode', async () => {
+ useSettingsStore.setState({
+ updateProxy: { mode: 'manual', url: 'http://127.0.0.1:7890' },
+ })
+ render()
+
+ fireEvent.click(screen.getByRole('button', { name: /Advanced update proxy/i }))
+ expect(screen.getByRole('button', { name: /Manual proxy/i })).toHaveAttribute('aria-pressed', 'true')
+
+ fireEvent.click(screen.getByRole('button', { name: /System proxy/i }))
+ const saveButton = screen.getByRole('button', { name: 'Save' })
+
+ await act(async () => {
+ fireEvent.click(saveButton)
+ })
+
+ expect(useSettingsStore.getState().setUpdateProxy).toHaveBeenCalledWith({
+ mode: 'system',
+ url: 'http://127.0.0.1:7890',
+ })
+ })
})
diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts
index 235dce8a..3c44a663 100644
--- a/desktop/src/i18n/locales/en.ts
+++ b/desktop/src/i18n/locales/en.ts
@@ -1387,6 +1387,17 @@ export const en = {
'update.later': 'Later',
'update.progress': 'Downloading update... {progress}%',
'update.progressBytes': 'Downloading update... {downloaded} downloaded',
+ 'update.proxyAdvanced': 'Advanced update proxy',
+ 'update.proxyModeManual': 'Manual proxy',
+ 'update.proxyModeManualDescription': 'Use a local HTTP proxy address you provide.',
+ 'update.proxyModeSystem': 'System proxy',
+ 'update.proxyModeSystemDescription': 'Use the OS HTTP/HTTPS proxy automatically.',
+ 'update.proxySave': 'Save',
+ 'update.proxyScopeHint': 'This only affects app update checks and downloads.',
+ 'update.proxyUrl': 'Proxy URL',
+ 'update.proxyUrlHint': 'HTTP and HTTPS proxy URLs are supported, for example http://127.0.0.1:7890.',
+ 'update.proxyUrlInvalid': 'Enter an HTTP or HTTPS proxy URL.',
+ 'update.proxyUrlRequired': 'Enter a proxy URL.',
'update.releaseNotes': 'Release Notes',
'update.restarting': 'Restarting to finish update...',
'update.upToDate': 'You are up to date on v{version}.',
diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts
index 750cfd9f..07ecbea3 100644
--- a/desktop/src/i18n/locales/zh.ts
+++ b/desktop/src/i18n/locales/zh.ts
@@ -1389,6 +1389,17 @@ export const zh: Record = {
'update.later': '稍后',
'update.progress': '正在下载更新... {progress}%',
'update.progressBytes': '正在下载更新... 已下载 {downloaded}',
+ 'update.proxyAdvanced': '高级更新代理',
+ 'update.proxyModeManual': '手动代理',
+ 'update.proxyModeManualDescription': '使用你填写的本地 HTTP 代理地址。',
+ 'update.proxyModeSystem': '系统代理',
+ 'update.proxyModeSystemDescription': '自动使用系统 HTTP/HTTPS 代理。',
+ 'update.proxySave': '保存',
+ 'update.proxyScopeHint': '这里只影响应用自身的更新检查和安装包下载。',
+ 'update.proxyUrl': '代理地址',
+ 'update.proxyUrlHint': '支持 HTTP 和 HTTPS 代理,例如 http://127.0.0.1:7890。',
+ 'update.proxyUrlInvalid': '请输入 HTTP 或 HTTPS 代理地址。',
+ 'update.proxyUrlRequired': '请输入代理地址。',
'update.releaseNotes': '更新说明',
'update.restarting': '正在重启以完成更新...',
'update.upToDate': '当前已是最新版本 v{version}。',
diff --git a/desktop/src/pages/Settings.tsx b/desktop/src/pages/Settings.tsx
index e6599d74..7eae08cb 100644
--- a/desktop/src/pages/Settings.tsx
+++ b/desktop/src/pages/Settings.tsx
@@ -9,7 +9,7 @@ import { ConfirmDialog } from '../components/shared/ConfirmDialog'
import { Input } from '../components/shared/Input'
import { Button } from '../components/shared/Button'
import { Dropdown } from '../components/shared/Dropdown'
-import type { PermissionMode, EffortLevel, ThemeMode, WebSearchMode } from '../types/settings'
+import type { PermissionMode, EffortLevel, ThemeMode, UpdateProxyMode, WebSearchMode } from '../types/settings'
import type { Locale } from '../i18n'
import type { SavedProvider, UpdateProviderInput, ProviderTestResult, ModelMapping, ApiFormat, ProviderAuthStrategy } from '../types/provider'
import type { ProviderPreset } from '../types/providerPreset'
@@ -2818,9 +2818,20 @@ const SOCIAL_LINKS = [
{ name: 'Xiaohongshu', icon: '/icons/xiaohongshu.svg', url: 'https://www.xiaohongshu.com/user/profile/5f58bd990000000001003753', label: '程序员阿江-Relakkes' },
] as const
+function isValidUpdateProxyUrl(value: string) {
+ try {
+ const url = new URL(value)
+ return url.protocol === 'http:' || url.protocol === 'https:'
+ } catch {
+ return false
+ }
+}
+
function AboutSettings() {
const t = useTranslation()
const [version, setVersion] = useState('')
+ const updateProxy = useSettingsStore((s) => s.updateProxy)
+ const setUpdateProxy = useSettingsStore((s) => s.setUpdateProxy)
const updateStatus = useUpdateStore((s) => s.status)
const availableVersion = useUpdateStore((s) => s.availableVersion)
const releaseNotes = useUpdateStore((s) => s.releaseNotes)
@@ -2832,6 +2843,10 @@ function AboutSettings() {
const checkForUpdates = useUpdateStore((s) => s.checkForUpdates)
const installUpdate = useUpdateStore((s) => s.installUpdate)
const initialize = useUpdateStore((s) => s.initialize)
+ const [showUpdateProxyAdvanced, setShowUpdateProxyAdvanced] = useState(false)
+ const [updateProxyDraft, setUpdateProxyDraft] = useState(updateProxy)
+ const [updateProxySaveError, setUpdateProxySaveError] = useState(null)
+ const [isSavingUpdateProxy, setIsSavingUpdateProxy] = useState(false)
useEffect(() => {
let cancelled = false
@@ -2854,6 +2869,11 @@ function AboutSettings() {
void initialize()
}, [initialize])
+ useEffect(() => {
+ setUpdateProxyDraft(updateProxy)
+ setUpdateProxySaveError(null)
+ }, [updateProxy])
+
const openUrl = (url: string) => {
import('@tauri-apps/plugin-shell').then((mod) => mod.open(url)).catch(() => window.open(url, '_blank'))
}
@@ -2867,6 +2887,48 @@ function AboutSettings() {
day: 'numeric',
})
: null
+ const updateProxyModes: Array<{ value: UpdateProxyMode; label: string; description: string }> = [
+ {
+ value: 'system',
+ label: t('update.proxyModeSystem'),
+ description: t('update.proxyModeSystemDescription'),
+ },
+ {
+ value: 'manual',
+ label: t('update.proxyModeManual'),
+ description: t('update.proxyModeManualDescription'),
+ },
+ ]
+ const manualProxyUrl = updateProxyDraft.url.trim()
+ const manualProxyError =
+ updateProxyDraft.mode === 'manual' && !manualProxyUrl
+ ? t('update.proxyUrlRequired')
+ : updateProxyDraft.mode === 'manual' && !isValidUpdateProxyUrl(manualProxyUrl)
+ ? t('update.proxyUrlInvalid')
+ : null
+ const updateProxyDirty =
+ updateProxyDraft.mode !== updateProxy.mode ||
+ updateProxyDraft.url.trim() !== updateProxy.url.trim()
+
+ const saveUpdateProxy = async () => {
+ if (manualProxyError) {
+ setUpdateProxySaveError(manualProxyError)
+ return
+ }
+
+ setIsSavingUpdateProxy(true)
+ setUpdateProxySaveError(null)
+ try {
+ await setUpdateProxy({
+ mode: updateProxyDraft.mode,
+ url: manualProxyUrl,
+ })
+ } catch (error) {
+ setUpdateProxySaveError(error instanceof Error ? error.message : String(error))
+ } finally {
+ setIsSavingUpdateProxy(false)
+ }
+ }
const hasKnownProgress = typeof totalBytes === 'number' && totalBytes > 0
const downloadedText = formatBytes(downloadedBytes)
@@ -2970,6 +3032,89 @@ function AboutSettings() {
)}
+
+
+
+ {showUpdateProxyAdvanced && (
+
+
+ {updateProxyModes.map((mode) => (
+
+ ))}
+
+
+ {updateProxyDraft.mode === 'manual' && (
+
+
{
+ setUpdateProxyDraft((current) => ({ ...current, url: event.target.value }))
+ setUpdateProxySaveError(null)
+ }}
+ />
+
+ {manualProxyError ?? t('update.proxyUrlHint')}
+
+
+ )}
+
+
+
+ {t('update.proxyScopeHint')}
+
+
+
+
+ {updateProxySaveError && (
+
+ {updateProxySaveError}
+
+ )}
+
+ )}
+
+
{(updateStatus === 'downloading' || updateStatus === 'restarting') && (
diff --git a/desktop/src/stores/settingsStore.test.ts b/desktop/src/stores/settingsStore.test.ts
index d57a27d7..aae26472 100644
--- a/desktop/src/stores/settingsStore.test.ts
+++ b/desktop/src/stores/settingsStore.test.ts
@@ -63,6 +63,109 @@ describe('settingsStore UI zoom', () => {
})
})
+describe('settingsStore update proxy persistence', () => {
+ beforeEach(() => {
+ vi.resetModules()
+ vi.clearAllMocks()
+ window.localStorage.clear()
+ })
+
+ it('defaults old user settings to automatic system proxy mode', async () => {
+ vi.doMock('../api/settings', () => ({
+ settingsApi: {
+ getUser: vi.fn().mockResolvedValue({}),
+ updateUser: vi.fn(),
+ getPermissionMode: vi.fn().mockResolvedValue({ mode: 'default' }),
+ setPermissionMode: vi.fn(),
+ getCliLauncherStatus: vi.fn(),
+ },
+ }))
+ vi.doMock('../api/models', () => ({
+ modelsApi: {
+ list: vi.fn().mockResolvedValue({ models: [] }),
+ getCurrent: vi.fn().mockResolvedValue({ model: null }),
+ setCurrent: vi.fn(),
+ getEffort: vi.fn().mockResolvedValue({ level: 'medium' }),
+ setEffort: vi.fn(),
+ },
+ }))
+ vi.doMock('../api/h5Access', () => ({
+ h5AccessApi: {
+ get: vi.fn().mockResolvedValue({
+ settings: {
+ enabled: false,
+ tokenPreview: null,
+ allowedOrigins: [],
+ publicBaseUrl: null,
+ },
+ }),
+ enable: vi.fn(),
+ disable: vi.fn(),
+ regenerate: vi.fn(),
+ update: vi.fn(),
+ },
+ }))
+
+ const { useSettingsStore } = await import('./settingsStore')
+
+ await useSettingsStore.getState().fetchAll()
+
+ expect(useSettingsStore.getState().updateProxy).toEqual({
+ mode: 'system',
+ url: '',
+ })
+ })
+
+ it('persists manual update proxy settings trimmed', async () => {
+ const updateUser = vi.fn().mockResolvedValue({})
+ vi.doMock('../api/settings', () => ({
+ settingsApi: {
+ getUser: vi.fn(),
+ updateUser,
+ getPermissionMode: vi.fn(),
+ setPermissionMode: vi.fn(),
+ getCliLauncherStatus: vi.fn(),
+ },
+ }))
+ vi.doMock('../api/models', () => ({
+ modelsApi: {
+ list: vi.fn(),
+ getCurrent: vi.fn(),
+ setCurrent: vi.fn(),
+ getEffort: vi.fn(),
+ setEffort: vi.fn(),
+ },
+ }))
+ vi.doMock('../api/h5Access', () => ({
+ h5AccessApi: {
+ get: vi.fn(),
+ enable: vi.fn(),
+ disable: vi.fn(),
+ regenerate: vi.fn(),
+ update: vi.fn(),
+ },
+ }))
+
+ const { useSettingsStore } = await import('./settingsStore')
+
+ await useSettingsStore.getState().setUpdateProxy({
+ mode: 'manual',
+ url: ' http://127.0.0.1:7890 ',
+ })
+
+ expect(useSettingsStore.getState().updateProxy).toEqual({
+ mode: 'manual',
+ url: 'http://127.0.0.1:7890',
+ })
+ expect(updateUser).toHaveBeenCalledWith({
+ updateProxy: {
+ mode: 'manual',
+ url: 'http://127.0.0.1:7890',
+ },
+ })
+ })
+})
+
describe('settingsStore desktop notification persistence', () => {
beforeEach(() => {
vi.resetModules()
diff --git a/desktop/src/stores/settingsStore.ts b/desktop/src/stores/settingsStore.ts
index e0d10d99..f2a24a2a 100644
--- a/desktop/src/stores/settingsStore.ts
+++ b/desktop/src/stores/settingsStore.ts
@@ -12,6 +12,8 @@ import {
type EffortLevel,
type ModelInfo,
type ThemeMode,
+ type UpdateProxyMode,
+ type UpdateProxySettings,
type WebSearchSettings,
} from '../types/settings'
import type { Locale } from '../i18n'
@@ -54,6 +56,7 @@ type SettingsStore = {
desktopNotificationsEnabled: boolean
desktopTerminal: DesktopTerminalSettings
webSearch: WebSearchSettings
+ updateProxy: UpdateProxySettings
h5Access: H5AccessSettings
h5AccessError: string | null
responseLanguage: string
@@ -73,6 +76,7 @@ type SettingsStore = {
setDesktopNotificationsEnabled: (enabled: boolean) => Promise
setDesktopTerminal: (settings: DesktopTerminalSettings) => Promise
setWebSearch: (settings: WebSearchSettings) => Promise
+ setUpdateProxy: (settings: UpdateProxySettings) => Promise
enableH5Access: () => Promise
disableH5Access: () => Promise
regenerateH5AccessToken: () => Promise
@@ -96,6 +100,11 @@ const DEFAULT_DESKTOP_TERMINAL_SETTINGS: DesktopTerminalSettings = {
customShellPath: '',
}
+const DEFAULT_UPDATE_PROXY_SETTINGS: UpdateProxySettings = {
+ mode: 'system',
+ url: '',
+}
+
export const useSettingsStore = create((set, get) => ({
permissionMode: 'default',
currentModel: null,
@@ -109,6 +118,7 @@ export const useSettingsStore = create((set, get) => ({
desktopNotificationsEnabled: false,
desktopTerminal: DEFAULT_DESKTOP_TERMINAL_SETTINGS,
webSearch: { mode: 'auto', tavilyApiKey: '', braveApiKey: '' },
+ updateProxy: DEFAULT_UPDATE_PROXY_SETTINGS,
h5Access: DEFAULT_H5_ACCESS_SETTINGS,
h5AccessError: null,
responseLanguage: '',
@@ -148,6 +158,7 @@ export const useSettingsStore = create((set, get) => ({
desktopNotificationsEnabled: userSettings.desktopNotificationsEnabled === true,
desktopTerminal: normalizeDesktopTerminalSettings(userSettings.desktopTerminal),
webSearch: normalizeWebSearchSettings(userSettings.webSearch),
+ updateProxy: normalizeUpdateProxySettings(userSettings.updateProxy),
h5Access: h5AccessResult.settings,
h5AccessError: h5AccessResult.error,
responseLanguage: typeof userSettings.language === 'string' ? userSettings.language : '',
@@ -274,6 +285,18 @@ export const useSettingsStore = create((set, get) => ({
}
},
+ setUpdateProxy: async (settings) => {
+ const prev = get().updateProxy
+ const next = normalizeUpdateProxySettings(settings)
+ set({ updateProxy: next })
+ try {
+ await settingsApi.updateUser({ updateProxy: next })
+ } catch (error) {
+ set({ updateProxy: prev })
+ throw error
+ }
+ },
+
enableH5Access: async () => {
set({ h5AccessError: null })
try {
@@ -351,6 +374,22 @@ function normalizeWebSearchSettings(settings: WebSearchSettings | undefined): We
}
}
+function isUpdateProxyMode(value: unknown): value is UpdateProxyMode {
+ return value === 'system' || value === 'manual'
+}
+
+function normalizeUpdateProxySettings(
+ settings: Partial | undefined,
+): UpdateProxySettings {
+ const mode = isUpdateProxyMode(settings?.mode)
+ ? settings.mode
+ : DEFAULT_UPDATE_PROXY_SETTINGS.mode
+ return {
+ mode,
+ url: typeof settings?.url === 'string' ? settings.url.trim() : '',
+ }
+}
+
function normalizeDesktopTerminalSettings(
settings: Partial | undefined,
): DesktopTerminalSettings {
diff --git a/desktop/src/stores/updateStore.test.ts b/desktop/src/stores/updateStore.test.ts
index 2d9cbf37..fad6532f 100644
--- a/desktop/src/stores/updateStore.test.ts
+++ b/desktop/src/stores/updateStore.test.ts
@@ -48,6 +48,29 @@ describe('updateStore', () => {
expect(useUpdateStore.getState().shouldPrompt).toBe(true)
})
+ it('passes the configured manual update proxy to update checks', async () => {
+ const update = {
+ version: '0.2.0',
+ body: 'Bug fixes and performance improvements',
+ close: vi.fn().mockResolvedValue(undefined),
+ }
+ check.mockResolvedValue(update)
+
+ vi.resetModules()
+ const { useSettingsStore } = await import('./settingsStore')
+ useSettingsStore.setState({
+ updateProxy: {
+ mode: 'manual',
+ url: 'http://127.0.0.1:7890',
+ },
+ })
+ const { useUpdateStore } = await import('./updateStore')
+
+ await useUpdateStore.getState().checkForUpdates()
+
+ expect(check).toHaveBeenCalledWith({ proxy: 'http://127.0.0.1:7890' })
+ })
+
it('does not re-prompt for the same version after dismissing once', async () => {
check.mockResolvedValue({
version: '0.2.0',
@@ -133,6 +156,50 @@ describe('updateStore', () => {
expect(relaunch).toHaveBeenCalledTimes(1)
})
+ it('refreshes the pending update when the proxy changes before install', async () => {
+ const staleClose = vi.fn().mockResolvedValue(undefined)
+ const freshDownload = vi.fn(async (onEvent?: (event: unknown) => void) => {
+ onEvent?.({ event: 'Started', data: { contentLength: 100 } })
+ onEvent?.({ event: 'Progress', data: { chunkLength: 100 } })
+ onEvent?.({ event: 'Finished' })
+ })
+ const freshInstall = vi.fn().mockResolvedValue(undefined)
+
+ check
+ .mockResolvedValueOnce({
+ version: '0.2.0',
+ body: 'Notes',
+ close: staleClose,
+ })
+ .mockResolvedValueOnce({
+ version: '0.2.0',
+ body: 'Notes',
+ download: freshDownload,
+ install: freshInstall,
+ close: vi.fn().mockResolvedValue(undefined),
+ })
+ invoke.mockResolvedValue(undefined)
+ relaunch.mockResolvedValue(undefined)
+
+ vi.resetModules()
+ const { useSettingsStore } = await import('./settingsStore')
+ const { useUpdateStore } = await import('./updateStore')
+
+ await useUpdateStore.getState().checkForUpdates()
+ useSettingsStore.setState({
+ updateProxy: {
+ mode: 'manual',
+ url: 'http://127.0.0.1:7890',
+ },
+ })
+ await useUpdateStore.getState().installUpdate()
+
+ expect(staleClose).toHaveBeenCalledTimes(1)
+ expect(check).toHaveBeenNthCalledWith(2, { proxy: 'http://127.0.0.1:7890' })
+ expect(freshDownload).toHaveBeenCalledTimes(1)
+ expect(freshInstall).toHaveBeenCalledTimes(1)
+ })
+
it('clears the native exit guard when install fails after sidecars stop', async () => {
const download = vi.fn(async (onEvent?: (event: unknown) => void) => {
onEvent?.({ event: 'Started', data: { contentLength: 100 } })
diff --git a/desktop/src/stores/updateStore.ts b/desktop/src/stores/updateStore.ts
index 1feaa5cd..b345cf3f 100644
--- a/desktop/src/stores/updateStore.ts
+++ b/desktop/src/stores/updateStore.ts
@@ -1,6 +1,8 @@
import { create } from 'zustand'
import type { Update } from '@tauri-apps/plugin-updater'
import { isTauriRuntime } from '../lib/desktopRuntime'
+import type { UpdateProxySettings } from '../types/settings'
+import { useSettingsStore } from './settingsStore'
export type UpdateStatus =
| 'idle'
@@ -34,6 +36,7 @@ type UpdateStore = {
}
let pendingUpdate: Update | null = null
+let pendingUpdateProxyKey: string | null = null
let startupCheckPromise: Promise | null = null
function readDismissedUpdateVersion(): string | null {
@@ -60,9 +63,26 @@ function writeDismissedUpdateVersion(version: string | null) {
}
}
-async function setPendingUpdate(next: Update | null) {
+function getUpdateProxyUrl(settings: UpdateProxySettings = useSettingsStore.getState().updateProxy) {
+ if (settings.mode !== 'manual') return null
+ const proxy = settings.url.trim()
+ return proxy || null
+}
+
+function getUpdateProxyKey(settings: UpdateProxySettings = useSettingsStore.getState().updateProxy) {
+ const proxy = getUpdateProxyUrl(settings)
+ return proxy ? `manual:${proxy}` : 'system'
+}
+
+function getUpdateCheckOptions() {
+ const proxy = getUpdateProxyUrl()
+ return proxy ? { proxy } : undefined
+}
+
+async function setPendingUpdate(next: Update | null, proxyKey: string | null) {
const previous = pendingUpdate
pendingUpdate = next
+ pendingUpdateProxyKey = next ? proxyKey : null
if (previous && previous !== next) {
try {
@@ -113,8 +133,9 @@ export const useUpdateStore = create((set, get) => ({
try {
const { check } = await import('@tauri-apps/plugin-updater')
- const update = await check()
- await setPendingUpdate(update)
+ const updateProxyKey = getUpdateProxyKey()
+ const update = await check(getUpdateCheckOptions())
+ await setPendingUpdate(update, updateProxyKey)
const checkedAt = Date.now()
@@ -174,6 +195,10 @@ export const useUpdateStore = create((set, get) => ({
if (!isTauriRuntime()) return
let update = pendingUpdate
+ if (update && pendingUpdateProxyKey !== getUpdateProxyKey()) {
+ await setPendingUpdate(null, null)
+ update = null
+ }
if (!update) {
update = await get().checkForUpdates()
if (!update) return
diff --git a/desktop/src/types/settings.ts b/desktop/src/types/settings.ts
index 4b1d985b..24c5e213 100644
--- a/desktop/src/types/settings.ts
+++ b/desktop/src/types/settings.ts
@@ -18,6 +18,13 @@ export type WebSearchSettings = {
braveApiKey?: string
}
+export type UpdateProxyMode = 'system' | 'manual'
+
+export type UpdateProxySettings = {
+ mode: UpdateProxyMode
+ url: string
+}
+
export type H5AccessSettings = {
enabled: boolean
tokenPreview: string | null
@@ -54,6 +61,7 @@ export type UserSettings = {
skipWebFetchPreflight?: boolean
desktopNotificationsEnabled?: boolean
webSearch?: WebSearchSettings
+ updateProxy?: Partial
language?: string
desktopTerminal?: Partial
[key: string]: unknown