Files
Cyrene_v2/internal/services/gateway/admin_write_memory_rag.go
T
Aska 9c2a37c4ac
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat: 管理面板补齐(RAG导入/记忆添加/会话接history/诚实表述) + design/16 §4.6 IM多会话隔离+第三方身份+黑白名单
agent-98 面板批次:
- 会话管理面板改接 history.ListSessions(主web+次级) + 新建次级会话按钮
- 记忆面板手动添加入口 POST /admin/memories(owner)
- 知识库真正实现 IndexDocument(分块+落projectindex) + POST /admin/rag 导入端点
- 三面板空态诚实表述(去除'后端M10+提供')
- RAG嵌入降级到keyword/line(无直属embedder,不伪造语义,符合红线)

agent-99 design/16 §4.6:
- model.Message.SenderName + 迁移0006 sender_name 列
- 第三方身份边界: session.ResolveActor判定(非LLM猜) + [第三方·昵称]前缀(不改源content)
- 群聊/陌生人落独立sess-xxx(persistIsolated),不写主会话,不广播
- 黑/白名单(allowGroups/denyGroups/allowUsers/denyUsers)最前拦截,不进LLM
- 多账号 CHANNELS_JSON 配置 + 每平台多实例
- 管理只读 GET /admin/channels/{platform}/messages(该平台非主人消息流)

实机验证(JWT): 会话管理返回主会话web / 记忆添加成功 / RAG导入documents非空+Cyrene命中; 后端build/vet/test全绿 + 前端build绿
2026-09-07 05:13:36 +08:00

128 lines
4.6 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"
"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})
}