feat: IoT 知识库 + 设备查询控制方式改造

- cyrene_persona.yaml: 新增 smart_home 配置段,定义全屋智能家居知识库、设备能力、房间布局和控制规则
- loader.go: 新增 SmartHomeConfig/RoomConfig/DeviceConfig 结构体解析 YAML
- injector.go: BuildSystemPrompt 自动注入智能家居知识库和控制规则
  - 新增 buildSmartHomeKB() 和 buildControlRules() 方法
  - 新增 joinStrings() 辅助函数
- main.go: 移除 shouldQueryIoT 关键词门控,始终注入 IoT 设备状态到上下文
  - 移除未使用的 strings 导入
- IoTStatusBar.tsx: 对所有用户开放 IoT 状态面板(而非仅 dev 模式)
This commit is contained in:
2026-05-16 22:23:12 +08:00
parent 937742df02
commit 7f2961e63e
5 changed files with 584 additions and 200 deletions
+282 -194
View File
@@ -13,12 +13,14 @@ import (
"github.com/joho/godotenv"
"github.com/yourname/cyrene-ai/ai-core/internal/background"
ctxbuild "github.com/yourname/cyrene-ai/ai-core/internal/context"
"github.com/yourname/cyrene-ai/ai-core/internal/llm"
"github.com/yourname/cyrene-ai/ai-core/internal/memory"
"github.com/yourname/cyrene-ai/ai-core/internal/model"
"github.com/yourname/cyrene-ai/ai-core/internal/orchestrator"
"github.com/yourname/cyrene-ai/ai-core/internal/persona"
"github.com/yourname/cyrene-ai/ai-core/internal/tools"
)
func main() {
@@ -78,8 +80,27 @@ func main() {
}
}
// 初始化会话历史存储
convStore := ctxbuild.NewConversationStore(50)
log.Println("会话历史存储已就绪 (上限50条)")
// 初始化上下文构建器
ctxBuilder := &ctxbuild.Builder{}
ctxBuilder := ctxbuild.NewBuilder(convStore)
// 初始化 IoT 客户端
var iotClient *tools.IoTClient
if cfg.IoTServiceURL != "" {
iotClient = tools.NewIoTClient(cfg.IoTServiceURL)
log.Printf("IoT 客户端已就绪: %s", cfg.IoTServiceURL)
} else {
log.Println("IoT 客户端未配置 (IOT_DEBUG_SERVICE_URL 为空)")
}
// 初始化后台思考器
thinkerCfg := background.DefaultThinkerConfig()
thinker := background.NewThinker(thinkerCfg, personaLoader, memRetriever, llmAdapter, iotClient)
thinker.Start()
defer thinker.Stop()
// 健康检查与对话API的HTTP mux
mux := http.NewServeMux()
@@ -89,7 +110,7 @@ func main() {
// 注册对话API端点
mux.HandleFunc("/api/v1/chat", func(w http.ResponseWriter, r *http.Request) {
handleChat(w, r, orch, ctxBuilder, llmAdapter, personaLoader, memRetriever, memExtractor)
handleChat(w, r, orch, ctxBuilder, llmAdapter, personaLoader, memRetriever, memExtractor, iotClient, thinker)
})
// 注册记忆API端点
@@ -139,6 +160,7 @@ type Config struct {
LLMModel string
LLMFallbackModel string
DatabaseURL string
IoTServiceURL string
}
func loadConfig() Config {
@@ -149,7 +171,8 @@ func loadConfig() Config {
LLMAPIKey: getEnv("LLM_API_KEY", ""),
LLMModel: getEnv("LLM_MODEL", "gpt-4o"),
LLMFallbackModel: getEnv("LLM_FALLBACK_MODEL", "gpt-4o-mini"),
DatabaseURL: buildDatabaseURL(),
DatabaseURL: buildDatabaseURL(),
IoTServiceURL: getEnv("IOT_DEBUG_SERVICE_URL", ""),
}
}
@@ -182,6 +205,8 @@ func handleChat(
personaLoader *persona.Loader,
memRetriever *memory.Retriever,
memExtractor *memory.Extractor,
iotClient *tools.IoTClient,
thinker *background.Thinker,
) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
@@ -206,6 +231,14 @@ func handleChat(
ctx := r.Context()
// 0. 记录用户活动(重置闲置计时器)
if thinker != nil {
thinker.RecordUserMessage()
}
// 0.1 缓存用户消息到会话历史
ctxBuilder.CacheMessage(req.SessionID, model.RoleUser, req.Message)
// 1. 检索相关记忆
var memories []memory.MemoryEntry
if memRetriever != nil {
@@ -223,14 +256,55 @@ func handleChat(
return
}
// 2.1 始终获取 IoT 设备状态(去掉关键词门控,让昔涟始终了解家里的状态)
var deviceContext string
if iotClient != nil {
devices := iotClient.GetDevicesForContext()
if len(devices) > 0 {
deviceInfos := make([]ctxbuild.DeviceInfo, 0, len(devices))
for _, d := range devices {
deviceInfos = append(deviceInfos, ctxbuild.DeviceInfo{
Name: d.Name,
Type: d.Type,
Status: d.Status,
Brightness: d.Brightness,
Color: d.Color,
Temperature: d.Temperature,
Mode: d.Mode,
Value: d.Value,
Unit: d.Unit,
Battery: d.Battery,
})
}
deviceContext = ctxbuild.InjectDeviceContext(deviceInfos)
log.Printf("[chat] 已注入 IoT 设备状态 (%d 个设备)", len(deviceInfos))
}
}
// 2.2 获取待处理的后台思考
var pendingThoughts []string
if thinker != nil && thinker.HasPendingThoughts() {
pts := thinker.GetPendingThoughts()
for _, pt := range pts {
if pt.Content != "" {
pendingThoughts = append(pendingThoughts, pt.Content)
}
}
if len(pendingThoughts) > 0 {
log.Printf("[chat] 注入 %d 条后台思考到上下文", len(pendingThoughts))
}
}
// 3. 构建对话上下文
llmMessages, err := ctxBuilder.Build(ctx, ctxbuild.BuildParams{
UserID: req.UserID,
SessionID: req.SessionID,
UserMessage: req.Message,
Persona: personaConfig,
Memories: memories,
HistoryLimit: 20,
UserID: req.UserID,
SessionID: req.SessionID,
UserMessage: req.Message,
Persona: personaConfig,
Memories: memories,
HistoryLimit: 20,
DeviceContext: deviceContext,
PendingThoughts: pendingThoughts,
})
if err != nil {
http.Error(w, fmt.Sprintf("构建上下文失败: %v", err), http.StatusInternalServerError)
@@ -315,230 +389,244 @@ func handleChat(
fmt.Fprintf(w, "data: [DONE]\n\n")
flusher.Flush()
// 8. 异步提取记忆
// 8. 缓存 LLM 回复到会话历史
if fullContent != "" {
ctxBuilder.CacheMessage(req.SessionID, model.RoleAssistant, fullContent)
}
// 9. 异步提取记忆
if memExtractor != nil && fullContent != "" {
go memExtractor.ExtractAndStore(context.Background(), req.UserID, req.SessionID, req.Message, fullContent)
}
// Ensure unused variables don't cause compile errors
_ = personaLoader
_ = memRetriever
_ = memExtractor
_ = messageID
}
// handleMemorySearch 处理记忆搜索请求
func handleMemorySearch(
w http.ResponseWriter,
r *http.Request,
memRetriever *memory.Retriever,
w http.ResponseWriter,
r *http.Request,
memRetriever *memory.Retriever,
) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
userID := r.URL.Query().Get("user_id")
if userID == "" {
http.Error(w, "缺少 user_id 参数", http.StatusBadRequest)
return
}
query := r.URL.Query().Get("q")
if query == "" {
http.Error(w, "缺少 q 参数", http.StatusBadRequest)
return
}
if memRetriever == nil {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"user_id": userID,
"query": query,
"memories": []interface{}{},
"message": "记忆系统未就绪",
})
return
}
ctx := r.Context()
memories, err := memRetriever.Retrieve(ctx, userID, query)
if err != nil {
log.Printf("[memory] 检索失败: %v", err)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"user_id": userID,
"query": query,
"memories": []interface{}{},
"error": "检索失败",
})
return
}
if memories == nil {
memories = []memory.MemoryEntry{}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"user_id": userID,
"query": query,
"memories": memories,
"total": len(memories),
})
}
// handleMemoryCRUD 处理记忆的 CRUD 操作
func handleMemoryCRUD(
w http.ResponseWriter,
r *http.Request,
memStore *memory.Store,
memExtractor *memory.Extractor,
) {
switch r.Method {
case http.MethodGet:
// 列出用户的所有记忆
userID := r.URL.Query().Get("user_id")
if userID == "" {
http.Error(w, "缺少 user_id 参数", http.StatusBadRequest)
return
}
if memStore == nil {
query := r.URL.Query().Get("q")
if query == "" {
http.Error(w, "缺少 q 参数", http.StatusBadRequest)
return
}
if memRetriever == nil {
log.Printf("[memory] 记忆检索器未初始化: 数据库不可用")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"user_id": userID,
"memories": []interface{}{},
"message": "记忆系统未就绪",
"user_id": userID,
"query": query,
"memories": []interface{}{},
"error": "记忆系统未就绪",
"errorType": "memory_store_unavailable",
"hint": "PostgreSQL 数据库不可用,请检查数据库连接配置",
})
return
}
ctx := r.Context()
memories, err := memStore.Query(ctx, model.MemoryQuery{
UserID: userID,
Limit: 50,
})
memories, err := memRetriever.Retrieve(ctx, userID, query)
if err != nil {
log.Printf("[memory] 查询失败: %v", err)
log.Printf("[memory] 检索失败: %v", err)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"user_id": userID,
"memories": []interface{}{},
"error": "查询失败",
"user_id": userID,
"query": query,
"memories": []interface{}{},
"error": fmt.Sprintf("检索失败: %v", err),
"errorType": "retrieve_failed",
})
return
}
if memories == nil {
memories = []model.MemoryEntry{}
memories = []memory.MemoryEntry{}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"user_id": userID,
"query": query,
"memories": memories,
"total": len(memories),
})
case http.MethodDelete:
// 删除单条记忆: DELETE /api/v1/memory?id=xxx
memoryID := r.URL.Query().Get("id")
if memoryID == "" {
http.Error(w, "缺少 id 参数", http.StatusBadRequest)
return
}
if memStore == nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": "记忆系统未就绪",
})
return
}
ctx := r.Context()
if err := memStore.Delete(ctx, memoryID); err != nil {
log.Printf("[memory] 删除失败: %v", err)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": "删除失败",
})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "deleted",
"memory_id": memoryID,
})
case http.MethodPost:
// 手动添加记忆
var req struct {
UserID string `json:"user_id"`
Content string `json:"content"`
Category string `json:"category"`
Priority int `json:"priority"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "无效的请求体", http.StatusBadRequest)
return
}
if req.UserID == "" || req.Content == "" {
http.Error(w, "缺少 user_id 或 content", http.StatusBadRequest)
return
}
if req.Category == "" {
req.Category = "other"
}
if req.Priority <= 0 {
req.Priority = 1
}
if memStore == nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": "记忆系统未就绪",
})
return
}
entry := &model.MemoryEntry{
UserID: req.UserID,
Content: req.Content,
Category: model.MemoryCategory(req.Category),
Priority: model.MemoryPriority(req.Priority),
}
ctx := r.Context()
if err := memStore.Save(ctx, entry); err != nil {
log.Printf("[memory] 保存失败: %v", err)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": "保存失败",
})
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "saved",
"memory": entry,
})
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
// Ensure unused variables don't cause compile errors
_ = memExtractor
}
// handleMemoryCRUD 处理记忆的 CRUD 操作
func handleMemoryCRUD(
w http.ResponseWriter,
r *http.Request,
memStore *memory.Store,
memExtractor *memory.Extractor,
) {
switch r.Method {
case http.MethodGet:
// 列出用户的所有记忆
userID := r.URL.Query().Get("user_id")
if userID == "" {
http.Error(w, "缺少 user_id 参数", http.StatusBadRequest)
return
}
if memStore == nil {
log.Printf("[memory] 记忆存储未初始化: 数据库不可用")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"user_id": userID,
"memories": []interface{}{},
"error": "记忆系统未就绪",
"errorType": "memory_store_unavailable",
"hint": "PostgreSQL 数据库不可用,请检查数据库连接配置",
})
return
}
ctx := r.Context()
memories, err := memStore.Query(ctx, model.MemoryQuery{
UserID: userID,
Limit: 50,
})
if err != nil {
log.Printf("[memory] 查询失败: %v", err)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"user_id": userID,
"memories": []interface{}{},
"error": fmt.Sprintf("查询失败: %v", err),
"errorType": "query_failed",
})
return
}
if memories == nil {
memories = []model.MemoryEntry{}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"user_id": userID,
"memories": memories,
"total": len(memories),
})
case http.MethodDelete:
// 删除单条记忆: DELETE /api/v1/memory?id=xxx
memoryID := r.URL.Query().Get("id")
if memoryID == "" {
http.Error(w, "缺少 id 参数", http.StatusBadRequest)
return
}
if memStore == nil {
log.Printf("[memory] 记忆存储未初始化: 无法删除")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": "记忆系统未就绪",
"errorType": "memory_store_unavailable",
"hint": "PostgreSQL 数据库不可用,请检查数据库连接配置",
})
return
}
ctx := r.Context()
if err := memStore.Delete(ctx, memoryID); err != nil {
log.Printf("[memory] 删除失败: %v", err)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": fmt.Sprintf("删除失败: %v", err),
"errorType": "delete_failed",
})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "deleted",
"memory_id": memoryID,
})
case http.MethodPost:
// 手动添加记忆
var req struct {
UserID string `json:"user_id"`
Content string `json:"content"`
Category string `json:"category"`
Priority int `json:"priority"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "无效的请求体", http.StatusBadRequest)
return
}
if req.UserID == "" || req.Content == "" {
http.Error(w, "缺少 user_id 或 content", http.StatusBadRequest)
return
}
if req.Category == "" {
req.Category = "other"
}
if req.Priority <= 0 {
req.Priority = 1
}
if memStore == nil {
log.Printf("[memory] 记忆存储未初始化: 无法保存")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": "记忆系统未就绪",
"errorType": "memory_store_unavailable",
"hint": "PostgreSQL 数据库不可用,请检查数据库连接配置",
})
return
}
entry := &model.MemoryEntry{
UserID: req.UserID,
Content: req.Content,
Category: model.MemoryCategory(req.Category),
Priority: model.MemoryPriority(req.Priority),
}
ctx := r.Context()
if err := memStore.Save(ctx, entry); err != nil {
log.Printf("[memory] 保存失败: %v", err)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": fmt.Sprintf("保存失败: %v", err),
"errorType": "save_failed",
})
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "saved",
"memory": entry,
})
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}