db / main.go
caidaohz's picture
更新README.md,完善项目描述和使用说明;优化main.go,改进chatID获取逻辑和响应处理
e811e5c
package main
import (
"bufio"
"crypto/md5"
"encoding/json"
"fmt"
"io"
"log"
"math/rand"
"net"
"net/http"
"os"
"regexp"
"slices"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/joho/godotenv"
)
// Message 消息结构体,表示对话中的一条消息
type Message struct {
Role string `json:"role" binding:"required,oneof=system user assistant"`
Content string `json:"content" binding:"required"`
}
// ChatCompletionRequest 聊天完成请求结构体
type ChatCompletionRequest struct {
Model string `json:"model"`
Messages []Message `json:"messages"`
Stream bool `json:"stream"`
Temperature float64 `json:"temperature"`
TopP float64 `json:"top_p"`
PresencePenalty float64 `json:"presence_penalty"`
FrequencyPenalty float64 `json:"frequency_penalty"`
MaxTokens int `json:"max_tokens"`
}
// Choice OpenAI 格式的选择结构体
type Choice struct {
Index int `json:"index"`
Delta *Delta `json:"delta,omitempty"`
Message *Message `json:"message,omitempty"`
FinishReason *string `json:"finish_reason"`
}
// Delta 流式响应中的增量内容
type Delta struct {
Role string `json:"role,omitempty"`
Content string `json:"content,omitempty"`
}
// Usage 令牌使用统计
type Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
}
// ChatCompletionResponse OpenAI 格式的聊天响应
type ChatCompletionResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []Choice `json:"choices"`
Usage *Usage `json:"usage,omitempty"`
}
// ModelInfo 模型信息结构体
type ModelInfo struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
OwnedBy string `json:"owned_by"`
}
// ModelList 模型列表结构体
type ModelList struct {
Object string `json:"object"`
Data []ModelInfo `json:"data"`
}
var (
apiKey string
enableCORS bool
randomUA bool
supportedModels = []string{"DeepSeek-R1", "DeepSeek-V3"}
modelToConfig = map[string]map[string]interface{}{
"DeepSeek-R1": {"model": "deepseek-huoshan", "isWebSearchEnabled": false},
"DeepSeek-V3": {"model": "deepseek-guiji", "isWebSearchEnabled": false},
}
apiDomain = "https://ai-chatbot.top"
defaultUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0"
deviceUAMap = make(map[string]string)
deviceUAMutex sync.Mutex
// 随机User-Agent列表
userAgents = []string{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/120.0.0.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0",
}
// 全局 HTTP 客户端配置,支持连接池和 HTTP/2
httpClient *http.Client
// chatID 缓存系统
chatIDCache struct {
sync.RWMutex
value string
expiresAt time.Time
}
)
// initHTTPClient 初始化优化的 HTTP 客户端,支持连接池和长连接
func initHTTPClient() {
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
DisableKeepAlives: false,
}
httpClient = &http.Client{
Transport: transport,
Timeout: 10 * time.Minute, // 10分钟超时支持长时间思考
}
}
// nanoid 生成指定长度的随机字符串
func nanoid(size int) string {
alphabet := "abcdefgh0ijkl1mno2pqrs3tuv4wxyz5ABCDEFGH6IJKL7MNO8PQRS9TUV-WXYZ_"
b := make([]byte, size)
for i := range b {
b[i] = alphabet[rand.Intn(len(alphabet))]
}
return string(b)
}
// generateDeviceID 生成设备ID
func generateDeviceID() string {
return fmt.Sprintf("%s_%s", uuid.New().String(), nanoid(20))
}
// getRandomUserAgent 获取随机User-Agent
func getRandomUserAgent() string {
return userAgents[rand.Intn(len(userAgents))]
}
// getUserAgent 获取或生成用户代理字符串
func getUserAgent(deviceID string) string {
if !randomUA {
return defaultUA
}
deviceUAMutex.Lock()
defer deviceUAMutex.Unlock()
if ua, ok := deviceUAMap[deviceID]; ok {
return ua
}
// 生成随机用户代理
randomUserAgent := getRandomUserAgent()
deviceUAMap[deviceID] = randomUserAgent
return randomUserAgent
}
const signSalt = "@!~chatbot.0868"
// generateSign 生成签名用于API认证
func generateSign(chatID string, timestamp int64) string {
msg := fmt.Sprintf("%s%d%s", chatID, timestamp, signSalt)
hash := md5.Sum([]byte(msg))
return fmt.Sprintf("%x", hash)
}
// getChatID 获取聊天ID,支持缓存和并发控制
var chatIDOnce sync.Once
func getChatID() (string, error) {
// 读取缓存
chatIDCache.RLock()
if time.Now().Before(chatIDCache.expiresAt) && chatIDCache.value != "" {
cachedID := chatIDCache.value
chatIDCache.RUnlock()
return cachedID, nil
}
chatIDCache.RUnlock()
// 使用 sync.Once 确保只获取一次 chatID
var err error
chatIDOnce.Do(func() {
// 使用全局 HTTP 客户端获取新的 chatID
req, _ := http.NewRequest("GET", apiDomain+"/", nil)
req.Header.Set("User-Agent", defaultUA)
req.Header.Set("Accept", "*/*")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2")
req.Header.Set("Referer", "https://ai-chatbot.top/")
resp, err := httpClient.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
reg := regexp.MustCompile(`[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}`)
ids := reg.FindAllString(string(body), -1)
if len(ids) == 0 {
err = fmt.Errorf("no chat id found")
return
}
chatID := ids[len(ids)-1]
// 更新缓存
chatIDCache.Lock()
chatIDCache.value = chatID
chatIDCache.expiresAt = time.Now().Add(5 * time.Minute)
chatIDCache.Unlock()
})
// 再次读取缓存
chatIDCache.RLock()
defer chatIDCache.RUnlock()
return chatIDCache.value, err
}
// preloadChatID 异步预取 chatID,在缓存过期前自动刷新
func preloadChatID() {
go func() {
for {
// 在缓存过期前 1 分钟刷新
sleepDuration := 4 * time.Minute
if !chatIDCache.expiresAt.IsZero() {
timeToExpiry := time.Until(chatIDCache.expiresAt)
if timeToExpiry > time.Minute {
sleepDuration = timeToExpiry - time.Minute
}
}
time.Sleep(sleepDuration)
getChatID() // 刷新缓存
}
}()
}
// ChatbotFinish ai-chatbot.top 的结束信息结构
type ChatbotFinish struct {
FinishReason string `json:"finishReason"`
Usage Usage `json:"usage"`
IsContinued bool `json:"isContinued"`
}
// parseChatbotLine 解析 ai-chatbot.top 响应的单行内容
func parseChatbotLine(line string) (string, string, error) {
if len(line) < 2 {
return "", "", fmt.Errorf("line too short")
}
colonIndex := strings.Index(line, ":")
if colonIndex == -1 {
return "", "", fmt.Errorf("no colon found")
}
prefix := line[:colonIndex]
content := line[colonIndex+1:]
return prefix, content, nil
}
// handleStreamResponse 处理流式响应并转换为 OpenAI 格式
func handleStreamResponse(resp *http.Response, c *gin.Context, model string, startTime time.Time) {
defer resp.Body.Close()
// 设置 SSE 响应头
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
scanner := bufio.NewScanner(resp.Body)
created := time.Now().Unix()
chatID := fmt.Sprintf("chatcmpl-%s", nanoid(29))
// 耗时统计相关变量
var firstTokenTime time.Time
firstTokenReceived := false
thinking := false
// 首先发送一个空chunk,兼容SSE客户端
chunk := ChatCompletionResponse{
ID: chatID,
Object: "chat.completion.chunk",
Created: created,
Model: model,
Choices: []Choice{{
Index: 0,
Delta: &Delta{Content: ""},
FinishReason: nil,
}},
}
sendChunk(c, chunk)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
prefix, content, err := parseChatbotLine(line)
if err != nil {
continue
}
switch prefix {
case "f":
// 忽略 messageId,OpenAI 不需要这个
continue
case "g":
// 思考过程内容
if !thinking {
thinking = true
// 记录首字时间
if !firstTokenReceived {
firstTokenTime = time.Now()
firstTokenReceived = true
firstTokenDuration := firstTokenTime.Sub(startTime)
fmt.Printf("[INFO] First token received in: %v\n", firstTokenDuration)
}
// 发送 <think> 开始标记
chunk := ChatCompletionResponse{
ID: chatID,
Object: "chat.completion.chunk",
Created: created,
Model: model,
Choices: []Choice{{
Index: 0,
Delta: &Delta{Content: "<think>"},
FinishReason: nil,
}},
}
sendChunk(c, chunk)
}
var contentStr string
if err := json.Unmarshal([]byte(content), &contentStr); err != nil {
continue
}
contentStr = strings.ReplaceAll(contentStr, "\\n", "\n")
chunk := ChatCompletionResponse{
ID: chatID,
Object: "chat.completion.chunk",
Created: created,
Model: model,
Choices: []Choice{{
Index: 0,
Delta: &Delta{Content: contentStr},
FinishReason: nil,
}},
}
jsonData, _ := json.Marshal(chunk)
fmt.Fprintf(c.Writer, "data: %s\n\n", jsonData)
c.Writer.(http.Flusher).Flush()
case "0":
// 最终回答内容
// 记录首字时间(如果还没记录)
if !firstTokenReceived {
firstTokenTime = time.Now()
firstTokenReceived = true
firstTokenDuration := firstTokenTime.Sub(startTime)
fmt.Printf("[INFO] First token received in: %v\n", firstTokenDuration)
}
if thinking {
thinking = false
// 发送 </think> 结束标记
chunk := ChatCompletionResponse{
ID: chatID,
Object: "chat.completion.chunk",
Created: created,
Model: model,
Choices: []Choice{{
Index: 0,
Delta: &Delta{Content: "</think>"},
FinishReason: nil,
}},
}
jsonData, _ := json.Marshal(chunk)
fmt.Fprintf(c.Writer, "data: %s\n\n", jsonData)
c.Writer.(http.Flusher).Flush()
}
var contentStr string
if err := json.Unmarshal([]byte(content), &contentStr); err != nil {
continue
}
contentStr = strings.ReplaceAll(contentStr, "\\n", "\n")
chunk := ChatCompletionResponse{
ID: chatID,
Object: "chat.completion.chunk",
Created: created,
Model: model,
Choices: []Choice{{
Index: 0,
Delta: &Delta{Content: contentStr},
FinishReason: nil,
}},
}
jsonData, _ := json.Marshal(chunk)
fmt.Fprintf(c.Writer, "data: %s\n\n", jsonData)
c.Writer.(http.Flusher).Flush()
case "e", "d":
// 结束信息
if thinking {
// 如果还在思考模式,发送结束标记
chunk := ChatCompletionResponse{
ID: chatID,
Object: "chat.completion.chunk",
Created: created,
Model: model,
Choices: []Choice{{
Index: 0,
Delta: &Delta{Content: "</think>"},
FinishReason: nil,
}},
}
sendChunk(c, chunk)
}
var finishInfo ChatbotFinish
if err := json.Unmarshal([]byte(content), &finishInfo); err != nil {
continue
}
finishReason := finishInfo.FinishReason
chunk := ChatCompletionResponse{
ID: chatID,
Object: "chat.completion.chunk",
Created: created,
Model: model,
Choices: []Choice{{
Index: 0,
Delta: &Delta{},
FinishReason: &finishReason,
}},
Usage: &Usage{
PromptTokens: finishInfo.Usage.PromptTokens,
CompletionTokens: finishInfo.Usage.CompletionTokens,
TotalTokens: finishInfo.Usage.PromptTokens + finishInfo.Usage.CompletionTokens,
},
}
sendChunk(c, chunk)
// 发送 [DONE] 信号
fmt.Fprintf(c.Writer, "data: [DONE]\n\n")
c.Writer.(http.Flusher).Flush()
// 记录总耗时
totalDuration := time.Since(startTime)
fmt.Printf("[INFO] Request completed in: %v\n", totalDuration)
return
}
}
}
// handleNonStreamResponse 处理非流式响应并转换为 OpenAI 格式
func handleNonStreamResponse(resp *http.Response, c *gin.Context, model string, startTime time.Time) {
scanner := bufio.NewScanner(resp.Body)
created := time.Now().Unix()
chatID := fmt.Sprintf("chatcmpl-%s", nanoid(29))
var fullContent strings.Builder
var thinkContent strings.Builder
var usage *Usage
// 耗时统计相关变量
var firstTokenTime time.Time
firstTokenReceived := false
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
prefix, content, err := parseChatbotLine(line)
if err != nil {
continue
}
switch prefix {
case "g":
// 思考过程内容
// 记录首字时间
if !firstTokenReceived {
firstTokenTime = time.Now()
firstTokenReceived = true
firstTokenDuration := firstTokenTime.Sub(startTime)
fmt.Printf("[INFO] First token received in: %v\n", firstTokenDuration)
}
var contentStr string
if err := json.Unmarshal([]byte(content), &contentStr); err != nil {
continue
}
contentStr = strings.ReplaceAll(contentStr, "\\n", "\n")
thinkContent.WriteString(contentStr)
case "0":
// 最终回答内容
// 记录首字时间(如果还没记录)
if !firstTokenReceived {
firstTokenTime = time.Now()
firstTokenReceived = true
firstTokenDuration := firstTokenTime.Sub(startTime)
fmt.Printf("[INFO] First token received in: %v\n", firstTokenDuration)
}
if thinkContent.Len() > 0 {
fullContent.WriteString("<think>")
fullContent.WriteString(thinkContent.String())
fullContent.WriteString("</think>")
thinkContent.Reset()
}
var contentStr string
if err := json.Unmarshal([]byte(content), &contentStr); err != nil {
continue
}
contentStr = strings.ReplaceAll(contentStr, "\\n", "\n")
fullContent.WriteString(contentStr)
case "e", "d":
// 结束信息
var finishInfo ChatbotFinish
if err := json.Unmarshal([]byte(content), &finishInfo); err != nil {
continue
}
usage = &Usage{
PromptTokens: finishInfo.Usage.PromptTokens,
CompletionTokens: finishInfo.Usage.CompletionTokens,
TotalTokens: finishInfo.Usage.PromptTokens + finishInfo.Usage.CompletionTokens,
}
}
}
// 如果还有未处理的思考内容,添加到最终内容中
if thinkContent.Len() > 0 {
fullContent.WriteString("<think>")
fullContent.WriteString(thinkContent.String())
fullContent.WriteString("</think>")
}
response := ChatCompletionResponse{
ID: chatID,
Object: "chat.completion",
Created: created,
Model: model,
Choices: []Choice{{
Index: 0,
Message: &Message{
Role: "assistant",
Content: fullContent.String(),
},
FinishReason: func() *string { s := "stop"; return &s }(),
}},
Usage: usage,
}
// 记录总耗时
totalDuration := time.Since(startTime)
fmt.Printf("[INFO] Request completed in: %v\n", totalDuration)
sendChunk(c, ChatCompletionResponse{
ID: response.ID,
Object: response.Object,
Created: response.Created,
Model: response.Model,
Choices: response.Choices,
Usage: response.Usage,
})
}
// verifyAPIKey API密钥验证中间件
func verifyAPIKey(c *gin.Context) {
key := c.GetHeader("Authorization")
if key == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"detail": "Missing Authorization header"})
return
}
if strings.HasPrefix(key, "Bearer ") {
key = strings.TrimSpace(strings.TrimPrefix(key, "Bearer "))
}
if key != apiKey {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"detail": "Invalid API key"})
return
}
c.Next()
}
// main 主函数,初始化并启动服务器
func main() {
_ = godotenv.Load()
apiKey = os.Getenv("API_KEY")
fmt.Println("[DEBUG] Loaded API_KEY:", apiKey)
if apiKey == "" {
log.Fatal("API_KEY not found in .env file")
}
// 读取RANDOM_UA环境变量
randomUAStr := strings.ToLower(os.Getenv("RANDOM_UA"))
randomUA = randomUAStr == "true" || randomUAStr == "1" || randomUAStr == "yes"
fmt.Println("[DEBUG] RANDOM_UA enabled:", randomUA)
// 初始化优化的 HTTP 客户端
initHTTPClient()
// 启动异步 chatID 预加载
preloadChatID()
enableCORS = true
gin.SetMode(gin.ReleaseMode)
r := gin.Default()
// CORS 中间件配置
if enableCORS {
r.Use(func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "*")
c.Writer.Header().Set("Access-Control-Allow-Headers", "*")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
})
}
// 健康检查端点不需要 API 验证
r.GET("/health", func(c *gin.Context) {
chatID, err := getChatID()
status := "inactive"
if err == nil && chatID != "" {
status = "active"
}
c.JSON(http.StatusOK, gin.H{"status": "ok", "session": status})
})
// 需要 API 验证的端点组
authorized := r.Group("/")
authorized.Use(verifyAPIKey)
// 聊天完成接口
authorized.POST("/v1/chat/completions", func(c *gin.Context) {
// 记录请求开始时间
startTime := time.Now()
var req ChatCompletionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if !contains(supportedModels, req.Model) {
req.Model = "DeepSeek-R1"
}
deviceID := generateDeviceID()
chatID, err := getChatID()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get chat id"})
return
}
timestamp := time.Now().UnixNano() / 1e6
sign := generateSign(chatID, timestamp)
// 构造请求载荷
messages := []map[string]string{}
for _, m := range req.Messages {
messages = append(messages, map[string]string{"role": m.Role, "content": m.Content})
}
payload := map[string]interface{}{
"id": chatID,
"messages": messages,
"selectedChatModel": modelToConfig[req.Model]["model"],
"isDeepThinkingEnabled": true,
"isWebSearchEnabled": modelToConfig[req.Model]["isWebSearchEnabled"],
}
// 构造请求头
headers := map[string]string{
"User-Agent": getUserAgent(deviceID),
"Accept": "*/*",
"Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
"Referer": "https://ai-chatbot.top/chat/" + chatID,
"Content-Type": "application/json",
"currentTime": fmt.Sprintf("%d", timestamp),
"sign": sign,
"Origin": "https://ai-chatbot.top",
"DNT": "1",
"Sec-GPC": "1",
"Connection": "keep-alive",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"Priority": "u=0",
}
// 构造cookies
cookies := []*http.Cookie{
{Name: "_ga_HVMZBNYJML", Value: "GS1.1.1742013194.1.1.1742013780.0.0.0"},
{Name: "_ga", Value: "GA1.1.1029622546.1742013195"},
}
// 使用全局 HTTP 客户端发送请求
jsonBytes, _ := json.Marshal(payload)
req2, _ := http.NewRequest("POST", apiDomain+"/api/chat", strings.NewReader(string(jsonBytes)))
for k, v := range headers {
req2.Header.Set(k, v)
}
for _, ck := range cookies {
req2.AddCookie(ck)
}
resp, err := httpClient.Do(req2)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to call ai-chatbot.top"})
return
}
defer resp.Body.Close()
// 根据 stream 参数选择处理方式
if req.Stream {
handleStreamResponse(resp, c, req.Model, startTime)
} else {
handleNonStreamResponse(resp, c, req.Model, startTime)
}
})
// 模型列表接口
authorized.GET("/v1/models", func(c *gin.Context) {
currentTime := time.Now().Unix()
models := []ModelInfo{}
for _, m := range supportedModels {
models = append(models, ModelInfo{
ID: m,
Object: "model",
Created: currentTime,
OwnedBy: "aichatbot",
})
}
c.JSON(http.StatusOK, ModelList{Object: "list", Data: models})
})
r.Run(":7860")
}
// contains 检查字符串数组中是否包含指定字符串
func contains(arr []string, s string) bool {
return slices.Contains(arr, s)
}
// sendChunk 发送 ChatCompletionResponse 结构体到客户端
func sendChunk(c *gin.Context, chunk ChatCompletionResponse) {
jsonData, _ := json.Marshal(chunk)
fmt.Fprintf(c.Writer, "data: %s\n\n", jsonData)
c.Writer.(http.Flusher).Flush()
}