feat: RAG 知识库真实数据 + 渠道端到端接线(并行∦wire.go)+ 主会话收口
【渠道端到端·接线(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:
1 parent
518bd0a230
commit
dbcf3df8a3
13 files changed
+1510
-104
No files matched your search
@@ -22,6 +22,7 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/iot"
|
||||
personareg "git.yeij.top/AskaEth/Cyrene_v2/internal/persona/registry"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/knowledge"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
@@ -188,6 +189,21 @@ type stubMemories struct {
|
||||
|
||||
func (s *stubMemories) List(ctx context.Context) ([]memory.MemoryItem, error) { return s.list, s.err }
|
||||
|
||||
// stubRag mimics RagStore, returning domain docs/hits the gateway maps to its
|
||||
// admin wire shape.
|
||||
type stubRag struct {
|
||||
docs []knowledge.Doc
|
||||
hits []knowledge.Hit
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *stubRag) ListDocuments(ctx context.Context) ([]knowledge.Doc, error) { return s.docs, s.err }
|
||||
func (s *stubRag) Search(ctx context.Context, query string) ([]knowledge.Hit, error) {
|
||||
return s.hits, s.err
|
||||
}
|
||||
func (s *stubRag) IndexDocument(ctx context.Context, doc knowledge.Doc) error { return s.err }
|
||||
func (s *stubRag) DeleteDocument(ctx context.Context, id string) error { return s.err }
|
||||
|
||||
// stubIoT mimics IoTStore.
|
||||
type stubIoT struct {
|
||||
list []iot.Device
|
||||
@@ -1219,3 +1235,132 @@ func reqWithOrigin(origin string) *http.Request {
|
||||
func reqWithQuery(k, v string) *http.Request {
|
||||
return httptest.NewRequest(http.MethodGet, "/ws/chat?"+k+"="+v, nil)
|
||||
}
|
||||
|
||||
// --- RAG knowledge base ----------------------------------------------------
|
||||
|
||||
func TestAdminRagListsAndSearches(t *testing.T) {
|
||||
t.Parallel()
|
||||
docs := []knowledge.Doc{{ID: "p1", Name: "开发时间线.md", Desc: "milestones", Chunks: 3, Embedded: true}}
|
||||
hits := []knowledge.Hit{{DocID: "p1", DocName: "开发时间线.md", Chapter: "M4", Text: "RAG 检索测试", Score: 1.0}}
|
||||
srv, ts, jwt := buildEnv(t, cfgDefault(), Deps{RAG: &stubRag{docs: docs, hits: hits}})
|
||||
_ = srv
|
||||
tok := signAny(t, jwt, "acct", auth.RoleOwner)
|
||||
|
||||
// List only.
|
||||
resp, body := doReq(t, http.MethodGet, ts.URL+"/api/v1/admin/rag", nil, tok)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("list status = %d, want 200 (body=%s)", resp.StatusCode, body)
|
||||
}
|
||||
var list struct {
|
||||
Documents []AdminRagDoc `json:"documents"`
|
||||
Hits []AdminRagHit `json:"hits"`
|
||||
}
|
||||
if err := json.Unmarshal(decodeBody(t, body)["data"], &list); err != nil {
|
||||
t.Fatalf("decode list: %v (body=%s)", err, body)
|
||||
}
|
||||
if len(list.Documents) != 1 || list.Documents[0].Name != "开发时间线.md" {
|
||||
t.Fatalf("documents = %+v, want one 开发时间线.md", list.Documents)
|
||||
}
|
||||
if list.Hits != nil {
|
||||
t.Fatalf("hits = %+v, want omitted without query", list.Hits)
|
||||
}
|
||||
|
||||
// List + search via ?query=.
|
||||
resp2, body2 := doReq(t, http.MethodGet, ts.URL+"/api/v1/admin/rag?query=rag", nil, tok)
|
||||
if resp2.StatusCode != http.StatusOK {
|
||||
t.Fatalf("search status = %d, want 200 (body=%s)", resp2.StatusCode, body2)
|
||||
}
|
||||
var search struct {
|
||||
Documents []AdminRagDoc `json:"documents"`
|
||||
Hits []AdminRagHit `json:"hits"`
|
||||
}
|
||||
if err := json.Unmarshal(decodeBody(t, body2)["data"], &search); err != nil {
|
||||
t.Fatalf("decode search: %v (body=%s)", err, body2)
|
||||
}
|
||||
if len(search.Hits) != 1 || search.Hits[0].DocName != "开发时间线.md" {
|
||||
t.Fatalf("hits = %+v, want one hit", search.Hits)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminRagQueryPost(t *testing.T) {
|
||||
t.Parallel()
|
||||
hits := []knowledge.Hit{{DocID: "p1", DocName: "开发时间线.md", Text: "RAG 检索测试", Score: 1.0}}
|
||||
srv, ts, jwt := buildEnv(t, cfgDefault(), Deps{RAG: &stubRag{hits: hits}})
|
||||
_ = srv
|
||||
tok := signAny(t, jwt, "acct", auth.RoleOwner)
|
||||
|
||||
resp, body := doReq(t, http.MethodPost, ts.URL+"/api/v1/admin/rag/query", map[string]any{"query": "rag"}, tok)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200 (body=%s)", resp.StatusCode, body)
|
||||
}
|
||||
var res struct {
|
||||
Query string `json:"query"`
|
||||
Hits []AdminRagHit `json:"hits"`
|
||||
}
|
||||
if err := json.Unmarshal(decodeBody(t, body)["data"], &res); err != nil {
|
||||
t.Fatalf("decode: %v (body=%s)", err, body)
|
||||
}
|
||||
if res.Query != "rag" || len(res.Hits) != 1 {
|
||||
t.Fatalf("response = %+v, want query=rag with 1 hit", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminRagEmptyWhenUnwired(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv, ts, jwt := buildEnv(t, cfgDefault(), Deps{})
|
||||
_ = srv
|
||||
tok := signAny(t, jwt, "acct", auth.RoleOwner)
|
||||
|
||||
// List degrades to empty (never a 500).
|
||||
resp, body := doReq(t, http.MethodGet, ts.URL+"/api/v1/admin/rag", nil, tok)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("list status = %d, want 200 (body=%s)", resp.StatusCode, body)
|
||||
}
|
||||
var list struct {
|
||||
Documents []AdminRagDoc `json:"documents"`
|
||||
}
|
||||
if err := json.Unmarshal(decodeBody(t, body)["data"], &list); err != nil {
|
||||
t.Fatalf("decode: %v (body=%s)", err, body)
|
||||
}
|
||||
if len(list.Documents) != 0 {
|
||||
t.Fatalf("documents = %+v, want empty", list.Documents)
|
||||
}
|
||||
|
||||
// Query reports a clear 501 (backend not implemented).
|
||||
resp2, body2 := doReq(t, http.MethodPost, ts.URL+"/api/v1/admin/rag/query", map[string]any{"query": "rag"}, tok)
|
||||
if resp2.StatusCode != http.StatusNotImplemented {
|
||||
t.Fatalf("query status = %d, want 501 (body=%s)", resp2.StatusCode, body2)
|
||||
}
|
||||
wantErrorShape(t, body2)
|
||||
if !strings.Contains(string(body2), "not_implemented") {
|
||||
t.Fatalf("error body = %s, want not_implemented code", body2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminRagDeleteNotImplemented(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv, ts, jwt := buildEnv(t, cfgDefault(), Deps{RAG: &stubRag{err: knowledge.ErrWriteUnsupported}})
|
||||
_ = srv
|
||||
tok := signAny(t, jwt, "acct", auth.RoleOwner)
|
||||
|
||||
resp, body := doReq(t, http.MethodPost, ts.URL+"/api/v1/admin/rag/delete", map[string]any{"id": "p1"}, tok)
|
||||
if resp.StatusCode != http.StatusNotImplemented {
|
||||
t.Fatalf("status = %d, want 501 (body=%s)", resp.StatusCode, body)
|
||||
}
|
||||
wantErrorShape(t, body)
|
||||
if !strings.Contains(string(body), "M10") {
|
||||
t.Fatalf("error body = %s, want M10 write message", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminRagDeleteRequiresID(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv, ts, jwt := buildEnv(t, cfgDefault(), Deps{RAG: &stubRag{}})
|
||||
_ = srv
|
||||
tok := signAny(t, jwt, "acct", auth.RoleOwner)
|
||||
|
||||
resp, _ := doReq(t, http.MethodPost, ts.URL+"/api/v1/admin/rag/delete", map[string]any{"id": " "}, tok)
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want 400", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user