mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 10:34:20 +00:00
The i18n middleware runs before UserAuth, so user settings weren't available when language was detected. Now GetLangFromContext checks user settings first (set by UserAuth) before falling back to the language set by middleware or Accept-Language header.
296 lines
8.1 KiB
Go
296 lines
8.1 KiB
Go
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/i18n"
|
|
"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"
|
|
)
|
|
|
|
//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("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 {
|
|
// 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()
|
|
|
|
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": "new_api_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()
|
|
|
|
// 设置路由
|
|
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.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)
|
|
|
|
return nil
|
|
}
|