feat(settings): add proxy setting

This commit is contained in:
kangfenmao 2024-07-23 00:28:41 +08:00
parent f434fe1231
commit 973d24271b
9 changed files with 68 additions and 11 deletions

View File

@ -1,6 +1,6 @@
import { electronApp, is, optimizer } from '@electron-toolkit/utils' import { electronApp, is, optimizer } from '@electron-toolkit/utils'
import * as Sentry from '@sentry/electron/main' import * as Sentry from '@sentry/electron/main'
import { app, BrowserWindow, ipcMain, Menu, MenuItem, shell } from 'electron' import { app, BrowserWindow, ipcMain, Menu, MenuItem, session, shell } from 'electron'
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer' import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
import windowStateKeeper from 'electron-window-state' import windowStateKeeper from 'electron-window-state'
import { join } from 'path' import { join } from 'path'
@ -101,6 +101,10 @@ app.whenReady().then(() => {
shell.openExternal(url) shell.openExternal(url)
}) })
ipcMain.handle('set-proxy', (_, proxy: string) => {
session.defaultSession.setProxy({ proxyRules: proxy })
})
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法) // 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
ipcMain.handle('check-for-update', async () => { ipcMain.handle('check-for-update', async () => {
autoUpdater.logger?.info('触发检查更新') autoUpdater.logger?.info('触发检查更新')

View File

@ -10,6 +10,7 @@ declare global {
}> }>
checkForUpdate: () => void checkForUpdate: () => void
openWebsite: (url: string) => void openWebsite: (url: string) => void
setProxy: (proxy: string) => void
} }
} }
} }

View File

@ -5,7 +5,8 @@ import { electronAPI } from '@electron-toolkit/preload'
const api = { const api = {
getAppInfo: () => ipcRenderer.invoke('get-app-info'), getAppInfo: () => ipcRenderer.invoke('get-app-info'),
checkForUpdate: () => ipcRenderer.invoke('check-for-update'), checkForUpdate: () => ipcRenderer.invoke('check-for-update'),
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url) openWebsite: (url: string) => ipcRenderer.invoke('open-website', url),
setProxy: (proxy: string) => ipcRenderer.invoke('set-proxy', proxy)
} }
// Use `contextBridge` APIs to expose Electron APIs to // Use `contextBridge` APIs to expose Electron APIs to

View File

@ -4,9 +4,11 @@ import { useAppDispatch } from '@renderer/store'
import { setAvatar } from '@renderer/store/runtime' import { setAvatar } from '@renderer/store/runtime'
import { runAsyncFunction } from '@renderer/utils' import { runAsyncFunction } from '@renderer/utils'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useSettings } from './useSettings'
export function useAppInit() { export function useAppInit() {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { proxyUrl } = useSettings()
useEffect(() => { useEffect(() => {
runAsyncFunction(async () => { runAsyncFunction(async () => {
@ -22,4 +24,8 @@ export function useAppInit() {
isPackaged && setTimeout(window.api.checkForUpdate, 3000) isPackaged && setTimeout(window.api.checkForUpdate, 3000)
}) })
}, []) }, [])
useEffect(() => {
proxyUrl && window.api.setProxy(proxyUrl)
}, [proxyUrl])
} }

View File

@ -39,6 +39,7 @@ const resources = {
'error.enter.api.key': 'Please enter your API key first', 'error.enter.api.key': 'Please enter your API key first',
'error.enter.api.host': 'Please enter your API host first', 'error.enter.api.host': 'Please enter your API host first',
'error.enter.model': 'Please select a model first', 'error.enter.model': 'Please select a model first',
'error.invalid.proxy.url': 'Invalid proxy URL',
'api.connection.failed': 'Connection failed', 'api.connection.failed': 'Connection failed',
'api.connection.success': 'Connection successful', 'api.connection.success': 'Connection successful',
'chat.completion.paused': 'Chat completion paused', 'chat.completion.paused': 'Chat completion paused',
@ -142,7 +143,8 @@ const resources = {
'about.feedback.title': '📝 Feedback', 'about.feedback.title': '📝 Feedback',
'about.feedback.button': 'Feedback', 'about.feedback.button': 'Feedback',
'about.contact.title': '📧 Contact', 'about.contact.title': '📧 Contact',
'about.contact.button': 'Email' 'about.contact.button': 'Email',
'proxy.title': 'Proxy Address'
} }
} }
}, },
@ -182,6 +184,7 @@ const resources = {
'error.enter.api.key': '请输入您的 API 密钥', 'error.enter.api.key': '请输入您的 API 密钥',
'error.enter.api.host': '请输入您的 API 地址', 'error.enter.api.host': '请输入您的 API 地址',
'error.enter.model': '请选择一个模型', 'error.enter.model': '请选择一个模型',
'error.invalid.proxy.url': '无效的代理地址',
'api.connection.failed': '连接失败', 'api.connection.failed': '连接失败',
'api.connection.success': '连接成功', 'api.connection.success': '连接成功',
'chat.completion.paused': '会话已停止', 'chat.completion.paused': '会话已停止',
@ -286,7 +289,8 @@ const resources = {
'about.feedback.title': '📝 意见反馈', 'about.feedback.title': '📝 意见反馈',
'about.feedback.button': '反馈', 'about.feedback.button': '反馈',
'about.contact.title': '📧 邮件联系', 'about.contact.title': '📧 邮件联系',
'about.contact.button': '邮件' 'about.contact.button': '邮件',
'proxy.title': '代理地址'
} }
} }
} }

View File

@ -1,20 +1,22 @@
import { FC } from 'react' import { FC, useState } from 'react'
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from './components' import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from './components'
import { Avatar, Select, Upload } from 'antd' import { Avatar, Input, Select, Upload } from 'antd'
import styled from 'styled-components' import styled from 'styled-components'
import LocalStorage from '@renderer/services/storage' import LocalStorage from '@renderer/services/storage'
import { compressImage } from '@renderer/utils' import { compressImage, isValidProxyUrl } from '@renderer/utils'
import useAvatar from '@renderer/hooks/useAvatar' import useAvatar from '@renderer/hooks/useAvatar'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setAvatar } from '@renderer/store/runtime' import { setAvatar } from '@renderer/store/runtime'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { setLanguage } from '@renderer/store/settings' import { setLanguage } from '@renderer/store/settings'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { setProxyUrl as _setProxyUrl } from '@renderer/store/settings'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
const GeneralSettings: FC = () => { const GeneralSettings: FC = () => {
const avatar = useAvatar() const avatar = useAvatar()
const { language } = useSettings() const { language, proxyUrl: storeProxyUrl } = useSettings()
const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { t } = useTranslation() const { t } = useTranslation()
@ -24,6 +26,16 @@ const GeneralSettings: FC = () => {
localStorage.setItem('language', value) localStorage.setItem('language', value)
} }
const onSetProxyUrl = () => {
if (!proxyUrl || !isValidProxyUrl(proxyUrl)) {
window.message.error({ content: t('message.error.invalid.proxy.url'), key: 'proxy-error' })
return
}
dispatch(_setProxyUrl(proxyUrl))
window.api.setProxy(proxyUrl)
}
return ( return (
<SettingContainer> <SettingContainer>
<SettingTitle>{t('settings.general.title')}</SettingTitle> <SettingTitle>{t('settings.general.title')}</SettingTitle>
@ -62,6 +74,18 @@ const GeneralSettings: FC = () => {
</Upload> </Upload>
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.proxy.title')}</SettingRowTitle>
<Input
placeholder="socks5://127.0.0.1:6153"
value={proxyUrl}
onChange={(e) => setProxyUrl(e.target.value)}
style={{ width: 300 }}
onBlur={() => onSetProxyUrl()}
type="url"
/>
</SettingRow>
<SettingDivider />
</SettingContainer> </SettingContainer>
) )
} }

View File

@ -250,7 +250,8 @@ const migrate = createMigrate({
...state, ...state,
settings: { settings: {
...state.settings, ...state.settings,
showAssistants: true showAssistants: true,
proxyUrl: undefined
} }
} }
} }

View File

@ -7,13 +7,15 @@ export interface SettingsState {
showAssistants: boolean showAssistants: boolean
sendMessageShortcut: SendMessageShortcut sendMessageShortcut: SendMessageShortcut
language: string language: string
proxyUrl?: string
} }
const initialState: SettingsState = { const initialState: SettingsState = {
showRightSidebar: true, showRightSidebar: true,
showAssistants: true, showAssistants: true,
sendMessageShortcut: 'Enter', sendMessageShortcut: 'Enter',
language: navigator.language language: navigator.language,
proxyUrl: undefined
} }
const settingsSlice = createSlice({ const settingsSlice = createSlice({
@ -31,10 +33,14 @@ const settingsSlice = createSlice({
}, },
setLanguage: (state, action: PayloadAction<string>) => { setLanguage: (state, action: PayloadAction<string>) => {
state.language = action.payload state.language = action.payload
},
setProxyUrl: (state, action: PayloadAction<string | undefined>) => {
state.proxyUrl = action.payload
} }
} }
}) })
export const { toggleRightSidebar, toggleShowAssistants, setSendMessageShortcut, setLanguage } = settingsSlice.actions export const { toggleRightSidebar, toggleShowAssistants, setSendMessageShortcut, setLanguage, setProxyUrl } =
settingsSlice.actions
export default settingsSlice.reducer export default settingsSlice.reducer

View File

@ -203,3 +203,13 @@ export function estimateHistoryTokenCount(assistant: Assistant, msgs: Message[])
export const capitalizeFirstLetter = (str: string) => { export const capitalizeFirstLetter = (str: string) => {
return str.charAt(0).toUpperCase() + str.slice(1) return str.charAt(0).toUpperCase() + str.slice(1)
} }
// is valid proxy url
export const isValidProxyUrl = (url: string) => {
try {
new URL(url)
return true
} catch (error) {
return false
}
}