Files
Sakurasan d5dac5a6fc feat: AutoZero 通用 Agent 框架(从 AutoGo 五阶段拆分完成)
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。
2026-10-26 00:54:46 +08:00

157 lines
4.0 KiB
Go
Raw Permalink 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 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
}