Skip to content
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ tools/authenticator/access_tokens.txt
*.txt
aurora
chatgpttoapi
chatgpt2api
tools/authenticator/.proxies.txt.swp
.env
*.har
Expand All @@ -16,4 +17,6 @@ tools/authenticator/.proxies.txt.swp
.claude
.gocache
.atomcode
CLAUDE.md
CLAUDE.md
MIGRATION_PLAN.md
_scratch/
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.3.0
2.3.1
126 changes: 119 additions & 7 deletions conversion/requests/chatgpt/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import (
"aurora/internal/tokens"
chatgpt_types "aurora/typings/chatgpt"
official_types "aurora/typings/official"
"aurora/httpclient"
"encoding/base64"
"io"
"net/http"
"strings"
)

func ConvertAPIRequest(api_request official_types.APIRequest, secret *tokens.Secret, proxy string) chatgpt_types.ChatGPTRequest {
func ConvertAPIRequest(api_request official_types.APIRequest, secret *tokens.Secret, proxy string, client httpclient.AuroraHttpClient) chatgpt_types.ChatGPTRequest {
chatgpt_request := chatgpt_types.NewChatGPTRequest()

// Model is passed directly to upstream; default to "auto" if not provided
Expand All @@ -22,7 +26,7 @@ func ConvertAPIRequest(api_request official_types.APIRequest, secret *tokens.Sec
if apiMessage.Role == "system" {
apiMessage.Role = "critic"
}
parts, metadata := buildMessageParts(apiMessage)
parts, metadata := buildMessageParts(apiMessage, client, secret, proxy)
if len(metadata) > 0 {
chatgpt_request.AddMultimodalMessage(apiMessage.Role, parts, metadata)
continue
Expand All @@ -39,9 +43,9 @@ func ConvertTTSAPIRequest(input string) chatgpt_types.ChatGPTRequest {
return chatgpt_request
}

func buildMessageParts(message official_types.APIMessage) ([]interface{}, map[string]interface{}) {
func buildMessageParts(message official_types.APIMessage, client httpclient.AuroraHttpClient, secret *tokens.Secret, proxy string) ([]interface{}, map[string]interface{}) {
text := message.Text()
files := enrichFiles(message.Files())
files := enrichFiles(message.Files(), client, secret, proxy)
if len(files) == 0 {
return []interface{}{text}, nil
}
Expand Down Expand Up @@ -106,15 +110,26 @@ func buildMessageParts(message official_types.APIMessage) ([]interface{}, map[st
}
}

func enrichFiles(files []official_types.FileAttachment) []official_types.FileAttachment {
func enrichFiles(files []official_types.FileAttachment, client httpclient.AuroraHttpClient, secret *tokens.Secret, proxy string) []official_types.FileAttachment {
enriched := make([]official_types.FileAttachment, 0, len(files))
seen := make(map[string]bool)
for _, file := range files {
id := fileID(file)
if id == "" || seen[id] {
continue
}
if uploaded, ok := backendchatgpt.LookupUploadedFile(id); ok {

// 处理 image_url 的 inline 数据(data: URL 或 http URL)
if file.Source != "" && client != nil && secret != nil {
if uploaded, ok := uploadInlineImage(file, client, secret, proxy); ok {
file = uploaded
} else {
// 免费账号或上传失败:丢弃图片,只保留文本
continue
}
}

if uploaded, ok := backendchatgpt.LookupUploadedFile(fileID(file)); ok {
if file.ID == "" {
file.ID = uploaded.ID
}
Expand All @@ -137,12 +152,109 @@ func enrichFiles(files []official_types.FileAttachment) []official_types.FileAtt
file.LibraryFileID = uploaded.LibraryFileID
}
}
seen[id] = true
seen[fileID(file)] = true
enriched = append(enriched, file)
}
return enriched
}

// uploadInlineImage 将 data: URL 或 http URL 图片上传到 ChatGPT 文件服务。
func uploadInlineImage(file official_types.FileAttachment, client httpclient.AuroraHttpClient, secret *tokens.Secret, proxy string) (official_types.FileAttachment, bool) {
src := file.Source
var data []byte
var filename string
var contentType string

if strings.HasPrefix(src, "data:") {
// data:image/png;base64,iVBOR...
commaIdx := strings.Index(src, ",")
if commaIdx < 0 {
return file, false
}
meta := src[:commaIdx]
b64data := src[commaIdx+1:]
// 提取 mime type
if semiIdx := strings.Index(meta, ";"); semiIdx > 5 {
contentType = meta[5:semiIdx]
}
var err error
data, err = base64.StdEncoding.DecodeString(b64data)
if err != nil {
// 尝试 raw base64
data, err = base64.RawStdEncoding.DecodeString(b64data)
if err != nil {
return file, false
}
}
filename = "image.png"
if contentType == "" {
contentType = "image/png"
}
} else if strings.HasPrefix(src, "http://") || strings.HasPrefix(src, "https://") {
// 下载远程图片
resp, err := http.Get(src)
if err != nil {
return file, false
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return file, false
}
data, err = io.ReadAll(resp.Body)
if err != nil {
return file, false
}
contentType = resp.Header.Get("Content-Type")
filename = guessFilenameFromURL(src)
} else {
return file, false
}

if len(data) == 0 {
return file, false
}
if contentType == "" {
contentType = http.DetectContentType(data)
}
if filename == "" {
filename = "image.png"
}

uploaded, _, err := backendchatgpt.UploadFile(client, secret, proxy, filename, contentType, data)
if err != nil {
// 免费 token 无法上传文件,回退:把 data URL 原样传递
return file, false
}

return official_types.FileAttachment{
ID: uploaded.FileID,
FileID: uploaded.FileID,
Name: uploaded.Filename,
FileName: uploaded.Filename,
Filename: uploaded.Filename,
MimeType: uploaded.MimeType,
MIMEType: uploaded.MimeType,
Size: uploaded.Bytes,
Width: uploaded.Width,
Height: uploaded.Height,
LibraryFileID: uploaded.LibraryFileID,
}, true
}

func guessFilenameFromURL(url string) string {
idx := strings.LastIndex(url, "/")
if idx >= 0 && idx < len(url)-1 {
name := url[idx+1:]
if q := strings.Index(name, "?"); q >= 0 {
name = name[:q]
}
if name != "" {
return name
}
}
return "image.png"
}

func fileID(file official_types.FileAttachment) string {
if strings.TrimSpace(file.FileID) != "" {
return strings.TrimSpace(file.FileID)
Expand Down
6 changes: 3 additions & 3 deletions initialize/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ func (h *Handler) nightmare(c *gin.Context) {
client := bogdanfinn.NewStdClient()

// Convert the chat request to a ChatGPT request
translated_request := chatgptrequestconverter.ConvertAPIRequest(original_request, secret, proxyUrl)
translated_request := chatgptrequestconverter.ConvertAPIRequest(original_request, secret, proxyUrl, client)
clientState := chatgpt.NewChatClientState()
clientState.ConversationID = translated_request.ConversationID
clientState.ParentMessageID = translated_request.ParentMessageID
Expand Down Expand Up @@ -438,7 +438,7 @@ func (h *Handler) responses(c *gin.Context) {
uid := uuid.NewString()
client := bogdanfinn.NewStdClient()

translated_request := chatgptrequestconverter.ConvertAPIRequest(original_request, secret, proxyUrl)
translated_request := chatgptrequestconverter.ConvertAPIRequest(original_request, secret, proxyUrl, client)
clientState := chatgpt.NewChatClientState()
clientState.ConversationID = translated_request.ConversationID
clientState.ParentMessageID = translated_request.ParentMessageID
Expand Down Expand Up @@ -726,7 +726,7 @@ func (h *Handler) files(c *gin.Context) {
contentType := formFile.Header.Get("Content-Type")
client := bogdanfinn.NewStdClient()
client.SetCookies("https://chatgpt.com", chatgpt.BasicCookies)
uploaded, status, err := chatgpt.UploadFile(client, secret, h.proxy.GetProxyIP(), formFile.Filename, contentType, c.PostForm("purpose"), data)
uploaded, status, err := chatgpt.UploadFile(client, secret, h.proxy.GetProxyIP(), formFile.Filename, contentType, data)
if err != nil {
c.JSON(status, gin.H{"error": gin.H{
"message": err.Error(),
Expand Down
Loading
Loading