- 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)
249 lines
12 KiB
Go
249 lines
12 KiB
Go
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:]
|
||
} |