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 中的数据
164 lines
4.4 KiB
Go
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
|
|
}
|