主人 2026-10-03 拍板「注释一律中文」;gateway 至此整包注释中文化完成(本会话共 3 批: 已推库 16 个文件 + 本批剩余文件)。 校验:机械校验(非注释改动行)= 空;剥注释后代码文本逐行比对全等(不一致文件数 0); 全树 go test ./... 58 包 ok / 0 FAIL;交叉编译绿。
171 lines
6.4 KiB
Go
171 lines
6.4 KiB
Go
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})
|
||
}
|