Add per-channel quota limits with reset scheduling and runtime checks so channel capacity can be capped and restored on configured periods.
337 lines
9.4 KiB
Go
337 lines
9.4 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/oauth"
|
|
perfmetrics "github.com/QuantumNous/new-api/pkg/perf_metrics"
|
|
"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"
|
|
|
|
_ "net/http/pprof"
|
|
)
|
|
|
|
//go:embed web/default/dist
|
|
var buildFS embed.FS
|
|
|
|
//go:embed web/default/dist/index.html
|
|
var indexPage []byte
|
|
|
|
//go:embed web/classic/dist
|
|
var classicBuildFS embed.FS
|
|
|
|
//go:embed web/classic/dist/index.html
|
|
var classicIndexPage []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()
|
|
// Channel quota reset task (daily/weekly/monthly/custom)
|
|
service.StartChannelQuotaResetTask()
|
|
|
|
// 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": "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, router.ThemeAssets{
|
|
DefaultBuildFS: buildFS,
|
|
DefaultIndexPage: indexPage,
|
|
ClassicBuildFS: classicBuildFS,
|
|
ClassicIndexPage: classicIndexPage,
|
|
})
|
|
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 := []byte(analyticsInjectBuilder.String())
|
|
placeholder := []byte("<!--umami-->\n")
|
|
indexPage = bytes.ReplaceAll(indexPage, placeholder, analyticsInject)
|
|
classicIndexPage = bytes.ReplaceAll(classicIndexPage, placeholder, 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 := []byte(analyticsInjectBuilder.String())
|
|
placeholder := []byte("<!--Google Analytics-->\n")
|
|
indexPage = bytes.ReplaceAll(indexPage, placeholder, analyticsInject)
|
|
classicIndexPage = bytes.ReplaceAll(classicIndexPage, placeholder, 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
|
|
}
|
|
|
|
perfmetrics.Init()
|
|
|
|
// 启动系统监控
|
|
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
|
|
}
|