@@ -3079,6 +3079,22 @@ func (s *AntigravityGatewayService) handleGeminiStreamingResponse(c *gin.Context
30793079 intervalCh = intervalTicker .C
30803080 }
30813081
3082+ // 下游 keepalive:防止代理/Cloudflare Tunnel 因连接空闲而断开
3083+ keepaliveInterval := time .Duration (0 )
3084+ if s .settingService .cfg != nil && s .settingService .cfg .Gateway .StreamKeepaliveInterval > 0 {
3085+ keepaliveInterval = time .Duration (s .settingService .cfg .Gateway .StreamKeepaliveInterval ) * time .Second
3086+ }
3087+ var keepaliveTicker * time.Ticker
3088+ if keepaliveInterval > 0 {
3089+ keepaliveTicker = time .NewTicker (keepaliveInterval )
3090+ defer keepaliveTicker .Stop ()
3091+ }
3092+ var keepaliveCh <- chan time.Time
3093+ if keepaliveTicker != nil {
3094+ keepaliveCh = keepaliveTicker .C
3095+ }
3096+ lastDataAt := time .Now ()
3097+
30823098 cw := newAntigravityClientWriter (c .Writer , flusher , "antigravity gemini" )
30833099
30843100 // 仅发送一次错误事件,避免多次写入导致协议混乱
@@ -3111,6 +3127,8 @@ func (s *AntigravityGatewayService) handleGeminiStreamingResponse(c *gin.Context
31113127 return nil , ev .err
31123128 }
31133129
3130+ lastDataAt = time .Now ()
3131+
31143132 line := ev .line
31153133 trimmed := strings .TrimRight (line , "\r \n " )
31163134 if strings .HasPrefix (trimmed , "data:" ) {
@@ -3170,6 +3188,19 @@ func (s *AntigravityGatewayService) handleGeminiStreamingResponse(c *gin.Context
31703188 logger .LegacyPrintf ("service.antigravity_gateway" , "Stream data interval timeout (antigravity)" )
31713189 sendErrorEvent ("stream_timeout" )
31723190 return & antigravityStreamResult {usage : usage , firstTokenMs : firstTokenMs }, fmt .Errorf ("stream data interval timeout" )
3191+
3192+ case <- keepaliveCh :
3193+ if cw .Disconnected () {
3194+ continue
3195+ }
3196+ if time .Since (lastDataAt ) < keepaliveInterval {
3197+ continue
3198+ }
3199+ // SSE ping/keepalive:保持连接活跃防止 Cloudflare Tunnel 等代理断开
3200+ if ! cw .Fprintf (":\n \n " ) {
3201+ logger .LegacyPrintf ("service.antigravity_gateway" , "Client disconnected during keepalive ping (antigravity gemini), continuing to drain upstream for billing" )
3202+ continue
3203+ }
31733204 }
31743205 }
31753206}
@@ -3895,6 +3926,22 @@ func (s *AntigravityGatewayService) handleClaudeStreamingResponse(c *gin.Context
38953926 intervalCh = intervalTicker .C
38963927 }
38973928
3929+ // 下游 keepalive:防止代理/Cloudflare Tunnel 因连接空闲而断开
3930+ keepaliveInterval := time .Duration (0 )
3931+ if s .settingService .cfg != nil && s .settingService .cfg .Gateway .StreamKeepaliveInterval > 0 {
3932+ keepaliveInterval = time .Duration (s .settingService .cfg .Gateway .StreamKeepaliveInterval ) * time .Second
3933+ }
3934+ var keepaliveTicker * time.Ticker
3935+ if keepaliveInterval > 0 {
3936+ keepaliveTicker = time .NewTicker (keepaliveInterval )
3937+ defer keepaliveTicker .Stop ()
3938+ }
3939+ var keepaliveCh <- chan time.Time
3940+ if keepaliveTicker != nil {
3941+ keepaliveCh = keepaliveTicker .C
3942+ }
3943+ lastDataAt := time .Now ()
3944+
38983945 cw := newAntigravityClientWriter (c .Writer , flusher , "antigravity claude" )
38993946
39003947 // 仅发送一次错误事件,避免多次写入导致协议混乱
@@ -3947,6 +3994,8 @@ func (s *AntigravityGatewayService) handleClaudeStreamingResponse(c *gin.Context
39473994 return nil , fmt .Errorf ("stream read error: %w" , ev .err )
39483995 }
39493996
3997+ lastDataAt = time .Now ()
3998+
39503999 // 处理 SSE 行,转换为 Claude 格式
39514000 claudeEvents := processor .ProcessLine (strings .TrimRight (ev .line , "\r \n " ))
39524001 if len (claudeEvents ) > 0 {
@@ -3969,6 +4018,20 @@ func (s *AntigravityGatewayService) handleClaudeStreamingResponse(c *gin.Context
39694018 logger .LegacyPrintf ("service.antigravity_gateway" , "Stream data interval timeout (antigravity)" )
39704019 sendErrorEvent ("stream_timeout" )
39714020 return & antigravityStreamResult {usage : convertUsage (nil ), firstTokenMs : firstTokenMs }, fmt .Errorf ("stream data interval timeout" )
4021+
4022+ case <- keepaliveCh :
4023+ if cw .Disconnected () {
4024+ continue
4025+ }
4026+ if time .Since (lastDataAt ) < keepaliveInterval {
4027+ continue
4028+ }
4029+ // SSE ping 事件:Anthropic 原生格式,客户端会正确处理,
4030+ // 同时保持连接活跃防止 Cloudflare Tunnel 等代理断开
4031+ if ! cw .Fprintf ("event: ping\n data: {\" type\" : \" ping\" }\n \n " ) {
4032+ logger .LegacyPrintf ("service.antigravity_gateway" , "Client disconnected during keepalive ping (antigravity claude), continuing to drain upstream for billing" )
4033+ continue
4034+ }
39724035 }
39734036 }
39744037}
@@ -4299,6 +4362,22 @@ func (s *AntigravityGatewayService) streamUpstreamResponse(c *gin.Context, resp
42994362 intervalCh = intervalTicker .C
43004363 }
43014364
4365+ // 下游 keepalive:防止代理/Cloudflare Tunnel 因连接空闲而断开
4366+ keepaliveInterval := time .Duration (0 )
4367+ if s .settingService .cfg != nil && s .settingService .cfg .Gateway .StreamKeepaliveInterval > 0 {
4368+ keepaliveInterval = time .Duration (s .settingService .cfg .Gateway .StreamKeepaliveInterval ) * time .Second
4369+ }
4370+ var keepaliveTicker * time.Ticker
4371+ if keepaliveInterval > 0 {
4372+ keepaliveTicker = time .NewTicker (keepaliveInterval )
4373+ defer keepaliveTicker .Stop ()
4374+ }
4375+ var keepaliveCh <- chan time.Time
4376+ if keepaliveTicker != nil {
4377+ keepaliveCh = keepaliveTicker .C
4378+ }
4379+ lastDataAt := time .Now ()
4380+
43024381 flusher , _ := c .Writer .(http.Flusher )
43034382 cw := newAntigravityClientWriter (c .Writer , flusher , "antigravity upstream" )
43044383
@@ -4316,6 +4395,8 @@ func (s *AntigravityGatewayService) streamUpstreamResponse(c *gin.Context, resp
43164395 return & antigravityStreamResult {usage : usage , firstTokenMs : firstTokenMs }
43174396 }
43184397
4398+ lastDataAt = time .Now ()
4399+
43194400 line := ev .line
43204401
43214402 // 记录首 token 时间
@@ -4341,6 +4422,20 @@ func (s *AntigravityGatewayService) streamUpstreamResponse(c *gin.Context, resp
43414422 }
43424423 logger .LegacyPrintf ("service.antigravity_gateway" , "Stream data interval timeout (antigravity upstream)" )
43434424 return & antigravityStreamResult {usage : usage , firstTokenMs : firstTokenMs }
4425+
4426+ case <- keepaliveCh :
4427+ if cw .Disconnected () {
4428+ continue
4429+ }
4430+ if time .Since (lastDataAt ) < keepaliveInterval {
4431+ continue
4432+ }
4433+ // SSE ping 事件:Anthropic 原生格式,客户端会正确处理,
4434+ // 同时保持连接活跃防止 Cloudflare Tunnel 等代理断开
4435+ if ! cw .Fprintf ("event: ping\n data: {\" type\" : \" ping\" }\n \n " ) {
4436+ logger .LegacyPrintf ("service.antigravity_gateway" , "Client disconnected during keepalive ping (antigravity upstream), continuing to drain upstream for billing" )
4437+ continue
4438+ }
43444439 }
43454440 }
43464441}
0 commit comments