Files
Cyrene_v2/internal/services/gateway/admin_write_memory_rag.go
T
AskaEth 074ffc4ebf
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(gateway): gateway 剩余文件注释改中文(逐行 1:1,代码零字节改动)
主人 2026-10-03 拍板「注释一律中文」;gateway 至此整包注释中文化完成(本会话共 3 批:
已推库 16 个文件 + 本批剩余文件)。
校验:机械校验(非注释改动行)= 空;剥注释后代码文本逐行比对全等(不一致文件数 0);
全树 go test ./... 58 包 ok / 0 FAIL;交叉编译绿。
2026-10-03 22:28:25 +08:00

171 lines
6.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package gateway
import (
"encoding/json"
"errors"
"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"
)
// 管理侧记忆 + 知识库写端点(仅 owner,带审计)。添加记忆
// 会把真实条目写入记忆存储(owner 私有,安全),而 RAG 索引
// 把文档导入项目索引使其可被检索。二者在各自
// 后端存储未接线时都退化为明确的 501。
// memoryAddRequest 是 POST /api/v1/admin/memories 的请求体。
type memoryAddRequest struct {
PersonaID string `json:"personaId"`
Content string `json:"content"`
Scope string `json:"scope"`
}
// adminAddMemory 为主人写入单条记忆。它是手动的
// 「添加记忆」路径:条目默认作用于 persona,因此行为与
// loop 已在写的「记住 X」路径一致。Scope 可省略(默认
// persona),或为 user|persona|project 之一;非法非空 scope 会被拒绝。
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 报告 raw 是否是已定义的记忆作用域之一。
func isValidMemoryScope(raw string) bool {
switch memory.Scope(strings.TrimSpace(raw)) {
case memory.ScopeUser, memory.ScopePersona, memory.ScopeProject:
return true
default:
return false
}
}
// ragIndexRequest 是 POST /api/v1/admin/rag 的请求体。
type ragIndexRequest struct {
Name string `json:"name"`
Desc string `json:"desc"`
Content string `json:"content"`
}
// adminIndexRag 导入一份知识文档:它把 name/description 与
// content 交给知识存储,后者切分它并索引进项目
// 索引,使文档可被关键词检索。它是管理侧的「导入文档」路径。
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})
}
// ragDeleteRequest 是 POST /api/v1/admin/rag/delete 的请求体。
type ragDeleteRequest struct {
ID string `json:"id"`
}
// adminDeleteRag 按 id 移除一份知识文档:存储删除
// 项目索引条目并级联其章节。文档缺失时返回 404,
// 存储无法删除时返回明确的 501,其他失败
// 返回 500 —— 绝不编造成功。该路由受 owner 门控且带审计。
func (s *Server) adminDeleteRag(w http.ResponseWriter, r *http.Request) {
if s.rag == nil {
s.auditClaims(r, "rag.delete", "error")
writeError(w, &apperr.AppError{Code: "not_implemented", Message: "知识库 RAG 服务未配置(未注入 RagStore)", HTTPCode: http.StatusNotImplemented})
return
}
var req ragDeleteRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "invalid request body"))
return
}
id := strings.TrimSpace(req.ID)
if id == "" {
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "id is required"))
return
}
if err := s.rag.DeleteDocument(r.Context(), id); err != nil {
s.auditClaims(r, "rag.delete", "error")
switch {
case errors.Is(err, knowledge.ErrDocumentNotFound):
writeError(w, apperr.Wrap(apperr.ErrNotFound, "not_found", "知识库文档 %q 不存在", id))
case errors.Is(err, knowledge.ErrWriteUnsupported):
writeError(w, &apperr.AppError{Code: "not_implemented", Message: "该知识库存储不支持删除", HTTPCode: http.StatusNotImplemented})
default:
s.log.Error("rag delete", "id", id, "err", err)
writeError(w, apperr.Wrap(apperr.ErrInternal, "rag_delete_failed", "failed to delete knowledge document"))
}
return
}
s.auditClaims(r, "rag.delete", "ok")
writeJSON(w, http.StatusOK, map[string]any{"id": id, "deleted": true})
}