Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions frontend/src/components/General/Index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ const autoStartEnabled = ref(getCachedValue('autoStart', false))
const autoUpdateEnabled = ref(getCachedValue('autoUpdate', true))
const autoConnectivityTestEnabled = ref(getCachedValue('autoConnectivityTest', false))
const switchNotifyEnabled = ref(getCachedValue('switchNotify', true)) // 切换通知开关

// 代理配置相关状态
const useProxy = ref(getCachedValue('useProxy', false))
const proxyAddress = ref(localStorage.getItem('app-settings-proxyAddress') || '')
const proxyType = ref(localStorage.getItem('app-settings-proxyType') || 'http')

const settingsLoading = ref(true)
const saveBusy = ref(false)

Expand Down Expand Up @@ -65,6 +71,9 @@ const loadAppSettings = async () => {
autoUpdateEnabled.value = data?.auto_update ?? true
autoConnectivityTestEnabled.value = data?.auto_connectivity_test ?? false
switchNotifyEnabled.value = data?.enable_switch_notify ?? true
useProxy.value = data?.use_proxy ?? false
proxyAddress.value = data?.proxy_address ?? ''
proxyType.value = data?.proxy_type ?? 'http'

// 缓存到 localStorage,下次打开时直接显示正确状态
localStorage.setItem('app-settings-heatmap', String(heatmapEnabled.value))
Expand All @@ -73,6 +82,9 @@ const loadAppSettings = async () => {
localStorage.setItem('app-settings-autoUpdate', String(autoUpdateEnabled.value))
localStorage.setItem('app-settings-autoConnectivityTest', String(autoConnectivityTestEnabled.value))
localStorage.setItem('app-settings-switchNotify', String(switchNotifyEnabled.value))
localStorage.setItem('app-settings-useProxy', String(useProxy.value))
localStorage.setItem('app-settings-proxyAddress', proxyAddress.value)
localStorage.setItem('app-settings-proxyType', proxyType.value)
} catch (error) {
console.error('failed to load app settings', error)
heatmapEnabled.value = true
Expand All @@ -81,6 +93,9 @@ const loadAppSettings = async () => {
autoUpdateEnabled.value = true
autoConnectivityTestEnabled.value = false
switchNotifyEnabled.value = true
useProxy.value = false
proxyAddress.value = ''
proxyType.value = 'http'
} finally {
settingsLoading.value = false
}
Expand All @@ -97,6 +112,9 @@ const persistAppSettings = async () => {
auto_update: autoUpdateEnabled.value,
auto_connectivity_test: autoConnectivityTestEnabled.value,
enable_switch_notify: switchNotifyEnabled.value,
use_proxy: useProxy.value,
proxy_address: proxyAddress.value,
proxy_type: proxyType.value,
}
await saveAppSettings(payload)

Expand Down Expand Up @@ -458,6 +476,51 @@ onMounted(async () => {
</div>
</section>

<!-- Proxy Settings -->
<section>
<h2 class="mac-section-title">{{ $t('components.general.title.proxy') }}</h2>
<div class="mac-panel">
<ListItem :label="$t('components.general.label.useProxy')">
<div class="toggle-with-hint">
<label class="mac-switch">
<input
type="checkbox"
:disabled="settingsLoading || saveBusy"
v-model="useProxy"
@change="persistAppSettings"
/>
<span></span>
</label>
<span class="hint-text">{{ $t('components.general.label.useProxyHint') }}</span>
</div>
</ListItem>

<template v-if="useProxy">
<ListItem :label="$t('components.general.label.proxyType')">
<select
v-model="proxyType"
:disabled="settingsLoading || saveBusy"
@change="persistAppSettings"
class="mac-select">
<option value="http">HTTP/HTTPS</option>
<option value="socks5">SOCKS5</option>
</select>
</ListItem>

<ListItem :label="$t('components.general.label.proxyAddress')">
<input
type="text"
v-model="proxyAddress"
@blur="persistAppSettings"
Comment on lines +511 to +514
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proxy address input uses @blur event for saving, which means changes are only persisted when the user clicks away from the input field. This can lead to data loss if the user enters a proxy address and immediately closes the settings without clicking elsewhere first. Consider using @input with debouncing or adding an explicit "Apply" button to ensure changes are saved reliably.

Copilot uses AI. Check for mistakes.
:placeholder="$t('components.general.label.proxyAddressPlaceholder')"
:disabled="settingsLoading || saveBusy"
class="mac-input proxy-address-input"
/>
</ListItem>
</template>
</div>
</section>

<!-- Network & WSL Settings -->
<NetworkWslSettings />

Expand Down Expand Up @@ -663,6 +726,11 @@ onMounted(async () => {
font-size: 12px;
}

.proxy-address-input {
width: 320px;
font-size: 12px;
}

.info-text.warning {
color: var(--mac-text-warning, #e67e22);
}
Expand Down
10 changes: 8 additions & 2 deletions frontend/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,8 @@
"update": "Application update",
"blacklist": "Blacklist Settings",
"dataImport": "Data Import",
"connectivity": "Connectivity Test"
"connectivity": "Connectivity Test",
"proxy": "Proxy Network Settings"
},
"label": {
"assistant_access": "Accessibility permissions",
Expand Down Expand Up @@ -548,7 +549,12 @@
"times": "times",
"minutes": "minutes",
"save": "Save",
"saving": "Saving..."
"saving": "Saving...",
"useProxy": "Use proxy server",
"useProxyHint": "When enabled, all requests to vendor APIs will be forwarded through the proxy",
"proxyType": "Proxy type",
"proxyAddress": "Proxy address",
"proxyAddressPlaceholder": "e.g., http://127.0.0.1:1080 or socks5://127.0.0.1:1080"
},
"subLabel": {
"sb_assistant_access": "Accessibility permission is required to operate clipboard contents",
Expand Down
10 changes: 8 additions & 2 deletions frontend/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,8 @@
"update": "应用更新",
"blacklist": "拉黑配置",
"dataImport": "数据导入",
"connectivity": "连通性检测"
"connectivity": "连通性检测",
"proxy": "代理网络设置"
},
"label": {
"assistant_access": "辅助功能访问权限",
Expand Down Expand Up @@ -549,7 +550,12 @@
"times": "次",
"minutes": "分钟",
"save": "保存",
"saving": "保存中..."
"saving": "保存中...",
"useProxy": "使用代理服务器",
"useProxyHint": "启用后所有发往供应商 API 的请求将通过代理转发",
"proxyType": "代理类型",
"proxyAddress": "代理地址",
"proxyAddressPlaceholder": "例如:http://127.0.0.1:1080 或 socks5://127.0.0.1:1080"
},
"subLabel": {
"sb_assistant_access": "需要无障碍访问权限来操作剪切板内容",
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/services/appSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export type AppSettings = {
auto_update: boolean
auto_connectivity_test: boolean
enable_switch_notify: boolean // 供应商切换通知开关
use_proxy: boolean // 是否启用代理服务器
proxy_address: string // 代理地址
proxy_type: string // 代理类型:http/https/socks5
}

const DEFAULT_SETTINGS: AppSettings = {
Expand All @@ -16,6 +19,9 @@ const DEFAULT_SETTINGS: AppSettings = {
auto_update: true,
auto_connectivity_test: false,
enable_switch_notify: true, // 默认开启
use_proxy: false, // 默认不使用代理
proxy_address: '', // 默认代理地址为空
proxy_type: 'http', // 默认代理类型为 HTTP
}

export const fetchAppSettings = async (): Promise<AppSettings> => {
Expand Down
17 changes: 16 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,22 @@ func main() {
}
log.Println("✅ 数据库写入队列已启动")

// 【修复】第三步:创建服务(现在可以安全使用数据库了)
// 【新增】第三步:初始化全局 HTTP 客户端(支持代理配置)
proxyConfig, err := services.GetProxyConfig()
if err != nil {
log.Printf("⚠️ 读取代理配置失败,使用默认配置: %v", err)
proxyConfig = services.ProxyConfig{UseProxy: false}
}
if err := services.InitHTTPClient(proxyConfig); err != nil {
log.Fatalf("初始化 HTTP 客户端失败: %v", err)
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using log.Fatalf when HTTP client initialization fails is overly strict and prevents the application from starting even if the proxy configuration error is non-critical. Since proxy configuration errors are already handled gracefully when reading config (line 98), initialization should also be non-fatal. Consider logging the error and falling back to a default client instead of terminating the entire application.

Suggested change
log.Fatalf("初始化 HTTP 客户端失败: %v", err)
log.Printf("⚠️ 初始化 HTTP 客户端失败,使用默认配置重试: %v", err)
fallbackConfig := services.ProxyConfig{UseProxy: false}
if fallbackErr := services.InitHTTPClient(fallbackConfig); fallbackErr != nil {
log.Printf("❌ 使用默认配置初始化 HTTP 客户端也失败: %v", fallbackErr)
} else {
proxyConfig = fallbackConfig
}

Copilot uses AI. Check for mistakes.
}
if proxyConfig.UseProxy {
log.Printf("✅ HTTP 客户端已初始化(代理: %s %s)", proxyConfig.ProxyType, proxyConfig.ProxyAddress)
} else {
log.Println("✅ HTTP 客户端已初始化(直连模式)")
}

// 【修复】第四步:创建服务(现在可以安全使用数据库了)
suiService, errt := services.NewSuiStore()
if errt != nil {
log.Fatalf("SuiStore 初始化失败: %v", errt)
Expand Down
38 changes: 28 additions & 10 deletions services/appsettings.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,20 @@ import (
const (
appSettingsDir = ".code-switch" // 【修复】修正拼写错误(原为 .codex-swtich)
appSettingsFile = "app.json"
oldSettingsDir = ".codex-swtich" // 旧的错误拼写
oldSettingsDir = ".codex-swtich" // 旧的错误拼写
migrationMarkerFile = ".migrated-from-codex-swtich" // 迁移标记文件
)

type AppSettings struct {
ShowHeatmap bool `json:"show_heatmap"`
ShowHomeTitle bool `json:"show_home_title"`
AutoStart bool `json:"auto_start"`
AutoUpdate bool `json:"auto_update"`
AutoConnectivityTest bool `json:"auto_connectivity_test"`
EnableSwitchNotify bool `json:"enable_switch_notify"` // 供应商切换通知开关
ShowHeatmap bool `json:"show_heatmap"`
ShowHomeTitle bool `json:"show_home_title"`
AutoStart bool `json:"auto_start"`
AutoUpdate bool `json:"auto_update"`
AutoConnectivityTest bool `json:"auto_connectivity_test"`
EnableSwitchNotify bool `json:"enable_switch_notify"` // 供应商切换通知开关
UseProxy bool `json:"use_proxy"` // 是否启用代理服务器
ProxyAddress string `json:"proxy_address"` // 代理地址(如 http://127.0.0.1:1080)
ProxyType string `json:"proxy_type"` // 代理类型:http/https/socks5
}

type AppSettingsService struct {
Expand Down Expand Up @@ -139,9 +142,12 @@ func (as *AppSettingsService) defaultSettings() AppSettings {
ShowHeatmap: true,
ShowHomeTitle: true,
AutoStart: autoStartEnabled,
AutoUpdate: true, // 默认开启自动更新
AutoConnectivityTest: true, // 默认开启自动可用性监控(开箱即用)
EnableSwitchNotify: true, // 默认开启切换通知
AutoUpdate: true, // 默认开启自动更新
AutoConnectivityTest: true, // 默认开启自动可用性监控(开箱即用)
EnableSwitchNotify: true, // 默认开启切换通知
UseProxy: false, // 默认不使用代理
ProxyAddress: "", // 默认代理地址为空
ProxyType: "http", // 默认代理类型为 HTTP
}
}

Expand Down Expand Up @@ -173,6 +179,18 @@ func (as *AppSettingsService) SaveAppSettings(settings AppSettings) (AppSettings
if err := as.saveLocked(settings); err != nil {
return settings, err
}

// 【新增】同步代理配置到全局 HTTP 客户端
proxyConfig := ProxyConfig{
UseProxy: settings.UseProxy,
ProxyAddress: settings.ProxyAddress,
ProxyType: settings.ProxyType,
}
if err := UpdateHTTPClient(proxyConfig); err != nil {
// 代理配置更新失败不应阻止设置保存,只记录错误
fmt.Printf("⚠️ 更新 HTTP 客户端代理配置失败: %v\n", err)
}
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using fmt.Printf for error logging is inconsistent with the logging pattern used elsewhere in the codebase, where log.Printf is the standard. This error should be logged using log.Printf instead to maintain consistency with the project's logging approach.

Copilot uses AI. Check for mistakes.

Comment on lines +183 to +193
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proxy address is saved without validation. Invalid proxy addresses (e.g., malformed URLs, missing ports) will only fail when attempting to use them, not at save time. This results in unclear error feedback to users. Consider adding validation to check if the proxy address is properly formatted before saving, and provide clear error messages if validation fails.

Copilot uses AI. Check for mistakes.
return settings, nil
}

Expand Down
10 changes: 1 addition & 9 deletions services/connectivitytestservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,7 @@ func NewConnectivityTestService(
"gemini": {},
},
autoTestEnabled: false,
client: &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
DisableCompression: true,
MaxIdleConnsPerHost: 5,
},
},
client: GetHTTPClientWithTimeout(10 * time.Second),
}
}

Expand Down
11 changes: 1 addition & 10 deletions services/healthcheckservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,16 +122,7 @@ func NewHealthCheckService(
"gemini": {},
},
pollInterval: time.Duration(DefaultPollIntervalSeconds) * time.Second,
client: &http.Client{
// 由每次请求的 context 控制超时,避免固定值截断自定义配置
Timeout: 0,
Transport: &http.Transport{
MaxIdleConns: 20,
IdleConnTimeout: 30 * time.Second,
DisableCompression: true,
MaxIdleConnsPerHost: 5,
},
},
client: GetHTTPClient(), // 使用全局客户端,超时由每次请求的 context 控制
}
}

Expand Down
Loading