Files
LoveLosita 26c350f378 Version: 0.4.4.dev.260307
feat: 🚀 增强会话管理与缓存机制

* 会话 ID 空值兜底,若 `conversation_id` 为空时自动生成 UUID
* 在响应头写入 `X-Conversation-ID`,供前端使用,保持同一会话状态

perf:  会话状态缓存优化

* 当缓存未命中但 DB 已确认/创建会话后,调用 `SetConversationStatus` 回写 Redis
* 缓存写回失败时记录日志,不中断聊天主流程,确保业务流畅性

fix: 🐛 修复历史消息顺序问题与编译错误

* 修复历史消息顺序问题,保证返回的 N 条历史消息按时间正序喂给模型

  * 通过反转 `created_at desc` 查询结果的切片,确保模型输入顺序正确
* 修复 `fmt.Errorf` 参数不匹配问题,修正编译错误
* 整理 `agent-cache.go` 为标准 UTF-8 编码,避免 Go 编译报错 `invalid UTF-8 encoding`

feat: 🛠️ 独立构建 MCP 服务器

* 使用 `Codex` 构建独立于后端的 MCP 服务器,简化与 Codex 的协作
* 通过该服务器方便 Codex 直接测试和查看 Redis 与 MySQL 中的数据
2026-03-07 15:25:40 +08:00

164 lines
4.4 KiB
Go

package config
import (
"fmt"
"os"
"strconv"
"strings"
"time"
)
type Config struct {
ServerName string
ServerVersion string
ProtocolVersion string
DefaultCaller string
ToolTimeout time.Duration
RateLimitRPS float64
RateLimitBurst float64
MaxResultRows int
AuditLogPath string
EnforceWhitelist bool
RedisScanMaxKeys int
RedisScanMaxCount int
RedisValueMaxItems int
RedisMaxStringBytes int
MySQL MySQLConfig
Redis RedisConfig
}
type MySQLConfig struct {
Host string
Port int
User string
Password string
Database string
Params string
AllowedDatabases []string
AllowedTables []string
}
type RedisConfig struct {
Addr string
Password string
DB int
}
func LoadFromEnv() (Config, error) {
cfg := Config{
ServerName: getEnv("MCP_SERVER_NAME", "smartflow-mcp-server"),
ServerVersion: getEnv("MCP_SERVER_VERSION", "0.1.0"),
ProtocolVersion: getEnv("MCP_PROTOCOL_VERSION", "2024-11-05"),
DefaultCaller: getEnv("MCP_DEFAULT_CALLER", "unknown"),
ToolTimeout: getEnvDurationMS("MCP_TOOL_TIMEOUT_MS", 5000),
RateLimitRPS: getEnvFloat("MCP_RATE_LIMIT_RPS", 5),
RateLimitBurst: getEnvFloat("MCP_RATE_LIMIT_BURST", 10),
MaxResultRows: getEnvInt("MCP_MAX_RESULT_ROWS", 500),
AuditLogPath: getEnv("MCP_AUDIT_LOG_PATH", "logs/audit.log"),
EnforceWhitelist: getEnvBool("MCP_ENFORCE_WHITELIST", false),
RedisScanMaxKeys: getEnvInt("MCP_REDIS_SCAN_MAX_KEYS", 200),
RedisScanMaxCount: getEnvInt("MCP_REDIS_SCAN_MAX_COUNT", 200),
RedisValueMaxItems: getEnvInt("MCP_REDIS_VALUE_MAX_ITEMS", 100),
RedisMaxStringBytes: getEnvInt("MCP_REDIS_MAX_STRING_BYTES", 4096),
MySQL: MySQLConfig{
Host: getEnv("MYSQL_HOST", "127.0.0.1"),
Port: getEnvInt("MYSQL_PORT", 3306),
User: getEnv("MYSQL_USER", ""),
Password: getEnv("MYSQL_PASSWORD", ""),
Database: getEnv("MYSQL_DATABASE", ""),
Params: getEnv("MYSQL_PARAMS", "charset=utf8mb4&parseTime=true&loc=Local"),
AllowedDatabases: splitCommaList(getEnv("MYSQL_ALLOWED_DATABASES", "")),
AllowedTables: splitCommaList(getEnv("MYSQL_ALLOWED_TABLES", "")),
},
Redis: RedisConfig{
Addr: getEnv("REDIS_ADDR", "127.0.0.1:6379"),
Password: getEnv("REDIS_PASSWORD", ""),
DB: getEnvInt("REDIS_DB", 0),
},
}
if cfg.MySQL.User == "" || cfg.MySQL.Database == "" {
return Config{}, fmt.Errorf("MYSQL_USER and MYSQL_DATABASE are required")
}
if cfg.Redis.Addr == "" {
return Config{}, fmt.Errorf("REDIS_ADDR is required")
}
if cfg.MaxResultRows <= 0 {
return Config{}, fmt.Errorf("MCP_MAX_RESULT_ROWS must be > 0")
}
if cfg.RedisScanMaxKeys <= 0 {
return Config{}, fmt.Errorf("MCP_REDIS_SCAN_MAX_KEYS must be > 0")
}
if cfg.RedisScanMaxCount <= 0 {
return Config{}, fmt.Errorf("MCP_REDIS_SCAN_MAX_COUNT must be > 0")
}
if cfg.ToolTimeout <= 0 {
return Config{}, fmt.Errorf("MCP_TOOL_TIMEOUT_MS must be > 0")
}
if cfg.RateLimitRPS <= 0 || cfg.RateLimitBurst <= 0 {
return Config{}, fmt.Errorf("MCP_RATE_LIMIT_RPS and MCP_RATE_LIMIT_BURST must be > 0")
}
return cfg, nil
}
func getEnv(key string, defaultValue string) string {
if v, ok := os.LookupEnv(key); ok {
return strings.TrimSpace(v)
}
return defaultValue
}
func getEnvInt(key string, defaultValue int) int {
v := getEnv(key, "")
if v == "" {
return defaultValue
}
n, err := strconv.Atoi(v)
if err != nil {
return defaultValue
}
return n
}
func getEnvFloat(key string, defaultValue float64) float64 {
v := getEnv(key, "")
if v == "" {
return defaultValue
}
n, err := strconv.ParseFloat(v, 64)
if err != nil {
return defaultValue
}
return n
}
func getEnvBool(key string, defaultValue bool) bool {
v := strings.ToLower(getEnv(key, ""))
if v == "" {
return defaultValue
}
return v == "1" || v == "true" || v == "yes" || v == "on"
}
func getEnvDurationMS(key string, defaultValueMs int) time.Duration {
ms := getEnvInt(key, defaultValueMs)
return time.Duration(ms) * time.Millisecond
}
func splitCommaList(raw string) []string {
if strings.TrimSpace(raw) == "" {
return nil
}
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
trimmed := strings.TrimSpace(p)
if trimmed != "" {
out = append(out, strings.ToLower(trimmed))
}
}
return out
}