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
+63 -5
View File
@@ -14,6 +14,7 @@ import (
"geekai/store/model"
"io"
"net/url"
"strings"
"time"
"github.com/imroc/req/v3"
@@ -35,18 +36,63 @@ func CalcTokens(text string, model string) (int, error) {
return len(token), nil
}
// OpenAIResponse 非流式 chat/completions 响应;content 使用 RawMessage 以兼容 string 与多段式数组。
type OpenAIResponse struct {
Model string `json:"model"`
Choices []struct {
Index int `json:"index"`
Message struct {
Role string `json:"role"`
Content string `json:"content"`
Role string `json:"role"`
Content json.RawMessage `json:"content"`
} `json:"message"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
}
// assistantContentPart 兼容 OpenAI 多模态 / 多段文本:{"type":"text","text":"..."} 等。
type assistantContentPart struct {
Type string `json:"type"`
Text string `json:"text"`
// 少数网关使用 content 字段承载文本
Inner string `json:"content"`
}
// NormalizeAssistantContent 将 message.content 规范为纯文本。
// 兼容:JSON string、null、OpenAI 数组段、以及单段对象。
func NormalizeAssistantContent(raw json.RawMessage) string {
s := strings.TrimSpace(string(raw))
if s == "" || s == "null" {
return ""
}
var plain string
if err := json.Unmarshal(raw, &plain); err == nil {
return plain
}
var parts []assistantContentPart
if err := json.Unmarshal(raw, &parts); err == nil && len(parts) > 0 {
var b strings.Builder
for _, p := range parts {
switch {
case p.Text != "":
b.WriteString(p.Text)
case p.Inner != "" && p.Type != "image_url" && p.Type != "image":
b.WriteString(p.Inner)
}
}
return b.String()
}
var one assistantContentPart
if err := json.Unmarshal(raw, &one); err == nil {
if one.Text != "" {
return one.Text
}
if one.Inner != "" {
return one.Inner
}
}
return ""
}
func OpenAIRequest(db *gorm.DB, prompt string, modelId int) (string, error) {
messages := make([]any, 1)
messages[0] = types.Message{
@@ -86,12 +132,16 @@ func SendOpenAIMessage(db *gorm.DB, messages []any, modelId int) (string, error)
apiURL = apiKey.ApiURL
}
logger.Infof("Sending %s request, API KEY:%s, PROXY: %s, Model: %s", apiURL, apiKey.ApiURL, apiKey.ProxyURL, chatModel.Name)
r, err := client.R().SetHeader("Body-Type", "application/json").
r, err := client.R().
SetHeader("Content-Type", "application/json").
SetHeader("Authorization", "Bearer "+apiKey.Value).
SetBody(types.ApiRequest{
Model: chatModel.Value,
Temperature: 0.9,
MaxTokens: 1024,
// gpt-5/o 系模型在复杂提示下可能先消耗大量 token 于推理过程,
// 1024 容易导致 finish_reason=length 且 content 为空。
MaxTokens: 4096,
MaxCompletionTokens: 4096,
Stream: false,
Messages: messages,
}).Post(apiURL)
@@ -108,9 +158,17 @@ func SendOpenAIMessage(db *gorm.DB, messages []any, modelId int) (string, error)
if err != nil {
return "", fmt.Errorf("解析API数据失败:%v, %s", err, string(body))
}
if len(response.Choices) == 0 {
return "", fmt.Errorf("模型返回 choices 为空:%s", string(body))
}
out := strings.TrimSpace(NormalizeAssistantContent(response.Choices[0].Message.Content))
if out == "" {
return "", fmt.Errorf("模型返回内容为空,请重试或更换模型(若使用推理模型,请确认网关已返回 content 或 reasoning_content")
}
// 更新 API KEY 的最后使用时间
db.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
return response.Choices[0].Message.Content, nil
return out, nil
}