@@ -39,6 +39,48 @@ func NewTunnelHandler(tunnelService *tunnel.Service, sseManager *sse.Manager) *T
3939 }
4040}
4141
42+ // normalizeCommandLine 规范化 commandLine,将 URL 参数按字母顺序排序
43+ // 这样可以确保参数顺序不同但内容相同的 URL 能够正确比较
44+ func normalizeCommandLine (commandLine string ) (string , error ) {
45+ // 查找 ? 的位置
46+ idx := strings .Index (commandLine , "?" )
47+ if idx == - 1 {
48+ // 没有查询参数,直接返回
49+ return commandLine , nil
50+ }
51+
52+ // 分离基础部分和查询参数部分
53+ base := commandLine [:idx ]
54+ queryStr := commandLine [idx + 1 :]
55+
56+ // 解析查询参数
57+ values , err := url .ParseQuery (queryStr )
58+ if err != nil {
59+ return "" , fmt .Errorf ("解析查询参数失败: %w" , err )
60+ }
61+
62+ // 获取所有的参数键并排序
63+ keys := make ([]string , 0 , len (values ))
64+ for k := range values {
65+ keys = append (keys , k )
66+ }
67+ sort .Strings (keys )
68+
69+ // 按排序后的顺序重建查询参数
70+ params := make ([]string , 0 , len (keys ))
71+ for _ , k := range keys {
72+ for _ , v := range values [k ] {
73+ params = append (params , fmt .Sprintf ("%s=%s" , k , v ))
74+ }
75+ }
76+
77+ // 重建完整的 commandLine
78+ if len (params ) > 0 {
79+ return base + "?" + strings .Join (params , "&" ), nil
80+ }
81+ return base , nil
82+ }
83+
4284// setupTunnelRoutes 设置隧道相关路由
4385func SetupTunnelRoutes (rg * gin.RouterGroup , tunnelService * tunnel.Service , sseManager * sse.Manager , sseProcessor * metrics.SSEProcessor ) {
4486 // 创建TunnelHandler实例
@@ -3183,71 +3225,99 @@ func (h *TunnelHandler) HandleUpdateTunnelV2(c *gin.Context) {
31833225 c .JSON (http .StatusInternalServerError , tunnel.TunnelResponse {Success : false , Error : "查询端点信息失败" })
31843226 return
31853227 }
3186- log .Infof ("[API] 准备调用 UpdateInstance: instanceID=%s, commandLine=%s" , instanceID , commandLine )
3187- if _ , err := nodepass .UpdateInstance (endpoint .ID , instanceID , commandLine ); err != nil {
3188- log .Errorf ("[API] UpdateInstanceV1 调用失败: %v" , err )
3189- // 若远端返回 405,则回退旧逻辑(删除+重建)
3190- if strings .Contains (err .Error (), "405" ) || strings .Contains (err .Error (), "404" ) {
3191- log .Infof ("[API] 检测到405/404错误,回退到旧逻辑" )
3192- // 删除旧实例
3193- if delErr := h .tunnelService .DeleteTunnelAndWait (instanceID , 3 * time .Second , true ); delErr != nil {
3194- c .JSON (http .StatusBadRequest , tunnel.TunnelResponse {Success : false , Error : "编辑实例失败,删除旧实例错误: " + delErr .Error ()})
3195- return
3196- }
3228+ // 新版本的隧道更新使用了PUT替代了原方案中删除隧道重新新建的逻辑
3229+ // 对于不更新隧道参数仅更新隧道名字,应当使用PATCH更新,使用PUT会导致CORE会返回409错误
3230+ var originalCommandLine string
3231+ if err := h .tunnelService .DB ().QueryRow (
3232+ `SELECT command_line FROM tunnels WHERE instance_id = ?` , instanceID ).Scan (& originalCommandLine ); err != nil {
3233+ log .Errorf ("[API] 查询现有command_line失败: %v" , err )
3234+ c .JSON (http .StatusInternalServerError , tunnel.TunnelResponse {Success : false , Error : "查询现有隧道配置失败" })
3235+ return
3236+ }
31973237
3198- // 重新创建,直接使用指针类型
3199- // 处理新增字段的默认值
3200- enableSSEStore := true
3201- if raw .EnableSSEStore != nil {
3202- enableSSEStore = * raw .EnableSSEStore
3203- }
3238+ // 规范化两个 commandLine 以便比较(参数顺序可能不同)
3239+ normalizedOriginal , err1 := normalizeCommandLine (originalCommandLine )
3240+ normalizedNew , err2 := normalizeCommandLine (commandLine )
3241+ if err1 != nil || err2 != nil {
3242+ log .Warnf ("[API] 规范化 commandLine 失败,使用原始比较: err1=%v, err2=%v" , err1 , err2 )
3243+ normalizedOriginal = originalCommandLine
3244+ normalizedNew = commandLine
3245+ }
32043246
3205- enableLogStore := true
3206- if raw .EnableLogStore != nil {
3207- enableLogStore = * raw .EnableLogStore
3208- }
3247+ if normalizedOriginal == normalizedNew {
3248+ log .Infof ("[API] 隧道配置未发生变化,仅更新隧道名字" )
3249+ if _ , err := nodepass .RenameInstance (endpoint .ID , instanceID , raw .Name ); err != nil {
3250+ log .Errorf ("[API] RenameInstance 调用失败: %v" , err )
3251+ c .JSON (http .StatusBadRequest , tunnel.TunnelResponse {Success : false , Error : "编辑实例失败,更新隧道名字失败: " + err .Error ()})
3252+ return
3253+ }
3254+ } else {
3255+ log .Infof ("[API] 准备调用 UpdateInstance: instanceID=%s, commandLine=%s" , instanceID , commandLine )
3256+ if _ , err := nodepass .UpdateInstance (endpoint .ID , instanceID , commandLine ); err != nil {
3257+ log .Errorf ("[API] UpdateInstanceV1 调用失败: %v" , err )
3258+ // 若远端返回 405,则回退旧逻辑(删除+重建)
3259+ if strings .Contains (err .Error (), "405" ) || strings .Contains (err .Error (), "404" ) {
3260+ log .Infof ("[API] 检测到405/404错误,回退到旧逻辑" )
3261+ // 删除旧实例
3262+ if delErr := h .tunnelService .DeleteTunnelAndWait (instanceID , 3 * time .Second , true ); delErr != nil {
3263+ c .JSON (http .StatusBadRequest , tunnel.TunnelResponse {Success : false , Error : "编辑实例失败,删除旧实例错误: " + delErr .Error ()})
3264+ return
3265+ }
32093266
3210- // 处理Mode字段的类型转换
3211- var modePtr * tunnel. TunnelMode
3212- if raw . Mode != nil {
3213- mode := tunnel . TunnelMode ( * raw .Mode )
3214- modePtr = & mode
3215- }
3267+ // 重新创建,直接使用指针类型
3268+ // 处理新增字段的默认值
3269+ enableSSEStore := true
3270+ if raw .EnableSSEStore != nil {
3271+ enableSSEStore = * raw . EnableSSEStore
3272+ }
32163273
3217- createReq := tunnel.CreateTunnelRequest {
3218- Name : raw .Name ,
3219- EndpointID : raw .EndpointID ,
3220- Type : raw .Type ,
3221- TunnelAddress : raw .TunnelAddress ,
3222- TunnelPort : tunnelPort ,
3223- TargetAddress : raw .TargetAddress ,
3224- TargetPort : targetPort ,
3225- TLSMode : tunnel .TLSMode (raw .TLSMode ),
3226- CertPath : raw .CertPath ,
3227- KeyPath : raw .KeyPath ,
3228- LogLevel : tunnel .LogLevel (raw .LogLevel ),
3229- Password : raw .Password ,
3230- ProxyProtocol : raw .ProxyProtocol ,
3231- Min : raw .Min ,
3232- Max : raw .Max ,
3233- Slot : raw .Slot , // 新增:最大连接数限制
3234- Mode : modePtr , // 新增:运行模式
3235- Read : raw .Read , // 新增:数据读取超时时间
3236- Rate : raw .Rate , // 新增:带宽速率限制
3237- EnableSSEStore : enableSSEStore , // 新增:是否启用SSE存储
3238- EnableLogStore : enableLogStore , // 新增:是否启用日志存储
3239- }
3240- newTunnel , crtErr := h .tunnelService .CreateTunnelAndWait (createReq , 3 * time .Second )
3241- if crtErr != nil {
3242- c .JSON (http .StatusBadRequest , tunnel.TunnelResponse {Success : false , Error : "编辑实例失败,创建新实例错误: " + crtErr .Error ()})
3274+ enableLogStore := true
3275+ if raw .EnableLogStore != nil {
3276+ enableLogStore = * raw .EnableLogStore
3277+ }
3278+
3279+ // 处理Mode字段的类型转换
3280+ var modePtr * tunnel.TunnelMode
3281+ if raw .Mode != nil {
3282+ mode := tunnel .TunnelMode (* raw .Mode )
3283+ modePtr = & mode
3284+ }
3285+
3286+ createReq := tunnel.CreateTunnelRequest {
3287+ Name : raw .Name ,
3288+ EndpointID : raw .EndpointID ,
3289+ Type : raw .Type ,
3290+ TunnelAddress : raw .TunnelAddress ,
3291+ TunnelPort : tunnelPort ,
3292+ TargetAddress : raw .TargetAddress ,
3293+ TargetPort : targetPort ,
3294+ TLSMode : tunnel .TLSMode (raw .TLSMode ),
3295+ CertPath : raw .CertPath ,
3296+ KeyPath : raw .KeyPath ,
3297+ LogLevel : tunnel .LogLevel (raw .LogLevel ),
3298+ Password : raw .Password ,
3299+ ProxyProtocol : raw .ProxyProtocol ,
3300+ Min : raw .Min ,
3301+ Max : raw .Max ,
3302+ Slot : raw .Slot , // 新增:最大连接数限制
3303+ Mode : modePtr , // 新增:运行模式
3304+ Read : raw .Read , // 新增:数据读取超时时间
3305+ Rate : raw .Rate , // 新增:带宽速率限制
3306+ EnableSSEStore : enableSSEStore , // 新增:是否启用SSE存储
3307+ EnableLogStore : enableLogStore , // 新增:是否启用日志存储
3308+ }
3309+ newTunnel , crtErr := h .tunnelService .CreateTunnelAndWait (createReq , 3 * time .Second )
3310+ if crtErr != nil {
3311+ c .JSON (http .StatusBadRequest , tunnel.TunnelResponse {Success : false , Error : "编辑实例失败,创建新实例错误: " + crtErr .Error ()})
3312+ return
3313+ }
3314+ c .JSON (http .StatusOK , tunnel.TunnelResponse {Success : true , Message : "编辑实例成功(回退旧逻辑)" , Tunnel : newTunnel })
32433315 return
32443316 }
3245- c .JSON (http .StatusOK , tunnel.TunnelResponse {Success : true , Message : "编辑实例成功(回退旧逻辑)" , Tunnel : newTunnel })
3317+ // 其他错误
3318+ c .JSON (http .StatusBadRequest , tunnel.TunnelResponse {Success : false , Error : err .Error ()})
32463319 return
32473320 }
3248- // 其他错误
3249- c .JSON (http .StatusBadRequest , tunnel.TunnelResponse {Success : false , Error : err .Error ()})
3250- return
32513321 }
32523322
32533323 // 调用成功后等待数据库同步
0 commit comments