-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathapp_clash.go
More file actions
228 lines (204 loc) · 7.22 KB
/
app_clash.go
File metadata and controls
228 lines (204 loc) · 7.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
package main
import (
"strings"
"time"
"windsurf-tools-wails/backend/models"
"windsurf-tools-wails/backend/services"
"windsurf-tools-wails/backend/utils"
)
// ── Clash IP 轮换:App 层封装 ──
// buildClashConfig 把 settings 翻译成 ClashRotatorConfig。
func buildClashConfig(s models.Settings) services.ClashRotatorConfig {
whitelist := make([]string, 0)
for _, n := range strings.Split(s.ClashNodes, ",") {
n = strings.TrimSpace(n)
if n != "" {
whitelist = append(whitelist, n)
}
}
interval := time.Duration(s.ClashIntervalMinutes) * time.Minute
if s.ClashIntervalMinutes <= 0 {
interval = 8 * time.Minute
}
return services.ClashRotatorConfig{
ControllerURL: strings.TrimSpace(s.ClashControllerURL),
Secret: strings.TrimSpace(s.ClashSecret),
Group: strings.TrimSpace(s.ClashGroup),
Whitelist: whitelist,
Interval: interval,
RotateOnRL: s.ClashRotateOnRateLimit,
LatencyTestURL: strings.TrimSpace(s.ClashLatencyTestURL),
LatencyMaxMs: s.ClashLatencyMaxMs,
}
}
// applyClashRotatorSettings 根据当前 settings 启停 / 重建 ClashRotator。
// 在 initBackend / UpdateSettings 中调用,应通过 a.mu 串行化。
func (a *App) applyClashRotatorSettings() {
if a.mitmProxy == nil {
return
}
settings := a.store.GetSettings()
a.mu.Lock()
defer a.mu.Unlock()
// 若已有实例,先停掉再决定是否重启
if a.clashRotator != nil {
a.clashRotator.Stop()
a.clashRotator = nil
}
if !settings.ClashRotateEnabled {
a.mitmProxy.SetOnUpstreamRateLimit(nil)
return
}
cfg := buildClashConfig(settings)
if cfg.ControllerURL == "" || cfg.Group == "" {
utils.DLog("[Clash] 启用但 controller_url 或 group 为空,跳过启动")
a.mitmProxy.SetOnUpstreamRateLimit(nil)
return
}
r := services.NewClashRotator(cfg, a.mitmProxy, func(msg string) {
utils.DLog("%s", msg)
})
r.Start()
a.clashRotator = r
a.mitmProxy.SetOnUpstreamRateLimit(func(detail string) {
r.TriggerRotate("rate_limit")
})
}
// stopClashRotator 关停(在 shutdown 中调用)。
func (a *App) stopClashRotator() {
a.mu.Lock()
defer a.mu.Unlock()
if a.clashRotator != nil {
a.clashRotator.Stop()
a.clashRotator = nil
}
}
// ── 暴露给前端的方法 ──
// TestClashController 探活并返回 selector 组列表。
func (a *App) TestClashController(controllerURL, secret string) services.ClashProbeResult {
return services.ProbeClashController(controllerURL, secret)
}
// ListClashGroupNodes 列出指定组内的节点(不含 DIRECT/REJECT/GLOBAL/组自身)。
func (a *App) ListClashGroupNodes(controllerURL, secret, group string) ([]string, error) {
return services.ListClashGroupNodes(controllerURL, secret, group)
}
// TriggerClashRotate UI 「立即换 IP」按钮。
func (a *App) TriggerClashRotate() bool {
a.mu.Lock()
r := a.clashRotator
a.mu.Unlock()
if r == nil {
return false
}
r.TriggerRotate("manual")
return true
}
// GetClashRotatorRunning 是否在运行(Settings UI 可用来显示状态)。
func (a *App) GetClashRotatorRunning() bool {
a.mu.Lock()
defer a.mu.Unlock()
return a.clashRotator != nil
}
// AutoSetupClashResult 「智能启用」按钮的反馈,给前端弹 toast 用。
type AutoSetupClashResult struct {
OK bool `json:"ok"`
Error string `json:"error,omitempty"`
Hint string `json:"hint,omitempty"`
Group string `json:"group,omitempty"` // 自动选中的组名
NodeCount int `json:"node_count,omitempty"` // 真节点数
From string `json:"from,omitempty"` // 切换前节点
To string `json:"to,omitempty"` // 立即切换后的节点
}
// AutoSetupClash 一键智能启用 Clash IP 轮换:
// 1. 探活控制器(要求用户已在设置里填了 controller_url + secret)
// 2. 自动挑选节点最多的 selector group
// 3. 写回 settings 并重启 rotator
// 4. 立即触发一次切换,验证端到端有效
// 5. 返回切换前后节点 + 真节点数,给 UI 显示
//
// 这是用户「点一下就好」的入口;用户不需要懂 group / 白名单 / type 的概念。
func (a *App) AutoSetupClash() AutoSetupClashResult {
if a.store == nil {
return AutoSetupClashResult{Error: "store 未初始化"}
}
settings := a.store.GetSettings()
url := strings.TrimSpace(settings.ClashControllerURL)
if url == "" {
return AutoSetupClashResult{
Error: "请先在「Clash IP 轮换」面板里填写控制器地址(如 http://127.0.0.1:9097)",
Hint: "Verge 默认 9097;Mihomo 默认 9090;ClashX 默认 9090。",
}
}
// ① 探活
probe := services.ProbeClashController(url, strings.TrimSpace(settings.ClashSecret))
if !probe.OK {
return AutoSetupClashResult{
Error: "控制器探活失败: " + probe.Error,
Hint: "检查 1) Clash 是否运行;2) external-controller 端口是否对;3) secret 是否对;4) 防火墙是否拦了。",
}
}
// ② 自动挑组
auto := services.AutoDetectClashGroup(url, strings.TrimSpace(settings.ClashSecret))
if !auto.OK {
hint := "请在 Clash 配置里增加一个 type=selector 的代理组。"
if len(auto.AllGroups) > 0 {
hint += " 或者手动在面板填以下其中一个:" + strings.Join(auto.AllGroups, " / ")
}
return AutoSetupClashResult{Error: auto.Error, Hint: hint}
}
// ③ 写回 settings —— 注意保留用户已配置的其它字段
settings.ClashRotateEnabled = true
settings.ClashGroup = auto.Group
// ★ 强制开启「限速自动切」:用户期望「智能启用 = 一切自动」,
// 之前若显式关过这个开关也会被覆盖。否则 rate-limit 触发时不切,
// 用户会困惑「为什么 IDE 报 rate limit 但 IP 没变」。
settings.ClashRotateOnRateLimit = true
// 不主动写白名单:让 type-aware 过滤兜底,避免覆盖用户现有白名单
if err := a.store.UpdateSettings(settings); err != nil {
return AutoSetupClashResult{Error: "保存设置失败: " + err.Error()}
}
// ④ 重启 rotator(applyClashRotatorSettings 会读最新 settings)
a.applyClashRotatorSettings()
// ⑤ 立即触发一次切换并捕获 from→to
a.mu.Lock()
r := a.clashRotator
a.mu.Unlock()
if r == nil {
return AutoSetupClashResult{
Error: "rotator 启动失败(applyClashRotatorSettings 未创建实例)",
Group: auto.Group, NodeCount: auto.NodeCount,
}
}
// 先记下当前节点
_, _, fromBefore := r.Stats()
r.TriggerRotate("manual")
// 等最多 3s 让 rotateOnce 完成(loop 协程异步执行)
deadline := time.Now().Add(3 * time.Second)
for time.Now().Before(deadline) {
_, _, last := r.Stats()
if last != "" && last != fromBefore {
fromBefore = last
break
}
time.Sleep(80 * time.Millisecond)
}
_, _, to := r.Stats()
return AutoSetupClashResult{
OK: true,
Group: auto.Group,
NodeCount: auto.NodeCount,
From: fromBefore,
To: to,
}
}
// AutoDetectClashGroup 仅做检测,不写设置 —— 用于 UI 的「自动检测」按钮预览。
func (a *App) AutoDetectClashGroup() services.AutoDetectClashGroupResult {
if a.store == nil {
return services.AutoDetectClashGroupResult{Error: "store 未初始化"}
}
s := a.store.GetSettings()
return services.AutoDetectClashGroup(
strings.TrimSpace(s.ClashControllerURL),
strings.TrimSpace(s.ClashSecret),
)
}