redline-api/controller/server_config.go
wangdong 75079c05d3 Initial commit: One API with custom Server Configuration UI
- Added Server Configuration settings page (backend + frontend)
- Added .env config download/import API
- Runtime config updates via UI
- Support for all 3 themes (default, air, berry)
- i18n support (zh/en)
2026-07-28 01:25:29 +08:00

249 lines
12 KiB
Go
Raw 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 controller
import (
"net/http"
"os"
"strconv"
"github.com/gin-gonic/gin"
"github.com/songquanpeng/one-api/common"
"github.com/songquanpeng/one-api/common/config"
)
type ServerConfigItem struct {
Key string `json:"key"`
Value string `json:"value"`
DefaultValue string `json:"default_value"`
RestartRequired bool `json:"restart_required"`
Description string `json:"description"`
Category string `json:"category"`
}
func GetServerConfigs(c *gin.Context) {
configs := collectServerConfigs()
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": configs,
})
}
func collectServerConfigs() []ServerConfigItem {
// Determine database type
dbType := "SQLite"
if common.UsingMySQL {
dbType = "MySQL"
} else if common.UsingPostgreSQL {
dbType = "PostgreSQL"
}
items := []ServerConfigItem{
// ===== Database =====
{Key: "db_type", Value: dbType, DefaultValue: "SQLite", RestartRequired: true, Description: "当前数据库类型(自动检测)", Category: "database"},
{Key: "SQL_DSN", Value: maskSecret(os.Getenv("SQL_DSN")), DefaultValue: "", RestartRequired: true, Description: "数据库连接字符串(留空=SQLite", Category: "database"},
{Key: "LOG_SQL_DSN", Value: maskSecret(os.Getenv("LOG_SQL_DSN")), DefaultValue: "", RestartRequired: true, Description: "日志数据库连接字符串(留空=使用主库)", Category: "database"},
{Key: "SQLITE_PATH", Value: os.Getenv("SQLITE_PATH"), DefaultValue: "one-api.db", RestartRequired: true, Description: "SQLite 数据库文件路径", Category: "database"},
{Key: "SQLITE_BUSY_TIMEOUT", Value: strconv.Itoa(common.SQLiteBusyTimeout), DefaultValue: "3000", RestartRequired: true, Description: "SQLite 忙等待超时(毫秒)", Category: "database"},
{Key: "SQL_MAX_IDLE_CONNS", Value: "", DefaultValue: "100", RestartRequired: true, Description: "数据库最大空闲连接数", Category: "database"},
{Key: "SQL_MAX_OPEN_CONNS", Value: "", DefaultValue: "1000", RestartRequired: true, Description: "数据库最大打开连接数", Category: "database"},
{Key: "SQL_MAX_LIFETIME", Value: "", DefaultValue: "60", RestartRequired: true, Description: "数据库连接最长存活时间(秒)", Category: "database"},
// ===== Redis =====
{Key: "REDIS_CONN_STRING", Value: maskSecret(os.Getenv("REDIS_CONN_STRING")), DefaultValue: "", RestartRequired: true, Description: "Redis 连接字符串(留空=不启用)", Category: "redis"},
{Key: "REDIS_PASSWORD", Value: maskSecret(os.Getenv("REDIS_PASSWORD")), DefaultValue: "", RestartRequired: true, Description: "Redis 密码", Category: "redis"},
{Key: "REDIS_MASTER_NAME", Value: os.Getenv("REDIS_MASTER_NAME"), DefaultValue: "", RestartRequired: true, Description: "Redis 主节点名称(集群模式)", Category: "redis"},
{Key: "SYNC_FREQUENCY", Value: strconv.Itoa(config.SyncFrequency), DefaultValue: "600", RestartRequired: true, Description: "缓存同步频率(秒)", Category: "redis"},
// ===== Performance =====
{Key: "DEBUG", Value: strconv.FormatBool(config.DebugEnabled), DefaultValue: "false", RestartRequired: false, Description: "启用调试模式", Category: "performance"},
{Key: "DEBUG_SQL", Value: strconv.FormatBool(config.DebugSQLEnabled), DefaultValue: "false", RestartRequired: false, Description: "启用 SQL 调试日志", Category: "performance"},
{Key: "MEMORY_CACHE_ENABLED", Value: strconv.FormatBool(config.MemoryCacheEnabled), DefaultValue: "false", RestartRequired: false, Description: "启用内存缓存", Category: "performance"},
{Key: "BATCH_UPDATE_ENABLED", Value: strconv.FormatBool(config.BatchUpdateEnabled), DefaultValue: "false", RestartRequired: false, Description: "启用批量更新", Category: "performance"},
{Key: "BATCH_UPDATE_INTERVAL", Value: strconv.Itoa(config.BatchUpdateInterval), DefaultValue: "5", RestartRequired: false, Description: "批量更新间隔(秒)", Category: "performance"},
{Key: "GIN_MODE", Value: os.Getenv("GIN_MODE"), DefaultValue: "release", RestartRequired: true, Description: "Gin 运行模式debug/release/test", Category: "performance"},
// ===== Network =====
{Key: "RELAY_TIMEOUT", Value: strconv.Itoa(config.RelayTimeout), DefaultValue: "0", RestartRequired: false, Description: "中转超时时间0=不超时)", Category: "network"},
{Key: "RELAY_PROXY", Value: config.RelayProxy, DefaultValue: "", RestartRequired: false, Description: "中转代理地址", Category: "network"},
{Key: "USER_CONTENT_REQUEST_PROXY", Value: config.UserContentRequestProxy, DefaultValue: "", RestartRequired: false, Description: "用户内容请求代理地址", Category: "network"},
{Key: "USER_CONTENT_REQUEST_TIMEOUT", Value: strconv.Itoa(config.UserContentRequestTimeout), DefaultValue: "30", RestartRequired: false, Description: "用户内容请求超时(秒)", Category: "network"},
{Key: "HTTPS_PROXY", Value: maskSecret(os.Getenv("HTTPS_PROXY")), DefaultValue: "", RestartRequired: false, Description: "HTTPS 代理地址", Category: "network"},
// ===== Rate Limit =====
{Key: "GLOBAL_API_RATE_LIMIT", Value: strconv.Itoa(config.GlobalApiRateLimitNum), DefaultValue: "480", RestartRequired: false, Description: "全局 API 速率限制(次/3分钟", Category: "ratelimit"},
{Key: "GLOBAL_WEB_RATE_LIMIT", Value: strconv.Itoa(config.GlobalWebRateLimitNum), DefaultValue: "240", RestartRequired: false, Description: "全局 Web 速率限制(次/3分钟", Category: "ratelimit"},
// ===== Metrics =====
{Key: "ENABLE_METRIC", Value: strconv.FormatBool(config.EnableMetric), DefaultValue: "false", RestartRequired: false, Description: "启用指标监控", Category: "metrics"},
{Key: "METRIC_QUEUE_SIZE", Value: strconv.Itoa(config.MetricQueueSize), DefaultValue: "10", RestartRequired: false, Description: "指标队列大小", Category: "metrics"},
{Key: "METRIC_SUCCESS_RATE_THRESHOLD", Value: strconv.FormatFloat(config.MetricSuccessRateThreshold, 'f', 2, 64), DefaultValue: "0.8", RestartRequired: false, Description: "成功率阈值", Category: "metrics"},
{Key: "CHANNEL_TEST_FREQUENCY", Value: os.Getenv("CHANNEL_TEST_FREQUENCY"), DefaultValue: "", RestartRequired: true, Description: "渠道自动测试频率(秒,留空=不自动测试)", Category: "metrics"},
// ===== Other =====
{Key: "PORT", Value: strconv.Itoa(*common.Port), DefaultValue: "3000", RestartRequired: true, Description: "服务监听端口", Category: "other"},
{Key: "SESSION_SECRET", Value: maskSecret(config.SessionSecret), DefaultValue: "自动生成 UUID", RestartRequired: true, Description: "Session 密钥", Category: "other"},
{Key: "NODE_TYPE", Value: os.Getenv("NODE_TYPE"), DefaultValue: "master", RestartRequired: true, Description: "节点类型master/slave", Category: "other"},
{Key: "POLLING_INTERVAL", Value: os.Getenv("POLLING_INTERVAL"), DefaultValue: "", RestartRequired: true, Description: "从节点轮询间隔(秒)", Category: "other"},
{Key: "GEMINI_SAFETY_SETTING", Value: config.GeminiSafetySetting, DefaultValue: "BLOCK_NONE", RestartRequired: false, Description: "Gemini 安全设置", Category: "other"},
{Key: "GEMINI_VERSION", Value: config.GeminiVersion, DefaultValue: "v1", RestartRequired: false, Description: "Gemini API 版本", Category: "other"},
{Key: "THEME", Value: config.Theme, DefaultValue: "default", RestartRequired: false, Description: "前端主题名称", Category: "other"},
{Key: "FRONTEND_BASE_URL", Value: os.Getenv("FRONTEND_BASE_URL"), DefaultValue: "", RestartRequired: true, Description: "前端基础 URL从节点使用", Category: "other"},
{Key: "ONLY_ONE_LOG_FILE", Value: strconv.FormatBool(config.OnlyOneLogFile), DefaultValue: "false", RestartRequired: false, Description: "仅使用一个日志文件", Category: "other"},
{Key: "ENFORCE_INCLUDE_USAGE", Value: strconv.FormatBool(config.EnforceIncludeUsage), DefaultValue: "false", RestartRequired: false, Description: "强制返回 usage 信息", Category: "other"},
{Key: "TEST_PROMPT", Value: config.TestPrompt, DefaultValue: "Output only your specific model name with no additional text.", RestartRequired: false, Description: "渠道测试提示词", Category: "other"},
}
// Fill in SQL_MAX_* values from env
if v := os.Getenv("SQL_MAX_IDLE_CONNS"); v != "" {
items[7].Value = v
}
if v := os.Getenv("SQL_MAX_OPEN_CONNS"); v != "" {
items[8].Value = v
}
if v := os.Getenv("SQL_MAX_LIFETIME"); v != "" {
items[9].Value = v
}
return items
}
func UpdateServerConfig(c *gin.Context) {
var req struct {
Key string `json:"key"`
Value string `json:"value"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "无效的参数",
})
return
}
// Check if this is a restart-required setting
restartRequiredKeys := map[string]bool{
"SQL_DSN": true, "LOG_SQL_DSN": true, "SQLITE_PATH": true,
"SQLITE_BUSY_TIMEOUT": true, "SQL_MAX_IDLE_CONNS": true,
"SQL_MAX_OPEN_CONNS": true, "SQL_MAX_LIFETIME": true,
"REDIS_CONN_STRING": true, "REDIS_PASSWORD": true,
"REDIS_MASTER_NAME": true, "SYNC_FREQUENCY": true,
"PORT": true, "SESSION_SECRET": true, "NODE_TYPE": true,
"POLLING_INTERVAL": true, "GIN_MODE": true,
"FRONTEND_BASE_URL": true, "CHANNEL_TEST_FREQUENCY": true,
"INITIAL_ROOT_TOKEN": true, "INITIAL_ROOT_ACCESS_TOKEN": true,
}
if restartRequiredKeys[req.Key] {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "该设置需要重启服务才能生效,请使用下载 .env 文件功能修改后重启,或直接在 .env 文件中修改。",
})
return
}
// Update runtime settings
err := updateRuntimeConfig(req.Key, req.Value)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "设置已更新",
})
}
func updateRuntimeConfig(key, value string) error {
switch key {
case "DEBUG":
config.DebugEnabled = value == "true"
case "DEBUG_SQL":
config.DebugSQLEnabled = value == "true"
case "MEMORY_CACHE_ENABLED":
config.MemoryCacheEnabled = value == "true"
case "BATCH_UPDATE_ENABLED":
config.BatchUpdateEnabled = value == "true"
case "BATCH_UPDATE_INTERVAL":
v, err := strconv.Atoi(value)
if err != nil {
return err
}
config.BatchUpdateInterval = v
case "RELAY_TIMEOUT":
v, err := strconv.Atoi(value)
if err != nil {
return err
}
config.RelayTimeout = v
case "RELAY_PROXY":
config.RelayProxy = value
case "USER_CONTENT_REQUEST_PROXY":
config.UserContentRequestProxy = value
case "USER_CONTENT_REQUEST_TIMEOUT":
v, err := strconv.Atoi(value)
if err != nil {
return err
}
config.UserContentRequestTimeout = v
case "HTTPS_PROXY":
os.Setenv("HTTPS_PROXY", value)
case "GEMINI_SAFETY_SETTING":
config.GeminiSafetySetting = value
case "GEMINI_VERSION":
config.GeminiVersion = value
case "THEME":
if !config.ValidThemes[value] {
return nil // silently ignore invalid themes
}
config.Theme = value
case "GLOBAL_API_RATE_LIMIT":
v, err := strconv.Atoi(value)
if err != nil {
return err
}
config.GlobalApiRateLimitNum = v
case "GLOBAL_WEB_RATE_LIMIT":
v, err := strconv.Atoi(value)
if err != nil {
return err
}
config.GlobalWebRateLimitNum = v
case "ENABLE_METRIC":
config.EnableMetric = value == "true"
case "METRIC_QUEUE_SIZE":
v, err := strconv.Atoi(value)
if err != nil {
return err
}
config.MetricQueueSize = v
case "METRIC_SUCCESS_RATE_THRESHOLD":
v, err := strconv.ParseFloat(value, 64)
if err != nil {
return err
}
config.MetricSuccessRateThreshold = v
case "ONLY_ONE_LOG_FILE":
config.OnlyOneLogFile = value == "true"
case "ENFORCE_INCLUDE_USAGE":
config.EnforceIncludeUsage = value == "true"
case "TEST_PROMPT":
config.TestPrompt = value
default:
return nil // unknown key, ignore silently
}
return nil
}
func maskSecret(s string) string {
if s == "" {
return ""
}
if len(s) <= 4 {
return "****"
}
return s[:2] + "****" + s[len(s)-2:]
}