Files
nyoungo c877c1bf8c fix(upstream): 额度耗尽立即轮换访客身份并识别 de/es/fr 额度文案
- 命中额度错误当场调用 RotateGuest(复用既有实现),不再等退避、也不再依赖 auto_refresh;后续路由与下一次请求直接用新访客身份
- quota_notice 信封(type=quota_notice / notice 对象)在通用 error 门之前判定,无 error 字段的额度通知不再被当成静默 delta
- 额度文案按站点语言识别:de guthaben/aufgebraucht、es agotado、fr épuisé/consomm、en quota/exhausted,并下钻 data/notice/quota_notice 嵌套字段
- cache message 失败与非 2xx 响应体也按同样规则归类,额度问题会走轮换与跨站 failover 而不是普通错误
2026-10-09 00:50:18 +08:00

198 lines
4.5 KiB
Go

package upstream
import (
"bufio"
"encoding/json"
"fmt"
"io"
"strings"
)
// Event is one translated upstream SSE message.
type Event struct {
Kind string // "start" | "delta" | "done"
Value string
}
const (
KindStart = "start"
KindDelta = "delta"
KindDone = "done"
)
// readSSE parses a text/event-stream body. Blank lines dispatch the pending
// event, ":" comment lines are ignored.
func readSSE(r io.Reader, handle func(event string, dataLines []string) error) error {
sc := bufio.NewScanner(r)
sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
eventType := "message"
var dataLines []string
flush := func() error {
if len(dataLines) == 0 {
return nil
}
ev, dl := eventType, dataLines
eventType, dataLines = "message", nil
return handle(ev, dl)
}
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "" {
if err := flush(); err != nil {
return err
}
continue
}
if strings.HasPrefix(line, ":") {
continue
}
if strings.HasPrefix(line, "event:") {
eventType = strings.TrimSpace(line[len("event:"):])
continue
}
if strings.HasPrefix(line, "data:") {
dataLines = append(dataLines, strings.TrimSpace(line[len("data:"):]))
}
}
if err := sc.Err(); err != nil {
return fmt.Errorf("sse read: %w", err)
}
return flush()
}
// translateEvent converts one upstream SSE event into zero or more events for
// the downstream client. It returns an error for upstream failure payloads.
func translateEvent(eventType string, dataLines []string) ([]Event, error) {
for _, line := range dataLines {
if line == "[DONE]" {
return []Event{{Kind: KindDone}}, nil
}
}
// SSE 规范:同一事件的多个 data 行用 "\n" 拼接才是完整负载,跨行 JSON
// 必须整条解析;逐行解析会把它丢掉。
var payload map[string]any
if err := json.Unmarshal([]byte(strings.Join(dataLines, "\n")), &payload); err != nil {
// 兼容上游偶尔夹带的非 JSON 行:退化为逐行解析,取最后一个能解析的。
for _, line := range dataLines {
var p map[string]any
if err := json.Unmarshal([]byte(line), &p); err == nil {
payload = p
}
}
}
if payload == nil {
return nil, nil
}
switch eventType {
case "message_start":
return []Event{{Kind: KindStart, Value: scalarString(payload["message_id"])}}, nil
case "done", "complete":
return []Event{{Kind: KindDone}}, nil
}
// The quota notice may travel as its own event type with no "error"
// field at all, and the mirrors word it per locale. Recognise the
// envelope before the generic error gate so a localized notice is never
// swallowed as an ordinary (silent) delta.
if hasQuotaEnvelope(payload) {
return nil, newQuota("sse quota exhausted: " + shortJSON(payload))
}
if eventType == "error" || hasErrorValue(payload) {
if isQuotaPayload(payload) {
return nil, newQuota("sse quota exhausted: " + shortJSON(payload))
}
if isSessionPayload(payload) {
return nil, newSession("sse session failed: " + shortJSON(payload))
}
return nil, errf("sse error: %s", shortJSON(payload))
}
delta := firstTruthy(payload, "delta", "message", "content")
if delta == nil {
return nil, nil
}
text := deltaText(delta)
if text == "" {
return nil, nil
}
return []Event{{Kind: KindDelta, Value: text}}, nil
}
func hasErrorValue(payload map[string]any) bool {
v, ok := payload["error"]
return ok && v != nil
}
func firstTruthy(payload map[string]any, keys ...string) any {
for _, k := range keys {
if v, ok := payload[k]; ok && v != nil && !isFalsy(v) {
return v
}
}
return nil
}
func isFalsy(v any) bool {
switch t := v.(type) {
case nil:
return true
case string:
return t == ""
case bool:
return !t
case float64:
return t == 0
case map[string]any:
return len(t) == 0
case []any:
return len(t) == 0
}
return false
}
func deltaText(v any) string {
switch t := v.(type) {
case string:
return t
case map[string]any:
for _, k := range []string{"text", "content", "message", "delta"} {
if s, ok := t[k].(string); ok && s != "" {
return s
}
}
raw, err := json.Marshal(t)
if err != nil {
return ""
}
return string(raw)
default:
return fmt.Sprintf("%v", t)
}
}
func scalarString(v any) string {
if v == nil {
return ""
}
if s, ok := v.(string); ok {
return s
}
return fmt.Sprintf("%v", v)
}
func shortJSON(payload map[string]any) string {
raw, err := json.Marshal(payload)
if err != nil {
return fmt.Sprintf("%v", payload)
}
s := string(raw)
if len(s) > 400 {
return truncate(s, 400)
}
return s
}