perf: 路由批量执行+CIDR聚合+并行脚本+连接步骤显示+CIDR刷新按钮
Release / build-macos (push) Canceled after 0s
Release / build-windows (push) Canceled after 0s
Release / release (push) Canceled after 0s

路由性能优化:
- 修复 macOS route 命令语法错误(去掉 -net 参数)
- 路由应用拆分为 Essential(ready前,4-6条命令<1s)+ Deferred(ready后,批量脚本)
- 新增 CIDR 聚合算法(cidrmerge.go),合并相邻 CIDR 块减少路由数量(10826->7500)
- 批量脚本执行:3平台各实现8个批量函数,临时脚本单次执行替代逐条进程创建
- 并行批量:拆为4个子shell并行执行,总时间降为1/4

连接步骤显示:
- stats 新增 connectStep + cidrError 原子变量
- 连接生命周期中设置步骤(fetch_cidrs/connecting/load_routes),UI 实时显示
- after-proxy CIDR 获取期间显示加载状态,获取失败显示错误提示

CIDR 刷新按钮:
- ipc 新增 CmdRefreshCIDR 命令
- daemon 处理刷新请求,session.RefreshCIDRs() 重新获取+添加路由
- UI 状态卡新增刷新按钮,proxy/bypass 模式已连接时显示

README:
- 补充 URL 获取时机注意事项(bypass模式after-proxy可能失败)
- 补充 CIDR 聚合与批量执行说明
This commit is contained in:
2026-07-09 09:44:49 +08:00
parent 7c49ae4d72
commit ebe082500f
15 changed files with 1244 additions and 196 deletions
+1
View File
@@ -49,6 +49,7 @@ type App struct {
routingModeLabel *widget.Label
cidrV4Label *widget.Label
cidrV6Label *widget.Label
refreshCIDRBtn *widget.Button
connectBtn *widget.Button
disconnectBtn *widget.Button
+112 -22
View File
@@ -78,6 +78,9 @@ func (a *App) buildMainWindow() fyne.CanvasObject {
a.cidrV6Label = widget.NewLabel("")
a.cidrV6Label.Hide()
a.refreshCIDRBtn = widget.NewButton(i18n.T("BtnRefreshCIDR"), a.onRefreshCIDR)
a.refreshCIDRBtn.Hide()
statusCard := widget.NewCard(i18n.T("StatusLabel"), "", container.NewVBox(
a.stateLabel,
a.ipLabel,
@@ -89,6 +92,7 @@ func (a *App) buildMainWindow() fyne.CanvasObject {
a.routingModeLabel,
a.cidrV4Label,
a.cidrV6Label,
a.refreshCIDRBtn,
))
// Buttons.
@@ -218,6 +222,24 @@ func (a *App) onConnect() {
}()
}
// onRefreshCIDR handles the Refresh CIDR button click.
func (a *App) onRefreshCIDR() {
a.mu.Lock()
client := a.ipcClient
a.mu.Unlock()
if client == nil {
return
}
a.refreshCIDRBtn.Disable()
go func() {
_ = ipc.SendRefreshCIDR(client)
time.Sleep(2 * time.Second)
fyne.Do(func() {
a.refreshCIDRBtn.Enable()
})
}()
}
// onDisconnect handles the Disconnect button click.
func (a *App) onDisconnect() {
a.mu.Lock()
@@ -243,6 +265,7 @@ func (a *App) onDisconnect() {
a.routingModeLabel.Hide()
a.cidrV4Label.Hide()
a.cidrV6Label.Hide()
a.refreshCIDRBtn.Hide()
}
// eventLoop reads IPC events from the daemon and updates the UI.
@@ -275,6 +298,7 @@ func (a *App) eventLoop() {
a.routingModeLabel.Hide()
a.cidrV4Label.Hide()
a.cidrV6Label.Hide()
a.refreshCIDRBtn.Hide()
a.setConnButtons(true, false)
}
})
@@ -287,7 +311,7 @@ func (a *App) eventLoop() {
current := a.ipcClient
a.mu.Unlock()
if current == client {
a.applyState(ev.State)
a.applyStateWithStep(ev.State, ev.ConnectStep)
}
})
case ipc.EvStats:
@@ -346,12 +370,27 @@ func authErrorMessage(code, fallback string) string {
// applyState updates UI elements for a state change.
func (a *App) applyState(state string) {
a.applyStateWithStep(state, "")
}
// applyStateWithStep updates UI elements for a state change, optionally
// showing a connection step description (e.g. "fetching CIDR lists").
func (a *App) applyStateWithStep(state, step string) {
stepLabel := connectStepLabel(step)
switch stats.State(state) {
case stats.StateConnected:
a.stateLabel.SetText(i18n.T("StateConnected"))
if stepLabel != "" {
a.stateLabel.SetText(i18n.T("StateConnected") + " (" + stepLabel + ")")
} else {
a.stateLabel.SetText(i18n.T("StateConnected"))
}
a.setConnButtons(false, true)
case stats.StateConnecting:
a.stateLabel.SetText(i18n.T("StateConnecting"))
if stepLabel != "" {
a.stateLabel.SetText(i18n.T("StateConnecting") + " (" + stepLabel + ")")
} else {
a.stateLabel.SetText(i18n.T("StateConnecting"))
}
a.setConnButtons(false, true)
case stats.StateReconnecting:
a.stateLabel.SetText(i18n.T("StateReconnecting"))
@@ -378,7 +417,12 @@ func (a *App) applyStats(s stats.Snapshot) {
a.ip6Label.SetText(i18n.T("Ip6None"))
}
if s.State == stats.StateConnected {
a.stateLabel.SetText(i18n.T("StateConnected"))
stepLabel := connectStepLabel(s.ConnectStep)
if stepLabel != "" {
a.stateLabel.SetText(i18n.T("StateConnected") + " (" + stepLabel + ")")
} else {
a.stateLabel.SetText(i18n.T("StateConnected"))
}
}
a.rxV4Label.SetText(i18n.T("RxV4Label", map[string]interface{}{
"bytes": formatBytes(s.RxBytesV4), "speed": formatSpeed(s.RxSpeedV4),
@@ -405,33 +449,64 @@ func (a *App) applyStats(s stats.Snapshot) {
// Routing mode + CIDR hit statistics.
if s.RoutingMode != "" {
modeLabel := routeModeLabel(s.RoutingMode)
if s.RouteLoading {
modeLabel += " (" + i18n.T("RouteLoading") + ")"
}
a.routingModeLabel.SetText(i18n.T("StatusRoutingMode", map[string]interface{}{"mode": modeLabel}))
a.routingModeLabel.Show()
switch s.RoutingMode {
case "proxy":
a.cidrV4Label.SetText(fmt.Sprintf("IPv4 CIDR: %d/%d %s",
s.CIDRV4Hits, s.CIDRV4Total, i18n.T("CIDRHit")))
a.cidrV6Label.SetText(fmt.Sprintf("IPv6 CIDR: %d/%d %s",
s.CIDRV6Hits, s.CIDRV6Total, i18n.T("CIDRHit")))
// Show CIDR error if present.
if s.CIDRError != "" {
a.cidrV4Label.SetText("IPv4 CIDR: " + i18n.T("CIDRFetchError"))
a.cidrV6Label.SetText("IPv6 CIDR: " + i18n.T("CIDRFetchError"))
a.cidrV4Label.Show()
a.cidrV6Label.Show()
case "bypass":
a.cidrV4Label.SetText(fmt.Sprintf("IPv4 CIDR: %d %s | %s: %d %s",
s.CIDRV4Total, i18n.T("CIDRConfigured"),
i18n.T("CIDRUnmatched"), s.CIDRV4Hits, i18n.T("CIDRDestinations")))
a.cidrV6Label.SetText(fmt.Sprintf("IPv6 CIDR: %d %s | %s: %d %s",
s.CIDRV6Total, i18n.T("CIDRConfigured"),
i18n.T("CIDRUnmatched"), s.CIDRV6Hits, i18n.T("CIDRDestinations")))
a.cidrV4Label.Show()
a.cidrV6Label.Show()
default:
a.cidrV4Label.Hide()
a.cidrV6Label.Hide()
a.refreshCIDRBtn.Show()
} else {
switch s.RoutingMode {
case "proxy":
if s.RouteLoading {
a.cidrV4Label.SetText(fmt.Sprintf("IPv4 CIDR: %d/%d (%s)",
s.CIDRV4Hits, s.CIDRV4Total, i18n.T("CIDRLoading")))
a.cidrV6Label.SetText(fmt.Sprintf("IPv6 CIDR: %d/%d (%s)",
s.CIDRV6Hits, s.CIDRV6Total, i18n.T("CIDRLoading")))
} else {
a.cidrV4Label.SetText(fmt.Sprintf("IPv4 CIDR: %d/%d %s",
s.CIDRV4Hits, s.CIDRV4Total, i18n.T("CIDRHit")))
a.cidrV6Label.SetText(fmt.Sprintf("IPv6 CIDR: %d/%d %s",
s.CIDRV6Hits, s.CIDRV6Total, i18n.T("CIDRHit")))
}
a.cidrV4Label.Show()
a.cidrV6Label.Show()
a.refreshCIDRBtn.Show()
case "bypass":
if s.RouteLoading {
a.cidrV4Label.SetText(fmt.Sprintf("IPv4 CIDR: %d/%d (%s)",
s.CIDRV4Hits, s.CIDRV4Total, i18n.T("CIDRLoading")))
a.cidrV6Label.SetText(fmt.Sprintf("IPv6 CIDR: %d/%d (%s)",
s.CIDRV6Hits, s.CIDRV6Total, i18n.T("CIDRLoading")))
} else {
a.cidrV4Label.SetText(fmt.Sprintf("IPv4 CIDR: %d %s | %s: %d %s",
s.CIDRV4Total, i18n.T("CIDRConfigured"),
i18n.T("CIDRUnmatched"), s.CIDRV4Hits, i18n.T("CIDRDestinations")))
a.cidrV6Label.SetText(fmt.Sprintf("IPv6 CIDR: %d %s | %s: %d %s",
s.CIDRV6Total, i18n.T("CIDRConfigured"),
i18n.T("CIDRUnmatched"), s.CIDRV6Hits, i18n.T("CIDRDestinations")))
}
a.cidrV4Label.Show()
a.cidrV6Label.Show()
a.refreshCIDRBtn.Show()
default:
a.cidrV4Label.Hide()
a.cidrV6Label.Hide()
a.refreshCIDRBtn.Hide()
}
}
} else {
a.routingModeLabel.Hide()
a.cidrV4Label.Hide()
a.cidrV6Label.Hide()
a.refreshCIDRBtn.Hide()
}
}
@@ -450,6 +525,21 @@ func routeModeLabel(code string) string {
}
}
// connectStepLabel translates a connection step code to a localised
// display string. Returns "" for unknown/empty steps.
func connectStepLabel(step string) string {
switch step {
case "fetch_cidrs":
return i18n.T("StepFetchCIDRs")
case "connecting":
return i18n.T("StepConnecting")
case "load_routes":
return i18n.T("StepLoadRoutes")
default:
return ""
}
}
// formatBytes formats a byte count human-readably.
func formatBytes(b int64) string {
const unit = 1024