- ratelimit(内存计数): 密钥级每日请求数/Token 配额、用户级每秒速率 (OT_RATELIMIT_USER_RPS), 超限返回 429 - 网关 Auth 前置配额/限流检查, finishUsage 累计密钥 token 用量 - 前端 Skeleton 组件 + Dashboard/管理总览加载态 - Go 服务托管 web/dist 静态资源(SPA 回退), 单端口即可访问前后端 Co-Authored-By: Claude <noreply@anthropic.com>
166 lines
4.9 KiB
Go
166 lines
4.9 KiB
Go
// Package config 加载服务配置:.env / 环境变量 / 默认值(viper)。
|
|
// 所有项均可用环境变量 OT_<KEY> 覆盖(点号转下划线,如 db.driver → OT_DB_DRIVER)。
|
|
package config
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
type Config struct {
|
|
Env string // development | production
|
|
Port int
|
|
DB DBConfig
|
|
JWT JWTConfig
|
|
Auth AuthConfig
|
|
Proxy ProxyConfig
|
|
RateLimit RateLimitConfig
|
|
Master string // 渠道密钥 AES-GCM 主密钥(来自环境变量)
|
|
}
|
|
|
|
// RateLimitConfig 限流参数(MVP 内存计数,Redis 后置)。
|
|
type RateLimitConfig struct {
|
|
UserRPS int // 用户级每秒请求数上限(0=不限制)
|
|
}
|
|
|
|
type DBConfig struct {
|
|
Driver string // sqlite | postgres
|
|
DSN string
|
|
}
|
|
|
|
type JWTConfig struct {
|
|
Secret string
|
|
AccessTTL time.Duration
|
|
RefreshTTL time.Duration
|
|
Issuer string
|
|
CookieName string
|
|
CookieSecure bool
|
|
CookieDomain string
|
|
}
|
|
|
|
type AuthConfig struct {
|
|
RegistrationMode string // open | invite
|
|
Argon2Time uint32
|
|
Argon2Memory uint32
|
|
Argon2Threads uint8
|
|
Argon2KeyLen uint32
|
|
SaltLen int
|
|
}
|
|
|
|
type ProxyConfig struct {
|
|
DefaultChannelName string // 首次启动自动创建的渠道名(如 openai)
|
|
UpstreamBaseURL string // 渠道 base_url 默认值
|
|
UpstreamKey string // 渠道上游 key 默认值
|
|
DefaultModel string // 渠道模型导入时使用的模型名
|
|
Timeout time.Duration
|
|
HealthInterval time.Duration // 渠道健康检查周期
|
|
HealthFailThreshold int // 连续失败 N 次进 cooldown
|
|
}
|
|
|
|
// loadDotEnv 读取 .env 并把 KEY=VALUE 注入环境变量(AutomaticEnv 自动映射 OT_ 前缀)。
|
|
// 已存在的环境变量优先,不覆盖。
|
|
func loadDotEnv() {
|
|
data, err := os.ReadFile(".env")
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
k, v, ok := strings.Cut(line, "=")
|
|
if !ok {
|
|
continue
|
|
}
|
|
k = strings.TrimSpace(k)
|
|
v = strings.Trim(strings.TrimSpace(v), `"'`)
|
|
if k != "" && os.Getenv(k) == "" {
|
|
_ = os.Setenv(k, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func Load() (*Config, error) {
|
|
loadDotEnv()
|
|
|
|
v := viper.New()
|
|
v.SetEnvPrefix("OT")
|
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
|
v.AutomaticEnv()
|
|
|
|
// 默认值(与 .env.example 对应)
|
|
v.SetDefault("env", "development")
|
|
v.SetDefault("port", 8080)
|
|
|
|
v.SetDefault("db.driver", "sqlite")
|
|
v.SetDefault("db.dsn", "data/openteam.db")
|
|
|
|
v.SetDefault("jwt.secret", "dev-only-secret-change-me")
|
|
v.SetDefault("jwt.access_ttl", "2h")
|
|
v.SetDefault("jwt.refresh_ttl", "168h")
|
|
v.SetDefault("jwt.issuer", "openteam")
|
|
v.SetDefault("jwt.cookie_name", "ot_refresh")
|
|
v.SetDefault("jwt.cookie_secure", false)
|
|
v.SetDefault("jwt.cookie_domain", "")
|
|
|
|
v.SetDefault("auth.registration_mode", "open")
|
|
v.SetDefault("auth.argon2_time", 3)
|
|
v.SetDefault("auth.argon2_memory", 64*1024) // 64 MiB
|
|
v.SetDefault("auth.argon2_threads", 2)
|
|
v.SetDefault("auth.argon2_keylen", 32)
|
|
v.SetDefault("auth.salt_len", 16)
|
|
|
|
v.SetDefault("proxy.default_channel_name", "openai")
|
|
v.SetDefault("proxy.upstream_base_url", "https://api.openai.com")
|
|
v.SetDefault("proxy.upstream_key", "")
|
|
v.SetDefault("proxy.default_model", "gpt-4o-mini")
|
|
v.SetDefault("proxy.timeout", "120s")
|
|
v.SetDefault("proxy.health_interval", "60s")
|
|
v.SetDefault("proxy.health_fail_threshold", 2)
|
|
|
|
v.SetDefault("ratelimit.user_rps", 20)
|
|
|
|
return &Config{
|
|
Env: v.GetString("env"),
|
|
Port: v.GetInt("port"),
|
|
DB: DBConfig{
|
|
Driver: v.GetString("db.driver"),
|
|
DSN: v.GetString("db.dsn"),
|
|
},
|
|
JWT: JWTConfig{
|
|
Secret: v.GetString("jwt.secret"),
|
|
AccessTTL: v.GetDuration("jwt.access_ttl"),
|
|
RefreshTTL: v.GetDuration("jwt.refresh_ttl"),
|
|
Issuer: v.GetString("jwt.issuer"),
|
|
CookieName: v.GetString("jwt.cookie_name"),
|
|
CookieSecure: v.GetBool("jwt.cookie_secure"),
|
|
CookieDomain: v.GetString("jwt.cookie_domain"),
|
|
},
|
|
Auth: AuthConfig{
|
|
RegistrationMode: v.GetString("auth.registration_mode"),
|
|
Argon2Time: v.GetUint32("auth.argon2_time"),
|
|
Argon2Memory: v.GetUint32("auth.argon2_memory"),
|
|
Argon2Threads: v.GetUint8("auth.argon2_threads"),
|
|
Argon2KeyLen: v.GetUint32("auth.argon2_keylen"),
|
|
SaltLen: v.GetInt("auth.salt_len"),
|
|
},
|
|
Proxy: ProxyConfig{
|
|
DefaultChannelName: v.GetString("proxy.default_channel_name"),
|
|
UpstreamBaseURL: v.GetString("proxy.upstream_base_url"),
|
|
UpstreamKey: v.GetString("proxy.upstream_key"),
|
|
DefaultModel: v.GetString("proxy.default_model"),
|
|
Timeout: v.GetDuration("proxy.timeout"),
|
|
HealthInterval: v.GetDuration("proxy.health_interval"),
|
|
HealthFailThreshold: v.GetInt("proxy.health_fail_threshold"),
|
|
},
|
|
RateLimit: RateLimitConfig{
|
|
UserRPS: v.GetInt("ratelimit.user_rps"),
|
|
},
|
|
Master: v.GetString("master_key"),
|
|
}, nil
|
|
}
|