A1 情绪接线: - loop 回合边界钩子(TurnEnd/OnTurnEnd/settleTurn, Result.Mood); 每轮一次非每步 - persona/emotion: React(valence五档) + Machine.Move(BFS最短合法路径一步) + Observer(observe/decay/好感度,仅变化才emit); 好感度步长读角色配置(cyrene delta=10, 0..700) - 落库 Mood(web session_persist.res.Mood + IM channelbridge/persist; user消息不带mood); WS mood事件(BroadcastMood,仅变化); per-persona隔离; 群聊/陌生人只读不写不发 - cmd/cyrene/emotion.go 生产接线 + wire.go 注入 loop.Options.OnTurnEnd - 22 测试(状态机被loop调用/Mood非空落库/mood帧shape/只变化才发/群聊只读) + 真二进制冒烟 - Docs: api-client v0.32 §3.7 情绪事件 + 开源对照清单(companion 应用对照) - 诚实: 本机无LLM凭据→未活体验证'模型返回后完整轮次'一环 拆分 channelbridge.go 816→257(纯搬移零逻辑): connect/inbound/outbound/blacklist/helpers.go; A1 的 IM mood 调用点落在新 inbound.go, 故与本提交同批 回滚 6 个 gofmt 顺手格式化文件(capacity_loop_test/flow/source_test/findchannel_test/admin_channels_platform_test/history) 缩 diff, 含 history.go 注释被转弯引号
174 lines
4.7 KiB
Go
174 lines
4.7 KiB
Go
package channelbridge
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
|
)
|
|
|
|
// This file holds the adapter registry and the connect lifecycle of the channel
|
|
// bridge (design/08 §0): adapters are registered through Add, dialed by Start
|
|
// (each within connectTimeout), torn down by Stop, and skipped by the credential
|
|
// gate when they carry no usable configuration.
|
|
|
|
// Add registers an adapter by its Name. It is safe to call before Start; adding
|
|
// after Start has no effect on already-running adapters.
|
|
func (b *Bridge) Add(a adapter.PlatformAdapter) {
|
|
if a == nil {
|
|
return
|
|
}
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
b.adapters[a.Name()] = a
|
|
}
|
|
|
|
// WebhookHandlers returns the inbound HTTP handlers of every registered adapter
|
|
// that exposes an HTTP callback (e.g. *webhook.Adapter), keyed by the endpoint
|
|
// path from the adapter's config. The gateway mounts these to receive external
|
|
// webhook callbacks into the runtime. A nil map is returned when no adapter
|
|
// provides an inbound handler.
|
|
func (b *Bridge) WebhookHandlers() map[string]http.Handler {
|
|
b.mu.RLock()
|
|
defer b.mu.RUnlock()
|
|
out := make(map[string]http.Handler)
|
|
for _, a := range b.adapters {
|
|
wh, ok := a.(webhookAdapter)
|
|
if !ok {
|
|
continue
|
|
}
|
|
path := wh.WebhookPath()
|
|
if path == "" {
|
|
// Adapter is not currently exposing a webhook (e.g. a websocket-mode
|
|
// KOOK); skipping avoids mounting a handler at a stale or colliding route.
|
|
continue
|
|
}
|
|
h := wh.InboundHandler()
|
|
if h == nil {
|
|
continue
|
|
}
|
|
out[path] = h
|
|
}
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Start connects every configured adapter, installs the inbound handler, and
|
|
// launches the read loops. Adapters without credentials (or whose Connect fails)
|
|
// are logged and left disconnected; they never fail startup. Start blocks only
|
|
// long enough to connect (bounded by connectTimeout per adapter) and returns. It
|
|
// is idempotent: a second call is a no-op.
|
|
func (b *Bridge) Start() {
|
|
b.mu.Lock()
|
|
if b.closed || b.started {
|
|
b.mu.Unlock()
|
|
return
|
|
}
|
|
b.ctx, b.cancel = context.WithCancel(context.Background())
|
|
b.started = true
|
|
b.mu.Unlock()
|
|
|
|
for _, a := range b.snapshot() {
|
|
b.connectAndRun(a)
|
|
}
|
|
b.log.Info("channel bridge started", "connected", b.ConnectedCount(),
|
|
"registered", len(b.snapshot()))
|
|
}
|
|
|
|
// Stop cancels the read loops, disconnects every adapter and waits for in-flight
|
|
// message processing to finish. It is idempotent.
|
|
func (b *Bridge) Stop(ctx context.Context) {
|
|
b.mu.Lock()
|
|
if b.closed {
|
|
b.mu.Unlock()
|
|
return
|
|
}
|
|
b.closed = true
|
|
cancel := b.cancel
|
|
b.mu.Unlock()
|
|
|
|
cancel()
|
|
for name, a := range b.snapshot() {
|
|
if err := a.Disconnect(ctx); err != nil {
|
|
b.log.Warn("channel disconnect", "channel", name, "error", err)
|
|
}
|
|
}
|
|
b.wg.Wait()
|
|
b.log.Info("channel bridge stopped")
|
|
}
|
|
|
|
// IsConnected reports whether the named adapter is currently connected.
|
|
func (b *Bridge) IsConnected(name string) bool {
|
|
b.mu.RLock()
|
|
defer b.mu.RUnlock()
|
|
return b.connected[name]
|
|
}
|
|
|
|
// ConnectedCount reports how many adapters are currently connected.
|
|
func (b *Bridge) ConnectedCount() int {
|
|
b.mu.RLock()
|
|
defer b.mu.RUnlock()
|
|
n := 0
|
|
for _, ok := range b.connected {
|
|
if ok {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
// snapshot returns a copy of the registered adapters for a lock-free iteration.
|
|
func (b *Bridge) snapshot() []adapter.PlatformAdapter {
|
|
b.mu.RLock()
|
|
defer b.mu.RUnlock()
|
|
out := make([]adapter.PlatformAdapter, 0, len(b.adapters))
|
|
for _, a := range b.adapters {
|
|
out = append(out, a)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// configured reports whether the adapter carries enough configuration to attempt
|
|
// a Connect. It inspects the adapter's own Config() snapshot for the credential
|
|
// each platform needs, so a bare (empty) adapter is skipped rather than dialed.
|
|
// This is what keeps weixin-ilink from falling into its blocking QR-login flow
|
|
// when no bot token is present.
|
|
func (b *Bridge) configured(a adapter.PlatformAdapter) bool {
|
|
cfg := a.Config()
|
|
switch basePlatform(a.Name()) {
|
|
case "telegram", "weixin-ilink", "kook":
|
|
return cfgStr(cfg, "bot_token") != ""
|
|
case "qq-obv11":
|
|
if cfgStr(cfg, "mode") == "client" {
|
|
return cfgStr(cfg, "remoteURL") != ""
|
|
}
|
|
return cfgStr(cfg, "listenAddr") != ""
|
|
case "webhook":
|
|
// Inbound-only: no external credential is required to hold the session.
|
|
return true
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
// cfgStr reads a string field from an adapter config map, returning "" when
|
|
// absent.
|
|
func cfgStr(m map[string]any, key string) string {
|
|
if m == nil {
|
|
return ""
|
|
}
|
|
if s, ok := m[key].(string); ok {
|
|
return s
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// adapter returns the registered adapter for a platform name, or nil.
|
|
func (b *Bridge) adapter(platform string) adapter.PlatformAdapter {
|
|
b.mu.RLock()
|
|
defer b.mu.RUnlock()
|
|
return b.adapters[platform]
|
|
}
|