feat(release): migrate GeekAI v4.3.0 to open source

- Sync backend and frontend from GeekAI Plus v4.3.0

- Remove commercial License flows and update open-source deployment defaults

- Preserve Docker Compose deployment and bump image tags to v4.3.0

BREAKING CHANGE: commercial License configuration and related endpoints are removed
This commit is contained in:
RockYang
2026-08-11 14:51:20 +08:00
parent 37e024acea
commit 9ccff4efbc
219 changed files with 29212 additions and 10802 deletions
+178 -37
View File
@@ -8,17 +8,174 @@ package utils
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"context"
"encoding/json"
"errors"
"fmt"
"geekai/core/types"
logger2 "geekai/logger"
"geekai/log"
"io"
"net"
"net/http"
"net/url"
"path/filepath"
"strings"
"time"
)
var logger = logger2.GetLogger()
var logger = log.GetLogger()
const (
defaultHTTPTimeout = 45 * time.Second
defaultTLSHandshakeTimeout = 10 * time.Second
defaultDialTimeout = 10 * time.Second
defaultResponseHeaderTimeout = 30 * time.Second
defaultIdleConnTimeout = 90 * time.Second
defaultMaxIdleConns = 100
defaultMaxIdleConnsPerHost = 10
)
func newHTTPClient(timeout time.Duration, proxy string) *http.Client {
if timeout <= 0 {
timeout = defaultHTTPTimeout
}
var proxyFn func(*http.Request) (*url.URL, error)
if strings.TrimSpace(proxy) != "" {
if proxyURL, err := url.Parse(proxy); err == nil && proxyURL != nil {
proxyFn = http.ProxyURL(proxyURL)
}
}
transport := &http.Transport{
Proxy: proxyFn,
DialContext: (&net.Dialer{
Timeout: defaultDialTimeout,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: defaultTLSHandshakeTimeout,
ResponseHeaderTimeout: defaultResponseHeaderTimeout,
IdleConnTimeout: defaultIdleConnTimeout,
MaxIdleConns: defaultMaxIdleConns,
MaxIdleConnsPerHost: defaultMaxIdleConnsPerHost,
}
return &http.Client{
Timeout: timeout,
Transport: transport,
}
}
func isRetryableError(err error) bool {
if err == nil {
return false
}
var ne net.Error
if errors.As(err, &ne) {
if ne.Timeout() || ne.Temporary() {
return true
}
}
// 兼容不同 Go/系统对握手超时的错误文本
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "tls handshake timeout") ||
strings.Contains(msg, "handshake timeout") ||
strings.Contains(msg, "connection reset by peer") ||
strings.Contains(msg, "connection timed out")
}
func retryDelay(attempt int) time.Duration {
// attempt 从 0 开始:1s, 2s, 4s...
d := time.Second * time.Duration(1<<attempt)
if d > 8*time.Second {
d = 8 * time.Second
}
return d
}
func readAllLimit(r io.Reader, maxBytes int64) ([]byte, error) {
if maxBytes > 0 {
return io.ReadAll(io.LimitReader(r, maxBytes))
}
return io.ReadAll(r)
}
// FetchURLBytes 发起 GET 并返回响应体(只在 2xx 认为成功)。
// 为了降低“偶发 HTTPS 握手超时”,对可重试错误/状态码会做有限重试。
func FetchURLBytes(ctx context.Context, rawURL string, proxy string, timeout time.Duration, retries int, maxBytes int64) ([]byte, int, error) {
if ctx == nil {
ctx = context.Background()
}
if timeout <= 0 {
timeout = defaultHTTPTimeout
}
if retries < 0 {
retries = 0
}
if maxBytes <= 0 {
maxBytes = 8 << 20 // 默认最多读取 8MiB
}
var lastErr error
for attempt := 0; attempt <= retries; attempt++ {
if ctx.Err() != nil {
return nil, 0, ctx.Err()
}
client := newHTTPClient(timeout, proxy)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
if err != nil {
return nil, 0, err
}
resp, err := client.Do(req)
if err != nil {
lastErr = err
if attempt < retries && isRetryableError(err) {
time.Sleep(retryDelay(attempt))
continue
}
return nil, 0, err
}
body, readErr := readAllLimit(resp.Body, maxBytes)
_ = resp.Body.Close()
if readErr != nil {
lastErr = readErr
if attempt < retries && isRetryableError(readErr) {
time.Sleep(retryDelay(attempt))
continue
}
return nil, resp.StatusCode, readErr
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
// 429/5xx 可能是短暂错误,允许有限重试
status := resp.StatusCode
lastErr = fmt.Errorf("request failed: status=%d", status)
if attempt < retries && (status == http.StatusTooManyRequests || status >= 500 && status <= 599) {
time.Sleep(retryDelay(attempt))
continue
}
// 避免把大 body 全塞进日志/错误里
preview := strings.TrimSpace(string(body))
if len(preview) > 256 {
preview = preview[:256]
}
if preview != "" {
return body, status, fmt.Errorf("request failed: status=%d body=%s", status, preview)
}
return body, status, lastErr
}
return body, resp.StatusCode, nil
}
return nil, 0, lastErr
}
// SendMsg 回复客户片段端消息
func SendMsg(client *types.WsClient, message types.ReplyMessage) {
@@ -54,36 +211,8 @@ func SendChannelMsg(ws *types.WsClient, channel types.WsChannel, message interfa
}
func DownloadImage(imageURL string, proxy string) ([]byte, error) {
var client *http.Client
if proxy == "" {
client = http.DefaultClient
} else {
proxyURL, _ := url.Parse(proxy)
client = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
},
}
}
request, err := http.NewRequest("GET", imageURL, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(request)
if err != nil {
return nil, err
}
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
imageBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return imageBytes, nil
body, _, err := FetchURLBytes(context.Background(), imageURL, proxy, defaultHTTPTimeout, 2, 32<<20)
return body, err
}
func GetBaseURL(strURL string) string {
@@ -103,14 +232,26 @@ func GetImgExt(filename string) string {
}
func GetFileSize(url string) (int64, error) {
resp, err := http.Get(url)
// 优先走 HEAD 获取 Content-Length,减少下载成本;若对方不支持,再 fallback 到 GET 统计字节数。
client := newHTTPClient(defaultHTTPTimeout, "")
if req, err := http.NewRequest(http.MethodHead, url, nil); err == nil {
if resp, err := client.Do(req); err == nil {
_ = resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode <= 299 && resp.ContentLength > 0 {
return resp.ContentLength, nil
}
}
}
resp, err := client.Get(url)
if err != nil {
return 0, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, err
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
// 尽量不把大 body 都读进来,只返回状态码
return 0, fmt.Errorf("get file size failed: status=%d", resp.StatusCode)
}
return int64(len(body)), nil
n, err := io.Copy(io.Discard, resp.Body)
return n, err
}