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
@@ -0,0 +1,81 @@
|
||||
package main
|
||||
|
||||
// This file builds the four built-in IM channel adapters from environment
|
||||
// configuration. The config package carries no channel block, so credentials are
|
||||
// read from CHANNEL_<NAME>_* env vars here and injected into each adapter's
|
||||
// concrete Config. Adapters whose construction fails are skipped rather than
|
||||
// aborting boot, matching the "no credentials → warn and continue" contract of
|
||||
// the channel bridge.
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/plugins/channels/qq_obv11"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/plugins/channels/telegram"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/plugins/channels/webhook"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/plugins/channels/weixin_ilink"
|
||||
)
|
||||
|
||||
// buildChannelAdapters constructs the four channel adapters from environment
|
||||
// configuration and returns the successfully built subset. Adapter construction
|
||||
// never dials the network; actual connection happens in the channel bridge.
|
||||
func buildChannelAdapters(log *slog.Logger) []adapter.PlatformAdapter {
|
||||
qq := qq_obv11.NewAdapter(qq_obv11.Config{
|
||||
Mode: envString("CHANNEL_QQ_OBV11_MODE", "server"),
|
||||
ListenAddr: os.Getenv("CHANNEL_QQ_OBV11_LISTEN_ADDR"),
|
||||
RemoteURL: os.Getenv("CHANNEL_QQ_OBV11_REMOTE_URL"),
|
||||
AccessToken: os.Getenv("CHANNEL_QQ_OBV11_ACCESS_TOKEN"),
|
||||
SelfID: os.Getenv("CHANNEL_QQ_OBV11_SELF_ID"),
|
||||
Logger: log,
|
||||
})
|
||||
|
||||
weixin, werr := weixin_ilink.New(weixin_ilink.Config{
|
||||
BaseURL: os.Getenv("CHANNEL_WEIXIN_ILINK_BASE_URL"),
|
||||
CDNBaseURL: os.Getenv("CHANNEL_WEIXIN_ILINK_CDN_BASE_URL"),
|
||||
BotToken: os.Getenv("CHANNEL_WEIXIN_ILINK_BOT_TOKEN"),
|
||||
BotID: os.Getenv("CHANNEL_WEIXIN_ILINK_BOT_ID"),
|
||||
OwnerID: os.Getenv("CHANNEL_WEIXIN_ILINK_OWNER_ID"),
|
||||
}, nil, log)
|
||||
if werr != nil {
|
||||
// A blank config defaults to a valid base URL, so this only fires on an
|
||||
// explicitly invalid one; skip the channel rather than fail startup.
|
||||
log.Warn("weixin-ilink adapter build skipped", "error", werr)
|
||||
weixin = nil
|
||||
}
|
||||
|
||||
tg := telegram.New(telegram.Config{
|
||||
BotToken: os.Getenv("CHANNEL_TELEGRAM_BOT_TOKEN"),
|
||||
WebhookURL: os.Getenv("CHANNEL_TELEGRAM_WEBHOOK_URL"),
|
||||
}, telegram.WithLogger(log))
|
||||
|
||||
// webhook.WithLogger does not tag provenance, so wrap base with the module
|
||||
// source here to satisfy the logging rule.
|
||||
wh := webhook.New(
|
||||
webhook.WithConfig(webhook.Config{
|
||||
EndpointPath: os.Getenv("CHANNEL_WEBHOOK_ENDPOINT_PATH"),
|
||||
ChannelID: os.Getenv("CHANNEL_WEBHOOK_CHANNEL_ID"),
|
||||
ChannelName: os.Getenv("CHANNEL_WEBHOOK_CHANNEL_NAME"),
|
||||
Secret: os.Getenv("CHANNEL_WEBHOOK_SECRET"),
|
||||
SignatureHeader: os.Getenv("CHANNEL_WEBHOOK_SIGNATURE_HEADER"),
|
||||
}),
|
||||
webhook.WithLogger(logger.WithSource(log, "plugins/channels/webhook")),
|
||||
)
|
||||
|
||||
out := []adapter.PlatformAdapter{qq, tg, wh}
|
||||
if weixin != nil {
|
||||
out = append(out, weixin)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// envString returns the value of key, or fallback when the variable is unset or
|
||||
// blank.
|
||||
func envString(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
+67
-57
@@ -26,6 +26,7 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/embedding"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/exec"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/guardrail"
|
||||
"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/memory/projectindex"
|
||||
@@ -41,12 +42,9 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/services/automations"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/services/backthink"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/services/channelbridge"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/services/gateway"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/user"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/plugins/channels/qq_obv11"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/plugins/channels/telegram"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/plugins/channels/webhook"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/plugins/channels/weixin_ilink"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/plugins/tools/basic"
|
||||
iotools "git.yeij.top/AskaEth/Cyrene_v2/plugins/tools/io"
|
||||
)
|
||||
@@ -70,28 +68,30 @@ const (
|
||||
// app holds every subsystem the single Cyrene binary assembles. It is built by
|
||||
// newApp and torn down by close on shutdown.
|
||||
type app struct {
|
||||
cfg *config.Config
|
||||
log *slog.Logger
|
||||
db *sql.DB
|
||||
bus bus.Bus
|
||||
personas *personareg.Registry
|
||||
caps *registry.Registry
|
||||
dispatcher *tool.Dispatcher
|
||||
memStore *memory.Store
|
||||
retriever *memory.Retriever
|
||||
loop *loop.Loop
|
||||
scheduler *orchestration.Scheduler
|
||||
tasks *task.Manager
|
||||
execProv exec.Provider
|
||||
iot *iot.Manager
|
||||
voice *voice.Chain
|
||||
mcpRegs []*mcp.Registry
|
||||
sessions *session.SessionManager
|
||||
automation *automations.Service
|
||||
backthink *backthink.Backthink
|
||||
vindex vector.Index
|
||||
owner *user.OwnerProfile
|
||||
gw *gateway.Server
|
||||
cfg *config.Config
|
||||
log *slog.Logger
|
||||
db *sql.DB
|
||||
bus bus.Bus
|
||||
personas *personareg.Registry
|
||||
caps *registry.Registry
|
||||
dispatcher *tool.Dispatcher
|
||||
memStore *memory.Store
|
||||
retriever *memory.Retriever
|
||||
projStore *projectindex.Store
|
||||
loop *loop.Loop
|
||||
scheduler *orchestration.Scheduler
|
||||
tasks *task.Manager
|
||||
execProv exec.Provider
|
||||
iot *iot.Manager
|
||||
voice *voice.Chain
|
||||
mcpRegs []*mcp.Registry
|
||||
sessions *session.SessionManager
|
||||
automation *automations.Service
|
||||
backthink *backthink.Backthink
|
||||
vindex vector.Index
|
||||
owner *user.OwnerProfile
|
||||
gw *gateway.Server
|
||||
channelBridge *channelbridge.Bridge
|
||||
}
|
||||
|
||||
// newApp assembles the full Cyrene runtime from cfg and log. It opens the
|
||||
@@ -237,6 +237,7 @@ func (a *app) wireMemory(ctx context.Context) error {
|
||||
a.log.Warn("project index unavailable", "error", err)
|
||||
projStore = nil
|
||||
}
|
||||
a.projStore = projStore
|
||||
|
||||
opts := []memory.RetrieverOption{memory.WithEmbedder(embedder)}
|
||||
if a.vindex != nil {
|
||||
@@ -398,34 +399,42 @@ func (a *app) wireAuxiliary() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// wireChannels constructs and registers the four IM channel adapters so the
|
||||
// admin API can enumerate them. They are registered but not connected: real
|
||||
// platform credentials are supplied per-deployment, and connecting with empty
|
||||
// config would dial external networks.
|
||||
// wireChannels constructs the four IM channel adapters from environment
|
||||
// configuration, registers them so the admin API can enumerate them, and wires
|
||||
// them into the channel bridge so a configured adapter connects and routes
|
||||
// inbound messages through session→actor→loop→reply. Adapters without
|
||||
// credentials stay disconnected (logged, never fatal): connecting with an empty
|
||||
// config would either fail or, for weixin-ilink, trigger a blocking scan-code
|
||||
// login.
|
||||
func (a *app) wireChannels() error {
|
||||
channels := []struct {
|
||||
name string
|
||||
value any
|
||||
policy registry.ToolPolicy
|
||||
}{
|
||||
{"qq-obv11", qq_obv11.NewAdapter(qq_obv11.Config{Logger: a.log}), registry.ToolPolicy{}},
|
||||
{"weixin-ilink", mustWeixin(a.log), registry.ToolPolicy{}},
|
||||
{"telegram", telegram.New(telegram.Config{}, telegram.WithLogger(a.log)), registry.ToolPolicy{}},
|
||||
{"webhook", webhook.New(webhook.WithLogger(a.log)), registry.ToolPolicy{}},
|
||||
}
|
||||
for _, ch := range channels {
|
||||
adapters := buildChannelAdapters(a.log)
|
||||
for _, ch := range adapters {
|
||||
capp := registry.Capability{
|
||||
Name: ch.name,
|
||||
Name: ch.Name(),
|
||||
Kind: registry.KindChannel,
|
||||
Description: ch.name,
|
||||
Policy: ch.policy,
|
||||
Value: ch.value,
|
||||
Description: ch.Name(),
|
||||
Value: ch,
|
||||
}
|
||||
if err := a.caps.Register(capp); err != nil {
|
||||
a.log.Warn("channel register skipped", "channel", ch.name, "error", err)
|
||||
a.log.Warn("channel register skipped", "channel", ch.Name(), "error", err)
|
||||
}
|
||||
}
|
||||
a.log.Info("channel adapters registered", "count", len(channels))
|
||||
|
||||
bridge, berr := channelbridge.New(channelbridge.Options{
|
||||
Log: logger.WithSource(a.log, "services/channelbridge"),
|
||||
Loop: a.loop,
|
||||
Sessions: a.sessions,
|
||||
Owner: a.owner,
|
||||
})
|
||||
if berr != nil {
|
||||
return fmt.Errorf("channel bridge: %w", berr)
|
||||
}
|
||||
for _, ch := range adapters {
|
||||
bridge.Add(ch)
|
||||
}
|
||||
bridge.Start()
|
||||
a.channelBridge = bridge
|
||||
a.log.Info("channel adapters registered", "count", len(adapters))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -435,6 +444,13 @@ func (a *app) wireChannels() error {
|
||||
func (a *app) wireFrontend() error {
|
||||
jwt := auth.New(a.cfg.Auth)
|
||||
apiKeys := auth.NewAPIKey(a.cfg.Auth)
|
||||
ragStore := knowledge.New(knowledge.Deps{
|
||||
Projects: a.projStore,
|
||||
Retriever: a.retriever,
|
||||
DefaultPersona: a.defaultPersonaID(),
|
||||
VectorOn: a.vindex != nil,
|
||||
Log: a.log,
|
||||
})
|
||||
gw, err := gateway.New(a.cfg, gateway.Deps{
|
||||
Log: logger.WithSource(a.log, "gateway"),
|
||||
Auth: jwt,
|
||||
@@ -450,6 +466,7 @@ func (a *app) wireFrontend() error {
|
||||
Automations: a.automation,
|
||||
Users: apiKeys,
|
||||
Backthink: a.backthink,
|
||||
RAG: ragStore,
|
||||
Streaming: true,
|
||||
Frontend: resolveFrontendFS(a.log),
|
||||
})
|
||||
@@ -502,6 +519,9 @@ func (a *app) close(ctx context.Context) {
|
||||
if a.automation != nil {
|
||||
_ = a.automation.Stop(ctx)
|
||||
}
|
||||
if a.channelBridge != nil {
|
||||
a.channelBridge.Stop(ctx)
|
||||
}
|
||||
if a.scheduler != nil {
|
||||
_ = a.scheduler.CancelAll(ctx)
|
||||
}
|
||||
@@ -545,13 +565,3 @@ func resolveSQLitePath(cfg *config.Config, dataDir string) string {
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// mustWeixin builds the weixin-ilink adapter, panicking on an invalid config
|
||||
// (which should never happen with defaults).
|
||||
func mustWeixin(log *slog.Logger) *weixin_ilink.Adapter {
|
||||
adapter, err := weixin_ilink.New(weixin_ilink.Config{}, nil, log)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("weixin-ilink adapter: %v", err))
|
||||
}
|
||||
return adapter
|
||||
}
|
||||
Reference in new issue
Block a user