Files
team-api/relay/common/stream_status.go
T

116 lines
3.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package common
import (
"sync"
)
// StreamEndReason 流式响应结束原因
type StreamEndReason string
const (
StreamEndReasonDone StreamEndReason = "done"
StreamEndReasonTimeout StreamEndReason = "timeout"
StreamEndReasonClientGone StreamEndReason = "client_gone"
StreamEndReasonError StreamEndReason = "error"
StreamEndReasonPanic StreamEndReason = "panic"
StreamEndReasonHandlerStop StreamEndReason = "handler_stop" // dataHandler 调用了 Stop()(写客户端失败/上游 error 主动终止,按中断处理)
StreamEndReasonPingFail StreamEndReason = "ping_fail" // ping 写入失败
StreamEndReasonScannerErr StreamEndReason = "scanner_err" // bufio.Scanner 错误
StreamEndReasonEOF StreamEndReason = "eof" // scanner EOF 未收到 [DONE]
)
const maxStreamErrors = 20
// StreamStatus 流式响应状态追踪
type StreamStatus struct {
mu sync.RWMutex
endOnce sync.Once
reason StreamEndReason
err error
errors []string
errCount int
}
// NewStreamStatus 创建新的流式状态追踪器
func NewStreamStatus() *StreamStatus {
return &StreamStatus{}
}
// SetEndReason 设置流结束原因(first-writer-wins,仅第一次调用生效)
func (s *StreamStatus) SetEndReason(reason StreamEndReason, err error) {
s.endOnce.Do(func() {
s.mu.Lock()
s.reason = reason
s.err = err
s.mu.Unlock()
})
}
// GetEndReason 获取流结束原因
func (s *StreamStatus) GetEndReason() StreamEndReason {
s.mu.RLock()
defer s.mu.RUnlock()
return s.reason
}
// RecordError 记录软错误(流继续处理)
func (s *StreamStatus) RecordError(msg string) {
s.mu.Lock()
defer s.mu.Unlock()
s.errCount++
if len(s.errors) < maxStreamErrors {
s.errors = append(s.errors, msg)
}
}
// ErrorCount 获取错误计数
func (s *StreamStatus) ErrorCount() int {
s.mu.RLock()
defer s.mu.RUnlock()
return s.errCount
}
// IsPartialStreamEnd 流已开始传输但未正常结束,应按已消费 usage 部分结算。
// handler_stop(dataHandler 调用 Stop)纳入中断判定:正常结束由 sr.Done()/scanner 的
// done 标记,实际走到 Stop 的只有「写客户端失败」(客户端不可达)与「上游 error 事件
// 主动终止」两种,均属已向客户端写出部分字节后的非正常结束,应按中断部分结算。
// 此前 handler_stop 不算中断,客户端断开时写失败若先于 ctx 取消被观察到(first-writer-wins
// 竞态),流会被误判为正常结束、跳过中断计费兜底,导致 0 token / 0 费用结算。
func (s *StreamStatus) IsPartialStreamEnd() bool {
r := s.GetEndReason()
return r == StreamEndReasonClientGone || r == StreamEndReasonScannerErr ||
r == StreamEndReasonTimeout || r == StreamEndReasonPingFail ||
r == StreamEndReasonHandlerStop
}
// IsNormalEnd 是否正常结束(handler_stop 属异常终止,不算正常结束)
func (s *StreamStatus) IsNormalEnd() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.reason == StreamEndReasonDone || s.reason == ""
}
// HasErrors 是否有错误
func (s *StreamStatus) HasErrors() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.reason == StreamEndReasonError || s.reason == StreamEndReasonPanic || s.errCount > 0
}
// Error 获取错误信息
func (s *StreamStatus) Error() error {
s.mu.RLock()
defer s.mu.RUnlock()
return s.err
}
// Summary 返回状态摘要
func (s *StreamStatus) Summary() string {
s.mu.RLock()
defer s.mu.RUnlock()
if s.err != nil {
return string(s.reason) + ": " + s.err.Error()
}
return string(s.reason)
}