llm(三协议 Provider)/ session(JSONL 持久化)/ tools(Registry、围栏、 builtin、computer)/ cdp(browser)/ skills / mcp / agent(循环、两级压缩、 编排、审批门控)+ 底座 jsonl/textx/execx/diag/scratch/spill/imagecache。 产品装配代码(config 桥接、schedule 工具、memory/plan/retro/prompt) 留在 AutoGo,宿主接入方式见 MIGRATION.md。
157 lines
4.0 KiB
Go
157 lines
4.0 KiB
Go
package llm
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"strconv"
|
||
"time"
|
||
)
|
||
|
||
// RetryConfig 配置重试策略。
|
||
type RetryConfig struct {
|
||
MaxRetries int // 最大重试次数(默认 3)
|
||
InitialBackoff time.Duration // 初始退避时间(默认 1s)
|
||
MaxBackoff time.Duration // 最大退避时间(默认 30s)
|
||
RetryOn []int // 需要重试的 HTTP 状态码(默认 429, 500-509)
|
||
}
|
||
|
||
// DefaultRetryConfig 默认重试配置。
|
||
var DefaultRetryConfig = RetryConfig{
|
||
MaxRetries: 3,
|
||
InitialBackoff: 1 * time.Second,
|
||
MaxBackoff: 30 * time.Second,
|
||
RetryOn: []int{429, 500, 502, 503, 504},
|
||
}
|
||
|
||
// globalRetryConfig 全局重试配置。
|
||
var globalRetryConfig = DefaultRetryConfig
|
||
|
||
// SetRetryConfig 设置全局重试配置。
|
||
func SetRetryConfig(cfg RetryConfig) {
|
||
globalRetryConfig = cfg
|
||
}
|
||
|
||
// GetRetryConfig 获取当前重试配置。
|
||
func GetRetryConfig() RetryConfig {
|
||
return globalRetryConfig
|
||
}
|
||
|
||
// doWithRetry 发送请求并对瞬态错误做指数退避重试(尊重 Retry-After)。
|
||
// 只覆盖「响应开始前」的阶段:HTTP 状态检查在 SSE 流开始之前,一旦 200 并
|
||
// 开始读流,错误不再重试(增量已经发出去了)。
|
||
func doWithRetry(ctx context.Context, client *http.Client, req *http.Request, body []byte) (*http.Response, error) {
|
||
return doWithRetryConfig(ctx, client, req, body, globalRetryConfig)
|
||
}
|
||
|
||
// doWithRetryConfig 使用指定配置执行重试。
|
||
func doWithRetryConfig(ctx context.Context, client *http.Client, req *http.Request, body []byte, cfg RetryConfig) (*http.Response, error) {
|
||
if cfg.MaxRetries <= 0 {
|
||
cfg.MaxRetries = 1
|
||
}
|
||
if cfg.InitialBackoff <= 0 {
|
||
cfg.InitialBackoff = 1 * time.Second
|
||
}
|
||
if cfg.MaxBackoff <= 0 {
|
||
cfg.MaxBackoff = 30 * time.Second
|
||
}
|
||
if len(cfg.RetryOn) == 0 {
|
||
cfg.RetryOn = DefaultRetryConfig.RetryOn
|
||
}
|
||
|
||
// 创建状态码查找表
|
||
retryOnMap := make(map[int]bool, len(cfg.RetryOn))
|
||
for _, code := range cfg.RetryOn {
|
||
retryOnMap[code] = true
|
||
}
|
||
|
||
var lastErr error
|
||
var lastRetryAfter time.Duration
|
||
|
||
for attempt := 0; attempt < cfg.MaxRetries; attempt++ {
|
||
if attempt > 0 {
|
||
wait := time.Duration(1<<(attempt-1)) * cfg.InitialBackoff
|
||
if wait > cfg.MaxBackoff {
|
||
wait = cfg.MaxBackoff
|
||
}
|
||
if lastRetryAfter > 0 {
|
||
wait = lastRetryAfter
|
||
}
|
||
select {
|
||
case <-ctx.Done():
|
||
return nil, ctx.Err()
|
||
case <-time.After(wait):
|
||
}
|
||
}
|
||
|
||
r := req.Clone(ctx)
|
||
r.Body = io.NopCloser(bytes.NewReader(body))
|
||
r.ContentLength = int64(len(body))
|
||
resp, err := client.Do(r)
|
||
|
||
if err != nil {
|
||
lastErr = err
|
||
lastRetryAfter = 0
|
||
continue // 网络错误:可重试
|
||
}
|
||
|
||
// 检查是否需要重试
|
||
if resp.StatusCode == http.StatusTooManyRequests || retryOnMap[resp.StatusCode] {
|
||
lastRetryAfter = parseRetryAfter(resp.Header.Get("Retry-After"))
|
||
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4096))
|
||
resp.Body.Close()
|
||
lastErr = fmt.Errorf("HTTP %d", resp.StatusCode)
|
||
continue
|
||
}
|
||
|
||
return resp, nil // 2xx 与 4xx 一律交还调用方(4xx 不可重试)
|
||
}
|
||
|
||
return nil, fmt.Errorf("after %d attempts: %w", cfg.MaxRetries, lastErr)
|
||
}
|
||
|
||
// isRetryable 检查错误是否可重试。
|
||
func isRetryable(err error) bool {
|
||
if err == nil {
|
||
return false
|
||
}
|
||
|
||
// 网络错误可重试
|
||
if _, ok := err.(interface{ Timeout() bool }); ok {
|
||
return true
|
||
}
|
||
|
||
// HTTP 错误
|
||
errStr := err.Error()
|
||
return contains(errStr, "timeout") ||
|
||
contains(errStr, "connection refused") ||
|
||
contains(errStr, "connection reset") ||
|
||
contains(errStr, "EOF")
|
||
}
|
||
|
||
// contains 检查字符串是否包含子串。
|
||
func contains(s, substr string) bool {
|
||
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsSubstr(s, substr))
|
||
}
|
||
|
||
func containsSubstr(s, substr string) bool {
|
||
for i := 0; i <= len(s)-len(substr); i++ {
|
||
if s[i:i+len(substr)] == substr {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func parseRetryAfter(v string) time.Duration {
|
||
if v == "" {
|
||
return 0
|
||
}
|
||
if secs, err := strconv.Atoi(v); err == nil && secs > 0 && secs <= 60 {
|
||
return time.Duration(secs) * time.Second
|
||
}
|
||
return 0
|
||
}
|