feat: implement batch team owner pooling functionality with dedicated upload, processing, logging, and results pages.

This commit is contained in:
2026-01-30 08:57:16 +08:00
parent 9dfa61ac05
commit 6d236419b9
11 changed files with 477 additions and 693 deletions

View File

@@ -3,11 +3,13 @@ package main
import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"codex-pool/internal/api"
"codex-pool/internal/config"
@@ -19,23 +21,30 @@ import (
)
func main() {
fmt.Println("============================================================")
fmt.Println(" Codex Pool - HTTP API Server")
fmt.Println("============================================================")
fmt.Println()
// ANSI 颜色代码
colorReset := "\033[0m"
colorCyan := "\033[36m"
colorGreen := "\033[32m"
colorYellow := "\033[33m"
colorGray := "\033[90m"
colorBold := "\033[1m"
fmt.Printf("%s%s============================================================%s\n", colorBold, colorCyan, colorReset)
fmt.Printf("%s%s Codex Pool - HTTP API Server%s\n", colorBold, colorCyan, colorReset)
fmt.Printf("%s%s============================================================%s\n\n", colorBold, colorCyan, colorReset)
// 确定数据目录
dataDir := "data"
// 确保数据目录存在
if err := os.MkdirAll(dataDir, 0755); err != nil {
fmt.Printf("[警告] 创建数据目录失败: %v, 使用当前目录\n", err)
fmt.Printf("%s[WARN]%s 创建数据目录失败: %v, 使用当前目录\n", colorYellow, colorReset, err)
dataDir = "."
}
// 初始化数据库 (先于配置)
dbPath := filepath.Join(dataDir, "codex-pool.db")
if err := database.Init(dbPath); err != nil {
fmt.Printf("[错误] 数据库初始化失败: %v\n", err)
fmt.Printf("%s[ERROR]%s 数据库初始化失败: %v\n", "\033[31m", colorReset, err)
os.Exit(1)
}
@@ -46,25 +55,25 @@ func main() {
// 初始化邮箱服务
if len(cfg.MailServices) > 0 {
mail.Init(cfg.MailServices)
fmt.Printf("[邮箱] 已加载 %d 个邮箱服务\n", len(cfg.MailServices))
fmt.Printf("%s[邮箱]%s 已加载 %d 个邮箱服务\n", colorGreen, colorReset, len(cfg.MailServices))
}
fmt.Printf("[配置] 数据库: %s\n", dbPath)
fmt.Printf("[配置] 端口: %d\n", cfg.Port)
fmt.Printf("%s[配置]%s 数据库: %s\n", colorGray, colorReset, dbPath)
fmt.Printf("%s[配置]%s 端口: %d\n", colorGray, colorReset, cfg.Port)
if cfg.S2AApiBase != "" {
fmt.Printf("[配置] S2A API: %s\n", cfg.S2AApiBase)
fmt.Printf("%s[配置]%s S2A API: %s\n", colorGray, colorReset, cfg.S2AApiBase)
} else {
fmt.Println("[配置] S2A API: 未配置 (请在Web界面配置)")
fmt.Printf("%s[配置]%s S2A API: %s未配置%s (请在Web界面配置)\n", colorGray, colorReset, colorYellow, colorReset)
}
if cfg.ProxyEnabled {
fmt.Printf("[配置] 代理: %s (已启用)\n", cfg.DefaultProxy)
fmt.Printf("%s[配置]%s 代理: %s (已启用)\n", colorGray, colorReset, cfg.DefaultProxy)
} else {
fmt.Println("[配置] 代理: 已禁用")
fmt.Printf("%s[配置]%s 代理: 已禁用\n", colorGray, colorReset)
}
if web.IsEmbedded() {
fmt.Println("[前端] 嵌入模式")
fmt.Printf("%s[前端]%s 嵌入模式\n", colorGreen, colorReset)
} else {
fmt.Println("[前端] 开发模式 (未嵌入)")
fmt.Printf("%s[前端]%s 开发模式 (未嵌入)\n", colorYellow, colorReset)
}
fmt.Println()
@@ -85,6 +94,7 @@ func startServer(cfg *config.Config) {
// S2A 代理 API
mux.HandleFunc("/api/s2a/test", api.CORS(handleS2ATest))
mux.HandleFunc("/api/s2a/proxy/", api.CORS(handleS2AProxy)) // 通配代理
// 邮箱服务 API
mux.HandleFunc("/api/mail/services", api.CORS(handleMailServices))
@@ -123,16 +133,22 @@ func startServer(cfg *config.Config) {
}
addr := fmt.Sprintf(":%d", cfg.Port)
// ANSI 颜色代码
colorReset := "\033[0m"
colorGreen := "\033[32m"
colorCyan := "\033[36m"
// 显示访问地址
fmt.Println("[服务] 启动于:")
fmt.Printf(" - 本地: http://localhost:%d\n", cfg.Port)
fmt.Printf("%s[服务]%s 启动于:\n", colorGreen, colorReset)
fmt.Printf(" - 本地: %shttp://localhost:%d%s\n", colorCyan, cfg.Port, colorReset)
if ip := getOutboundIP(); ip != "" {
fmt.Printf(" - 外部: http://%s:%d\n", ip, cfg.Port)
fmt.Printf(" - 外部: %shttp://%s:%d%s\n", colorCyan, ip, cfg.Port, colorReset)
}
fmt.Println()
if err := http.ListenAndServe(addr, mux); err != nil {
fmt.Printf("[错误] 服务启动失败: %v\n", err)
fmt.Printf("\033[31m[ERROR]\033[0m 服务启动失败: %v\n", err)
os.Exit(1)
}
}
@@ -241,6 +257,59 @@ func handleS2ATest(w http.ResponseWriter, r *http.Request) {
})
}
// handleS2AProxy 代理 S2A API 请求
func handleS2AProxy(w http.ResponseWriter, r *http.Request) {
if config.Global == nil || config.Global.S2AApiBase == "" || config.Global.S2AAdminKey == "" {
api.Error(w, http.StatusBadRequest, "S2A 配置未设置")
return
}
// 提取路径: /api/s2a/proxy/xxx -> /api/v1/admin/xxx
path := strings.TrimPrefix(r.URL.Path, "/api/s2a/proxy")
targetURL := config.Global.S2AApiBase + "/api/v1/admin" + path
if r.URL.RawQuery != "" {
targetURL += "?" + r.URL.RawQuery
}
// 创建代理请求
proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body)
if err != nil {
api.Error(w, http.StatusInternalServerError, "创建请求失败")
return
}
// 复制请求头
for key, values := range r.Header {
for _, value := range values {
proxyReq.Header.Add(key, value)
}
}
// 设置认证头
proxyReq.Header.Set("Authorization", "Bearer "+config.Global.S2AAdminKey)
proxyReq.Header.Set("Content-Type", "application/json")
// 发送请求
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(proxyReq)
if err != nil {
api.Error(w, http.StatusBadGateway, fmt.Sprintf("请求 S2A 失败: %v", err))
return
}
defer resp.Body.Close()
// 复制响应头
for key, values := range resp.Header {
for _, value := range values {
w.Header().Add(key, value)
}
}
// 复制响应状态和内容
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}
func handleMailServices(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":