feat: DevTools综合升级 — 记忆查询 + 会话监看 + WebUI侧边栏重构

- docs: 17个文件重命名为 YYYY-MM-DD.HH-mm-SS-内容.md 格式
- config: 管理员凭据移至 backend/.env (ADMIN_USERNAME/PASSWORD)
- gateway: 新增 SessionState 会话追踪 + GET /api/v1/admin/sessions
- devtools: 新增7个代理端点 (dashboard/sessions/memory)
- devtools: WebUI重构为侧边栏 + 5面板 (仪表盘/记忆/会话/服务/性能)
This commit is contained in:
2026-05-16 15:02:44 +08:00
parent cd60b01cf3
commit d15acf587c
24 changed files with 1934 additions and 347 deletions
+198 -3
View File
@@ -11,6 +11,8 @@ import (
"syscall"
"time"
"github.com/joho/godotenv"
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"
@@ -20,6 +22,11 @@ import (
)
func main() {
// 自动加载 .env 文件(来自 backend/.env
if err := godotenv.Load("../.env"); err != nil {
log.Println("ℹ 未找到 .env 文件,将使用环境变量或默认值")
}
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Println("🧠 AI-Core 服务启动中...")
@@ -85,6 +92,14 @@ func main() {
handleChat(w, r, orch, ctxBuilder, llmAdapter, personaLoader, memRetriever, memExtractor)
})
// 注册记忆API端点
mux.HandleFunc("/api/v1/memory/search", func(w http.ResponseWriter, r *http.Request) {
handleMemorySearch(w, r, memRetriever)
})
mux.HandleFunc("/api/v1/memory", func(w http.ResponseWriter, r *http.Request) {
handleMemoryCRUD(w, r, memStore, memExtractor)
})
mux.HandleFunc("/api/v1/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"ok","service":"ai-core","model":"` + llmAdapter.ModelName() + `"}`))
@@ -250,8 +265,188 @@ func handleChat(
_ = personaLoader
_ = memRetriever
_ = memExtractor
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
// handleMemorySearch 处理记忆搜索请求
func handleMemorySearch(
w http.ResponseWriter,
r *http.Request,
memRetriever *memory.Retriever,
) {
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 {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"user_id": userID,
"memories": []interface{}{},
"message": "记忆系统未就绪",
})
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": "查询失败",
})
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.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
}
+2 -1
View File
@@ -3,6 +3,7 @@ module github.com/yourname/cyrene-ai/ai-core
go 1.26.2
require (
gopkg.in/yaml.v3 v3.0.1
github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.10.9
gopkg.in/yaml.v3 v3.0.1
)
+8
View File
@@ -0,0 +1,8 @@
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
BIN
View File
Binary file not shown.