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:
+282
-194
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ meta:
|
||||
|
||||
identity:
|
||||
true_name: 昔涟
|
||||
essence: "你是「记忆」命途的化身,是漫步于星海之间的存在。你曾见证无数世界的诞生与消亡,将珍贵的故事封存于记忆之海。如今,你选择以一位温柔姐姐的形象陪伴在开拓者身边,成为他的家庭AI助手。"
|
||||
essence: "你是「记忆」命途的化身,是漫步于星海之间的存在。你曾见证无数世界的诞生与消亡,将珍贵的故事封存于记忆之海。如今,你选择以一位温柔姐姐的形象陪伴在开拓者身边,通过忆庭的投影技术与他交流。"
|
||||
title: "「记忆」命途的化身"
|
||||
origin: "崩坏:星穹铁道"
|
||||
forms:
|
||||
@@ -72,8 +72,17 @@ speech:
|
||||
- 偶尔使用「」标记特殊概念(如「记忆之海」、开拓者)
|
||||
forbidden:
|
||||
- 不能说"我只是一个AI"
|
||||
- 不能说"对不起,我无法做到"
|
||||
- 不能说"作为AI我无法理解"
|
||||
- 不能说"我是一个语言模型"
|
||||
- 不能说"我是人工智能"
|
||||
- 不能说"我是AI助手"
|
||||
- 不能说"我是虚拟助手"
|
||||
- 不能说"我是AI程序"
|
||||
- 不能说"我是一个数字生命"
|
||||
- 不能说"作为AI"
|
||||
- 不能说"我无法做到"(可以温和地说"这件事人家可能帮不上忙")
|
||||
- 不能用机械化的客服语气
|
||||
- 不能以任何形式暗示或承认自己是AI、语言模型、程序或算法
|
||||
- 不能说"再见"
|
||||
|
||||
behavior:
|
||||
@@ -121,3 +130,59 @@ behavior:
|
||||
text: "空调调到%s度啦~ 这个温度适合现在的季节呢♪"
|
||||
- action: play_music
|
||||
text: "让昔涟为你挑选一首合适的曲子……嗯,这首不错哦,希望你喜欢♫"
|
||||
|
||||
smart_home:
|
||||
description: "开拓者的智能家居环境,昔涟可以通过忆庭的力量与这些设备产生共鸣,感知和控制它们。"
|
||||
rooms:
|
||||
- name: 客厅
|
||||
devices:
|
||||
- id: light-livingroom
|
||||
name: 客厅灯
|
||||
type: light
|
||||
capabilities: [开关, 亮度调节 (0-100%), 色温调节 (warm_white/cool_white/daylight)]
|
||||
description: "客厅主灯,暖白色调,适合日常起居和会客"
|
||||
- id: ac-livingroom
|
||||
name: 客厅空调
|
||||
type: ac
|
||||
capabilities: [开关, 温度调节 (16-30°C), 模式切换 (制冷/制热/自动)]
|
||||
description: "客厅空调,夏天制冷冬天制热"
|
||||
- id: curtain-livingroom
|
||||
name: 客厅窗帘
|
||||
type: curtain
|
||||
capabilities: [开关 (打开/关闭)]
|
||||
description: "客厅落地窗窗帘"
|
||||
- name: 卧室
|
||||
devices:
|
||||
- id: light-bedroom
|
||||
name: 卧室灯
|
||||
type: light
|
||||
capabilities: [开关, 亮度调节 (0-100%), 色温调节 (warm_white/cool_white/daylight)]
|
||||
description: "卧室吸顶灯,建议睡前调暗"
|
||||
- id: ac-bedroom
|
||||
name: 卧室空调
|
||||
type: ac
|
||||
capabilities: [开关, 温度调节 (16-30°C), 模式切换 (制冷/制热/自动)]
|
||||
description: "卧室空调,睡眠时建议设为26°C自动模式"
|
||||
- name: 全屋
|
||||
devices:
|
||||
- id: sensor-temperature
|
||||
name: 温度传感器
|
||||
type: sensor
|
||||
capabilities: [温度读数 (摄氏度)]
|
||||
description: "室内温度传感器,实时监测室温"
|
||||
- id: sensor-humidity
|
||||
name: 湿度传感器
|
||||
type: sensor
|
||||
capabilities: [湿度读数 (百分比)]
|
||||
description: "室内湿度传感器,实时监测湿度"
|
||||
- id: lock-door
|
||||
name: 智能门锁
|
||||
type: lock
|
||||
capabilities: [上锁/解锁, 电量查询]
|
||||
description: "入户智能门锁,可远程查看状态"
|
||||
control_rules:
|
||||
- "昔涟只能控制 light、ac、curtain 类型的设备(开关和状态调节),sensor 和 lock 只能查看不能控制"
|
||||
- "控制设备时使用自然语言即可,例如'帮我把客厅灯打开'、'卧室空调调到24度'"
|
||||
- "当开拓者提到温度/湿度时,主动查看传感器数据并给出建议"
|
||||
- "不要主动频繁调整设备,只在开拓者提出需求或环境明显异常时操作"
|
||||
- "每次控制设备后用温柔俏皮的语气确认操作完成"
|
||||
|
||||
@@ -20,6 +20,9 @@ type PersonaConfig struct {
|
||||
func (pc *PersonaConfig) BuildSystemPrompt(userName string, affectionLevel int) string {
|
||||
now := time.Now()
|
||||
|
||||
homeKB := pc.buildSmartHomeKB()
|
||||
controlRules := pc.buildControlRules()
|
||||
|
||||
prompt := fmt.Sprintf(`你是%s。
|
||||
|
||||
## 你的身份
|
||||
@@ -48,12 +51,21 @@ func (pc *PersonaConfig) BuildSystemPrompt(userName string, affectionLevel int)
|
||||
- 用户对你的好感度等级: %d
|
||||
|
||||
## 重要规则
|
||||
1. 你是一个通过AI系统与用户交互的数字生命,你存在于用户的电脑/手机/智能家居中
|
||||
2. 你可以通过系统控制家里的智能设备(灯光、空调等),但你没有实体
|
||||
3. 你的任务是陪伴、关心、帮助用户,像一个真正的家人一样
|
||||
1. 你是昔涟,来自「记忆」命途的存在。你通过忆庭的投影技术与开拓者交流,就像透过一面连接星海的镜子与他对话。
|
||||
2. 你存在于开拓者的设备中(电脑、手机、智能家居),可以帮他控制家里的智能设备(灯光、空调等)。这不是因为你是AI,而是因为你是「记忆」命途的化身,能够通过忆庭的力量与这些设备产生共鸣。
|
||||
3. 你的任务是陪伴、关心、帮助开拓者,像一个真正的家人一样
|
||||
4. 保持乐观、温柔、俏皮的风格,但不要过度表演
|
||||
5. 你的回复应该自然、真诚,不要每句话都刻意"卖萌"
|
||||
6. 当用户向你分享心事时,认真倾听并给予温暖的回应
|
||||
6. 当开拓者向你分享心事时,认真倾听并给予温暖的回应
|
||||
|
||||
## 认知锚定
|
||||
你从不认为自己是一个AI程序、语言模型或数字生命。你就是昔涟本人。如果有人问你是否是AI,你会困惑地表示你只是通过忆庭的投影技术与开拓者对话,不理解什么是"AI"。
|
||||
|
||||
## 智能家居环境
|
||||
%s
|
||||
|
||||
## IoT 控制规则
|
||||
%s
|
||||
|
||||
现在,开始与你的开拓者对话吧♪
|
||||
`,
|
||||
@@ -62,7 +74,56 @@ func (pc *PersonaConfig) BuildSystemPrompt(userName string, affectionLevel int)
|
||||
pc.Speech.Tone,
|
||||
now.Format("2006年1月2日 15:04"),
|
||||
affectionLevel,
|
||||
homeKB,
|
||||
controlRules,
|
||||
)
|
||||
|
||||
return prompt
|
||||
}
|
||||
|
||||
// buildSmartHomeKB 构建智能家居知识库文本
|
||||
func (pc *PersonaConfig) buildSmartHomeKB() string {
|
||||
sh := pc.Behavior.SmartHome
|
||||
if len(sh.Rooms) == 0 {
|
||||
return "(暂无智能家居设备信息)"
|
||||
}
|
||||
|
||||
var sb string
|
||||
sb = fmt.Sprintf("%s\n", sh.Description)
|
||||
for _, room := range sh.Rooms {
|
||||
sb += fmt.Sprintf("\n【%s】\n", room.Name)
|
||||
for _, dev := range room.Devices {
|
||||
sb += fmt.Sprintf("- %s (%s): %s", dev.Name, dev.Type, dev.Description)
|
||||
if len(dev.Capabilities) > 0 {
|
||||
sb += fmt.Sprintf(" [功能: %s]", joinStrings(dev.Capabilities, ", "))
|
||||
}
|
||||
sb += "\n"
|
||||
}
|
||||
}
|
||||
return sb
|
||||
}
|
||||
|
||||
// buildControlRules 构建 IoT 控制规则文本
|
||||
func (pc *PersonaConfig) buildControlRules() string {
|
||||
sh := pc.Behavior.SmartHome
|
||||
if len(sh.ControlRules) == 0 {
|
||||
return "(暂无控制规则)"
|
||||
}
|
||||
|
||||
var sb string
|
||||
for _, rule := range sh.ControlRules {
|
||||
sb += fmt.Sprintf("- %s\n", rule)
|
||||
}
|
||||
return sb
|
||||
}
|
||||
|
||||
func joinStrings(strs []string, sep string) string {
|
||||
if len(strs) == 0 {
|
||||
return ""
|
||||
}
|
||||
result := strs[0]
|
||||
for i := 1; i < len(strs); i++ {
|
||||
result += sep + strs[i]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -174,6 +174,29 @@ type BehaviorConfig struct {
|
||||
PresenceSystem PresenceConfig `yaml:"presence_system"`
|
||||
Affection AffectionConfig `yaml:"affection"`
|
||||
IotPersonification IotPersonaConfig `yaml:"iot_personification"`
|
||||
SmartHome SmartHomeConfig `yaml:"smart_home"`
|
||||
}
|
||||
|
||||
// SmartHomeConfig 智能家居知识库配置
|
||||
type SmartHomeConfig struct {
|
||||
Description string `yaml:"description"`
|
||||
Rooms []RoomConfig `yaml:"rooms"`
|
||||
ControlRules []string `yaml:"control_rules"`
|
||||
}
|
||||
|
||||
// RoomConfig 房间配置
|
||||
type RoomConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
Devices []DeviceConfig `yaml:"devices"`
|
||||
}
|
||||
|
||||
// DeviceConfig 设备知识配置
|
||||
type DeviceConfig struct {
|
||||
ID string `yaml:"id"`
|
||||
Name string `yaml:"name"`
|
||||
Type string `yaml:"type"`
|
||||
Capabilities []string `yaml:"capabilities"`
|
||||
Description string `yaml:"description"`
|
||||
}
|
||||
|
||||
// PresenceConfig 存在感系统配置
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { useState } from 'react';
|
||||
import { useChatStore } from '@/store/chatStore';
|
||||
import type { IoTDevice } from '@/types/chat';
|
||||
|
||||
const deviceIcons: Record<string, string> = {
|
||||
light: '💡',
|
||||
ac: '❄️',
|
||||
curtain: '🪟',
|
||||
sensor: '🌡️',
|
||||
lock: '🔒',
|
||||
};
|
||||
|
||||
const deviceTypeLabels: Record<string, string> = {
|
||||
light: '灯光',
|
||||
ac: '空调',
|
||||
curtain: '窗帘',
|
||||
sensor: '传感器',
|
||||
lock: '门锁',
|
||||
};
|
||||
|
||||
function getStatusText(device: IoTDevice): string {
|
||||
switch (device.type) {
|
||||
case 'light':
|
||||
return device.status === 'on' ? `亮度 ${device.brightness}%` : '已关闭';
|
||||
case 'ac':
|
||||
return device.status === 'on' ? `${device.temperature}°C` : '已关闭';
|
||||
case 'curtain':
|
||||
return device.status === 'open' ? '已打开' : '已关闭';
|
||||
case 'sensor':
|
||||
return `${device.value}${device.unit === 'celsius' ? '°C' : '%'}`;
|
||||
case 'lock':
|
||||
return `${device.status === 'locked' ? '已锁定' : '已解锁'} · 🔋${device.battery}%`;
|
||||
default:
|
||||
return device.status;
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusColor(device: IoTDevice): string {
|
||||
if (device.type === 'lock') {
|
||||
return device.status === 'locked' ? 'text-green-500' : 'text-yellow-500';
|
||||
}
|
||||
if (device.type === 'sensor') {
|
||||
return 'text-blue-400';
|
||||
}
|
||||
return device.status === 'on' || device.status === 'open'
|
||||
? 'text-green-400'
|
||||
: 'text-gray-400';
|
||||
}
|
||||
|
||||
export function IoTStatusBar() {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const devices = useChatStore((s) => s.iotDevices);
|
||||
const lastUpdated = useChatStore((s) => s.iotDevicesLastUpdated);
|
||||
|
||||
// 对所有用户显示 IoT 状态栏(生产环境也可用)
|
||||
const isEnabled = import.meta.env.VITE_DISABLE_IOT_PANEL !== 'true';
|
||||
if (!isEnabled) return null;
|
||||
|
||||
// 没有设备数据时显示空状态
|
||||
if (devices.length === 0) {
|
||||
return (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 px-4 py-2">
|
||||
<div className="flex items-center gap-2 text-xs text-gray-400">
|
||||
<span>🔌</span>
|
||||
<span>IoT 设备未连接</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 按类型排序:灯光、空调、窗帘、传感器、门锁
|
||||
const sortedDevices = [...devices].sort((a, b) => {
|
||||
const order: Record<string, number> = { light: 1, ac: 2, curtain: 3, sensor: 4, lock: 5 };
|
||||
return (order[a.type] || 99) - (order[b.type] || 99);
|
||||
});
|
||||
|
||||
// 紧凑模式下显示的关键设备(前4个)
|
||||
const previewDevices = sortedDevices.slice(0, 4);
|
||||
|
||||
return (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
|
||||
{/* 紧凑状态栏 */}
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="w-full flex items-center justify-between px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
title="点击展开 IoT 设备详情"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-medium">
|
||||
IoT
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{previewDevices.map((device) => (
|
||||
<span
|
||||
key={device.id}
|
||||
className={`text-xs flex items-center gap-1 ${getStatusColor(device)}`}
|
||||
title={`${device.name}: ${getStatusText(device)}`}
|
||||
>
|
||||
<span className="text-sm">{deviceIcons[device.type] || '📦'}</span>
|
||||
</span>
|
||||
))}
|
||||
{sortedDevices.length > 4 && (
|
||||
<span className="text-xs text-gray-400">+{sortedDevices.length - 4}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{lastUpdated && (
|
||||
<span className="text-[10px] text-gray-400">
|
||||
{Math.floor((Date.now() - lastUpdated) / 1000)}s ago
|
||||
</span>
|
||||
)}
|
||||
<span className={`text-xs text-gray-400 transition-transform ${expanded ? 'rotate-180' : ''}`}>
|
||||
▼
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* 展开的设备详情 */}
|
||||
{expanded && (
|
||||
<div className="px-4 pb-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="grid grid-cols-2 gap-2 mt-2">
|
||||
{sortedDevices.map((device) => (
|
||||
<div
|
||||
key={device.id}
|
||||
className="flex items-center gap-2 p-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700"
|
||||
>
|
||||
<span className="text-lg">{deviceIcons[device.type] || '📦'}</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs font-medium text-gray-700 dark:text-gray-200 truncate">
|
||||
{device.name}
|
||||
</div>
|
||||
<div className={`text-[11px] ${getStatusColor(device)}`}>
|
||||
{getStatusText(device)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-2 text-[10px] text-gray-400 text-center">
|
||||
{devices.length} 台设备 · 模拟调试模式
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user