Files
Cyrene/backend/gateway/cmd/main.go
T
AskaEth 0717928496 fix: Phase 6联调 — 插件管理器端口修正 + 多模型配置系统整合 + 历史消息刷新修复
## 调试日志

### 1. 插件管理器启动失败
- **症状**: DevTools 显示插件管理器一直"已停止",手动启动正常
- **排查**: 对比 process-manager.js 传入的环境变量 vs plugin-manager config.go 读取的变量
- **根因**: config.js 传入 PLUGIN_MANAGER_PORT=8094,但 config.go 读取 os.Getenv("PORT"),env 名不匹配。且 process.env 中 PORT 泄露时被误读为 9090,与 DevTools 端口冲突
- **修复**: config.js 将 PLUGIN_MANAGER_PORT → PORT,使 env 名与代码一致 (c3055f4)

### 2. 历史消息刷新后消失
- **症状**: 浏览器刷新后聊天历史清空
- **排查**: WebSocket history_response handler 中 if (msg.messages) 对空数组 [] 为 truthy
- **根因**: 后端返回空的 history_response (缓存为空) 时,空数组覆盖了 HTTP 已加载的消息
- **修复**: useWebSocket.ts 改为 if (msg.messages && msg.messages.length > 0),空数组走 else-if 分支仅打日志,不覆盖已有消息

### 3. Phase 6 多模型配置系统
- Gateway: ModelsConfigStore (JSON文件持久化) + Admin CRUD API (providers/models/routing)
- ai-core: ModelSelector 支持按 purpose 选择 + fallback_chain,无配置时回退 .env
- DevTools: 模型配置管理面板 (Providers/Models/Routing 三Tab)、在线模型查询代理、路由表单 checkbox 多选、关键词搜索过滤
- .gitignore: models.json + platform_configs.json

### 4. 多端客户端追踪
- Hub 新增 knownClients 映射 (clientID → KnownClient),在线/离线状态追踪
- 客户端备注持久化到 PostgreSQL
- DevTools 客户端管理面板

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 21:23:10 +08:00

234 lines
8.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"context"
"github.com/yourname/cyrene-ai/pkg/logger"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
"golang.org/x/crypto/bcrypt"
"github.com/yourname/cyrene-ai/gateway/internal/config"
"github.com/yourname/cyrene-ai/gateway/internal/engine"
"github.com/yourname/cyrene-ai/gateway/internal/handler"
"github.com/yourname/cyrene-ai/gateway/internal/middleware"
"github.com/yourname/cyrene-ai/gateway/internal/router"
"github.com/yourname/cyrene-ai/gateway/internal/store"
"github.com/yourname/cyrene-ai/gateway/internal/ws"
)
func main() {
logger.SetDefault(logger.New("gateway"))
// 自动加载 .env 文件(来自 backend/.env
if err := godotenv.Load("../.env"); err != nil {
logger.Println("ℹ 未找到 .env 文件,将使用环境变量或默认值")
}
// 加载配置
cfg := config.Load()
// 确保上传目录存在
if err := os.MkdirAll("./uploads", 0755); err != nil {
logger.Printf("⚠ 创建上传目录失败: %v", err)
}
// 初始化数据库持久化存储 (降级:连接失败不崩溃)
var sessionStore *store.SessionStore
var reminderStore *store.ReminderStore
var briefingStore *store.BriefingStore
var automationStore *store.AutomationStore
var fileStore *store.FileStore
var knowledgeStore *store.KnowledgeStore
var ruleEngine *engine.RuleEngine
databaseURL := cfg.DatabaseURL()
if s, err := store.NewSessionStore(databaseURL); err != nil {
logger.Printf("⚠ 会话持久化存储初始化失败 (数据库不可用): %v", err)
logger.Println("⚠ Gateway 将以仅内存模式运行 — 会话数据在重启后丢失")
} else {
sessionStore = s
logger.Println("✅ 会话持久化存储已启用 (PostgreSQL)")
// 初始化 users 表
if err := store.CreateUsersTable(s.DB()); err != nil {
logger.Printf("⚠ 创建 users 表失败: %v", err)
} else {
logger.Println("✅ Users 表已就绪")
}
// 种子数据:如果没有 admin 用户,创建默认 admin
if existingAdmin, err := store.GetUserByUsername(s.DB(), cfg.AdminUsername); err != nil {
logger.Printf("⚠ 查询管理员用户失败: %v", err)
} else if existingAdmin == nil {
logger.Printf("🔧 未找到管理员用户,创建默认 %s (username: %s)...", cfg.AdminUsername, cfg.AdminUsername)
defaultAdminPassword := cfg.AdminPassword
passwordHash, err := bcrypt.GenerateFromPassword([]byte(defaultAdminPassword), bcrypt.DefaultCost)
if err != nil {
logger.Printf("⚠ 管理员密码哈希生成失败: %v", err)
} else {
if _, err := store.CreateUser(s.DB(), cfg.AdminUsername, string(passwordHash), true); err != nil {
logger.Printf("⚠ 创建默认管理员失败: %v", err)
} else {
logger.Printf("✅ 默认管理员用户已创建 (username: %s)", cfg.AdminUsername)
}
}
} else {
logger.Println("✅ 管理员用户已存在")
}
// 清理旧的管理员用户 (is_admin=true 但 username 与当前 ADMIN_USERNAME 不同)
// 当 .env 中 ADMIN_USERNAME 变更时,旧的 admin 用户会成为孤立的会话持有者
if allUsers, err := store.ListUsers(s.DB()); err != nil {
logger.Printf("⚠ 查询所有用户失败: %v", err)
} else {
for _, u := range allUsers {
if u.IsAdmin && u.Username != cfg.AdminUsername {
logger.Printf("🗑 清理旧管理员用户: %s (id=%d)", u.Username, u.ID)
if err := store.DeleteUser(s.DB(), u.ID); err != nil {
logger.Printf("⚠ 删除旧管理员用户失败: %s, err=%v", u.Username, err)
}
}
}
}
// 初始化提醒存储(复用同一数据库连接)
if rs, err := store.NewReminderStore(s.DB()); err != nil {
logger.Printf("⚠ 提醒存储初始化失败: %v", err)
} else {
reminderStore = rs
logger.Println("✅ 提醒持久化存储已启用 (PostgreSQL)")
}
// 初始化简报存储(复用同一数据库连接)
if bs, err := store.NewBriefingStore(s.DB()); err != nil {
logger.Printf("⚠ 简报存储初始化失败: %v", err)
} else {
briefingStore = bs
logger.Println("✅ 简报持久化存储已启用 (PostgreSQL)")
}
// 初始化自动化存储(复用同一数据库连接)
if as, err := store.NewAutomationStore(s.DB()); err != nil {
logger.Printf("⚠ 自动化存储初始化失败: %v", err)
} else {
automationStore = as
logger.Println("✅ 自动化持久化存储已启用 (PostgreSQL)")
}
// 初始化文件存储(复用同一数据库连接)
if fs, err := store.NewFileStore(s.DB()); err != nil {
logger.Printf("⚠ 文件存储初始化失败: %v", err)
} else {
fileStore = fs
logger.Println("✅ 文件持久化存储已启用 (PostgreSQL)")
}
// 初始化知识库存储(复用同一数据库连接)
if ks, err := store.NewKnowledgeStore(s.DB()); err != nil {
logger.Printf("⚠ 知识库存储初始化失败: %v", err)
} else {
knowledgeStore = ks
logger.Println("✅ 知识库持久化存储已启用 (PostgreSQL)")
}
}
// 初始化 WebSocket Hub
hub := ws.NewHub()
hub.SetStore(sessionStore)
hub.SetIdleTimeout(cfg.SessionIdleTimeoutMin)
hub.SetAICoreConfig(cfg.AICoreURL, cfg.InternalServiceToken)
// 初始化规则引擎 (需要 Hub)
if automationStore != nil {
ruleEngine = engine.NewRuleEngine(automationStore, hub)
ruleEngine.Start()
logger.Println("✅ 规则引擎已启动")
}
// 初始化Gin
if cfg.Env == "production" {
gin.SetMode(gin.ReleaseMode)
}
r := gin.New()
// 中间件
r.Use(middleware.CORS(cfg.AllowedOrigins))
r.Use(middleware.RequestLogging())
r.Use(gin.Recovery())
// 启动 WebSocket Hub
go hub.Run()
// 启动闲置会话清理 (标记超时会话为 idle,不删除)
hub.StartIdleCleanup()
// 启动 IoT 设备状态广播(每10秒向所有WebSocket客户端推送设备状态)
hub.StartIoTBroadcast(cfg.IoTDebugServiceURL)
// 注册路由
var db interface{}
if sessionStore != nil {
db = sessionStore.DB()
}
// 初始化模型配置存储 (Phase 6)
modelConfigStore, err := config.NewModelsConfigStore("../models.json")
if err != nil {
logger.Printf("[WARN] 模型配置存储初始化失败 (将仅使用 .env 回退): %v", err)
modelConfigStore = nil
} else if modelConfigStore.HasConfig() {
logger.Println("[INFO] 模型配置文件已加载 (models.json)")
} else {
logger.Println("[INFO] 模型配置文件不存在,回退到 .env LLM 配置")
}
router.Setup(r, hub, cfg, sessionStore, reminderStore, briefingStore, automationStore, fileStore, ruleEngine, knowledgeStore, nil, db, modelConfigStore)
// 启动提醒调度器
if reminderStore != nil {
handler.StartReminderScheduler(reminderStore, hub)
}
// 启动简报调度器
if briefingStore != nil && reminderStore != nil {
briefingHandler := handler.NewBriefingHandler(cfg, hub, briefingStore, reminderStore)
handler.StartBriefingScheduler(briefingHandler, briefingStore, cfg.BriefingTime)
}
// 启动服务
srv := &http.Server{
Addr: ":" + cfg.Port,
Handler: r,
}
go func() {
logger.Printf("🚀 Gateway 启动在端口 %s", cfg.Port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatalf("服务启动失败: %v", err)
}
}()
// 优雅关闭
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
logger.Println("正在关闭服务...")
hub.StopIoTBroadcast()
// 关闭数据库连接
if sessionStore != nil {
if err := sessionStore.Close(); err != nil {
logger.Printf("⚠ 关闭数据库连接失败: %v", err)
}
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(ctx)
logger.Println("服务已关闭")
}