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绿
This commit is contained in:
1 parent
37e98ec584
commit
9c2a37c4ac
32 files changed
+1768
-136
No files matched your search
@@ -0,0 +1,127 @@
|
||||
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})
|
||||
}
|
||||
Reference in new issue
Block a user