package gateway import ( "encoding/json" "net/http" "strings" apperr "git.yeij.top/AskaEth/Cyrene_v2/internal/errors" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/knowledge" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory" ) // Admin memory + knowledge-base write endpoints (owner-only, audited). The memory // add writes a real item into the memory store (owner-private, safe), and the RAG // index imports a document into the project index so it becomes searchable. Both // degrade to a clear 501 when their backing store is not wired. // memoryAddRequest is the POST /api/v1/admin/memories body. type memoryAddRequest struct { PersonaID string `json:"personaId"` Content string `json:"content"` Scope string `json:"scope"` } // adminAddMemory writes a single memory item for the owner. It is the manual // "添加记忆" path: the item is scoped to the persona (default), so it behaves like // the "记住 X" path the loop already writes. Scope may be omitted (default // persona) or one of user|persona|project; an invalid non-empty scope is rejected. func (s *Server) adminAddMemory(w http.ResponseWriter, r *http.Request) { var req memoryAddRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "invalid request body")) return } personaID := strings.TrimSpace(req.PersonaID) if personaID == "" { personaID = "cyrene" } content := strings.TrimSpace(req.Content) if content == "" { s.auditClaims(r, "memory.add", "error") writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "content is required")) return } rawScope := strings.TrimSpace(req.Scope) if rawScope != "" && !isValidMemoryScope(rawScope) { s.auditClaims(r, "memory.add", "error") writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "scope must be one of user|persona|project")) return } store, ok := s.memories.(*memory.Store) if !ok { s.auditClaims(r, "memory.add", "error") writeError(w, &apperr.AppError{Code: "not_implemented", Message: "记忆存储未配置(未注入 memory.Store)", HTTPCode: http.StatusNotImplemented}) return } stored, err := store.Add(r.Context(), memory.MemoryItem{ PersonaID: personaID, Content: content, Scope: memory.Scope(rawScope), }) if err != nil { s.auditClaims(r, "memory.add", "error") s.log.Error("memory add", "err", err) writeError(w, apperr.Wrap(apperr.ErrInternal, "memory_add_failed", "failed to add memory")) return } s.auditClaims(r, "memory.add", "ok") writeJSON(w, http.StatusCreated, map[string]any{"id": stored.ID, "scope": string(stored.Scope), "text": stored.Content}) } // isValidMemoryScope reports whether raw is one of the defined memory scopes. func isValidMemoryScope(raw string) bool { switch memory.Scope(strings.TrimSpace(raw)) { case memory.ScopeUser, memory.ScopePersona, memory.ScopeProject: return true default: return false } } // ragIndexRequest is the POST /api/v1/admin/rag body. type ragIndexRequest struct { Name string `json:"name"` Desc string `json:"desc"` Content string `json:"content"` } // adminIndexRag imports one knowledge document: it hands the name/description and // content to the knowledge store, which chunks it and indexes it into the project // index so the document becomes keyword/searchable. It is the admin "导入文档" path. func (s *Server) adminIndexRag(w http.ResponseWriter, r *http.Request) { var req ragIndexRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "invalid request body")) return } name := strings.TrimSpace(req.Name) if name == "" { s.auditClaims(r, "rag.index", "error") writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "name is required")) return } content := strings.TrimSpace(req.Content) if content == "" { s.auditClaims(r, "rag.index", "error") writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "content is required")) return } if s.rag == nil { s.auditClaims(r, "rag.index", "error") writeError(w, &apperr.AppError{Code: "not_implemented", Message: "知识库 RAG 服务未配置(未注入 RagStore)", HTTPCode: http.StatusNotImplemented}) return } if err := s.rag.IndexDocument(r.Context(), knowledge.Doc{ Name: name, Desc: strings.TrimSpace(req.Desc), Content: content, }); err != nil { s.auditClaims(r, "rag.index", "error") s.log.Error("rag index", "err", err) writeError(w, apperr.Wrap(apperr.ErrInternal, "rag_index_failed", "failed to index document")) return } s.auditClaims(r, "rag.index", "ok") writeJSON(w, http.StatusCreated, map[string]any{"name": name}) }