feat: Phase 1+2 架构进化 — 连续思考链/主动消息决策/情感状态机/离线自主思考 (86文件)

Phase 1 (基础设施):
- ThinkChain 思考链连续性 + 差异化思考提示词 (persistent)
- AutonomousToolPolicy 工具安全策略 (safe/unsafe/conditional)
- MessageScheduler 自适应消息节奏 (Idle/Available/Busy)
- SessionEnrichmentStore 渐进式上下文丰富 (5层)
- ConversationBus 事件总线 + ResponseCache (dedup)
- pkg/logger 统一日志 + 所有 handler 替换 fmt.Printf
- NPE 守卫/链路优化/数据库表修复/Go workspace

Phase 2 (人格交互):
- EmotionState/EmotionTracker 情感状态机 (5种心情, 情绪衰减)
- ProactiveGuard 主动消息多维决策 (静默时段/紧急度/频率/校验)
- Gateway↔ai-core 在线状态感知链路 (presence notification)
- 离线思考频率控制 + 重连问候 + 离线消息排队

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 15:25:12 +08:00
parent b123a36aae
commit 87214b9441
86 changed files with 3085 additions and 582 deletions
+35 -33
View File
@@ -2,7 +2,7 @@ package main
import (
"context"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"net/http"
"os"
"os/signal"
@@ -23,9 +23,10 @@ import (
)
func main() {
logger.SetDefault(logger.New("gateway"))
// 自动加载 .env 文件(来自 backend/.env
if err := godotenv.Load("../.env"); err != nil {
log.Println("ℹ 未找到 .env 文件,将使用环境变量或默认值")
logger.Println("ℹ 未找到 .env 文件,将使用环境变量或默认值")
}
// 加载配置
@@ -33,7 +34,7 @@ func main() {
// 确保上传目录存在
if err := os.MkdirAll("./uploads", 0755); err != nil {
log.Printf("⚠ 创建上传目录失败: %v", err)
logger.Printf("⚠ 创建上传目录失败: %v", err)
}
// 初始化数据库持久化存储 (降级:连接失败不崩溃)
@@ -46,49 +47,49 @@ func main() {
var ruleEngine *engine.RuleEngine
databaseURL := cfg.DatabaseURL()
if s, err := store.NewSessionStore(databaseURL); err != nil {
log.Printf("⚠ 会话持久化存储初始化失败 (数据库不可用): %v", err)
log.Println("⚠ Gateway 将以仅内存模式运行 — 会话数据在重启后丢失")
logger.Printf("⚠ 会话持久化存储初始化失败 (数据库不可用): %v", err)
logger.Println("⚠ Gateway 将以仅内存模式运行 — 会话数据在重启后丢失")
} else {
sessionStore = s
log.Println("✅ 会话持久化存储已启用 (PostgreSQL)")
logger.Println("✅ 会话持久化存储已启用 (PostgreSQL)")
// 初始化 users 表
if err := store.CreateUsersTable(s.DB()); err != nil {
log.Printf("⚠ 创建 users 表失败: %v", err)
logger.Printf("⚠ 创建 users 表失败: %v", err)
} else {
log.Println("✅ Users 表已就绪")
logger.Println("✅ Users 表已就绪")
}
// 种子数据:如果没有 admin 用户,创建默认 admin
if existingAdmin, err := store.GetUserByUsername(s.DB(), cfg.AdminUsername); err != nil {
log.Printf("⚠ 查询管理员用户失败: %v", err)
logger.Printf("⚠ 查询管理员用户失败: %v", err)
} else if existingAdmin == nil {
log.Printf("🔧 未找到管理员用户,创建默认 %s (username: %s)...", cfg.AdminUsername, cfg.AdminUsername)
logger.Printf("🔧 未找到管理员用户,创建默认 %s (username: %s)...", cfg.AdminUsername, cfg.AdminUsername)
defaultAdminPassword := cfg.AdminPassword
passwordHash, err := bcrypt.GenerateFromPassword([]byte(defaultAdminPassword), bcrypt.DefaultCost)
if err != nil {
log.Printf("⚠ 管理员密码哈希生成失败: %v", err)
logger.Printf("⚠ 管理员密码哈希生成失败: %v", err)
} else {
if _, err := store.CreateUser(s.DB(), cfg.AdminUsername, string(passwordHash), true); err != nil {
log.Printf("⚠ 创建默认管理员失败: %v", err)
logger.Printf("⚠ 创建默认管理员失败: %v", err)
} else {
log.Printf("✅ 默认管理员用户已创建 (username: %s)", cfg.AdminUsername)
logger.Printf("✅ 默认管理员用户已创建 (username: %s)", cfg.AdminUsername)
}
}
} else {
log.Println("✅ 管理员用户已存在")
logger.Println("✅ 管理员用户已存在")
}
// 清理旧的管理员用户 (is_admin=true 但 username 与当前 ADMIN_USERNAME 不同)
// 当 .env 中 ADMIN_USERNAME 变更时,旧的 admin 用户会成为孤立的会话持有者
if allUsers, err := store.ListUsers(s.DB()); err != nil {
log.Printf("⚠ 查询所有用户失败: %v", err)
logger.Printf("⚠ 查询所有用户失败: %v", err)
} else {
for _, u := range allUsers {
if u.IsAdmin && u.Username != cfg.AdminUsername {
log.Printf("🗑 清理旧管理员用户: %s (id=%d)", u.Username, u.ID)
logger.Printf("🗑 清理旧管理员用户: %s (id=%d)", u.Username, u.ID)
if err := store.DeleteUser(s.DB(), u.ID); err != nil {
log.Printf("⚠ 删除旧管理员用户失败: %s, err=%v", u.Username, err)
logger.Printf("⚠ 删除旧管理员用户失败: %s, err=%v", u.Username, err)
}
}
}
@@ -96,42 +97,42 @@ func main() {
// 初始化提醒存储(复用同一数据库连接)
if rs, err := store.NewReminderStore(s.DB()); err != nil {
log.Printf("⚠ 提醒存储初始化失败: %v", err)
logger.Printf("⚠ 提醒存储初始化失败: %v", err)
} else {
reminderStore = rs
log.Println("✅ 提醒持久化存储已启用 (PostgreSQL)")
logger.Println("✅ 提醒持久化存储已启用 (PostgreSQL)")
}
// 初始化简报存储(复用同一数据库连接)
if bs, err := store.NewBriefingStore(s.DB()); err != nil {
log.Printf("⚠ 简报存储初始化失败: %v", err)
logger.Printf("⚠ 简报存储初始化失败: %v", err)
} else {
briefingStore = bs
log.Println("✅ 简报持久化存储已启用 (PostgreSQL)")
logger.Println("✅ 简报持久化存储已启用 (PostgreSQL)")
}
// 初始化自动化存储(复用同一数据库连接)
if as, err := store.NewAutomationStore(s.DB()); err != nil {
log.Printf("⚠ 自动化存储初始化失败: %v", err)
logger.Printf("⚠ 自动化存储初始化失败: %v", err)
} else {
automationStore = as
log.Println("✅ 自动化持久化存储已启用 (PostgreSQL)")
logger.Println("✅ 自动化持久化存储已启用 (PostgreSQL)")
}
// 初始化文件存储(复用同一数据库连接)
if fs, err := store.NewFileStore(s.DB()); err != nil {
log.Printf("⚠ 文件存储初始化失败: %v", err)
logger.Printf("⚠ 文件存储初始化失败: %v", err)
} else {
fileStore = fs
log.Println("✅ 文件持久化存储已启用 (PostgreSQL)")
logger.Println("✅ 文件持久化存储已启用 (PostgreSQL)")
}
// 初始化知识库存储(复用同一数据库连接)
if ks, err := store.NewKnowledgeStore(s.DB()); err != nil {
log.Printf("⚠ 知识库存储初始化失败: %v", err)
logger.Printf("⚠ 知识库存储初始化失败: %v", err)
} else {
knowledgeStore = ks
log.Println("✅ 知识库持久化存储已启用 (PostgreSQL)")
logger.Println("✅ 知识库持久化存储已启用 (PostgreSQL)")
}
}
@@ -139,12 +140,13 @@ func main() {
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()
log.Println("✅ 规则引擎已启动")
logger.Println("✅ 规则引擎已启动")
}
// 初始化Gin
@@ -192,9 +194,9 @@ func main() {
}
go func() {
log.Printf("🚀 Gateway 启动在端口 %s", cfg.Port)
logger.Printf("🚀 Gateway 启动在端口 %s", cfg.Port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("服务启动失败: %v", err)
logger.Fatalf("服务启动失败: %v", err)
}
}()
@@ -202,19 +204,19 @@ func main() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("正在关闭服务...")
logger.Println("正在关闭服务...")
hub.StopIoTBroadcast()
// 关闭数据库连接
if sessionStore != nil {
if err := sessionStore.Close(); err != nil {
log.Printf("⚠ 关闭数据库连接失败: %v", err)
logger.Printf("⚠ 关闭数据库连接失败: %v", err)
}
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(ctx)
log.Println("服务已关闭")
logger.Println("服务已关闭")
}
+3
View File
@@ -31,6 +31,7 @@ require (
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/yourname/cyrene-ai/pkg/logger v0.0.0
golang.org/x/arch v0.8.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
@@ -38,3 +39,5 @@ require (
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/yourname/cyrene-ai/pkg/logger => ../pkg/logger
+16 -16
View File
@@ -6,7 +6,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"net/http"
"os"
"strings"
@@ -91,7 +91,7 @@ func (e *RuleEngine) Start() {
e.mu.Unlock()
go e.loop()
log.Printf("[RuleEngine] 规则引擎已启动 (IoT服务地址: %s)", e.iotServiceURL)
logger.Printf("[RuleEngine] 规则引擎已启动 (IoT服务地址: %s)", e.iotServiceURL)
}
// Stop 停止规则引擎
@@ -104,7 +104,7 @@ func (e *RuleEngine) Stop() {
}
close(e.stopCh)
e.running = false
log.Println("[RuleEngine] 规则引擎已停止")
logger.Println("[RuleEngine] 规则引擎已停止")
}
// loop 规则引擎主循环
@@ -129,7 +129,7 @@ func (e *RuleEngine) loop() {
func (e *RuleEngine) evaluateAllRules() {
rules, err := e.store.GetEnabledRules()
if err != nil {
log.Printf("[RuleEngine] 获取启用的规则失败: %v", err)
logger.Printf("[RuleEngine] 获取启用的规则失败: %v", err)
return
}
@@ -162,7 +162,7 @@ func (e *RuleEngine) evaluateRule(rule *store.AutomationRule) bool {
var triggerCfg TriggerConfig
if rule.TriggerConfig != nil {
if err := json.Unmarshal(*rule.TriggerConfig, &triggerCfg); err != nil {
log.Printf("[RuleEngine] 解析触发器配置失败: rule=%s err=%v", rule.ID, err)
logger.Printf("[RuleEngine] 解析触发器配置失败: rule=%s err=%v", rule.ID, err)
return false
}
}
@@ -189,7 +189,7 @@ func (e *RuleEngine) evaluateRule(rule *store.AutomationRule) bool {
var conditions []Condition
if rule.Conditions != nil {
if err := json.Unmarshal(*rule.Conditions, &conditions); err != nil {
log.Printf("[RuleEngine] 解析条件失败: rule=%s err=%v", rule.ID, err)
logger.Printf("[RuleEngine] 解析条件失败: rule=%s err=%v", rule.ID, err)
return false
}
}
@@ -240,7 +240,7 @@ func (e *RuleEngine) evaluateDeviceStateTrigger(cfg TriggerConfig) bool {
// 从 IoT 服务获取设备状态
devices, err := e.fetchIoTDevices()
if err != nil {
log.Printf("[RuleEngine] 获取设备状态失败: %v", err)
logger.Printf("[RuleEngine] 获取设备状态失败: %v", err)
return false
}
@@ -340,12 +340,12 @@ func (e *RuleEngine) ExecuteRuleActions(rule *store.AutomationRule) {
var actions []Action
if rule.Actions != nil {
if err := json.Unmarshal(*rule.Actions, &actions); err != nil {
log.Printf("[RuleEngine] 解析动作失败: rule=%s err=%v", rule.ID, err)
logger.Printf("[RuleEngine] 解析动作失败: rule=%s err=%v", rule.ID, err)
return
}
}
log.Printf("[RuleEngine] 执行规则 %s (%s) 的 %d 个动作", rule.ID, rule.Name, len(actions))
logger.Printf("[RuleEngine] 执行规则 %s (%s) 的 %d 个动作", rule.ID, rule.Name, len(actions))
for _, action := range actions {
switch action.Type {
@@ -354,7 +354,7 @@ func (e *RuleEngine) ExecuteRuleActions(rule *store.AutomationRule) {
case "notify":
e.executeNotify(action, rule.UserID)
default:
log.Printf("[RuleEngine] 未知动作类型: %s", action.Type)
logger.Printf("[RuleEngine] 未知动作类型: %s", action.Type)
}
}
}
@@ -366,7 +366,7 @@ func (e *RuleEngine) ExecuteScene(sceneID, userID string) error {
return fmt.Errorf("获取场景规则失败: %w", err)
}
log.Printf("[RuleEngine] 执行场景 %s,共 %d 条关联规则", sceneID, len(rules))
logger.Printf("[RuleEngine] 执行场景 %s,共 %d 条关联规则", sceneID, len(rules))
for _, rule := range rules {
if rule.Enabled {
@@ -393,17 +393,17 @@ func (e *RuleEngine) executeSetDevice(action Action) {
resp, err := e.httpClient.Post(url, "application/json", bytes.NewReader(bodyBytes))
if err != nil {
log.Printf("[RuleEngine] 设备控制请求失败: device=%s property=%s err=%v",
logger.Printf("[RuleEngine] 设备控制请求失败: device=%s property=%s err=%v",
action.DeviceID, action.Property, err)
return
}
defer resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
log.Printf("[RuleEngine] 设备控制成功: device=%s property=%s value=%v",
logger.Printf("[RuleEngine] 设备控制成功: device=%s property=%s value=%v",
action.DeviceID, action.Property, action.Value)
} else {
log.Printf("[RuleEngine] 设备控制失败: device=%s property=%s status=%d",
logger.Printf("[RuleEngine] 设备控制失败: device=%s property=%s status=%d",
action.DeviceID, action.Property, resp.StatusCode)
}
}
@@ -427,12 +427,12 @@ func (e *RuleEngine) executeNotify(action Action, userID string) {
data, err := json.Marshal(msg)
if err != nil {
log.Printf("[RuleEngine] 序列化通知失败: %v", err)
logger.Printf("[RuleEngine] 序列化通知失败: %v", err)
return
}
e.hub.SendToUser(userID, data)
log.Printf("[RuleEngine] 通知已发送: user=%s title=%s", userID, action.Title)
logger.Printf("[RuleEngine] 通知已发送: user=%s title=%s", userID, action.Title)
}
// ========== 辅助方法 ==========
@@ -3,7 +3,7 @@ package handler
import (
"database/sql"
"fmt"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"net/http"
"regexp"
"strings"
@@ -167,12 +167,12 @@ func (h *AuthHandler) Login(c *gin.Context) {
// 密码正确,迁移 admin 到 users 表
passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
log.Printf("⚠ 迁移管理员密码哈希失败: %v", err)
logger.Printf("⚠ 迁移管理员密码哈希失败: %v", err)
} else {
if _, err := store.CreateUser(h.db, req.Username, string(passwordHash), true); err != nil {
log.Printf("⚠ 迁移管理员到 users 表失败: %v", err)
logger.Printf("⚠ 迁移管理员到 users 表失败: %v", err)
} else {
log.Println("✅ 管理员已迁移到 users 表")
logger.Println("✅ 管理员已迁移到 users 表")
}
}
userID = "admin"
@@ -4,7 +4,7 @@ import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"net/http"
"github.com/gin-gonic/gin"
@@ -76,7 +76,7 @@ func (h *AutomationHandler) ListRules(c *gin.Context) {
rules, err := h.store.GetRulesByUser(userID)
if err != nil {
log.Printf("[automation] 获取规则列表失败: %v", err)
logger.Printf("[automation] 获取规则列表失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取规则列表失败"})
return
}
@@ -124,7 +124,7 @@ func (h *AutomationHandler) CreateRule(c *gin.Context) {
}
if err := h.store.CreateRule(rule); err != nil {
log.Printf("[automation] 创建规则失败: %v", err)
logger.Printf("[automation] 创建规则失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建规则失败"})
return
}
@@ -140,7 +140,7 @@ func (h *AutomationHandler) GetRule(c *gin.Context) {
id := c.Param("id")
rule, err := h.store.GetRule(id)
if err != nil {
log.Printf("[automation] 获取规则失败: %v", err)
logger.Printf("[automation] 获取规则失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取规则失败"})
return
}
@@ -161,7 +161,7 @@ func (h *AutomationHandler) UpdateRule(c *gin.Context) {
// 先获取规则验证所有权
existing, err := h.store.GetRule(id)
if err != nil {
log.Printf("[automation] 获取规则失败: %v", err)
logger.Printf("[automation] 获取规则失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取规则失败"})
return
}
@@ -204,7 +204,7 @@ func (h *AutomationHandler) UpdateRule(c *gin.Context) {
}
if err := h.store.UpdateRule(existing); err != nil {
log.Printf("[automation] 更新规则失败: %v", err)
logger.Printf("[automation] 更新规则失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新规则失败"})
return
}
@@ -223,7 +223,7 @@ func (h *AutomationHandler) DeleteRule(c *gin.Context) {
existing, err := h.store.GetRule(id)
if err != nil {
log.Printf("[automation] 获取规则失败: %v", err)
logger.Printf("[automation] 获取规则失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取规则失败"})
return
}
@@ -237,7 +237,7 @@ func (h *AutomationHandler) DeleteRule(c *gin.Context) {
}
if err := h.store.DeleteRule(id); err != nil {
log.Printf("[automation] 删除规则失败: %v", err)
logger.Printf("[automation] 删除规则失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除规则失败"})
return
}
@@ -253,7 +253,7 @@ func (h *AutomationHandler) TriggerRule(c *gin.Context) {
rule, err := h.store.GetRule(id)
if err != nil {
log.Printf("[automation] 获取规则失败: %v", err)
logger.Printf("[automation] 获取规则失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取规则失败"})
return
}
@@ -288,7 +288,7 @@ func (h *AutomationHandler) ListScenes(c *gin.Context) {
scenes, err := h.store.GetScenesByUser(userID)
if err != nil {
log.Printf("[automation] 获取场景列表失败: %v", err)
logger.Printf("[automation] 获取场景列表失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取场景列表失败"})
return
}
@@ -325,7 +325,7 @@ func (h *AutomationHandler) CreateScene(c *gin.Context) {
}
if err := h.store.CreateScene(scene); err != nil {
log.Printf("[automation] 创建场景失败: %v", err)
logger.Printf("[automation] 创建场景失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建场景失败"})
return
}
@@ -341,7 +341,7 @@ func (h *AutomationHandler) GetScene(c *gin.Context) {
id := c.Param("id")
scene, err := h.store.GetScene(id)
if err != nil {
log.Printf("[automation] 获取场景失败: %v", err)
logger.Printf("[automation] 获取场景失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取场景失败"})
return
}
@@ -361,7 +361,7 @@ func (h *AutomationHandler) UpdateScene(c *gin.Context) {
existing, err := h.store.GetScene(id)
if err != nil {
log.Printf("[automation] 获取场景失败: %v", err)
logger.Printf("[automation] 获取场景失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取场景失败"})
return
}
@@ -391,7 +391,7 @@ func (h *AutomationHandler) UpdateScene(c *gin.Context) {
}
if err := h.store.UpdateScene(existing); err != nil {
log.Printf("[automation] 更新场景失败: %v", err)
logger.Printf("[automation] 更新场景失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新场景失败"})
return
}
@@ -410,7 +410,7 @@ func (h *AutomationHandler) DeleteScene(c *gin.Context) {
existing, err := h.store.GetScene(id)
if err != nil {
log.Printf("[automation] 获取场景失败: %v", err)
logger.Printf("[automation] 获取场景失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取场景失败"})
return
}
@@ -424,7 +424,7 @@ func (h *AutomationHandler) DeleteScene(c *gin.Context) {
}
if err := h.store.DeleteScene(id); err != nil {
log.Printf("[automation] 删除场景失败: %v", err)
logger.Printf("[automation] 删除场景失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除场景失败"})
return
}
@@ -440,7 +440,7 @@ func (h *AutomationHandler) ExecuteScene(c *gin.Context) {
// 验证场景存在
scene, err := h.store.GetScene(id)
if err != nil {
log.Printf("[automation] 获取场景失败: %v", err)
logger.Printf("[automation] 获取场景失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取场景失败"})
return
}
@@ -452,7 +452,7 @@ func (h *AutomationHandler) ExecuteScene(c *gin.Context) {
userID := middleware.GetUserID(c)
if err := h.engine.ExecuteScene(id, userID); err != nil {
log.Printf("[automation] 执行场景失败: %v", err)
logger.Printf("[automation] 执行场景失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "执行场景失败"})
return
}
@@ -5,7 +5,7 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"net/http"
"strings"
"time"
@@ -66,7 +66,7 @@ func (h *BriefingHandler) GetBriefing(c *gin.Context) {
briefing, err := h.briefingStore.GetBriefingByDate(userID, date)
if err != nil {
log.Printf("[briefing] 查询简报失败: user=%s date=%s err=%v", userID, date, err)
logger.Printf("[briefing] 查询简报失败: user=%s date=%s err=%v", userID, date, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询简报失败: " + err.Error()})
return
}
@@ -106,7 +106,7 @@ func (h *BriefingHandler) GetLatestBriefings(c *gin.Context) {
briefings, err := h.briefingStore.GetLatestBriefings(userID, limit)
if err != nil {
log.Printf("[briefing] 查询简报列表失败: user=%s err=%v", userID, err)
logger.Printf("[briefing] 查询简报列表失败: user=%s err=%v", userID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询简报列表失败: " + err.Error()})
return
}
@@ -139,7 +139,7 @@ func (h *BriefingHandler) Generate(c *gin.Context) {
result, err := h.GenerateDailyBriefing(req.UserID)
if err != nil {
log.Printf("[briefing] 生成简报失败: user=%s err=%v", req.UserID, err)
logger.Printf("[briefing] 生成简报失败: user=%s err=%v", req.UserID, err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "生成简报失败: " + err.Error(),
"success": false,
@@ -173,10 +173,10 @@ func (h *BriefingHandler) GenerateDailyBriefing(userID string) (*store.Briefing,
}
// 1. 获取天气数据
log.Printf("[briefing] 获取天气数据...")
logger.Printf("[briefing] 获取天气数据...")
weather, err := h.fetchWeather("Shanghai")
if err != nil {
log.Printf("[briefing] 天气获取失败 (降级): %v", err)
logger.Printf("[briefing] 天气获取失败 (降级): %v", err)
weather = &store.WeatherData{
Location: "未知",
Temp: 0,
@@ -185,13 +185,13 @@ func (h *BriefingHandler) GenerateDailyBriefing(userID string) (*store.Briefing,
}
}
briefing.Weather = weather
log.Printf("[briefing] 天气: %s %.1f°C %s", weather.Location, weather.Temp, weather.Condition)
logger.Printf("[briefing] 天气: %s %.1f°C %s", weather.Location, weather.Temp, weather.Condition)
// 2. 获取今日待办提醒
log.Printf("[briefing] 获取待办提醒...")
logger.Printf("[briefing] 获取待办提醒...")
reminders, err := h.reminderStore.GetRemindersByUser(userID, "pending", 10, 0)
if err != nil {
log.Printf("[briefing] 获取提醒失败: %v", err)
logger.Printf("[briefing] 获取提醒失败: %v", err)
} else {
now := time.Now()
endOfDay := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location())
@@ -205,22 +205,22 @@ func (h *BriefingHandler) GenerateDailyBriefing(userID string) (*store.Briefing,
}
}
}
log.Printf("[briefing] 今日待办: %d 项", len(briefing.Reminders))
logger.Printf("[briefing] 今日待办: %d 项", len(briefing.Reminders))
// 3. 获取新闻摘要(通过 tool-engine web_search
log.Printf("[briefing] 获取新闻摘要...")
logger.Printf("[briefing] 获取新闻摘要...")
news, err := h.fetchNews()
if err != nil {
log.Printf("[briefing] 新闻获取失败 (降级): %v", err)
logger.Printf("[briefing] 新闻获取失败 (降级): %v", err)
}
briefing.News = news
log.Printf("[briefing] 新闻: %d 条", len(news))
logger.Printf("[briefing] 新闻: %d 条", len(news))
// 4. 生成 AI 摘要
log.Printf("[briefing] 生成 AI 摘要...")
logger.Printf("[briefing] 生成 AI 摘要...")
summary, err := h.generateAISummary(briefing)
if err != nil {
log.Printf("[briefing] AI 摘要生成失败 (降级): %v", err)
logger.Printf("[briefing] AI 摘要生成失败 (降级): %v", err)
summary = h.buildFallbackSummary(briefing)
briefing.SummarySource = "fallback"
} else {
@@ -238,7 +238,7 @@ func (h *BriefingHandler) GenerateDailyBriefing(userID string) (*store.Briefing,
return nil, fmt.Errorf("保存简报失败: %w", err)
}
log.Printf("[briefing] 简报已生成: user=%s date=%s", userID, today)
logger.Printf("[briefing] 简报已生成: user=%s date=%s", userID, today)
return briefing, nil
}
@@ -362,7 +362,7 @@ func (h *BriefingHandler) fetchNews() ([]store.NewsItem, error) {
}
if result.Error != "" {
log.Printf("[briefing] 新闻搜索失败: %s", result.Error)
logger.Printf("[briefing] 新闻搜索失败: %s", result.Error)
// 返回降级新闻
return []store.NewsItem{
{
@@ -585,7 +585,7 @@ func (h *BriefingHandler) pushBriefingNotification(userID string, b *store.Brief
data, err := json.Marshal(msg)
if err != nil {
log.Printf("[briefing] 序列化简报通知失败: %v", err)
logger.Printf("[briefing] 序列化简报通知失败: %v", err)
return
}
@@ -596,10 +596,10 @@ func (h *BriefingHandler) pushBriefingNotification(userID string, b *store.Brief
b.Status = "delivered"
b.DeliveredAt = &now
if err := h.briefingStore.CreateOrUpdateBriefing(b); err != nil {
log.Printf("[briefing] 更新简报送达状态失败: %v", err)
logger.Printf("[briefing] 更新简报送达状态失败: %v", err)
}
log.Printf("[briefing] 简报通知已推送: user=%s date=%s", userID, b.Date)
logger.Printf("[briefing] 简报通知已推送: user=%s date=%s", userID, b.Date)
}
// StartBriefingScheduler 启动简报调度器
@@ -614,7 +614,7 @@ func StartBriefingScheduler(handler *BriefingHandler, briefingStore *store.Brief
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
log.Printf("[BriefingScheduler] 简报调度器已启动 (简报时间: %s)", briefingTime)
logger.Printf("[BriefingScheduler] 简报调度器已启动 (简报时间: %s)", briefingTime)
// 记录今天是否已触发
lastTriggeredDate := ""
@@ -626,13 +626,13 @@ func StartBriefingScheduler(handler *BriefingHandler, briefingStore *store.Brief
// 检查是否到达简报时间且今天尚未触发
if currentTime == briefingTime && currentDate != lastTriggeredDate {
log.Printf("[BriefingScheduler] 触发每日简报生成: %s", currentDate)
logger.Printf("[BriefingScheduler] 触发每日简报生成: %s", currentDate)
lastTriggeredDate = currentDate
// 获取所有用户
users, err := briefingStore.GetAllUsers()
if err != nil {
log.Printf("[BriefingScheduler] 获取用户列表失败: %v", err)
logger.Printf("[BriefingScheduler] 获取用户列表失败: %v", err)
continue
}
@@ -642,21 +642,21 @@ func StartBriefingScheduler(handler *BriefingHandler, briefingStore *store.Brief
}
if len(users) == 0 {
log.Println("[BriefingScheduler] 没有找到用户,跳过简报生成")
logger.Println("[BriefingScheduler] 没有找到用户,跳过简报生成")
continue
}
for _, userID := range users {
log.Printf("[BriefingScheduler] 为用户 %s 生成简报...", userID)
logger.Printf("[BriefingScheduler] 为用户 %s 生成简报...", userID)
result, err := handler.GenerateDailyBriefing(userID)
if err != nil {
log.Printf("[BriefingScheduler] 生成简报失败: user=%s err=%v", userID, err)
logger.Printf("[BriefingScheduler] 生成简报失败: user=%s err=%v", userID, err)
continue
}
handler.pushBriefingNotification(userID, result)
}
log.Printf("[BriefingScheduler] 每日简报已生成完毕,共 %d 个用户", len(users))
logger.Printf("[BriefingScheduler] 每日简报已生成完毕,共 %d 个用户", len(users))
}
}
}()
@@ -8,7 +8,7 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"net/http"
"strings"
"time"
@@ -88,7 +88,7 @@ func (h *ChatHandler) HandleWebSocket(c *gin.Context) {
// 升级WebSocket连接
conn, err := h.upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Printf("[WS] 升级连接失败: %v", err)
logger.Printf("[WS] 升级连接失败: %v", err)
return
}
@@ -115,7 +115,7 @@ func (h *ChatHandler) handleMessage(client *ws.Client, msg ws.ClientMessage) {
case "history":
h.handleHistoryRequest(client, msg)
default:
log.Printf("[WS] 未知消息类型: %s from user=%s", msg.Type, client.UserID)
logger.Printf("[WS] 未知消息类型: %s from user=%s", msg.Type, client.UserID)
}
}
@@ -128,8 +128,8 @@ func (h *ChatHandler) handleChatMessage(client *ws.Client, msg ws.ClientMessage)
// 持久化用户消息到数据库(在 WebSocket 发送之前)
if h.sessionStore != nil && h.sessionStore.IsAvailable() {
if err := h.sessionStore.AddMessage(client.SessionID, "user", msg.Content); err != nil {
log.Printf("[chat] 持久化用户消息失败: %v", err)
if err := h.sessionStore.AddMessage(client.SessionID, "user", "chat", msg.Content); err != nil {
logger.Printf("[chat] 持久化用户消息失败: %v", err)
}
}
@@ -151,7 +151,7 @@ func (h *ChatHandler) handleChatMessage(client *ws.Client, msg ws.ClientMessage)
}
reqBody, err := json.Marshal(aiReq)
if err != nil {
log.Printf("[chat] 序列化请求失败: %v", err)
logger.Printf("[chat] 序列化请求失败: %v", err)
h.hub.UpdateSessionState(client.SessionID, "error")
client.SendMessage(ws.ServerMessage{
Type: "error",
@@ -183,7 +183,7 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
aiCoreURL := h.cfg.AICoreURL + "/api/v1/chat"
httpReq, err := http.NewRequest("POST", aiCoreURL, bytes.NewReader(reqBody))
if err != nil {
log.Printf("[chat] 创建 AI-Core 请求失败: %v", err)
logger.Printf("[chat] 创建 AI-Core 请求失败: %v", err)
h.hub.UpdateSessionState(client.SessionID, "error")
client.SendMessage(ws.ServerMessage{
Type: "error",
@@ -199,7 +199,7 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
httpClient := &http.Client{Timeout: 120 * time.Second}
resp, err := httpClient.Do(httpReq)
if err != nil {
log.Printf("[chat] AI-Core 调用失败: %v", err)
logger.Printf("[chat] AI-Core 调用失败: %v", err)
h.hub.UpdateSessionState(client.SessionID, "error")
client.SendMessage(ws.ServerMessage{
Type: "error",
@@ -213,7 +213,7 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
log.Printf("[chat] AI-Core 返回错误 [%d]: %s", resp.StatusCode, string(body))
logger.Printf("[chat] AI-Core 返回错误 [%d]: %s", resp.StatusCode, string(body))
h.hub.UpdateSessionState(client.SessionID, "error")
client.SendMessage(ws.ServerMessage{
Type: "error",
@@ -273,13 +273,13 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
ReviewMessages []ws.ReviewMessage `json:"review_messages,omitempty"`
}
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
log.Printf("[chat] 解析 SSE delta 失败: %v, raw=%s", err, data)
logger.Printf("[chat] 解析 SSE delta 失败: %v, raw=%s", err, data)
continue
}
// 错误处理
if chunk.Error != "" {
log.Printf("[chat] AI-Core 流式错误: %s", chunk.Error)
logger.Printf("[chat] AI-Core 流式错误: %s", chunk.Error)
h.hub.UpdateSessionState(client.SessionID, "error")
client.SendMessage(ws.ServerMessage{
Type: "error",
@@ -312,8 +312,8 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
reviewMsgID := fmt.Sprintf("%s_r%d", msgID, i)
// 持久化每条审查消息
if h.sessionStore != nil && h.sessionStore.IsAvailable() {
if err := h.sessionStore.AddMessage(client.SessionID, role, rm.Content); err != nil {
log.Printf("[chat] 持久化审查消息失败: %v", err)
if err := h.sessionStore.AddMessage(client.SessionID, role, msgType, rm.Content); err != nil {
logger.Printf("[chat] 持久化审查消息失败: %v", err)
}
}
h.hub.CacheMessage(client.UserID, client.SessionID, ws.Message{
@@ -331,9 +331,9 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
SessionID: client.SessionID,
Timestamp: time.Now().UnixMilli(),
})
// 小延迟让消息逐条到达,更像真人
if i < len(chunk.ReviewMessages)-1 {
time.Sleep(800 * time.Millisecond)
// 使用 MessageScheduler 计算的 per-message 延迟
if rm.DelayMs > 0 {
time.Sleep(time.Duration(rm.DelayMs) * time.Millisecond)
}
}
hasReview = true
@@ -366,7 +366,7 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
}
if err := scanner.Err(); err != nil {
log.Printf("[chat] SSE 读取错误: %v", err)
logger.Printf("[chat] SSE 读取错误: %v", err)
h.hub.UpdateSessionState(client.SessionID, "error")
client.SendMessage(ws.ServerMessage{
Type: "error",
@@ -416,8 +416,8 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
// 如果有审查消息,每条已单独持久化,跳过 fullText 以避免重复
if !hasReview && fullText != "" {
if h.sessionStore != nil && h.sessionStore.IsAvailable() {
if err := h.sessionStore.AddMessage(client.SessionID, "assistant", fullText); err != nil {
log.Printf("[chat] 持久化 AI 回复失败: %v", err)
if err := h.sessionStore.AddMessage(client.SessionID, "assistant", "chat", fullText); err != nil {
logger.Printf("[chat] 持久化 AI 回复失败: %v", err)
}
}
@@ -466,18 +466,20 @@ func (h *ChatHandler) handleHistoryRequest(client *ws.Client, msg ws.ClientMessa
if len(messages) == 0 && h.sessionStore != nil && h.sessionStore.IsAvailable() {
dbMessages, err := h.sessionStore.GetMessages(sessionID, 50, 0)
if err == nil && len(dbMessages) > 0 {
log.Printf("[history] 从数据库恢复会话历史: session=%s, %d 条消息", sessionID, len(dbMessages))
logger.Printf("[history] 从数据库恢复会话历史: session=%s, %d 条消息", sessionID, len(dbMessages))
// 恢复到内存缓存
for _, dbMsg := range dbMessages {
messages = append(messages, ws.Message{
ID: fmt.Sprintf("db_%d", dbMsg.ID),
Role: dbMsg.Role,
MsgType: dbMsg.MsgType,
Content: dbMsg.Content,
Timestamp: dbMsg.CreatedAt.UnixMilli(),
})
h.hub.CacheMessage(client.UserID, sessionID, ws.Message{
ID: fmt.Sprintf("db_%d", dbMsg.ID),
Role: dbMsg.Role,
MsgType: dbMsg.MsgType,
Content: dbMsg.Content,
Timestamp: dbMsg.CreatedAt.UnixMilli(),
})
@@ -497,7 +499,7 @@ func (h *ChatHandler) handleHistoryRequest(client *ws.Client, msg ws.ClientMessa
}
if err := client.SendMessage(response); err != nil {
log.Printf("[WS] 发送历史消息失败: %v", err)
logger.Printf("[WS] 发送历史消息失败: %v", err)
}
}
@@ -535,10 +537,22 @@ func (h *ChatHandler) HandleProactiveMessage(c *gin.Context) {
// 检查用户是否在线
onlineCount := h.hub.UserClientCount(req.UserID)
if onlineCount == 0 {
// Phase 2: 离线时排队,等待用户重连后推送
data, _ := json.Marshal(ws.ServerMessage{
Type: "response",
MessageID: "proactive_" + generateID(),
Content: req.Content,
Role: "assistant",
MsgType: "proactive",
SessionID: req.SessionID,
Timestamp: time.Now().UnixMilli(),
})
h.hub.QueueProactiveMessage(req.UserID, data)
logger.Printf("[proactive] 用户离线,消息已排队: user=%s", req.UserID)
c.JSON(http.StatusOK, gin.H{
"success": false,
"reason": "user_offline",
"message": "用户不在线,消息未发送",
"success": true,
"reason": "queued",
"message": "用户线,消息已排队等待重连后推送",
})
return
}
@@ -557,7 +571,7 @@ func (h *ChatHandler) HandleProactiveMessage(c *gin.Context) {
data, err := json.Marshal(msg)
if err != nil {
log.Printf("[proactive] 序列化消息失败: %v", err)
logger.Printf("[proactive] 序列化消息失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "内部错误"})
return
}
@@ -577,7 +591,7 @@ func (h *ChatHandler) HandleProactiveMessage(c *gin.Context) {
})
h.hub.RecordMessage(sessionID, "assistant", req.Content)
log.Printf("[proactive] 主动消息已推送: user=%s, online=%d, content_len=%d", req.UserID, onlineCount, len(req.Content))
logger.Printf("[proactive] 主动消息已推送: user=%s, online=%d, content_len=%d", req.UserID, onlineCount, len(req.Content))
c.JSON(http.StatusOK, gin.H{
"success": true,
@@ -10,7 +10,7 @@ import (
"image/jpeg"
"image/png"
"io"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"net/http"
"os"
"path/filepath"
@@ -144,7 +144,7 @@ func (h *FileHandler) Upload(c *gin.Context) {
dateDir := time.Now().Format("2006-01-02")
storedDir := filepath.Join(h.uploadDir, dateDir)
if err := os.MkdirAll(storedDir, 0755); err != nil {
log.Printf("[FileHandler] 创建上传目录失败: %v", err)
logger.Printf("[FileHandler] 创建上传目录失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建上传目录失败", "errorType": "server_error"})
return
}
@@ -160,7 +160,7 @@ func (h *FileHandler) Upload(c *gin.Context) {
// 保存到磁盘
dst, err := os.Create(storedPath)
if err != nil {
log.Printf("[FileHandler] 创建文件失败: %v", err)
logger.Printf("[FileHandler] 创建文件失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败", "errorType": "server_error"})
return
}
@@ -169,7 +169,7 @@ func (h *FileHandler) Upload(c *gin.Context) {
written, err := io.Copy(dst, teeReader)
if err != nil {
os.Remove(storedPath)
log.Printf("[FileHandler] 写入文件失败: %v", err)
logger.Printf("[FileHandler] 写入文件失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "写入文件失败", "errorType": "server_error"})
return
}
@@ -180,7 +180,7 @@ func (h *FileHandler) Upload(c *gin.Context) {
if existing, err := h.store.GetFileByHash(hash); err == nil && existing != nil {
// 删除刚保存的重复文件
os.Remove(storedPath)
log.Printf("[FileHandler] 文件去重: 复用已有文件 %s (hash=%s)", existing.ID, hash[:16])
logger.Printf("[FileHandler] 文件去重: 复用已有文件 %s (hash=%s)", existing.ID, hash[:16])
c.JSON(http.StatusOK, gin.H{
"id": existing.ID,
@@ -208,12 +208,12 @@ func (h *FileHandler) Upload(c *gin.Context) {
if err := h.store.CreateFile(fileRecord); err != nil {
os.Remove(storedPath)
log.Printf("[FileHandler] 创建文件记录失败: %v", err)
logger.Printf("[FileHandler] 创建文件记录失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建文件记录失败", "errorType": "db_error"})
return
}
log.Printf("[FileHandler] 文件上传成功: %s (%s, %d bytes, hash=%s)", fileID, safeFilename, written, hash[:16])
logger.Printf("[FileHandler] 文件上传成功: %s (%s, %d bytes, hash=%s)", fileID, safeFilename, written, hash[:16])
c.JSON(http.StatusCreated, gin.H{
"id": fileID,
@@ -240,7 +240,7 @@ func (h *FileHandler) List(c *gin.Context) {
files, total, err := h.store.GetUserFiles(userID, page, limit)
if err != nil {
log.Printf("[FileHandler] 查询文件列表失败: %v", err)
logger.Printf("[FileHandler] 查询文件列表失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询文件列表失败", "errorType": "db_error"})
return
}
@@ -288,7 +288,7 @@ func (h *FileHandler) Get(c *gin.Context) {
f, err := h.store.GetFile(fileID)
if err != nil {
log.Printf("[FileHandler] 查询文件失败: %v", err)
logger.Printf("[FileHandler] 查询文件失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询文件失败", "errorType": "db_error"})
return
}
@@ -338,7 +338,7 @@ func (h *FileHandler) Download(c *gin.Context) {
f, err := h.store.GetFile(fileID)
if err != nil {
log.Printf("[FileHandler] 查询文件失败: %v", err)
logger.Printf("[FileHandler] 查询文件失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询文件失败", "errorType": "db_error"})
return
}
@@ -385,7 +385,7 @@ func (h *FileHandler) Delete(c *gin.Context) {
f, err := h.store.GetFile(fileID)
if err != nil {
log.Printf("[FileHandler] 查询文件失败: %v", err)
logger.Printf("[FileHandler] 查询文件失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询文件失败", "errorType": "db_error"})
return
}
@@ -405,12 +405,12 @@ func (h *FileHandler) Delete(c *gin.Context) {
// 删除磁盘上的文件(忽略错误,可能已被删除)
if err := os.Remove(f.StoredPath); err != nil && !os.IsNotExist(err) {
log.Printf("[FileHandler] 删除磁盘文件失败 (stored_path=%s): %v", f.StoredPath, err)
logger.Printf("[FileHandler] 删除磁盘文件失败 (stored_path=%s): %v", f.StoredPath, err)
}
// 删除数据库记录
if err := h.store.DeleteFile(fileID); err != nil {
log.Printf("[FileHandler] 删除文件记录失败: %v", err)
logger.Printf("[FileHandler] 删除文件记录失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除文件记录失败", "errorType": "db_error"})
return
}
@@ -432,7 +432,7 @@ func (h *FileHandler) Thumbnail(c *gin.Context) {
f, err := h.store.GetFile(fileID)
if err != nil {
log.Printf("[FileHandler] 查询文件失败: %v", err)
logger.Printf("[FileHandler] 查询文件失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询文件失败", "errorType": "db_error"})
return
}
@@ -457,7 +457,7 @@ func (h *FileHandler) Thumbnail(c *gin.Context) {
c.Data(http.StatusOK, contentType, thumbData)
return
} else {
log.Printf("[FileHandler] 生成缩略图失败: %v", err)
logger.Printf("[FileHandler] 生成缩略图失败: %v", err)
}
}
@@ -11,7 +11,7 @@ import (
_ "image/jpeg"
_ "image/png"
"io"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"net/http"
"os"
"sort"
@@ -116,7 +116,7 @@ func (h *ImageHandler) analyzeByFileID(c *gin.Context, userID, fileID string) {
f, err := h.fileStore.GetFile(fileID)
if err != nil {
log.Printf("[ImageHandler] 查询文件失败: %v", err)
logger.Printf("[ImageHandler] 查询文件失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询文件失败", "errorType": "db_error"})
return
}
@@ -135,7 +135,7 @@ func (h *ImageHandler) analyzeByFileID(c *gin.Context, userID, fileID string) {
result, err := h.analyzeImage(f.StoredPath, f.MimeType, f.Size)
if err != nil {
log.Printf("[ImageHandler] 图片分析失败: %v", err)
logger.Printf("[ImageHandler] 图片分析失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "图片分析失败: " + err.Error(), "errorType": "analysis_error"})
return
}
@@ -192,7 +192,7 @@ func (h *ImageHandler) analyzeUploadedFile(c *gin.Context, userID string, file i
result, err := h.analyzeImage(tmpFile.Name(), mimeType, int64(len(data)))
if err != nil {
log.Printf("[ImageHandler] 图片分析失败: %v", err)
logger.Printf("[ImageHandler] 图片分析失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "图片分析失败: " + err.Error(), "errorType": "analysis_error"})
return
}
@@ -209,7 +209,7 @@ func (h *ImageHandler) analyzeImage(filePath, mimeType string, fileSize int64) (
if err == nil {
return result, nil
}
log.Printf("[ImageHandler] OpenAI Vision 分析失败,降级到本地分析: %v", err)
logger.Printf("[ImageHandler] OpenAI Vision 分析失败,降级到本地分析: %v", err)
}
// 降级到本地分析
@@ -2,7 +2,7 @@ package handler
import (
"html"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"net/http"
"os"
"strings"
@@ -83,7 +83,7 @@ func (h *KnowledgeHandler) CreateKB(c *gin.Context) {
}
if err := h.store.CreateKB(kb); err != nil {
log.Printf("[KnowledgeHandler] 创建知识库失败: %v", err)
logger.Printf("[KnowledgeHandler] 创建知识库失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建知识库失败", "errorType": "db_error"})
return
}
@@ -101,7 +101,7 @@ func (h *KnowledgeHandler) ListKBs(c *gin.Context) {
kbs, err := h.store.GetKBsByUser(userID)
if err != nil {
log.Printf("[KnowledgeHandler] 查询知识库列表失败: %v", err)
logger.Printf("[KnowledgeHandler] 查询知识库列表失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询知识库列表失败", "errorType": "db_error"})
return
}
@@ -120,7 +120,7 @@ func (h *KnowledgeHandler) GetKB(c *gin.Context) {
kb, err := h.store.GetKB(kbID)
if err != nil {
log.Printf("[KnowledgeHandler] 查询知识库失败: %v", err)
logger.Printf("[KnowledgeHandler] 查询知识库失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询知识库失败", "errorType": "db_error"})
return
}
@@ -136,7 +136,7 @@ func (h *KnowledgeHandler) GetKB(c *gin.Context) {
// 获取文档列表
docs, err := h.store.GetDocumentsByKB(kbID)
if err != nil {
log.Printf("[KnowledgeHandler] 查询文档列表失败: %v", err)
logger.Printf("[KnowledgeHandler] 查询文档列表失败: %v", err)
docs = []store.KnowledgeDocument{}
}
@@ -163,7 +163,7 @@ func (h *KnowledgeHandler) UpdateKB(c *gin.Context) {
kb, err := h.store.GetKB(kbID)
if err != nil {
log.Printf("[KnowledgeHandler] 查询知识库失败: %v", err)
logger.Printf("[KnowledgeHandler] 查询知识库失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询知识库失败", "errorType": "db_error"})
return
}
@@ -177,7 +177,7 @@ func (h *KnowledgeHandler) UpdateKB(c *gin.Context) {
}
if err := h.store.UpdateKB(kbID, html.EscapeString(req.Name), html.EscapeString(req.Description)); err != nil {
log.Printf("[KnowledgeHandler] 更新知识库失败: %v", err)
logger.Printf("[KnowledgeHandler] 更新知识库失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新知识库失败", "errorType": "db_error"})
return
}
@@ -196,7 +196,7 @@ func (h *KnowledgeHandler) DeleteKB(c *gin.Context) {
kb, err := h.store.GetKB(kbID)
if err != nil {
log.Printf("[KnowledgeHandler] 查询知识库失败: %v", err)
logger.Printf("[KnowledgeHandler] 查询知识库失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询知识库失败", "errorType": "db_error"})
return
}
@@ -210,7 +210,7 @@ func (h *KnowledgeHandler) DeleteKB(c *gin.Context) {
}
if err := h.store.DeleteKB(kbID); err != nil {
log.Printf("[KnowledgeHandler] 删除知识库失败: %v", err)
logger.Printf("[KnowledgeHandler] 删除知识库失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除知识库失败", "errorType": "db_error"})
return
}
@@ -230,7 +230,7 @@ func (h *KnowledgeHandler) AddDocument(c *gin.Context) {
// 检查知识库是否存在且属于当前用户
kb, err := h.store.GetKB(kbID)
if err != nil {
log.Printf("[KnowledgeHandler] 查询知识库失败: %v", err)
logger.Printf("[KnowledgeHandler] 查询知识库失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询知识库失败", "errorType": "db_error"})
return
}
@@ -324,7 +324,7 @@ func (h *KnowledgeHandler) AddDocument(c *gin.Context) {
}
if err := h.store.AddDocument(doc); err != nil {
log.Printf("[KnowledgeHandler] 添加文档失败: %v", err)
logger.Printf("[KnowledgeHandler] 添加文档失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "添加文档失败", "errorType": "db_error"})
return
}
@@ -332,7 +332,7 @@ func (h *KnowledgeHandler) AddDocument(c *gin.Context) {
// 自动分块
chunkCount, err := h.store.ChunkDocument(doc.ID)
if err != nil {
log.Printf("[KnowledgeHandler] 文档分块失败: %v", err)
logger.Printf("[KnowledgeHandler] 文档分块失败: %v", err)
// 分块失败不影响文档创建
}
@@ -353,7 +353,7 @@ func (h *KnowledgeHandler) ListDocuments(c *gin.Context) {
// 检查权限
kb, err := h.store.GetKB(kbID)
if err != nil {
log.Printf("[KnowledgeHandler] 查询知识库失败: %v", err)
logger.Printf("[KnowledgeHandler] 查询知识库失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询知识库失败", "errorType": "db_error"})
return
}
@@ -368,7 +368,7 @@ func (h *KnowledgeHandler) ListDocuments(c *gin.Context) {
docs, err := h.store.GetDocumentsByKB(kbID)
if err != nil {
log.Printf("[KnowledgeHandler] 查询文档列表失败: %v", err)
logger.Printf("[KnowledgeHandler] 查询文档列表失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询文档列表失败", "errorType": "db_error"})
return
}
@@ -387,7 +387,7 @@ func (h *KnowledgeHandler) GetDocument(c *gin.Context) {
doc, err := h.store.GetDocument(docID)
if err != nil {
log.Printf("[KnowledgeHandler] 查询文档失败: %v", err)
logger.Printf("[KnowledgeHandler] 查询文档失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询文档失败", "errorType": "db_error"})
return
}
@@ -403,7 +403,7 @@ func (h *KnowledgeHandler) GetDocument(c *gin.Context) {
// 获取分块
chunks, err := h.store.GetChunksByDocID(docID)
if err != nil {
log.Printf("[KnowledgeHandler] 查询分块失败: %v", err)
logger.Printf("[KnowledgeHandler] 查询分块失败: %v", err)
chunks = []store.KnowledgeChunk{}
}
@@ -424,7 +424,7 @@ func (h *KnowledgeHandler) DeleteDocument(c *gin.Context) {
doc, err := h.store.GetDocument(docID)
if err != nil {
log.Printf("[KnowledgeHandler] 查询文档失败: %v", err)
logger.Printf("[KnowledgeHandler] 查询文档失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询文档失败", "errorType": "db_error"})
return
}
@@ -438,7 +438,7 @@ func (h *KnowledgeHandler) DeleteDocument(c *gin.Context) {
}
if err := h.store.DeleteDocument(docID); err != nil {
log.Printf("[KnowledgeHandler] 删除文档失败: %v", err)
logger.Printf("[KnowledgeHandler] 删除文档失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除文档失败", "errorType": "db_error"})
return
}
@@ -483,7 +483,7 @@ func (h *KnowledgeHandler) Search(c *gin.Context) {
}
kbResults, searchErr := h.store.SearchChunks(kbID, req.Query, req.Limit)
if searchErr != nil {
log.Printf("[KnowledgeHandler] 搜索知识库 %s 失败: %v", kbID, searchErr)
logger.Printf("[KnowledgeHandler] 搜索知识库 %s 失败: %v", kbID, searchErr)
continue
}
results = append(results, kbResults...)
@@ -496,7 +496,7 @@ func (h *KnowledgeHandler) Search(c *gin.Context) {
}
if err != nil {
log.Printf("[KnowledgeHandler] 搜索失败: %v", err)
logger.Printf("[KnowledgeHandler] 搜索失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "搜索失败", "errorType": "db_error"})
return
}
@@ -6,7 +6,7 @@ import (
"fmt"
"html"
"io"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"net/http"
"time"
@@ -65,7 +65,7 @@ func (h *MemoryHandler) Query(c *gin.Context) {
resp, err := h.client.Do(httpReq)
if err != nil {
log.Printf("[memory] Memory-Service 不可达 (Query): %v", err)
logger.Printf("[memory] Memory-Service 不可达 (Query): %v", err)
c.JSON(http.StatusBadGateway, gin.H{
"error": fmt.Sprintf("Memory-Service 不可达: %v", err),
"errorType": "memory_service_unreachable",
@@ -97,7 +97,7 @@ func (h *MemoryHandler) List(c *gin.Context) {
resp, err := h.client.Get(url)
if err != nil {
log.Printf("[memory] Memory-Service 不可达 (List): %v", err)
logger.Printf("[memory] Memory-Service 不可达 (List): %v", err)
c.JSON(http.StatusBadGateway, gin.H{
"error": fmt.Sprintf("Memory-Service 不可达: %v", err),
"errorType": "memory_service_unreachable",
@@ -163,7 +163,7 @@ func (h *MemoryHandler) Add(c *gin.Context) {
resp, err := h.client.Do(httpReq)
if err != nil {
log.Printf("[memory] Memory-Service 不可达 (Add): %v", err)
logger.Printf("[memory] Memory-Service 不可达 (Add): %v", err)
c.JSON(http.StatusBadGateway, gin.H{
"error": fmt.Sprintf("Memory-Service 不可达: %v", err),
"errorType": "memory_service_unreachable",
@@ -198,7 +198,7 @@ func (h *MemoryHandler) Delete(c *gin.Context) {
resp, err := h.client.Do(req)
if err != nil {
log.Printf("[memory] Memory-Service 不可达 (Delete): %v", err)
logger.Printf("[memory] Memory-Service 不可达 (Delete): %v", err)
c.JSON(http.StatusBadGateway, gin.H{
"error": fmt.Sprintf("Memory-Service 不可达: %v", err),
"errorType": "memory_service_unreachable",
@@ -2,7 +2,7 @@ package handler
import (
"encoding/json"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"net/http"
"time"
@@ -54,7 +54,7 @@ func (h *NotificationHandler) Push(c *gin.Context) {
data, err := json.Marshal(msg)
if err != nil {
log.Printf("[notification] 序列化通知失败: %v", err)
logger.Printf("[notification] 序列化通知失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "内部错误"})
return
}
@@ -62,7 +62,7 @@ func (h *NotificationHandler) Push(c *gin.Context) {
// 通过 Hub 推送给指定用户
h.hub.SendToUser(req.UserID, data)
log.Printf("[notification] 通知已推送: user=%s type=%s title=%s", req.UserID, req.Type, req.Title)
logger.Printf("[notification] 通知已推送: user=%s type=%s title=%s", req.UserID, req.Type, req.Title)
c.JSON(http.StatusOK, gin.H{
"success": true,
@@ -99,7 +99,7 @@ func (h *NotificationHandler) InternalNotify(c *gin.Context) {
data, err := json.Marshal(msg)
if err != nil {
log.Printf("[notification] 序列化通知失败: %v", err)
logger.Printf("[notification] 序列化通知失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "内部错误"})
return
}
@@ -107,7 +107,7 @@ func (h *NotificationHandler) InternalNotify(c *gin.Context) {
// 通过 Hub 推送给指定用户
h.hub.SendToUser(req.UserID, data)
log.Printf("[notification] 内部通知已推送: user=%s type=%s title=%s", req.UserID, req.Type, req.Title)
logger.Printf("[notification] 内部通知已推送: user=%s type=%s title=%s", req.UserID, req.Type, req.Title)
c.JSON(http.StatusOK, gin.H{
"success": true,
@@ -3,7 +3,7 @@ package handler
import (
"encoding/json"
"html"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"net/http"
"strconv"
"time"
@@ -71,7 +71,7 @@ func (h *ReminderHandler) List(c *gin.Context) {
reminders, err := h.store.GetRemindersByUser(userID, status, limit, offset)
if err != nil {
log.Printf("[reminder] 获取提醒列表失败: %v", err)
logger.Printf("[reminder] 获取提醒列表失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取提醒列表失败"})
return
}
@@ -124,12 +124,12 @@ func (h *ReminderHandler) Create(c *gin.Context) {
}
if err := h.store.CreateReminder(reminder); err != nil {
log.Printf("[reminder] 创建提醒失败: %v", err)
logger.Printf("[reminder] 创建提醒失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建提醒失败"})
return
}
log.Printf("[reminder] 提醒已创建: id=%s user=%s title=%s remind_at=%s repeat=%s",
logger.Printf("[reminder] 提醒已创建: id=%s user=%s title=%s remind_at=%s repeat=%s",
reminder.ID, userID, reminder.Title, remindAt.Format(time.RFC3339), repeatType)
c.JSON(http.StatusCreated, gin.H{
@@ -204,12 +204,12 @@ func (h *ReminderHandler) Update(c *gin.Context) {
}
if err := h.store.UpdateReminder(id, existing); err != nil {
log.Printf("[reminder] 更新提醒失败: %v", err)
logger.Printf("[reminder] 更新提醒失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新提醒失败"})
return
}
log.Printf("[reminder] 提醒已更新: id=%s", id)
logger.Printf("[reminder] 提醒已更新: id=%s", id)
c.JSON(http.StatusOK, gin.H{
"success": true,
@@ -227,12 +227,12 @@ func (h *ReminderHandler) Delete(c *gin.Context) {
}
if err := h.store.DeleteReminder(id); err != nil {
log.Printf("[reminder] 删除提醒失败: %v", err)
logger.Printf("[reminder] 删除提醒失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除提醒失败"})
return
}
log.Printf("[reminder] 提醒已删除: id=%s", id)
logger.Printf("[reminder] 提醒已删除: id=%s", id)
c.JSON(http.StatusOK, gin.H{
"success": true,
@@ -247,7 +247,7 @@ func StartReminderScheduler(s *store.ReminderStore, hub *ws.Hub) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
log.Println("[ReminderScheduler] 提醒调度器已启动 (检查间隔: 30秒)")
logger.Println("[ReminderScheduler] 提醒调度器已启动 (检查间隔: 30秒)")
for range ticker.C {
checkAndNotify(s, hub)
@@ -259,7 +259,7 @@ func StartReminderScheduler(s *store.ReminderStore, hub *ws.Hub) {
func checkAndNotify(s *store.ReminderStore, hub *ws.Hub) {
reminders, err := s.GetDueReminders()
if err != nil {
log.Printf("[ReminderScheduler] 获取到期提醒失败: %v", err)
logger.Printf("[ReminderScheduler] 获取到期提醒失败: %v", err)
return
}
@@ -290,7 +290,7 @@ func checkAndNotify(s *store.ReminderStore, hub *ws.Hub) {
data, err := json.Marshal(msg)
if err != nil {
log.Printf("[ReminderScheduler] 序列化通知失败: %v", err)
logger.Printf("[ReminderScheduler] 序列化通知失败: %v", err)
continue
}
@@ -299,7 +299,7 @@ func checkAndNotify(s *store.ReminderStore, hub *ws.Hub) {
// 3. 标记为已通知
if err := s.MarkNotified(r.ID); err != nil {
log.Printf("[ReminderScheduler] 标记已通知失败: id=%s err=%v", r.ID, err)
logger.Printf("[ReminderScheduler] 标记已通知失败: id=%s err=%v", r.ID, err)
}
// 4. 处理重复提醒
@@ -308,9 +308,9 @@ func checkAndNotify(s *store.ReminderStore, hub *ws.Hub) {
r.RemindAt = nextTime
r.Notified = false
if err := s.UpdateReminder(r.ID, &r); err != nil {
log.Printf("[ReminderScheduler] 更新重复提醒失败: id=%s err=%v", r.ID, err)
logger.Printf("[ReminderScheduler] 更新重复提醒失败: id=%s err=%v", r.ID, err)
} else {
log.Printf("[ReminderScheduler] 重复提醒已更新: id=%s next=%s", r.ID, nextTime.Format(time.RFC3339))
logger.Printf("[ReminderScheduler] 重复提醒已更新: id=%s next=%s", r.ID, nextTime.Format(time.RFC3339))
}
} else {
// 非重复提醒:标记为已完成
@@ -318,11 +318,11 @@ func checkAndNotify(s *store.ReminderStore, hub *ws.Hub) {
r.Status = "completed"
r.CompletedAt = &now
if err := s.UpdateReminder(r.ID, &r); err != nil {
log.Printf("[ReminderScheduler] 标记提醒完成失败: id=%s err=%v", r.ID, err)
logger.Printf("[ReminderScheduler] 标记提醒完成失败: id=%s err=%v", r.ID, err)
}
}
log.Printf("[ReminderScheduler] 提醒已推送: user=%s title=%s id=%s", r.UserID, r.Title, r.ID)
logger.Printf("[ReminderScheduler] 提醒已推送: user=%s title=%s id=%s", r.UserID, r.Title, r.ID)
}
}
@@ -4,7 +4,7 @@ import (
"crypto/rand"
"encoding/json"
"fmt"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"net/http"
"strings"
"time"
@@ -68,7 +68,7 @@ func (h *SessionHandler) Create(c *gin.Context) {
if h.useDB {
if err := h.store.CreateSession(userID, req.SessionID, req.Title, req.IsMain); err != nil {
log.Printf("[SessionHandler] 创建会话失败: %v", err)
logger.Printf("[SessionHandler] 创建会话失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建会话失败", "errorType": "db_error"})
return
}
@@ -106,7 +106,7 @@ func (h *SessionHandler) List(c *gin.Context) {
if h.useDB {
sessions, err := h.store.GetUserSessions(userID)
if err != nil {
log.Printf("[SessionHandler] 查询会话列表失败: %v", err)
logger.Printf("[SessionHandler] 查询会话列表失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询会话失败", "errorType": "db_error"})
return
}
@@ -139,7 +139,7 @@ func (h *SessionHandler) Get(c *gin.Context) {
if h.useDB {
session, err := h.store.GetSession(sessionID)
if err != nil {
log.Printf("[SessionHandler] 查询会话失败: %v", err)
logger.Printf("[SessionHandler] 查询会话失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询会话失败", "errorType": "db_error"})
return
}
@@ -186,7 +186,7 @@ func (h *SessionHandler) Delete(c *gin.Context) {
// 所有权校验:先获取session再验证归属
session, err := h.store.GetSession(sessionID)
if err != nil {
log.Printf("[SessionHandler] 查询会话失败: %v", err)
logger.Printf("[SessionHandler] 查询会话失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除会话失败", "errorType": "db_error"})
return
}
@@ -203,7 +203,7 @@ func (h *SessionHandler) Delete(c *gin.Context) {
}
if err := h.store.DeleteSession(sessionID); err != nil {
log.Printf("[SessionHandler] 删除会话失败: %v", err)
logger.Printf("[SessionHandler] 删除会话失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除会话失败", "errorType": "db_error"})
return
}
@@ -235,7 +235,7 @@ func (h *SessionHandler) DeleteAll(c *gin.Context) {
if h.useDB {
if err := h.store.DeleteAllUserSessions(userID); err != nil {
log.Printf("[SessionHandler] 删除用户所有会话失败: %v", err)
logger.Printf("[SessionHandler] 删除用户所有会话失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除会话失败", "errorType": "db_error"})
return
}
@@ -254,7 +254,7 @@ func (h *SessionHandler) GetMessages(c *gin.Context) {
if h.useDB {
session, err := h.store.GetSession(sessionID)
if err != nil {
log.Printf("[SessionHandler] 查询会话失败: %v", err)
logger.Printf("[SessionHandler] 查询会话失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询消息失败", "errorType": "db_error"})
return
}
@@ -302,7 +302,7 @@ func (h *SessionHandler) GetMessages(c *gin.Context) {
if h.useDB {
messages, err := h.store.GetMessages(sessionID, limit, offset)
if err != nil {
log.Printf("[SessionHandler] 查询消息失败: %v", err)
logger.Printf("[SessionHandler] 查询消息失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询消息失败", "errorType": "db_error"})
return
}
@@ -313,6 +313,7 @@ func (h *SessionHandler) GetMessages(c *gin.Context) {
"id": m.ID,
"session_id": m.SessionID,
"role": m.Role,
"msg_type": m.MsgType,
"content": m.Content,
"created_at": m.CreatedAt.UnixMilli(),
})
@@ -339,7 +340,7 @@ func (h *SessionHandler) ClearMessages(c *gin.Context) {
// 所有权校验
session, err := h.store.GetSession(sessionID)
if err != nil {
log.Printf("[SessionHandler] 查询会话失败: %v", err)
logger.Printf("[SessionHandler] 查询会话失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "清空消息失败", "errorType": "db_error"})
return
}
@@ -356,7 +357,7 @@ func (h *SessionHandler) ClearMessages(c *gin.Context) {
}
if err := h.store.ClearSessionMessages(sessionID); err != nil {
log.Printf("[SessionHandler] 清空消息失败: %v", err)
logger.Printf("[SessionHandler] 清空消息失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "清空消息失败", "errorType": "db_error"})
return
}
@@ -469,7 +470,7 @@ func (h *SessionHandler) SearchMessages(c *gin.Context) {
results, total, err := h.store.SearchMessages(userID, query, limit, offset)
if err != nil {
log.Printf("[SessionHandler] 搜索消息失败: %v", err)
logger.Printf("[SessionHandler] 搜索消息失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "搜索失败", "errorType": "db_error"})
return
}
@@ -531,7 +532,7 @@ func (h *SessionHandler) ExportSession(c *gin.Context) {
// 获取会话信息
session, err := h.store.GetSession(sessionID)
if err != nil {
log.Printf("[SessionHandler] 查询会话失败: %v", err)
logger.Printf("[SessionHandler] 查询会话失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询会话失败", "errorType": "db_error"})
return
}
@@ -552,7 +553,7 @@ func (h *SessionHandler) ExportSession(c *gin.Context) {
// 获取所有消息 (不限制数量,导出全部)
messages, err := h.store.GetMessages(sessionID, 0, 0)
if err != nil {
log.Printf("[SessionHandler] 查询消息失败: %v", err)
logger.Printf("[SessionHandler] 查询消息失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询消息失败", "errorType": "db_error"})
return
}
@@ -613,7 +614,7 @@ func (h *SessionHandler) exportJSON(c *gin.Context, session *store.Session, mess
jsonBytes, err := json.MarshalIndent(data, "", " ")
if err != nil {
log.Printf("[SessionHandler] JSON序列化失败: %v", err)
logger.Printf("[SessionHandler] JSON序列化失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "导出失败", "errorType": "serialization_error"})
return
}
@@ -4,7 +4,7 @@ import (
"bytes"
"encoding/json"
"io"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"net/http"
"strings"
@@ -50,7 +50,7 @@ func (h *VoiceHandler) Transcribe(c *gin.Context) {
resp, err := h.client.Do(proxyReq)
if err != nil {
log.Printf("[voice] Voice-Service 不可达 (Transcribe): %v", err)
logger.Printf("[voice] Voice-Service 不可达 (Transcribe): %v", err)
c.JSON(http.StatusBadGateway, gin.H{
"error": "Voice-Service 不可达: " + err.Error(),
"errorType": "voice_service_unreachable",
@@ -88,7 +88,7 @@ func (h *VoiceHandler) TTSSynthesize(c *gin.Context) {
resp, err := h.client.Do(proxyReq)
if err != nil {
log.Printf("[voice] Voice-Service 不可达 (TTS): %v", err)
logger.Printf("[voice] Voice-Service 不可达 (TTS): %v", err)
c.JSON(http.StatusBadGateway, gin.H{
"error": "Voice-Service 不可达: " + err.Error(),
"errorType": "voice_service_unreachable",
@@ -115,7 +115,7 @@ func (h *VoiceHandler) TTSVoices(c *gin.Context) {
resp, err := h.client.Get(url)
if err != nil {
log.Printf("[voice] Voice-Service 不可达 (Voices): %v", err)
logger.Printf("[voice] Voice-Service 不可达 (Voices): %v", err)
c.JSON(http.StatusBadGateway, gin.H{
"error": "Voice-Service 不可达: " + err.Error(),
"errorType": "voice_service_unreachable",
@@ -139,7 +139,7 @@ func (h *VoiceHandler) TTSStatus(c *gin.Context) {
resp, err := h.client.Get(url)
if err != nil {
log.Printf("[voice] Voice-Service 不可达 (TTS Status): %v", err)
logger.Printf("[voice] Voice-Service 不可达 (TTS Status): %v", err)
c.JSON(http.StatusBadGateway, gin.H{
"error": "Voice-Service 不可达: " + err.Error(),
"errorType": "voice_service_unreachable",
@@ -162,7 +162,7 @@ func (h *VoiceHandler) VoiceStatus(c *gin.Context) {
resp, err := h.client.Get(url)
if err != nil {
log.Printf("[voice] Voice-Service 不可达 (Status): %v", err)
logger.Printf("[voice] Voice-Service 不可达 (Status): %v", err)
c.JSON(http.StatusBadGateway, gin.H{
"error": "Voice-Service 不可达: " + err.Error(),
"errorType": "voice_service_unreachable",
@@ -8,7 +8,7 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"net/http"
"strings"
"time"
@@ -95,7 +95,7 @@ func (h *WebhookHandler) HandleGenericWebhook(c *gin.Context) {
// 调用 AI-Core 获取回复
resp, err := h.callAICore(userID, sessionID, req.Message, mode, platform)
if err != nil {
log.Printf("[webhook] AI-Core 调用失败 (platform=%s): %v", platform, err)
logger.Printf("[webhook] AI-Core 调用失败 (platform=%s): %v", platform, err)
c.JSON(502, GenericWebhookResponse{Error: "AI 服务暂不可用: " + err.Error()})
return
}
@@ -244,7 +244,7 @@ func (h *WebhookHandler) HandleDiscordWebhook(c *gin.Context) {
// 但这里简化处理:直接同步调用 AI-Core(如果调用超过 3 秒,Discord 会显示超时)
resp, err := h.callAICore("ext_"+userID, sessionID, message, "text", "discord")
if err != nil {
log.Printf("[webhook:discord] AI-Core 调用失败: %v", err)
logger.Printf("[webhook:discord] AI-Core 调用失败: %v", err)
c.JSON(200, DiscordResponse{
Type: 4,
Data: &DiscordResponseData{Content: "昔涟暂时无法回应喵...AI 服务异常: " + err.Error() + ""},
@@ -1,7 +1,7 @@
package middleware
import (
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"time"
"github.com/gin-gonic/gin"
@@ -29,7 +29,7 @@ func RequestLogging() gin.HandlerFunc {
logLevel = "[WARN]"
}
log.Printf("%s %s %s %d %v %s",
logger.Printf("%s %s %s %d %v %s",
logLevel, method, path, statusCode, duration, clientIP,
)
}
@@ -4,7 +4,7 @@ import (
"database/sql"
"encoding/json"
"fmt"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"time"
)
@@ -47,7 +47,7 @@ func NewAutomationStore(db *sql.DB) (*AutomationStore, error) {
return nil, fmt.Errorf("自动化表迁移失败: %w", err)
}
log.Println("[AutomationStore] 自动化持久化存储已初始化")
logger.Println("[AutomationStore] 自动化持久化存储已初始化")
return store, nil
}
@@ -4,7 +4,7 @@ import (
"database/sql"
"encoding/json"
"fmt"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"time"
)
@@ -60,7 +60,7 @@ func NewBriefingStore(db *sql.DB) (*BriefingStore, error) {
return nil, fmt.Errorf("简报表迁移失败: %w", err)
}
log.Println("[BriefingStore] 简报持久化存储已初始化")
logger.Println("[BriefingStore] 简报持久化存储已初始化")
return store, nil
}
+2 -2
View File
@@ -3,7 +3,7 @@ package store
import (
"database/sql"
"fmt"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"time"
)
@@ -33,7 +33,7 @@ func NewFileStore(db *sql.DB) (*FileStore, error) {
return nil, fmt.Errorf("文件表迁移失败: %w", err)
}
log.Println("[FileStore] 文件持久化存储已初始化")
logger.Println("[FileStore] 文件持久化存储已初始化")
return store, nil
}
@@ -4,7 +4,7 @@ import (
"crypto/rand"
"database/sql"
"fmt"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"strings"
"time"
"unicode/utf8"
@@ -73,7 +73,7 @@ func NewKnowledgeStore(db *sql.DB) (*KnowledgeStore, error) {
return nil, fmt.Errorf("知识库表迁移失败: %w", err)
}
log.Println("[KnowledgeStore] 知识库持久化存储已初始化")
logger.Println("[KnowledgeStore] 知识库持久化存储已初始化")
return store, nil
}
@@ -133,7 +133,7 @@ func (s *KnowledgeStore) migrate() error {
// 尝试创建 GIN 索引(可能因权限或扩展问题失败,但不影响功能)
_, err := s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_kc_tsv_gin ON knowledge_chunks USING GIN(tsv)`)
if err != nil {
log.Printf("[KnowledgeStore] ⚠ GIN索引创建失败(将使用ILIKE降级搜索): %v", err)
logger.Printf("[KnowledgeStore] ⚠ GIN索引创建失败(将使用ILIKE降级搜索): %v", err)
}
return nil
@@ -260,7 +260,7 @@ func (s *KnowledgeStore) AddDocument(doc *KnowledgeDocument) error {
// 更新知识库统计
if err := s.updateKBStats(doc.KBID); err != nil {
log.Printf("[KnowledgeStore] 更新知识库统计失败: %v", err)
logger.Printf("[KnowledgeStore] 更新知识库统计失败: %v", err)
}
return nil
@@ -340,7 +340,7 @@ func (s *KnowledgeStore) DeleteDocument(id string) error {
// 更新知识库统计
if err := s.updateKBStats(kbID); err != nil {
log.Printf("[KnowledgeStore] 更新知识库统计失败: %v", err)
logger.Printf("[KnowledgeStore] 更新知识库统计失败: %v", err)
}
return nil
@@ -452,12 +452,12 @@ func (s *KnowledgeStore) ChunkDocument(docID string) (int, error) {
// 更新文档的分块计数
if err := s.UpdateDocumentChunkCount(docID, len(chunks)); err != nil {
log.Printf("[KnowledgeStore] 更新文档分块计数失败: %v", err)
logger.Printf("[KnowledgeStore] 更新文档分块计数失败: %v", err)
}
// 更新知识库统计
if err := s.updateKBStats(doc.KBID); err != nil {
log.Printf("[KnowledgeStore] 更新知识库统计失败: %v", err)
logger.Printf("[KnowledgeStore] 更新知识库统计失败: %v", err)
}
return len(chunks), nil
@@ -474,7 +474,7 @@ func (s *KnowledgeStore) SearchChunks(kbID, query string, limit int) ([]SearchCh
// 尝试使用 PostgreSQL 全文搜索
results, err := s.searchWithFullText(kbID, query, limit)
if err != nil {
log.Printf("[KnowledgeStore] 全文搜索失败,降级为ILIKE: %v", err)
logger.Printf("[KnowledgeStore] 全文搜索失败,降级为ILIKE: %v", err)
// 降级为 ILIKE
results, err = s.searchWithILike(kbID, query, limit)
if err != nil {
@@ -3,7 +3,7 @@ package store
import (
"database/sql"
"fmt"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"time"
)
@@ -35,7 +35,7 @@ func NewReminderStore(db *sql.DB) (*ReminderStore, error) {
return nil, fmt.Errorf("提醒表迁移失败: %w", err)
}
log.Println("[ReminderStore] 提醒持久化存储已初始化")
logger.Println("[ReminderStore] 提醒持久化存储已初始化")
return store, nil
}
@@ -3,7 +3,7 @@ package store
import (
"database/sql"
"fmt"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"time"
_ "github.com/lib/pq"
@@ -24,6 +24,7 @@ type Message struct {
ID int `json:"id"`
SessionID string `json:"session_id"`
Role string `json:"role"`
MsgType string `json:"msg_type"`
Content string `json:"content"`
CreatedAt time.Time `json:"created_at"`
}
@@ -60,7 +61,7 @@ func NewSessionStore(databaseURL string) (*SessionStore, error) {
return nil, fmt.Errorf("数据库迁移失败: %w", err)
}
log.Println("[SessionStore] PostgreSQL 持久化存储已初始化")
logger.Println("[SessionStore] PostgreSQL 持久化存储已初始化")
return store, nil
}
@@ -82,11 +83,15 @@ func (s *SessionStore) migrate() error {
id SERIAL PRIMARY KEY,
session_id VARCHAR(64) REFERENCES sessions(id) ON DELETE CASCADE,
role VARCHAR(16) NOT NULL,
msg_type VARCHAR(16) DEFAULT 'chat',
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
)`,
`CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id)`,
`CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(session_id, created_at)`,
// 为已存在的数据库添加 msg_type 列 (Phase 0.1)
`ALTER TABLE messages ADD COLUMN IF NOT EXISTS msg_type VARCHAR(16) DEFAULT 'chat'`,
}
for _, q := range queries {
@@ -200,10 +205,10 @@ func (s *SessionStore) DeleteAllUserSessions(userID string) error {
}
// AddMessage 添加一条消息到会话
func (s *SessionStore) AddMessage(sessionID, role, content string) error {
func (s *SessionStore) AddMessage(sessionID, role, msgType, content string) error {
_, err := s.db.Exec(
`INSERT INTO messages (session_id, role, content) VALUES ($1, $2, $3)`,
sessionID, role, content,
`INSERT INTO messages (session_id, role, msg_type, content) VALUES ($1, $2, $3, $4)`,
sessionID, role, msgType, content,
)
if err != nil {
return fmt.Errorf("添加消息失败: %w", err)
@@ -221,7 +226,7 @@ func (s *SessionStore) GetMessages(sessionID string, limit, offset int) ([]Messa
}
rows, err := s.db.Query(
`SELECT id, session_id, role, content, created_at
`SELECT id, session_id, role, COALESCE(msg_type, 'chat'), content, created_at
FROM messages WHERE session_id = $1
ORDER BY created_at ASC
LIMIT $2 OFFSET $3`,
@@ -235,7 +240,7 @@ func (s *SessionStore) GetMessages(sessionID string, limit, offset int) ([]Messa
var messages []Message
for rows.Next() {
var msg Message
if err := rows.Scan(&msg.ID, &msg.SessionID, &msg.Role, &msg.Content, &msg.CreatedAt); err != nil {
if err := rows.Scan(&msg.ID, &msg.SessionID, &msg.Role, &msg.MsgType, &msg.Content, &msg.CreatedAt); err != nil {
return nil, fmt.Errorf("扫描消息行失败: %w", err)
}
messages = append(messages, msg)
@@ -262,6 +267,7 @@ type SearchResult struct {
SessionID string `json:"session_id"`
SessionTitle string `json:"session_title"`
Role string `json:"role"`
MsgType string `json:"msg_type"`
Content string `json:"content"`
CreatedAt time.Time `json:"created_at"`
}
@@ -287,7 +293,7 @@ func (s *SessionStore) SearchMessages(userID, query string, limit, offset int) (
// 分页查询,关联 sessions 获取会话标题
rows, err := s.db.Query(
`SELECT m.id, m.session_id, COALESCE(s.title, '') AS session_title, m.role, m.content, m.created_at
`SELECT m.id, m.session_id, COALESCE(s.title, '') AS session_title, m.role, COALESCE(m.msg_type, 'chat'), m.content, m.created_at
FROM messages m
JOIN sessions s ON m.session_id = s.id
WHERE s.user_id = $1 AND m.content ILIKE '%' || $2 || '%'
@@ -303,7 +309,7 @@ func (s *SessionStore) SearchMessages(userID, query string, limit, offset int) (
var results []SearchResult
for rows.Next() {
var r SearchResult
if err := rows.Scan(&r.MessageID, &r.SessionID, &r.SessionTitle, &r.Role, &r.Content, &r.CreatedAt); err != nil {
if err := rows.Scan(&r.MessageID, &r.SessionID, &r.SessionTitle, &r.Role, &r.MsgType, &r.Content, &r.CreatedAt); err != nil {
return nil, 0, fmt.Errorf("扫描搜索结果行失败: %w", err)
}
results = append(results, r)
+5 -5
View File
@@ -2,7 +2,7 @@ package ws
import (
"encoding/json"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"time"
"github.com/gorilla/websocket"
@@ -60,7 +60,7 @@ func (c *Client) ReadPump(onMessage func(client *Client, msg ClientMessage)) {
_, rawMessage, err := c.Conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure, websocket.CloseAbnormalClosure) {
log.Printf("[WS] 读取错误: user=%s err=%v", c.UserID, err)
logger.Printf("[WS] 读取错误: user=%s err=%v", c.UserID, err)
}
break
}
@@ -68,7 +68,7 @@ func (c *Client) ReadPump(onMessage func(client *Client, msg ClientMessage)) {
// 解析消息
var msg ClientMessage
if err := json.Unmarshal(rawMessage, &msg); err != nil {
log.Printf("[WS] 消息解析失败: user=%s err=%v", c.UserID, err)
logger.Printf("[WS] 消息解析失败: user=%s err=%v", c.UserID, err)
continue
}
@@ -109,7 +109,7 @@ func (c *Client) WritePump() {
}
if err := c.Conn.WriteMessage(websocket.TextMessage, message); err != nil {
log.Printf("[WS] 写入错误: user=%s err=%v", c.UserID, err)
logger.Printf("[WS] 写入错误: user=%s err=%v", c.UserID, err)
return
}
@@ -134,7 +134,7 @@ func (c *Client) SendMessage(msg ServerMessage) error {
return nil
default:
// 通道满:记录警告并返回错误(避免静默丢弃)
log.Printf("[WS] 发送通道已满,丢弃消息: type=%s user=%s", msg.Type, c.UserID)
logger.Printf("[WS] 发送通道已满,丢弃消息: type=%s user=%s", msg.Type, c.UserID)
return nil
}
}
+93 -15
View File
@@ -3,9 +3,10 @@ package ws
import (
"encoding/json"
"fmt"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"net/http"
"os"
"strings"
"sync"
"time"
@@ -70,6 +71,11 @@ type Hub struct {
// 闲置超时时间
idleTimeout time.Duration
// Phase 2: 离线主动消息队列 + 在线状态通知
pendingProactive map[string][]json.RawMessage // userID -> queued messages
aiCoreURL string
internalToken string
}
// SetStore 设置持久化存储 (可选)
@@ -93,6 +99,7 @@ func NewHub() *Hub {
sessions: make(map[string]*SessionState),
iotStopCh: make(chan struct{}),
idleTimeout: 30 * time.Minute, // 默认30分钟
pendingProactive: make(map[string][]json.RawMessage),
}
}
@@ -102,7 +109,7 @@ func (h *Hub) StartIdleCleanup() {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("[WS] 闲置会话清理 panic 恢复: %v", r)
logger.Printf("[WS] 闲置会话清理 panic 恢复: %v", r)
}
}()
ticker := time.NewTicker(5 * time.Minute)
@@ -112,7 +119,7 @@ func (h *Hub) StartIdleCleanup() {
h.cleanupIdleSessions()
}
}()
log.Printf("[WS] 闲置会话清理已启动 (超时: %v)", h.idleTimeout)
logger.Printf("[WS] 闲置会话清理已启动 (超时: %v)", h.idleTimeout)
}
// cleanupIdleSessions 标记超时会话为 idle(不删除状态)
@@ -148,7 +155,7 @@ func (h *Hub) cleanupIdleSessions() {
}
if idleCount > 0 {
log.Printf("[WS] 闲置清理: %d 个会话标记为 idle", idleCount)
logger.Printf("[WS] 闲置清理: %d 个会话标记为 idle", idleCount)
}
}
@@ -170,6 +177,58 @@ func (h *Hub) GetAllActiveSessions() []*SessionState {
return result
}
// SetAICoreConfig sets the ai-core URL and internal token for presence notifications.
func (h *Hub) SetAICoreConfig(url, token string) {
h.aiCoreURL = url
h.internalToken = token
}
// QueueProactiveMessage queues a proactive message for offline delivery.
func (h *Hub) QueueProactiveMessage(userID string, msg json.RawMessage) {
h.mu.Lock()
defer h.mu.Unlock()
h.pendingProactive[userID] = append(h.pendingProactive[userID], msg)
// Keep only the most recent 3 messages
if len(h.pendingProactive[userID]) > 3 {
h.pendingProactive[userID] = h.pendingProactive[userID][1:]
}
}
// FlushPendingProactive returns and clears queued proactive messages for a user.
func (h *Hub) FlushPendingProactive(userID string) []json.RawMessage {
h.mu.Lock()
defer h.mu.Unlock()
msgs := h.pendingProactive[userID]
delete(h.pendingProactive, userID)
return msgs
}
// notifyAICorePresence sends a presence update to ai-core.
func (h *Hub) notifyAICorePresence(userID, status, sessionID string) {
if h.aiCoreURL == "" || h.internalToken == "" {
return
}
body, _ := json.Marshal(map[string]string{
"user_id": userID,
"status": status,
"session_id": sessionID,
"timestamp": fmt.Sprintf("%d", time.Now().Unix()),
})
go func() {
req, _ := http.NewRequest("POST", h.aiCoreURL+"/api/v1/internal/presence", strings.NewReader(string(body)))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Internal-Token", h.internalToken)
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
logger.Printf("[presence] 通知 ai-core 失败: %v", err)
return
}
resp.Body.Close()
logger.Printf("[presence] 通知 ai-core: user=%s status=%s", userID, status)
}()
}
// Run 启动Hub主循环
func (h *Hub) Run() {
for {
@@ -195,10 +254,29 @@ func (h *Hub) Run() {
MessageCount: 0,
}
}
h.mu.Unlock()
// Phase 2: 检测是否为重连 (之前处于离线状态)
wasOffline := len(h.userClients[client.UserID]) == 1 // 刚加入,之前为0
h.mu.Unlock()
log.Printf("[WS] 客户端连接: user=%s session=%s (当前连接数: %d)",
client.UserID, client.SessionID, len(h.clients))
// 重连后推送积压的主动消息
if wasOffline {
pending := h.FlushPendingProactive(client.UserID)
if len(pending) > 0 {
logger.Printf("[proactive] 推送 %d 条积压消息给重连用户 %s", len(pending), client.UserID)
// 只推送最新的一条
go func() {
// small delay for WS connection to stabilize
time.Sleep(500 * time.Millisecond)
h.SendToUser(client.UserID, pending[len(pending)-1])
}()
}
}
// 通知 ai-core 用户上线
h.notifyAICorePresence(client.UserID, "online", client.SessionID)
logger.Printf("[WS] 客户端连接: user=%s session=%s (当前连接数: %d)",
client.UserID, client.SessionID, len(h.clients))
case client := <-h.unregister:
h.mu.Lock()
@@ -233,7 +311,7 @@ func (h *Hub) Run() {
}
h.mu.Unlock()
log.Printf("[WS] 客户端断开: user=%s session=%s (当前连接数: %d)",
logger.Printf("[WS] 客户端断开: user=%s session=%s (当前连接数: %d)",
client.UserID, client.SessionID, len(h.clients))
case message := <-h.broadcast:
@@ -287,7 +365,7 @@ func (h *Hub) Run() {
}
h.mu.Unlock()
log.Printf("[WS] 广播清理 %d 个失效客户端 (当前连接数: %d)",
logger.Printf("[WS] 广播清理 %d 个失效客户端 (当前连接数: %d)",
len(staleClients), len(h.clients))
}
}
@@ -504,12 +582,12 @@ func (h *Hub) StartIoTBroadcast(iotServiceURL string) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("[IoT广播] 轮询循环 panic 恢复: %v", r)
logger.Printf("[IoT广播] 轮询循环 panic 恢复: %v", r)
}
}()
h.iotPollLoop()
}()
log.Printf("[IoT广播] 已启动 (IoT服务地址: %s)", iotServiceURL)
logger.Printf("[IoT广播] 已启动 (IoT服务地址: %s)", iotServiceURL)
}
// StopIoTBroadcast 停止 IoT 设备广播
@@ -522,7 +600,7 @@ func (h *Hub) StopIoTBroadcast() {
}
close(h.iotStopCh)
h.iotPollRunning = false
log.Println("[IoT广播] 已停止")
logger.Println("[IoT广播] 已停止")
}
// iotPollLoop IoT 设备轮询循环
@@ -563,7 +641,7 @@ func (h *Hub) pollAndBroadcastIoT() {
devices, err := fetchIoTDevices(url)
if err != nil {
log.Printf("[IoT广播] 获取设备失败: %v", err)
logger.Printf("[IoT广播] 获取设备失败: %v", err)
// 即使失败也发送空列表,让前端知道 IoT 服务状态
devices = []IotDeviceInfo{}
}
@@ -576,7 +654,7 @@ func (h *Hub) pollAndBroadcastIoT() {
data, err := json.Marshal(msg)
if err != nil {
log.Printf("[IoT广播] 消息序列化失败: %v", err)
logger.Printf("[IoT广播] 消息序列化失败: %v", err)
return
}
@@ -586,7 +664,7 @@ func (h *Hub) pollAndBroadcastIoT() {
for _, d := range devices {
deviceNames = append(deviceNames, d.Name)
}
log.Printf("[IoT广播] 已推送 %d 个设备状态到 %d 个客户端: %v", len(devices), h.ClientCount(), deviceNames)
logger.Printf("[IoT广播] 已推送 %d 个设备状态到 %d 个客户端: %v", len(devices), h.ClientCount(), deviceNames)
}
// fetchIoTDevices 从 IoT 调试服务获取设备列表
+40 -20
View File
@@ -25,31 +25,51 @@ type ClientMessage struct {
// ReviewMessage 审查后的结构化消息(动作/聊天分离)
type ReviewMessage struct {
Type string `json:"type"` // "action" | "chat"
Type string `json:"type"` // "action" | "chat"
Content string `json:"content"`
DelayMs int `json:"delay_ms,omitempty"` // ms to wait before sending (0 = immediate)
}
// 服务端 → 客户端消息
type ServerMessage struct {
Type string `json:"type"` // response | segment | audio | error | device_update | pong | history_response | stream_chunk | stream_end | background_thinking | notification | multi_message | stream_segments | review
MessageID string `json:"message_id"`
Text string `json:"text,omitempty"`
Content string `json:"content,omitempty"` // stream_chunk 的增量文本
Role string `json:"role,omitempty"` // stream 消息的角色
SessionID string `json:"session_id,omitempty"` // 会话 ID
Segments []VoiceSegment `json:"segments,omitempty"` // 断句数组
FullAudioURL string `json:"full_audio_url,omitempty"`
ResponseMode string `json:"response_mode"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
Error string `json:"error,omitempty"`
Timestamp int64 `json:"timestamp"`
Messages []Message `json:"messages,omitempty"` // 历史消息列表
Devices []IotDeviceInfo `json:"devices,omitempty"` // IoT 设备状态
ThinkingStatus string `json:"thinking_status,omitempty"` // 后台思考状态
Notification *NotificationInfo `json:"notification,omitempty"` // 通知推送
MultiMessage *MultiMessagePayload `json:"multi_message,omitempty"` // 多条消息批量发
ReviewMessages []ReviewMessage `json:"review_messages,omitempty"` // 审查后的结构化消息列表
MsgType string `json:"msg_type,omitempty"` // 消息展示类型: action | chat
Type string `json:"type"` // response | segment | audio | error | device_update | pong | history_response | stream_chunk | stream_end | background_thinking | notification | multi_message | stream_segments | review | thinking | tool_progress | system_info
MessageID string `json:"message_id"`
Text string `json:"text,omitempty"`
Content string `json:"content,omitempty"` // stream_chunk 的增量文本
Role string `json:"role,omitempty"` // stream 消息的角色
SessionID string `json:"session_id,omitempty"` // 会话 ID
Segments []VoiceSegment `json:"segments,omitempty"` // 断句数组
FullAudioURL string `json:"full_audio_url,omitempty"`
ResponseMode string `json:"response_mode"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
Error string `json:"error,omitempty"`
Timestamp int64 `json:"timestamp"`
Messages []Message `json:"messages,omitempty"` // 历史消息列表
Devices []IotDeviceInfo `json:"devices,omitempty"` // IoT 设备状态
ThinkingStatus string `json:"thinking_status,omitempty"` // 后台思考状态
ThinkingContent string `json:"thinking_content,omitempty"` // 思考内容 (thinking 类型)
Notification *NotificationInfo `json:"notification,omitempty"` // 通知推
MultiMessage *MultiMessagePayload `json:"multi_message,omitempty"` // 多条消息批量发送
ReviewMessages []ReviewMessage `json:"review_messages,omitempty"` // 审查后的结构化消息列表
MsgType string `json:"msg_type,omitempty"` // 消息展示类型: action | chat | thinking | tool_progress | system_info
ToolProgress *ToolProgressInfo `json:"tool_progress,omitempty"` // 工具执行进度
SystemInfo *SystemInfoPayload `json:"system_info,omitempty"` // 系统通知信息
ProtocolVersion int `json:"protocol_version,omitempty"` // 协议版本
}
// ToolProgressInfo 工具执行进度
type ToolProgressInfo struct {
ToolName string `json:"tool_name"`
Status string `json:"status"` // started, running, completed, failed
Progress float64 `json:"progress"`
Message string `json:"message"`
}
// SystemInfoPayload 系统信息负载
type SystemInfoPayload struct {
Level string `json:"level"` // info, warning, error
Message string `json:"message"`
Action string `json:"action,omitempty"`
}
// MultiMessagePayload 多条消息的容器 (对应昔涟的多消息回复风格)