feat: Implement core backend server with file upload, monitoring, S2A API proxy, and team processing features.
This commit is contained in:
@@ -104,6 +104,7 @@ func startServer(cfg *config.Config) {
|
||||
mux.HandleFunc("/api/db/owners", api.CORS(handleGetOwners))
|
||||
mux.HandleFunc("/api/db/owners/stats", api.CORS(handleGetOwnerStats))
|
||||
mux.HandleFunc("/api/db/owners/clear", api.CORS(handleClearOwners))
|
||||
mux.HandleFunc("/api/upload/validate", api.CORS(handleUploadValidate))
|
||||
|
||||
// 注册测试 API
|
||||
mux.HandleFunc("/api/register/test", api.CORS(handleRegisterTest))
|
||||
|
||||
273
backend/cmd/upload.go
Normal file
273
backend/cmd/upload.go
Normal file
@@ -0,0 +1,273 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"codex-pool/internal/api"
|
||||
"codex-pool/internal/database"
|
||||
)
|
||||
|
||||
type uploadValidateRequest struct {
|
||||
Content string `json:"content"`
|
||||
Accounts []accountRecord `json:"accounts"`
|
||||
}
|
||||
|
||||
type accountRecord struct {
|
||||
Account string `json:"account"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Token string `json:"token"`
|
||||
AccessTok string `json:"access_token"`
|
||||
AccountID string `json:"account_id"`
|
||||
}
|
||||
|
||||
func handleUploadValidate(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
api.Error(w, http.StatusMethodNotAllowed, "仅支持 POST")
|
||||
return
|
||||
}
|
||||
if database.Instance == nil {
|
||||
api.Error(w, http.StatusInternalServerError, "数据库未初始化")
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 10<<20))
|
||||
if err != nil {
|
||||
api.Error(w, http.StatusBadRequest, "读取请求失败")
|
||||
return
|
||||
}
|
||||
|
||||
var req uploadValidateRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
// 如果不是 JSON,直接把 body 当作原始内容
|
||||
req.Content = string(body)
|
||||
}
|
||||
|
||||
var records []accountRecord
|
||||
switch {
|
||||
case len(req.Accounts) > 0:
|
||||
records = req.Accounts
|
||||
case strings.TrimSpace(req.Content) != "":
|
||||
parsed, parseErr := parseAccountsFlexible(req.Content)
|
||||
if parseErr != nil {
|
||||
api.Error(w, http.StatusBadRequest, parseErr.Error())
|
||||
return
|
||||
}
|
||||
records = parsed
|
||||
default:
|
||||
api.Error(w, http.StatusBadRequest, "未提供账号内容")
|
||||
return
|
||||
}
|
||||
|
||||
owners := make([]database.TeamOwner, 0, len(records))
|
||||
for i, rec := range records {
|
||||
owner, err := normalizeOwner(rec, i+1)
|
||||
if err != nil {
|
||||
api.Error(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
owners = append(owners, owner)
|
||||
}
|
||||
if len(owners) == 0 {
|
||||
api.Error(w, http.StatusBadRequest, "未解析到有效账号")
|
||||
return
|
||||
}
|
||||
|
||||
inserted, err := database.Instance.AddTeamOwners(owners)
|
||||
if err != nil {
|
||||
api.Error(w, http.StatusInternalServerError, fmt.Sprintf("写入数据库失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
stats := database.Instance.GetOwnerStats()
|
||||
api.Success(w, map[string]interface{}{
|
||||
"imported": inserted,
|
||||
"total": len(owners),
|
||||
"stats": stats,
|
||||
})
|
||||
}
|
||||
|
||||
func normalizeOwner(rec accountRecord, index int) (database.TeamOwner, error) {
|
||||
email := strings.TrimSpace(rec.Email)
|
||||
if email == "" {
|
||||
email = strings.TrimSpace(rec.Account)
|
||||
}
|
||||
if email == "" {
|
||||
return database.TeamOwner{}, fmt.Errorf("第 %d 条记录缺少 account/email 字段", index)
|
||||
}
|
||||
|
||||
password := strings.TrimSpace(rec.Password)
|
||||
if password == "" {
|
||||
return database.TeamOwner{}, fmt.Errorf("第 %d 条记录缺少 password 字段", index)
|
||||
}
|
||||
|
||||
token := strings.TrimSpace(rec.Token)
|
||||
if token == "" {
|
||||
token = strings.TrimSpace(rec.AccessTok)
|
||||
}
|
||||
if token == "" {
|
||||
return database.TeamOwner{}, fmt.Errorf("第 %d 条记录缺少 token 字段", index)
|
||||
}
|
||||
|
||||
accountID := strings.TrimSpace(rec.AccountID)
|
||||
|
||||
return database.TeamOwner{
|
||||
Email: email,
|
||||
Password: password,
|
||||
Token: token,
|
||||
AccountID: accountID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseAccountsFlexible(raw string) ([]accountRecord, error) {
|
||||
raw = strings.TrimSpace(strings.TrimPrefix(raw, "\uFEFF"))
|
||||
if raw == "" {
|
||||
return nil, fmt.Errorf("内容为空")
|
||||
}
|
||||
|
||||
cleaned := stripJSONComments(raw)
|
||||
cleaned = removeTrailingCommas(cleaned)
|
||||
trimmed := strings.TrimSpace(cleaned)
|
||||
if trimmed == "" {
|
||||
return nil, fmt.Errorf("内容为空")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(trimmed, "[") {
|
||||
var arr []accountRecord
|
||||
if err := json.Unmarshal([]byte(trimmed), &arr); err == nil {
|
||||
return arr, nil
|
||||
}
|
||||
} else if strings.HasPrefix(trimmed, "{") {
|
||||
var single accountRecord
|
||||
if err := json.Unmarshal([]byte(trimmed), &single); err == nil {
|
||||
return []accountRecord{single}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// JSONL 回退
|
||||
lines := strings.Split(raw, "\n")
|
||||
records := make([]accountRecord, 0, len(lines))
|
||||
for i, line := range lines {
|
||||
line = strings.TrimSpace(stripJSONComments(line))
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
line = strings.TrimSpace(removeTrailingCommas(line))
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
var rec accountRecord
|
||||
if err := json.Unmarshal([]byte(line), &rec); err != nil {
|
||||
return nil, fmt.Errorf("第 %d 行解析失败: %v", i+1, err)
|
||||
}
|
||||
records = append(records, rec)
|
||||
}
|
||||
if len(records) == 0 {
|
||||
return nil, fmt.Errorf("未解析到有效账号")
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func stripJSONComments(input string) string {
|
||||
var b strings.Builder
|
||||
b.Grow(len(input))
|
||||
|
||||
inString := false
|
||||
escaped := false
|
||||
for i := 0; i < len(input); i++ {
|
||||
ch := input[i]
|
||||
if inString {
|
||||
b.WriteByte(ch)
|
||||
if escaped {
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
if ch == '\\' {
|
||||
escaped = true
|
||||
} else if ch == '"' {
|
||||
inString = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if ch == '"' {
|
||||
inString = true
|
||||
b.WriteByte(ch)
|
||||
continue
|
||||
}
|
||||
|
||||
if ch == '/' && i+1 < len(input) && input[i+1] == '/' {
|
||||
for i+1 < len(input) && input[i+1] != '\n' {
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
if ch == '/' && i+1 < len(input) && input[i+1] == '*' {
|
||||
i += 2
|
||||
for i+1 < len(input) {
|
||||
if input[i] == '*' && input[i+1] == '/' {
|
||||
i++
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
b.WriteByte(ch)
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func removeTrailingCommas(input string) string {
|
||||
var b strings.Builder
|
||||
b.Grow(len(input))
|
||||
|
||||
inString := false
|
||||
escaped := false
|
||||
for i := 0; i < len(input); i++ {
|
||||
ch := input[i]
|
||||
if inString {
|
||||
b.WriteByte(ch)
|
||||
if escaped {
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
if ch == '\\' {
|
||||
escaped = true
|
||||
} else if ch == '"' {
|
||||
inString = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if ch == '"' {
|
||||
inString = true
|
||||
b.WriteByte(ch)
|
||||
continue
|
||||
}
|
||||
|
||||
if ch == ',' {
|
||||
j := i + 1
|
||||
for j < len(input) && unicode.IsSpace(rune(input[j])) {
|
||||
j++
|
||||
}
|
||||
if j < len(input) && (input[j] == ']' || input[j] == '}') {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteByte(ch)
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
Reference in New Issue
Block a user