Skip to content

Commit d06a3e3

Browse files
committed
update: 优化增加批量删除效果
fix: 强刷实例解析ipv6的问题 feat: 支持检测系统代理
1 parent bea71c2 commit d06a3e3

7 files changed

Lines changed: 171 additions & 109 deletions

File tree

app/tunnels/page.tsx

Lines changed: 41 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -248,56 +248,56 @@ export default function TunnelsPage() {
248248
const handleBatchDelete = async () => {
249249
if (!selectedKeys || (selectedKeys instanceof Set && selectedKeys.size === 0)) return;
250250

251-
try {
252-
// 计算待删除的 ID 列表
253-
let ids: number[] = [];
254-
if (selectedKeys === "all") {
255-
ids = filteredItems.map((t) => Number(t.id));
256-
} else {
257-
ids = Array.from(selectedKeys as Set<string>).map((id) => Number(id));
258-
}
251+
// 计算待删除的 ID 列表
252+
let ids: number[] = [];
253+
if (selectedKeys === "all") {
254+
ids = filteredItems.map((t) => Number(t.id));
255+
} else {
256+
ids = Array.from(selectedKeys as Set<string>).map((id) => Number(id));
257+
}
259258

260-
// 提示开始删除
261-
addToast({
262-
title: "批量删除中...",
263-
description: "正在删除所选实例,请稍候",
264-
color: "primary",
265-
});
266-
const response = await fetch(buildApiUrl('/api/tunnels/batch'), {
259+
// 使用 promise 形式的 Toast
260+
addToast({
261+
timeout: 1,
262+
title: "批量删除中...",
263+
description: "正在删除所选实例,请稍候",
264+
color: "primary",
265+
promise: fetch(buildApiUrl('/api/tunnels/batch'), {
267266
method: 'DELETE',
268267
headers: {
269268
'Content-Type': 'application/json',
270269
},
271270
body: JSON.stringify({ ids }),
272-
});
271+
})
272+
.then(async (response) => {
273+
const data = await response.json();
274+
if (!response.ok) {
275+
throw new Error(data?.error || '批量删除失败');
276+
}
273277

274-
const data = await response.json();
278+
// 成功提示
279+
addToast({
280+
title: '批量删除成功',
281+
description: `已删除 ${data.deleted || ids.length} 个实例`,
282+
color: 'success',
283+
});
275284

276-
if (!response.ok) {
277-
addToast({
278-
title: '批量删除失败',
279-
description: data?.error || '批量删除失败',
280-
color: 'danger',
281-
});
282-
throw new Error(data?.error || '批量删除失败');
283-
}
284-
285-
addToast({
286-
title: '批量删除成功',
287-
description: `已删除 ${data.deleted || ids.length} 个实例`,
288-
color: 'success',
289-
});
285+
// 清空选择并刷新
286+
setSelectedKeys(new Set<string>());
287+
fetchTunnels();
290288

291-
// 清空选择并刷新
292-
setSelectedKeys(new Set<string>());
293-
fetchTunnels();
294-
} catch (error) {
295-
addToast({
296-
title: '批量删除失败',
297-
description: error instanceof Error ? error.message : '未知错误',
298-
color: 'danger',
299-
});
300-
}
289+
return data;
290+
})
291+
.catch((error) => {
292+
// 失败提示
293+
addToast({
294+
title: '批量删除失败',
295+
description: error instanceof Error ? error.message : '未知错误',
296+
color: 'danger',
297+
});
298+
throw error;
299+
}),
300+
});
301301
};
302302

303303
// 初始加载

go.mod

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ go 1.23
55
require (
66
github.com/google/uuid v1.6.0
77
github.com/gorilla/mux v1.8.1
8+
github.com/mattn/go-ieproxy v0.0.12
89
github.com/mattn/go-sqlite3 v1.14.22
910
github.com/r3labs/sse/v2 v2.10.0
1011
github.com/sirupsen/logrus v1.9.3
11-
golang.org/x/crypto v0.17.0
12+
golang.org/x/crypto v0.23.0
1213
)
1314

1415
require (
15-
golang.org/x/net v0.10.0 // indirect
16-
golang.org/x/sys v0.15.0 // indirect
16+
golang.org/x/net v0.25.0 // indirect
17+
golang.org/x/sys v0.20.0 // indirect
18+
golang.org/x/text v0.15.0 // indirect
1719
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
1820
)

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
55
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
66
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
77
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
8+
github.com/mattn/go-ieproxy v0.0.12 h1:OZkUFJC3ESNZPQ+6LzC3VJIFSnreeFLQyqvBWtvfL2M=
9+
github.com/mattn/go-ieproxy v0.0.12/go.mod h1:Vn+N61199DAnVeTgaF8eoB9PvLO8P3OBnG95ENh7B7c=
810
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
911
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
1012
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -19,14 +21,22 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
1921
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
2022
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
2123
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
24+
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
25+
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
2226
golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
2327
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
2428
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
29+
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
30+
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
2531
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
2632
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
2733
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
2834
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
35+
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
36+
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
2937
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
38+
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
39+
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
3040
gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y=
3141
gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI=
3242
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

internal/api/endpoint.go

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,32 +1015,51 @@ func parseInstanceURL(raw, mode string) struct {
10151015
hostPart = raw
10161016
}
10171017

1018-
if hostPart != "" {
1019-
if strings.Contains(hostPart, ":") {
1020-
parts := strings.SplitN(hostPart, ":", 2)
1021-
res.TunnelAddress = parts[0]
1022-
res.TunnelPort = parts[1]
1018+
// 内部帮助函数: 解析地址与端口, 支持 IPv6 字面量如 [2001:db8::1]:8080
1019+
parsePart := func(part string) (addr string, port string) {
1020+
part = strings.TrimSpace(part)
1021+
if part == "" {
1022+
return "", ""
1023+
}
1024+
// IPv6 使用方括号包裹,如 [2401:b60:16:24c::]:10101
1025+
if strings.HasPrefix(part, "[") {
1026+
if end := strings.Index(part, "]"); end != -1 {
1027+
addr = part[:end+1] // 连同 ']' 一并保留,方便后续直接展示
1028+
// 判断是否包含端口
1029+
if len(part) > end+1 && part[end+1] == ':' {
1030+
port = part[end+2:]
1031+
}
1032+
return
1033+
}
1034+
}
1035+
// 其它情况按冒号分割 (IPv4/域名 或 "ip:port")
1036+
if strings.Contains(part, ":") {
1037+
pieces := strings.SplitN(part, ":", 2)
1038+
addr = pieces[0]
1039+
port = pieces[1]
10231040
} else {
1024-
if _, err := strconv.Atoi(hostPart); err == nil {
1025-
res.TunnelPort = hostPart
1041+
// 仅端口或仅地址
1042+
if _, err := strconv.Atoi(part); err == nil {
1043+
port = part
10261044
} else {
1027-
res.TunnelAddress = hostPart
1045+
addr = part
10281046
}
10291047
}
1048+
return
10301049
}
10311050

1051+
// 解析 hostPart -> tunnelAddress:tunnelPort (兼容 IPv6)
1052+
if hostPart != "" {
1053+
addr, port := parsePart(hostPart)
1054+
res.TunnelAddress = addr
1055+
res.TunnelPort = port
1056+
}
1057+
1058+
// 解析 pathPart -> targetAddress:targetPort (兼容 IPv6)
10321059
if pathPart != "" {
1033-
if strings.Contains(pathPart, ":") {
1034-
parts := strings.SplitN(pathPart, ":", 2)
1035-
res.TargetAddress = parts[0]
1036-
res.TargetPort = parts[1]
1037-
} else {
1038-
if _, err := strconv.Atoi(pathPart); err == nil {
1039-
res.TargetPort = pathPart
1040-
} else {
1041-
res.TargetAddress = pathPart
1042-
}
1043-
}
1060+
addr, port := parsePart(pathPart)
1061+
res.TargetAddress = addr
1062+
res.TargetPort = port
10441063
}
10451064

10461065
if query != "" {

internal/nodepass/client.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import (
77
"fmt"
88
"net/http"
99
"time"
10+
11+
// 引入系统代理检测 (Windows/macOS)
12+
"github.com/mattn/go-ieproxy"
1013
)
1114

1215
// Client 封装与 NodePass HTTP API 的交互
@@ -31,6 +34,8 @@ func NewClient(baseURL, apiPath, apiKey string, httpClient *http.Client) *Client
3134
if httpClient == nil {
3235
// 复制默认 Transport 并禁用证书校验,以支持自建/自签名 SSL
3336
tr := http.DefaultTransport.(*http.Transport).Clone()
37+
// 启用系统/环境代理检测:先读 env,再回退到系统代理
38+
tr.Proxy = ieproxy.GetProxyFunc()
3439
if tr.TLSClientConfig == nil {
3540
tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
3641
} else {

internal/sse/service.go

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,34 @@ type parsedURL struct {
671671
Max string
672672
}
673673

674+
// 内部工具函数: 解析地址:端口 (兼容 IPv6 字面量)
675+
func parsePart(part string) (addr, port string) {
676+
part = strings.TrimSpace(part)
677+
if part == "" {
678+
return "", ""
679+
}
680+
if strings.HasPrefix(part, "[") {
681+
if end := strings.Index(part, "]"); end != -1 {
682+
addr = part[:end+1]
683+
if len(part) > end+1 && part[end+1] == ':' {
684+
port = part[end+2:]
685+
}
686+
return
687+
}
688+
}
689+
if strings.Contains(part, ":") {
690+
pieces := strings.SplitN(part, ":", 2)
691+
addr, port = pieces[0], pieces[1]
692+
} else {
693+
if _, err := strconv.Atoi(part); err == nil {
694+
port = part
695+
} else {
696+
addr = part
697+
}
698+
}
699+
return
700+
}
701+
674702
func parseInstanceURL(raw, mode string) parsedURL {
675703
// 默认值
676704
res := parsedURL{
@@ -705,35 +733,18 @@ func parseInstanceURL(raw, mode string) parsedURL {
705733
hostPart = raw
706734
}
707735

708-
// hostPart => tunnel address:port
736+
// hostPart
709737
if hostPart != "" {
710-
if strings.Contains(hostPart, ":") {
711-
parts := strings.SplitN(hostPart, ":", 2)
712-
res.TunnelAddress = parts[0]
713-
res.TunnelPort = parts[1]
714-
} else {
715-
// 只有端口或地址
716-
if _, err := strconv.Atoi(hostPart); err == nil {
717-
res.TunnelPort = hostPart
718-
} else {
719-
res.TunnelAddress = hostPart
720-
}
721-
}
738+
addr, port := parsePart(hostPart)
739+
res.TunnelAddress = addr
740+
res.TunnelPort = port
722741
}
723742

724-
// pathPart => target address:port
743+
// pathPart
725744
if pathPart != "" {
726-
if strings.Contains(pathPart, ":") {
727-
parts := strings.SplitN(pathPart, ":", 2)
728-
res.TargetAddress = parts[0]
729-
res.TargetPort = parts[1]
730-
} else {
731-
if _, err := strconv.Atoi(pathPart); err == nil {
732-
res.TargetPort = pathPart
733-
} else {
734-
res.TargetAddress = pathPart
735-
}
736-
}
745+
addr, port := parsePart(pathPart)
746+
res.TargetAddress = addr
747+
res.TargetPort = port
737748
}
738749

739750
// query params

internal/tunnel/service.go

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -77,34 +77,47 @@ func parseInstanceURL(raw, mode string) parsedURL {
7777
hostPart = raw
7878
}
7979

80-
// 解析 hostPart -> tunnelAddress:tunnelPort
81-
if hostPart != "" {
82-
if strings.Contains(hostPart, ":") {
83-
parts := strings.SplitN(hostPart, ":", 2)
84-
res.TunnelAddress = parts[0]
85-
res.TunnelPort = parts[1]
80+
// 内部工具函数: 解析 "addr:port" 片段 (兼容 IPv6 字面量,如 [::1]:8080)
81+
parsePart := func(part string) (addr, port string) {
82+
part = strings.TrimSpace(part)
83+
if part == "" {
84+
return "", ""
85+
}
86+
// IPv6 Literals
87+
if strings.HasPrefix(part, "[") {
88+
if end := strings.Index(part, "]"); end != -1 {
89+
addr = part[:end+1]
90+
if len(part) > end+1 && part[end+1] == ':' {
91+
port = part[end+2:]
92+
}
93+
return
94+
}
95+
}
96+
if strings.Contains(part, ":") {
97+
pieces := strings.SplitN(part, ":", 2)
98+
addr, port = pieces[0], pieces[1]
8699
} else {
87-
if _, err := strconv.Atoi(hostPart); err == nil {
88-
res.TunnelPort = hostPart
100+
if _, err := strconv.Atoi(part); err == nil {
101+
port = part
89102
} else {
90-
res.TunnelAddress = hostPart
103+
addr = part
91104
}
92105
}
106+
return
107+
}
108+
109+
// 解析 hostPart -> tunnelAddress:tunnelPort (兼容 IPv6)
110+
if hostPart != "" {
111+
addr, port := parsePart(hostPart)
112+
res.TunnelAddress = addr
113+
res.TunnelPort = port
93114
}
94115

95-
// 解析 pathPart -> targetAddress:targetPort
116+
// 解析 pathPart -> targetAddress:targetPort (兼容 IPv6)
96117
if pathPart != "" {
97-
if strings.Contains(pathPart, ":") {
98-
parts := strings.SplitN(pathPart, ":", 2)
99-
res.TargetAddress = parts[0]
100-
res.TargetPort = parts[1]
101-
} else {
102-
if _, err := strconv.Atoi(pathPart); err == nil {
103-
res.TargetPort = pathPart
104-
} else {
105-
res.TargetAddress = pathPart
106-
}
107-
}
118+
addr, port := parsePart(pathPart)
119+
res.TargetAddress = addr
120+
res.TargetPort = port
108121
}
109122

110123
// 解析查询参数
@@ -336,6 +349,8 @@ func (s *Service) CreateTunnel(req CreateTunnelRequest) (*Tunnel, error) {
336349
npClient := nodepass.NewClient(endpointURL, endpointAPIPath, endpointAPIKey, nil)
337350
instanceID, remoteStatus, err := npClient.CreateInstance(commandLine)
338351
if err != nil {
352+
// 记录 NodePass API 错误,包含关键上下文信息
353+
log.Errorf("[NodePass] 创建实例失败 endpoint=%d cmd=%s err=%v", req.EndpointID, commandLine, err)
339354
return nil, err
340355
}
341356

0 commit comments

Comments
 (0)