d1b8f8e3b2
- 删除 briefing_handler.go / briefing_store.go 及所有相关路由与配置 - 移除 Gateway Config 中 ToolEngineURL / BriefingTime 字段 - 移除 DevTools 中 gateway 的 TOOL_ENGINE_URL 环境变量 - webhook 错误提示从"AI 服务异常/暂不可用"改为昔涟自然口吻 - markdown 导出中昔涟头像从 🤖 改为 💫 - 后台思考提示词"系统会误解析"改为"我会误解析" Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
268 lines
6.8 KiB
Go
268 lines
6.8 KiB
Go
package config
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/golang-jwt/jwt/v5"
|
||
)
|
||
|
||
// Config 应用配置
|
||
type Config struct {
|
||
Env string
|
||
Port string
|
||
|
||
// 数据库
|
||
PostgresHost string
|
||
PostgresPort string
|
||
PostgresUser string
|
||
PostgresPass string
|
||
PostgresDB string
|
||
|
||
// Redis
|
||
RedisHost string
|
||
RedisPort string
|
||
RedisPass string
|
||
|
||
// JWT
|
||
JWTSecret string
|
||
JWTExpiryHours time.Duration
|
||
|
||
// 管理员账户 (开发阶段使用)
|
||
AdminUsername string
|
||
AdminPassword string
|
||
AdminNickname string // 昔涟对用户的基本称呼
|
||
|
||
// 注册开关
|
||
RegistrationEnabled bool
|
||
|
||
// AI-Core 服务
|
||
AICoreURL string
|
||
|
||
// Memory 服务
|
||
MemoryServiceURL string
|
||
|
||
// IoT 调试服务
|
||
IoTDebugServiceURL string
|
||
|
||
// Voice 语音识别服务
|
||
VoiceServiceURL string
|
||
|
||
// LLM (透传给AI-Core,Gateway可能也需要)
|
||
LLMAPIURL string
|
||
LLMAPIKey string
|
||
LLMModel string
|
||
|
||
// WebSocket
|
||
WSMaxConnections int
|
||
|
||
// 会话闲置超时 (分钟) — 超过此时间后会话标记为 idle 但不删除
|
||
SessionIdleTimeoutMin int
|
||
|
||
// Webhook (第三方平台接入)
|
||
WebhookAPIKey string
|
||
|
||
// Internal Service Token (内部服务间认证)
|
||
InternalServiceToken string
|
||
|
||
// CORS 允许的 Origin 白名单
|
||
AllowedOrigins []string
|
||
}
|
||
|
||
// Load 从环境变量加载配置
|
||
// 注意:JWT_SECRET 和 INTERNAL_SERVICE_TOKEN 必须在环境变量中设置,否则启动时 panic
|
||
func Load() *Config {
|
||
jwtSecret := os.Getenv("JWT_SECRET")
|
||
if jwtSecret == "" {
|
||
panic("致命错误: 环境变量 JWT_SECRET 未设置,服务拒绝启动。请在 .env 文件中设置 JWT_SECRET。")
|
||
}
|
||
|
||
internalServiceToken := os.Getenv("INTERNAL_SERVICE_TOKEN")
|
||
if internalServiceToken == "" {
|
||
panic("致命错误: 环境变量 INTERNAL_SERVICE_TOKEN 未设置,服务拒绝启动。请在 .env 文件中设置 INTERNAL_SERVICE_TOKEN。")
|
||
}
|
||
|
||
// IoT 服务 URL:优先使用 IOT_SERVICE_URL,回退到 IOT_DEBUG_SERVICE_URL(向后兼容)
|
||
iotServiceURL := os.Getenv("IOT_SERVICE_URL")
|
||
if iotServiceURL == "" {
|
||
iotServiceURL = getEnv("IOT_DEBUG_SERVICE_URL", "http://localhost:8083")
|
||
}
|
||
|
||
return &Config{
|
||
Env: getEnv("ENV", "development"),
|
||
Port: getEnv("GATEWAY_PORT", "8080"),
|
||
|
||
PostgresHost: getEnv("POSTGRES_HOST", "localhost"),
|
||
PostgresPort: getEnv("POSTGRES_PORT", "5432"),
|
||
PostgresUser: getEnv("POSTGRES_USER", "cyrene"),
|
||
PostgresPass: getEnv("POSTGRES_PASSWORD", "cyrene_pass"),
|
||
PostgresDB: getEnv("POSTGRES_DB", "cyrene_ai"),
|
||
|
||
RedisHost: getEnv("REDIS_HOST", "localhost"),
|
||
RedisPort: getEnv("REDIS_PORT", "6379"),
|
||
RedisPass: getEnv("REDIS_PASSWORD", ""),
|
||
|
||
JWTSecret: jwtSecret,
|
||
JWTExpiryHours: time.Duration(getEnvInt("JWT_EXPIRY_HOURS", 720)) * time.Hour,
|
||
|
||
// 管理员账户 (开发阶段使用)
|
||
AdminUsername: getEnv("ADMIN_USERNAME", "admin"),
|
||
AdminPassword: getEnv("ADMIN_PASSWORD", "cyrene-dev-admin"),
|
||
AdminNickname: getEnv("ADMIN_NICKNAME", "管理员"),
|
||
|
||
// 注册开关 (开发阶段默认关闭)
|
||
RegistrationEnabled: getEnvBool("REGISTRATION_ENABLED", false),
|
||
|
||
AICoreURL: getEnv("AI_CORE_URL", "http://localhost:8081"),
|
||
|
||
MemoryServiceURL: getEnv("MEMORY_SERVICE_URL", "http://localhost:8091"),
|
||
|
||
IoTDebugServiceURL: iotServiceURL,
|
||
|
||
VoiceServiceURL: getEnv("VOICE_SERVICE_URL", "http://localhost:8093"),
|
||
|
||
LLMAPIURL: getEnv("LLM_API_URL", "https://api.openai.com/v1"),
|
||
LLMAPIKey: getEnv("LLM_API_KEY", ""),
|
||
LLMModel: getEnv("LLM_MODEL", "gpt-4o"),
|
||
|
||
WSMaxConnections: getEnvInt("WS_MAX_CONNECTIONS", 1000),
|
||
SessionIdleTimeoutMin: getEnvInt("SESSION_IDLE_TIMEOUT_MIN", 30),
|
||
|
||
WebhookAPIKey: getEnv("WEBHOOK_API_KEY", ""),
|
||
InternalServiceToken: internalServiceToken,
|
||
|
||
AllowedOrigins: parseAllowedOrigins(getEnv("ALLOWED_ORIGINS", "http://localhost:5173,http://localhost:5199,http://localhost:3000")),
|
||
|
||
}
|
||
}
|
||
|
||
// DatabaseURL 构建 PostgreSQL 连接字符串
|
||
func (c *Config) DatabaseURL() string {
|
||
return fmt.Sprintf(
|
||
"postgres://%s:%s@%s:%s/%s?sslmode=disable",
|
||
c.PostgresUser, c.PostgresPass,
|
||
c.PostgresHost, c.PostgresPort,
|
||
c.PostgresDB,
|
||
)
|
||
}
|
||
|
||
// GenerateToken 生成JWT token (短期 access token)
|
||
func (c *Config) GenerateToken(userID string) (string, error) {
|
||
claims := jwt.MapClaims{
|
||
"user_id": userID,
|
||
"type": "access",
|
||
"exp": time.Now().Add(c.JWTExpiryHours).Unix(),
|
||
"iat": time.Now().Unix(),
|
||
}
|
||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||
return token.SignedString([]byte(c.JWTSecret))
|
||
}
|
||
|
||
// GenerateRefreshToken 生成 refresh token (长期有效,30天)
|
||
func (c *Config) GenerateRefreshToken(userID string) (string, error) {
|
||
claims := jwt.MapClaims{
|
||
"user_id": userID,
|
||
"type": "refresh",
|
||
"exp": time.Now().Add(30 * 24 * time.Hour).Unix(), // 30天
|
||
"iat": time.Now().Unix(),
|
||
}
|
||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||
return token.SignedString([]byte(c.JWTSecret))
|
||
}
|
||
|
||
// ValidateToken 验证JWT token
|
||
func (c *Config) ValidateToken(tokenString string) (string, error) {
|
||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||
return nil, jwt.ErrSignatureInvalid
|
||
}
|
||
return []byte(c.JWTSecret), nil
|
||
})
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
claims, ok := token.Claims.(jwt.MapClaims)
|
||
if !ok || !token.Valid {
|
||
return "", jwt.ErrSignatureInvalid
|
||
}
|
||
|
||
userID, _ := claims["user_id"].(string)
|
||
return userID, nil
|
||
}
|
||
|
||
// ValidateRefreshToken 验证 refresh token
|
||
func (c *Config) ValidateRefreshToken(tokenString string) (string, error) {
|
||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||
return nil, jwt.ErrSignatureInvalid
|
||
}
|
||
return []byte(c.JWTSecret), nil
|
||
})
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
claims, ok := token.Claims.(jwt.MapClaims)
|
||
if !ok || !token.Valid {
|
||
return "", jwt.ErrSignatureInvalid
|
||
}
|
||
|
||
// 验证类型必须是 "refresh"
|
||
tokenType, _ := claims["type"].(string)
|
||
if tokenType != "refresh" {
|
||
return "", fmt.Errorf("无效的刷新令牌类型")
|
||
}
|
||
|
||
userID, _ := claims["user_id"].(string)
|
||
return userID, nil
|
||
}
|
||
|
||
func getEnv(key, fallback string) string {
|
||
if v := os.Getenv(key); v != "" {
|
||
return v
|
||
}
|
||
return fallback
|
||
}
|
||
|
||
func getEnvInt(key string, fallback int) int {
|
||
v := os.Getenv(key)
|
||
if v == "" {
|
||
return fallback
|
||
}
|
||
var result int
|
||
for _, c := range v {
|
||
if c < '0' || c > '9' {
|
||
return fallback
|
||
}
|
||
result = result*10 + int(c-'0')
|
||
}
|
||
return result
|
||
}
|
||
|
||
func getEnvBool(key string, fallback bool) bool {
|
||
v := os.Getenv(key)
|
||
if v == "" {
|
||
return fallback
|
||
}
|
||
return v == "true" || v == "1" || v == "yes"
|
||
}
|
||
|
||
// parseAllowedOrigins 解析逗号分隔的 origins 字符串为切片
|
||
func parseAllowedOrigins(s string) []string {
|
||
if s == "" {
|
||
return []string{}
|
||
}
|
||
parts := strings.Split(s, ",")
|
||
result := make([]string, 0, len(parts))
|
||
for _, p := range parts {
|
||
p = strings.TrimSpace(p)
|
||
if p != "" {
|
||
result = append(result, p)
|
||
}
|
||
}
|
||
return result
|
||
}
|