tokenFactory/main.go

337 lines
9.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"bytes"
"embed"
"fmt"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/controller"
_ "github.com/QuantumNous/new-api/docs"
"github.com/QuantumNous/new-api/i18n"
"github.com/QuantumNous/new-api/logger"
"github.com/QuantumNous/new-api/middleware"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/oauth"
"github.com/QuantumNous/new-api/relay"
"github.com/QuantumNous/new-api/router"
"github.com/QuantumNous/new-api/service"
_ "github.com/QuantumNous/new-api/setting/performance_setting"
"github.com/QuantumNous/new-api/setting/ratio_setting"
"github.com/bytedance/gopkg/util/gopool"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
_ "net/http/pprof"
)
// @title TokenFactory API
// @version 1.0
// @description TokenFactory backend API documentation powered by swaggo.
// @BasePath /api
// @securityDefinitions.apikey ApiUserID
// @in header
// @name New-Api-User
// @description 必填。当前登录用户ID需与会话用户或 access_token 对应用户一致。
// @securityDefinitions.apikey CookieAuth
// @in header
// @name Cookie
// @description 可选。手动传浏览器会话 Cookie例如session=xxx; session_2=yyy。
//go:embed web/dist
var buildFS embed.FS
//go:embed web/dist/index.html
var indexPage []byte
func main() {
startTime := time.Now()
err := InitResources()
if err != nil {
common.FatalLog("failed to initialize resources: " + err.Error())
return
}
common.SysLog("TokenFactory " + common.Version + " started")
if os.Getenv("GIN_MODE") != "debug" {
gin.SetMode(gin.ReleaseMode)
}
if common.DebugEnabled {
common.SysLog("running in debug mode")
}
defer func() {
err := model.CloseDB()
if err != nil {
common.FatalLog("failed to close database: " + err.Error())
}
}()
if common.RedisEnabled {
// for compatibility with old versions
common.MemoryCacheEnabled = true
}
if common.MemoryCacheEnabled {
common.SysLog("memory cache enabled")
common.SysLog(fmt.Sprintf("sync frequency: %d seconds", common.SyncFrequency))
// Add panic recovery and retry for InitChannelCache
func() {
defer func() {
if r := recover(); r != nil {
common.SysLog(fmt.Sprintf("InitChannelCache panic: %v, retrying once", r))
// Retry once
_, _, fixErr := model.FixAbility()
if fixErr != nil {
common.FatalLog(fmt.Sprintf("InitChannelCache failed: %s", fixErr.Error()))
}
}
}()
model.InitChannelCache()
}()
go model.SyncChannelCache(common.SyncFrequency)
}
// 热更新配置
go model.SyncOptions(common.SyncFrequency)
// 数据看板
go model.UpdateQuotaData()
if os.Getenv("CHANNEL_UPDATE_FREQUENCY") != "" {
frequency, err := strconv.Atoi(os.Getenv("CHANNEL_UPDATE_FREQUENCY"))
if err != nil {
common.FatalLog("failed to parse CHANNEL_UPDATE_FREQUENCY: " + err.Error())
}
go controller.AutomaticallyUpdateChannels(frequency)
}
go controller.AutomaticallyTestChannels()
// Codex credential auto-refresh check every 10 minutes, refresh when expires within 1 day
service.StartCodexCredentialAutoRefreshTask()
// Subscription quota reset task (daily/weekly/monthly/custom)
service.StartSubscriptionQuotaResetTask()
// Wire task polling adaptor factory (breaks service -> relay import cycle)
service.GetTaskAdaptorFunc = func(platform constant.TaskPlatform) service.TaskPollingAdaptor {
a := relay.GetTaskAdaptor(platform)
if a == nil {
return nil
}
return a
}
// Channel upstream model update check task
controller.StartChannelUpstreamModelUpdateTask()
if common.IsMasterNode && constant.UpdateTask {
gopool.Go(func() {
controller.UpdateMidjourneyTaskBulk()
})
gopool.Go(func() {
controller.UpdateTaskBulk()
})
}
if os.Getenv("BATCH_UPDATE_ENABLED") == "true" {
common.BatchUpdateEnabled = true
common.SysLog("batch update enabled with interval " + strconv.Itoa(common.BatchUpdateInterval) + "s")
model.InitBatchUpdater()
}
if os.Getenv("ENABLE_PPROF") == "true" {
gopool.Go(func() {
log.Println(http.ListenAndServe("0.0.0.0:8005", nil))
})
go common.Monitor()
common.SysLog("pprof enabled")
}
err = common.StartPyroScope()
if err != nil {
common.SysError(fmt.Sprintf("start pyroscope error : %v", err))
}
// Initialize HTTP server
server := gin.New()
server.Use(gin.CustomRecovery(func(c *gin.Context, err any) {
common.SysLog(fmt.Sprintf("panic detected: %v", err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"message": fmt.Sprintf("Panic detected, error: %v. Please submit a issue here: https://github.com/Calcium-Ion/new-api", err),
"type": "token_factory_panic",
},
})
}))
// This will cause SSE not to work!!!
//server.Use(gzip.Gzip(gzip.DefaultCompression))
server.Use(middleware.RequestId())
server.Use(middleware.PoweredBy())
server.Use(middleware.I18n())
middleware.SetUpLogger(server)
// Initialize session store
store := cookie.NewStore([]byte(common.SessionSecret))
store.Options(sessions.Options{
Path: "/",
MaxAge: 2592000, // 30 days
HttpOnly: true,
Secure: false,
SameSite: http.SameSiteStrictMode,
})
server.Use(sessions.Sessions("session", store))
InjectUmamiAnalytics()
InjectGoogleAnalytics()
// 注册 Swagger 文档路由。
server.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// 设置路由
router.SetRouter(server, buildFS, indexPage)
var port = os.Getenv("PORT")
if port == "" {
port = strconv.Itoa(*common.Port)
}
// Log startup success message
common.LogStartupSuccess(startTime, port)
err = server.Run(":" + port)
if err != nil {
common.FatalLog("failed to start HTTP server: " + err.Error())
}
}
func InjectUmamiAnalytics() {
analyticsInjectBuilder := &strings.Builder{}
if os.Getenv("UMAMI_WEBSITE_ID") != "" {
umamiSiteID := os.Getenv("UMAMI_WEBSITE_ID")
umamiScriptURL := os.Getenv("UMAMI_SCRIPT_URL")
if umamiScriptURL == "" {
umamiScriptURL = "https://analytics.umami.is/script.js"
}
analyticsInjectBuilder.WriteString("<script defer src=\"")
analyticsInjectBuilder.WriteString(umamiScriptURL)
analyticsInjectBuilder.WriteString("\" data-website-id=\"")
analyticsInjectBuilder.WriteString(umamiSiteID)
analyticsInjectBuilder.WriteString("\"></script>")
}
analyticsInjectBuilder.WriteString("<!--Umami QuantumNous-->\n")
analyticsInject := analyticsInjectBuilder.String()
indexPage = bytes.ReplaceAll(indexPage, []byte("<!--umami-->\n"), []byte(analyticsInject))
}
func InjectGoogleAnalytics() {
analyticsInjectBuilder := &strings.Builder{}
if os.Getenv("GOOGLE_ANALYTICS_ID") != "" {
gaID := os.Getenv("GOOGLE_ANALYTICS_ID")
// Google Analytics 4 (gtag.js)
analyticsInjectBuilder.WriteString("<script async src=\"https://www.googletagmanager.com/gtag/js?id=")
analyticsInjectBuilder.WriteString(gaID)
analyticsInjectBuilder.WriteString("\"></script>")
analyticsInjectBuilder.WriteString("<script>")
analyticsInjectBuilder.WriteString("window.dataLayer = window.dataLayer || [];")
analyticsInjectBuilder.WriteString("function gtag(){dataLayer.push(arguments);}")
analyticsInjectBuilder.WriteString("gtag('js', new Date());")
analyticsInjectBuilder.WriteString("gtag('config', '")
analyticsInjectBuilder.WriteString(gaID)
analyticsInjectBuilder.WriteString("');")
analyticsInjectBuilder.WriteString("</script>")
}
analyticsInjectBuilder.WriteString("<!--Google Analytics QuantumNous-->\n")
analyticsInject := analyticsInjectBuilder.String()
indexPage = bytes.ReplaceAll(indexPage, []byte("<!--Google Analytics-->\n"), []byte(analyticsInject))
}
func InitResources() error {
// Initialize resources here if needed
// This is a placeholder function for future resource initialization
err := godotenv.Load(".env")
if err != nil {
if common.DebugEnabled {
common.SysLog("No .env file found, using default environment variables. If needed, please create a .env file and set the relevant variables.")
}
}
// 加载环境变量
common.InitEnv()
logger.SetupLogger()
// Initialize model settings
ratio_setting.InitRatioSettings()
service.InitHttpClient()
service.InitOssHttpClient()
service.InitTokenEncoders()
// Initialize SQL Database
err = model.InitDB()
if err != nil {
common.FatalLog("failed to initialize database: " + err.Error())
return err
}
model.CheckSetup()
// Initialize options, should after model.InitDB()
model.InitOptionMap()
// 清理旧的磁盘缓存文件
common.CleanupOldCacheFiles()
// 初始化模型
model.GetPricing()
// Initialize SQL Database
err = model.InitLogDB()
if err != nil {
return err
}
// Initialize Redis
err = common.InitRedisClient()
if err != nil {
return err
}
// 启动系统监控
common.StartSystemMonitor()
// Initialize i18n
err = i18n.Init()
if err != nil {
common.SysError("failed to initialize i18n: " + err.Error())
// Don't return error, i18n is not critical
} else {
common.SysLog("i18n initialized with languages: " + strings.Join(i18n.SupportedLanguages(), ", "))
}
// Register user language loader for lazy loading
i18n.SetUserLangLoader(model.GetUserLanguage)
// Load custom OAuth providers from database
err = oauth.LoadCustomProviders()
if err != nil {
common.SysError("failed to load custom OAuth providers: " + err.Error())
// Don't return error, custom OAuth is not critical
}
return nil
}