mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-06-10 10:10:49 +00:00
28fab9507a
- Backend: depth-limited WalkDir (max 3 levels default) with on-demand subtree loading
- Backend: new GET /v1/storage/size SSE endpoint with 60min in-memory cache
- Backend: raw binary file serving (?raw=true) with MIME detection and download support
- Frontend: lazy tree expansion with loading spinners for deep folders
- Frontend: streaming size display with cache info tooltip
- Frontend: image viewer (blob URL), unsupported file UI, download button, colored size badges
- Frontend: file-type icons for 13 categories (md, json, yaml, images, video, etc.)
- Fix sidebar connection status text overflow on collapse
- Apply go fix modernization (interface{} → any) across http handlers
265 lines
8.0 KiB
Go
265 lines
8.0 KiB
Go
package http
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
func (h *KnowledgeGraphHandler) handleListEntities(w http.ResponseWriter, r *http.Request) {
|
|
agentID := r.PathValue("agentID")
|
|
userID := r.URL.Query().Get("user_id")
|
|
entityType := r.URL.Query().Get("type")
|
|
query := r.URL.Query().Get("q")
|
|
|
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
|
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
|
|
// If query is provided, use search
|
|
if query != "" {
|
|
entities, err := h.store.SearchEntities(r.Context(), agentID, userID, query, limit)
|
|
if err != nil {
|
|
slog.Warn("kg.search_entities failed", "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if entities == nil {
|
|
entities = []store.Entity{}
|
|
}
|
|
writeJSON(w, http.StatusOK, entities)
|
|
return
|
|
}
|
|
|
|
entities, err := h.store.ListEntities(r.Context(), agentID, userID, store.EntityListOptions{
|
|
EntityType: entityType,
|
|
Limit: limit,
|
|
Offset: offset,
|
|
})
|
|
if err != nil {
|
|
slog.Warn("kg.list_entities failed", "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if entities == nil {
|
|
entities = []store.Entity{}
|
|
}
|
|
writeJSON(w, http.StatusOK, entities)
|
|
}
|
|
|
|
func (h *KnowledgeGraphHandler) handleGetEntity(w http.ResponseWriter, r *http.Request) {
|
|
locale := extractLocale(r)
|
|
agentID := r.PathValue("agentID")
|
|
entityID := r.PathValue("entityID")
|
|
userID := r.URL.Query().Get("user_id")
|
|
|
|
entity, err := h.store.GetEntity(r.Context(), agentID, userID, entityID)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "entity", entityID)})
|
|
return
|
|
}
|
|
|
|
relations, err := h.store.ListRelations(r.Context(), agentID, userID, entityID)
|
|
if err != nil {
|
|
relations = []store.Relation{}
|
|
}
|
|
if relations == nil {
|
|
relations = []store.Relation{}
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"entity": entity,
|
|
"relations": relations,
|
|
})
|
|
}
|
|
|
|
func (h *KnowledgeGraphHandler) handleUpsertEntity(w http.ResponseWriter, r *http.Request) {
|
|
locale := extractLocale(r)
|
|
agentID := r.PathValue("agentID")
|
|
|
|
var entity store.Entity
|
|
if err := json.NewDecoder(r.Body).Decode(&entity); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
|
return
|
|
}
|
|
entity.AgentID = agentID
|
|
|
|
if entity.ExternalID == "" || entity.Name == "" || entity.EntityType == "" {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgEntityFieldsRequired)})
|
|
return
|
|
}
|
|
if entity.Confidence <= 0 {
|
|
entity.Confidence = 1.0
|
|
}
|
|
|
|
if err := h.store.UpsertEntity(r.Context(), &entity); err != nil {
|
|
slog.Warn("kg.upsert_entity failed", "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (h *KnowledgeGraphHandler) handleDeleteEntity(w http.ResponseWriter, r *http.Request) {
|
|
agentID := r.PathValue("agentID")
|
|
entityID := r.PathValue("entityID")
|
|
userID := r.URL.Query().Get("user_id")
|
|
|
|
if err := h.store.DeleteEntity(r.Context(), agentID, userID, entityID); err != nil {
|
|
slog.Warn("kg.delete_entity failed", "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
|
}
|
|
|
|
func (h *KnowledgeGraphHandler) handleTraverse(w http.ResponseWriter, r *http.Request) {
|
|
locale := extractLocale(r)
|
|
agentID := r.PathValue("agentID")
|
|
|
|
var body struct {
|
|
EntityID string `json:"entity_id"`
|
|
UserID string `json:"user_id"`
|
|
MaxDepth int `json:"max_depth"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
|
return
|
|
}
|
|
if body.EntityID == "" {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgEntityIDRequired)})
|
|
return
|
|
}
|
|
if body.MaxDepth <= 0 {
|
|
body.MaxDepth = 2
|
|
}
|
|
if body.MaxDepth > 3 {
|
|
body.MaxDepth = 3
|
|
}
|
|
|
|
results, err := h.store.Traverse(r.Context(), agentID, body.UserID, body.EntityID, body.MaxDepth)
|
|
if err != nil {
|
|
slog.Warn("kg.traverse failed", "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if results == nil {
|
|
results = []store.TraversalResult{}
|
|
}
|
|
writeJSON(w, http.StatusOK, results)
|
|
}
|
|
|
|
func (h *KnowledgeGraphHandler) handleExtract(w http.ResponseWriter, r *http.Request) {
|
|
locale := extractLocale(r)
|
|
agentID := r.PathValue("agentID")
|
|
|
|
var body struct {
|
|
Text string `json:"text"`
|
|
UserID string `json:"user_id"`
|
|
Provider string `json:"provider"`
|
|
Model string `json:"model"`
|
|
MinConf float64 `json:"min_confidence"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
|
return
|
|
}
|
|
if body.Text == "" {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgTextRequired)})
|
|
return
|
|
}
|
|
if body.Provider == "" || body.Model == "" {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgProviderModelRequired)})
|
|
return
|
|
}
|
|
|
|
extractor := h.NewExtractor(body.Provider, body.Model, body.MinConf)
|
|
if extractor == nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidProviderOrModel)})
|
|
return
|
|
}
|
|
|
|
result, err := extractor.Extract(r.Context(), body.Text)
|
|
if err != nil {
|
|
slog.Warn("kg.extract failed", "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Ingest extracted entities and relations into the store
|
|
for i := range result.Entities {
|
|
result.Entities[i].AgentID = agentID
|
|
result.Entities[i].UserID = body.UserID
|
|
}
|
|
for i := range result.Relations {
|
|
result.Relations[i].AgentID = agentID
|
|
result.Relations[i].UserID = body.UserID
|
|
}
|
|
|
|
if err := h.store.IngestExtraction(r.Context(), agentID, body.UserID, result.Entities, result.Relations); err != nil {
|
|
slog.Warn("kg.ingest_extraction failed", "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"entities": len(result.Entities),
|
|
"relations": len(result.Relations),
|
|
})
|
|
}
|
|
|
|
func (h *KnowledgeGraphHandler) handleStats(w http.ResponseWriter, r *http.Request) {
|
|
agentID := r.PathValue("agentID")
|
|
userID := r.URL.Query().Get("user_id")
|
|
|
|
stats, err := h.store.Stats(r.Context(), agentID, userID)
|
|
if err != nil {
|
|
slog.Warn("kg.stats failed", "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, stats)
|
|
}
|
|
|
|
// handleGraph returns all entities + relations for graph visualization.
|
|
func (h *KnowledgeGraphHandler) handleGraph(w http.ResponseWriter, r *http.Request) {
|
|
agentID := r.PathValue("agentID")
|
|
userID := r.URL.Query().Get("user_id")
|
|
|
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
|
if limit <= 0 {
|
|
limit = 200
|
|
}
|
|
|
|
entities, err := h.store.ListEntities(r.Context(), agentID, userID, store.EntityListOptions{Limit: limit})
|
|
if err != nil {
|
|
slog.Warn("kg.graph entities failed", "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if entities == nil {
|
|
entities = []store.Entity{}
|
|
}
|
|
|
|
relations, err := h.store.ListAllRelations(r.Context(), agentID, userID, limit*3)
|
|
if err != nil {
|
|
slog.Warn("kg.graph relations failed", "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if relations == nil {
|
|
relations = []store.Relation{}
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"entities": entities,
|
|
"relations": relations,
|
|
})
|
|
}
|