|
|
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/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() |
|
|
|
|
|
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") |
|
|
} |
|
|
|
|
|
|
|
|
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()) |
|
|
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>") |
|
|
} |
|
|
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>") |
|
|
} |
|
|
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() |
|
|
|
|
|
|
|
|
model.GetPricing() |
|
|
|
|
|
|
|
|
err = model.InitLogDB() |
|
|
if err != nil { |
|
|
return err |
|
|
} |
|
|
|
|
|
|
|
|
err = common.InitRedisClient() |
|
|
if err != nil { |
|
|
return err |
|
|
} |
|
|
return nil |
|
|
} |
|
|
|