| 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/logger"
|
| "github.com/QuantumNous/new-api/middleware"
|
| "github.com/QuantumNous/new-api/model"
|
| "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"
|
|
|
| _ "net/http/pprof"
|
| )
|
|
|
|
|
| var buildFS embed.FS
|
|
|
|
|
| var indexPage []byte
|
|
|
| func main() {
|
| startTime := time.Now()
|
|
|
| err := InitResources()
|
| if err != nil {
|
| common.FatalLog("failed to initialize resources: " + err.Error())
|
| return
|
| }
|
|
|
| common.SysLog("New API " + 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 {
|
|
|
| common.MemoryCacheEnabled = true
|
| }
|
| if common.MemoryCacheEnabled {
|
| common.SysLog("memory cache enabled")
|
| common.SysLog(fmt.Sprintf("sync frequency: %d seconds", common.SyncFrequency))
|
|
|
|
|
| func() {
|
| defer func() {
|
| if r := recover(); r != nil {
|
| common.SysLog(fmt.Sprintf("InitChannelCache panic: %v, retrying once", r))
|
|
|
| _, _, 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()
|
|
|
|
|
| service.StartCodexCredentialAutoRefreshTask()
|
|
|
| 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))
|
| }
|
|
|
|
|
| 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": "new_api_panic",
|
| },
|
| })
|
| }))
|
|
|
|
|
| server.Use(middleware.RequestId())
|
| server.Use(middleware.PoweredBy())
|
| middleware.SetUpLogger(server)
|
|
|
| store := cookie.NewStore([]byte(common.SessionSecret))
|
| store.Options(sessions.Options{
|
| Path: "/",
|
| MaxAge: 2592000,
|
| HttpOnly: true,
|
| Secure: false,
|
| SameSite: http.SameSiteStrictMode,
|
| })
|
| server.Use(sessions.Sessions("session", store))
|
|
|
| InjectUmamiAnalytics()
|
| InjectGoogleAnalytics()
|
|
|
|
|
| router.SetRouter(server, buildFS, indexPage)
|
| var port = os.Getenv("PORT")
|
| if port == "" {
|
| port = strconv.Itoa(*common.Port)
|
| }
|
|
|
|
|
| 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")
|
|
|
| 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 {
|
|
|
|
|
| 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()
|
|
|
|
|
| ratio_setting.InitRatioSettings()
|
|
|
| service.InitHttpClient()
|
|
|
| service.InitTokenEncoders()
|
|
|
|
|
| err = model.InitDB()
|
| if err != nil {
|
| common.FatalLog("failed to initialize database: " + err.Error())
|
| return err
|
| }
|
|
|
| model.CheckSetup()
|
|
|
|
|
| model.InitOptionMap()
|
|
|
|
|
| common.CleanupOldCacheFiles()
|
|
|
|
|
| model.GetPricing()
|
|
|
|
|
| err = model.InitLogDB()
|
| if err != nil {
|
| return err
|
| }
|
|
|
|
|
| err = common.InitRedisClient()
|
| if err != nil {
|
| return err
|
| }
|
| return nil
|
| }
|
|
|