- 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)
207 lines
6.0 KiB
Go
207 lines
6.0 KiB
Go
package controller
|
||
|
||
import (
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"os"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/songquanpeng/one-api/common"
|
||
"github.com/songquanpeng/one-api/common/config"
|
||
)
|
||
|
||
func DownloadServerConfig(c *gin.Context) {
|
||
configs := collectServerConfigs()
|
||
var lines []string
|
||
|
||
lines = append(lines, "# One API Server Configuration")
|
||
lines = append(lines, "# Generated at runtime - download from settings page")
|
||
lines = append(lines, "")
|
||
|
||
// Group by category for readability
|
||
categories := []string{"database", "redis", "performance", "network", "ratelimit", "metrics", "other"}
|
||
categoryNames := map[string]string{
|
||
"database": "# ===== Database =====\n",
|
||
"redis": "\n# ===== Redis =====\n",
|
||
"performance": "\n# ===== Performance =====\n",
|
||
"network": "\n# ===== Network =====\n",
|
||
"ratelimit": "\n# ===== Rate Limit =====\n",
|
||
"metrics": "\n# ===== Metrics =====\n",
|
||
"other": "\n# ===== Other =====\n",
|
||
}
|
||
|
||
for _, cat := range categories {
|
||
lines = append(lines, categoryNames[cat])
|
||
for _, item := range configs {
|
||
if item.Category != cat {
|
||
continue
|
||
}
|
||
// Skip the virtual db_type key
|
||
if item.Key == "db_type" {
|
||
continue
|
||
}
|
||
// Get the actual value from env/runtime for the download
|
||
actualValue := getActualConfigValue(item.Key)
|
||
if item.Key == "SESSION_SECRET" || item.Key == "REDIS_PASSWORD" ||
|
||
strings.HasSuffix(item.Key, "TOKEN") || strings.HasSuffix(item.Key, "TOKEN") ||
|
||
strings.HasSuffix(item.Key, "SECRET") || strings.HasSuffix(item.Key, "Secret") ||
|
||
item.Key == "SQL_DSN" || item.Key == "LOG_SQL_DSN" ||
|
||
item.Key == "REDIS_CONN_STRING" || item.Key == "HTTPS_PROXY" {
|
||
// Include masked values for secrets
|
||
lines = append(lines, fmt.Sprintf("# %s=%s", item.Key, item.Value))
|
||
lines = append(lines, fmt.Sprintf("# %s (masked above, set manually if needed)", item.Key))
|
||
} else {
|
||
lines = append(lines, fmt.Sprintf("%s=%s", item.Key, actualValue))
|
||
}
|
||
}
|
||
}
|
||
|
||
content := strings.Join(lines, "\n")
|
||
c.Header("Content-Disposition", "attachment; filename=server-config.env")
|
||
c.Header("Content-Type", "text/plain; charset=utf-8")
|
||
c.String(http.StatusOK, content)
|
||
}
|
||
|
||
func ImportServerConfig(c *gin.Context) {
|
||
file, _, err := c.Request.FormFile("file")
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"success": false,
|
||
"message": "请上传 .env 配置文件",
|
||
})
|
||
return
|
||
}
|
||
defer file.Close()
|
||
|
||
content, err := io.ReadAll(file)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"success": false,
|
||
"message": "读取文件失败:" + err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
// Validate basic .env format
|
||
lines := strings.Split(string(content), "\n")
|
||
validLines := 0
|
||
for _, line := range lines {
|
||
line = strings.TrimSpace(line)
|
||
if line == "" || strings.HasPrefix(line, "#") {
|
||
continue
|
||
}
|
||
if strings.Contains(line, "=") {
|
||
validLines++
|
||
}
|
||
}
|
||
|
||
if validLines == 0 {
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"success": false,
|
||
"message": "无效的 .env 文件格式,未找到有效的配置项",
|
||
})
|
||
return
|
||
}
|
||
|
||
// Determine the .env file path
|
||
envPath := ".env"
|
||
if p := os.Getenv("SERVER_CONFIG_PATH"); p != "" {
|
||
envPath = p
|
||
}
|
||
|
||
err = os.WriteFile(envPath, content, 0644)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"success": false,
|
||
"message": "写入配置文件失败:" + err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"message": fmt.Sprintf("配置文件已保存到 %s,请重启服务以生效。", envPath),
|
||
})
|
||
}
|
||
|
||
func getActualConfigValue(key string) string {
|
||
switch key {
|
||
case "PORT":
|
||
return strconv.Itoa(*common.Port)
|
||
case "SESSION_SECRET":
|
||
return config.SessionSecret
|
||
case "SQL_DSN", "LOG_SQL_DSN", "REDIS_CONN_STRING", "REDIS_PASSWORD",
|
||
"REDIS_MASTER_NAME", "NODE_TYPE", "POLLING_INTERVAL",
|
||
"GIN_MODE", "FRONTEND_BASE_URL", "CHANNEL_TEST_FREQUENCY",
|
||
"INITIAL_ROOT_TOKEN", "INITIAL_ROOT_ACCESS_TOKEN", "HTTPS_PROXY":
|
||
return os.Getenv(key)
|
||
case "SQLITE_PATH":
|
||
if os.Getenv("SQLITE_PATH") != "" {
|
||
return os.Getenv("SQLITE_PATH")
|
||
}
|
||
return common.SQLitePath
|
||
case "SQLITE_BUSY_TIMEOUT":
|
||
return strconv.Itoa(common.SQLiteBusyTimeout)
|
||
case "SQL_MAX_IDLE_CONNS":
|
||
if v := os.Getenv("SQL_MAX_IDLE_CONNS"); v != "" {
|
||
return v
|
||
}
|
||
return "100"
|
||
case "SQL_MAX_OPEN_CONNS":
|
||
if v := os.Getenv("SQL_MAX_OPEN_CONNS"); v != "" {
|
||
return v
|
||
}
|
||
return "1000"
|
||
case "SQL_MAX_LIFETIME":
|
||
if v := os.Getenv("SQL_MAX_LIFETIME"); v != "" {
|
||
return v
|
||
}
|
||
return "60"
|
||
case "SYNC_FREQUENCY":
|
||
return strconv.Itoa(config.SyncFrequency)
|
||
case "DEBUG":
|
||
return strconv.FormatBool(config.DebugEnabled)
|
||
case "DEBUG_SQL":
|
||
return strconv.FormatBool(config.DebugSQLEnabled)
|
||
case "MEMORY_CACHE_ENABLED":
|
||
return strconv.FormatBool(config.MemoryCacheEnabled)
|
||
case "BATCH_UPDATE_ENABLED":
|
||
return strconv.FormatBool(config.BatchUpdateEnabled)
|
||
case "BATCH_UPDATE_INTERVAL":
|
||
return strconv.Itoa(config.BatchUpdateInterval)
|
||
case "RELAY_TIMEOUT":
|
||
return strconv.Itoa(config.RelayTimeout)
|
||
case "RELAY_PROXY":
|
||
return config.RelayProxy
|
||
case "USER_CONTENT_REQUEST_PROXY":
|
||
return config.UserContentRequestProxy
|
||
case "USER_CONTENT_REQUEST_TIMEOUT":
|
||
return strconv.Itoa(config.UserContentRequestTimeout)
|
||
case "GEMINI_SAFETY_SETTING":
|
||
return config.GeminiSafetySetting
|
||
case "GEMINI_VERSION":
|
||
return config.GeminiVersion
|
||
case "THEME":
|
||
return config.Theme
|
||
case "GLOBAL_API_RATE_LIMIT":
|
||
return strconv.Itoa(config.GlobalApiRateLimitNum)
|
||
case "GLOBAL_WEB_RATE_LIMIT":
|
||
return strconv.Itoa(config.GlobalWebRateLimitNum)
|
||
case "ENABLE_METRIC":
|
||
return strconv.FormatBool(config.EnableMetric)
|
||
case "METRIC_QUEUE_SIZE":
|
||
return strconv.Itoa(config.MetricQueueSize)
|
||
case "METRIC_SUCCESS_RATE_THRESHOLD":
|
||
return strconv.FormatFloat(config.MetricSuccessRateThreshold, 'f', 2, 64)
|
||
case "ONLY_ONE_LOG_FILE":
|
||
return strconv.FormatBool(config.OnlyOneLogFile)
|
||
case "ENFORCE_INCLUDE_USAGE":
|
||
return strconv.FormatBool(config.EnforceIncludeUsage)
|
||
case "TEST_PROMPT":
|
||
return config.TestPrompt
|
||
}
|
||
return ""
|
||
} |