fix: 第一轮修复 - 记忆管理/IoT操控/历史消息持久化/动作消息/链路优化/安全配置

- 修复记忆管理数据库连接不可用 (ai-core重编译+Unicode修复)
- 修复IoT子会话工具调用链路日志缺失
- 新增最终审查子会话(review_provider) 支持消息格式解析拆分
- 实现历史消息持久化(后端存储+前端分页加载)
- 前端新增动作消息(ActionMessage)类型和渲染
- 优化对话链路速度(非阻塞子会话+快速问候通道)
- JWT密钥环境变量化(无默认值启动panic)
- Token自动刷新机制(401拦截器+refresh接口)
- WebSocket指数退避重连(jitter+最大10次)
- localStorage清理一致性(cyrene_前缀+版本检查)
- IoT环境变量统一为IOT_SERVICE_URL
This commit is contained in:
2026-05-21 23:10:07 +08:00
parent 8b7d4ec19a
commit a058b0ab8e
53 changed files with 5535 additions and 241 deletions
+61 -4
View File
@@ -78,7 +78,24 @@ type Config struct {
}
// 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"),
@@ -93,7 +110,7 @@ func Load() *Config {
RedisPort: getEnv("REDIS_PORT", "6379"),
RedisPass: getEnv("REDIS_PASSWORD", ""),
JWTSecret: getEnv("JWT_SECRET", "change-me-in-production"),
JWTSecret: jwtSecret,
JWTExpiryHours: time.Duration(getEnvInt("JWT_EXPIRY_HOURS", 720)) * time.Hour,
// 管理员账户 (开发阶段使用)
@@ -108,7 +125,7 @@ func Load() *Config {
MemoryServiceURL: getEnv("MEMORY_SERVICE_URL", "http://localhost:8091"),
IoTDebugServiceURL: getEnv("IOT_DEBUG_SERVICE_URL", "http://localhost:8083"),
IoTDebugServiceURL: iotServiceURL,
VoiceServiceURL: getEnv("VOICE_SERVICE_URL", "http://localhost:8093"),
@@ -122,7 +139,7 @@ func Load() *Config {
SessionIdleTimeoutMin: getEnvInt("SESSION_IDLE_TIMEOUT_MIN", 30),
WebhookAPIKey: getEnv("WEBHOOK_API_KEY", ""),
InternalServiceToken: getEnv("INTERNAL_SERVICE_TOKEN", "cyrene-internal-token-change-me"),
InternalServiceToken: internalServiceToken,
AllowedOrigins: parseAllowedOrigins(getEnv("ALLOWED_ORIGINS", "http://localhost:5173,http://localhost:5199,http://localhost:3000")),
@@ -140,10 +157,11 @@ func (c *Config) DatabaseURL() string {
)
}
// GenerateToken 生成JWT token
// 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(),
}
@@ -151,6 +169,18 @@ func (c *Config) GenerateToken(userID string) (string, error) {
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) {
@@ -172,6 +202,33 @@ func (c *Config) ValidateToken(tokenString string) (string, error) {
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