feat: RAG 知识库真实数据 + 渠道端到端接线(并行∦wire.go)+ 主会话收口
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

【渠道端到端·接线(agent-72)】
- 新建 internal/services/channelbridge/:桥接服务(Start 对配置渠道 Connect(10s超时/warn)+SetMessageHandler+ReadMessages;
  入站 UnifiedMessage→sessions.GetOrCreate(ResolveActor 白名单结构性识别)→loop.Run→SendMessage 回执,chat 文本)。
- cmd/cyrene/channels.go:从 CHANNEL_* env 读 4 渠道凭据(telegram/qq_obv11/weixin_ilink/webhook)。
- wire.go:app 加 channelBridge 字段,wireChannels 构造适配器+bridge.Start(),close() 收尾。
- 无真凭据降级:无 CHANNEL_* → configured()false 跳过不 dial;Connect 失败 Warn 不阻启动;
  webhook 仅入站(回执 ErrUnsupported 记 debug)。身份识别绝不靠提示词。收发端到端待凭据验证。

【RAG 知识库(agent-71)】
- internal/runtime/knowledge/:真实数据源(doc=资料/记忆=经历 分离 design/03 §3③)。
  ListDocuments 走 projectindex.ListProjects(+ListChapters)、Search 走 projectindex.Search(标题/描述+章节
  命中行+上下文)+retriever.Retrieve(配 vector 真语义,命中标 semantic:true)。
- gateway/server.go:RagStore 接口+Deps.RAG 可空+3 路由(GET /admin/rag、POST /admin/rag/query、/delete)。
- admin.go:adminListRag/adminSearchRag(空 query 400)/adminDeleteRag(写 501 not_implemented,M10)。
- projectindex/store.go:ListProjects/ListChapters。
- 前端 Rag.tsx:docs 从 GET /admin/rag 拉、检索走 POST /admin/rag/query、删除调真端点(501 显示 Notice),
  上传/分块/嵌入显示 M10 Notice;api.ts/types.ts 加 adminListRag/adminSearchRag/adminDeleteRag。

【主会话收口 wire.go】projStore 提到 app struct+wireMemory 赋值;knowledge.New(...) 注入 gateway.Deps.RAG。

实机验证:admin/rag 200(documents[] 空表非壳)、channels 枚举 4 渠道、health ok。
go build/vet/test 全绿 + 前端 tsc。wire.go 567 行。

遗留(记录):① 群聊/陌生人 MemWritable 记忆隔离未接 loop(AGENTS.md 红线,待修) ② webhook InboundHandler
需 gateway 挂载 ③ RAG 语义路径 persona-scoped(project 记忆其他 persona 不浮现)。
This commit is contained in:
Aska committed 2026-09-06 20:09:09 +08:00
1 parent 518bd0a230
commit dbcf3df8a3
13 files changed
+1510 -104

No files matched your search

+146
View File
@@ -6,11 +6,13 @@ import (
"errors"
"net/http"
"strconv"
"strings"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/auth"
capreg "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
apperr "git.yeij.top/AskaEth/Cyrene_v2/internal/errors"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/knowledge"
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
)
@@ -251,6 +253,150 @@ func (s *Server) adminListBackthink(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, out)
}
// AdminRagDoc is the wire shape of one knowledge-base document (project-index
// entry) in the admin RAG list.
type AdminRagDoc struct {
ID string `json:"id"`
Name string `json:"name"`
Desc string `json:"desc,omitempty"`
Chunks int `json:"chunks"`
Embedded bool `json:"embedded"`
}
// AdminRagHit is the wire shape of one RAG retrieval hit.
type AdminRagHit struct {
DocID string `json:"docId"`
DocName string `json:"docName"`
Chapter string `json:"chapter,omitempty"`
Text string `json:"text"`
Score float64 `json:"score"`
Semantic bool `json:"semantic"`
}
// AdminRagResponse is the wire shape of GET /api/v1/admin/rag. Hits is present
// only when a query parameter was supplied, so callers distinguish "no search
// requested" from "searched and found nothing".
type AdminRagResponse struct {
Documents []AdminRagDoc `json:"documents"`
Hits []AdminRagHit `json:"hits,omitempty"`
}
// ragQueryRequest is the POST /api/v1/admin/rag/query body.
type ragQueryRequest struct {
Query string `json:"query"`
}
// ragDeleteRequest is the POST /api/v1/admin/rag/delete body.
type ragDeleteRequest struct {
ID string `json:"id"`
}
// adminListRag lists the knowledge-base documents and, when ?query= is present,
// returns the matching hits too. Without a wired RagStore it degrades to an empty
// list (backward compatible — never a 500).
func (s *Server) adminListRag(w http.ResponseWriter, r *http.Request) {
out := AdminRagResponse{Documents: make([]AdminRagDoc, 0)}
if s.rag == nil {
writeJSON(w, http.StatusOK, out)
return
}
docs, err := s.rag.ListDocuments(r.Context())
if err != nil {
s.log.Error("list rag", "err", err)
writeError(w, apperr.Wrap(apperr.ErrInternal, "rag_failed", "failed to list knowledge documents"))
return
}
out.Documents = ragDocsFrom(docs)
if query := strings.TrimSpace(r.URL.Query().Get("query")); query != "" {
hits, herr := s.rag.Search(r.Context(), query)
if herr != nil {
s.log.Error("search rag", "err", herr)
writeError(w, apperr.Wrap(apperr.ErrInternal, "rag_search_failed", "failed to search knowledge base"))
return
}
out.Hits = ragHitsFrom(hits)
}
writeJSON(w, http.StatusOK, out)
}
// adminSearchRag runs a knowledge-base query and returns the matching hits.
func (s *Server) adminSearchRag(w http.ResponseWriter, r *http.Request) {
if s.rag == nil {
s.auditClaims(r, "rag.query", "error")
writeError(w, &apperr.AppError{Code: "not_implemented", Message: "知识库 RAG 服务未配置(未注入 RagStore)", HTTPCode: http.StatusNotImplemented})
return
}
var req ragQueryRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "invalid request body"))
return
}
if strings.TrimSpace(req.Query) == "" {
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "query is required"))
return
}
hits, err := s.rag.Search(r.Context(), req.Query)
if err != nil {
s.log.Error("search rag", "err", err)
writeError(w, apperr.Wrap(apperr.ErrInternal, "rag_search_failed", "failed to search knowledge base"))
return
}
s.auditClaims(r, "rag.query", "ok")
writeJSON(w, http.StatusOK, map[string]any{"query": req.Query, "hits": ragHitsFrom(hits)})
}
// adminDeleteRag answers the document delete honestly: write operations
// (upload/chunk/embed/delete) are milestone M10, so it returns a clear 501 rather
// than a fabricated success. The route is audited as an admin write.
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
}
if strings.TrimSpace(req.ID) == "" {
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "id is required"))
return
}
if err := s.rag.DeleteDocument(r.Context(), req.ID); err != nil {
s.auditClaims(r, "rag.delete", "error")
writeError(w, &apperr.AppError{Code: "not_implemented", Message: "知识库写操作(上传/分块/嵌入/删除)由 M10 后端提供", HTTPCode: http.StatusNotImplemented})
return
}
s.auditClaims(r, "rag.delete", "ok")
writeJSON(w, http.StatusOK, map[string]any{"id": req.ID})
}
// ragDocsFrom maps knowledge docs to their admin wire shape.
func ragDocsFrom(docs []knowledge.Doc) []AdminRagDoc {
out := make([]AdminRagDoc, 0, len(docs))
for _, d := range docs {
out = append(out, AdminRagDoc{ID: d.ID, Name: d.Name, Desc: d.Desc, Chunks: d.Chunks, Embedded: d.Embedded})
}
return out
}
// ragHitsFrom maps knowledge hits to their admin wire shape.
func ragHitsFrom(hits []knowledge.Hit) []AdminRagHit {
out := make([]AdminRagHit, 0, len(hits))
for _, h := range hits {
out = append(out, AdminRagHit{
DocID: h.DocID,
DocName: h.DocName,
Chapter: h.Chapter,
Text: h.Text,
Score: h.Score,
Semantic: h.Semantic,
})
}
return out
}
// adminSetPolicy upserts the reachability markers for a capability. It is the
// audited admin write example (owner-only, enforced by the route registration).
func (s *Server) adminSetPolicy(w http.ResponseWriter, r *http.Request) {