package main import ( "context" "fmt" "git.yeij.top/AskaEth/Cyrene_v2/internal/auth" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/knowledge" "git.yeij.top/AskaEth/Cyrene_v2/internal/services/gateway" ) // capacityEmitter is the gateway-facing sink for capacity_awaken broadcasts. // *gateway.Server implements it; tests substitute a fake to capture the args. type capacityEmitter interface { BroadcastCapacityAwaken(sessionID, fromState, toState, tool string) } // wireFrontend builds the gateway with the embedded/external SPA filesystem and // the JWT/RBAC authenticator. Session/Persona/Memories are wired to the concrete // managers so the admin/user list endpoints return real data. 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, }) auditStore, aerr := gateway.NewSQLiteAuditStore(context.Background(), a.db, a.log) if aerr != nil { a.log.Warn("audit store unavailable, falling back to in-memory", "error", aerr) auditStore = nil } // Opaque refresh tokens (B2) share the primary SQLite handle. A failure here // degrades gracefully: login simply omits refreshToken and /auth/refresh // reports not-configured, rather than failing the whole boot. The store is // held as the interface so a constructor failure leaves it nil (a typed nil // pointer in the interface would be non-nil and panic on first call). var refreshStore gateway.RefreshGrantStore if store, rerr := auth.NewRefreshStore(context.Background(), a.db, a.log); rerr != nil { a.log.Warn("refresh token store unavailable", "error", rerr) } else { refreshStore = store } // a.history is built in wireAuxiliary so both the channel bridge (wireChannels) // and the gateway share the same persistent store. History was a no-op dependency // before the IM path, so nothing downstream depends on its creation order change. gw, err := gateway.New(a.cfg, gateway.Deps{ Log: logger.WithSource(a.log, "gateway"), Auth: jwt, Bus: a.bus, Registry: a.caps, Loop: a.loop, Session: a.sessions, Personas: a.personas, Memories: a.memStore, History: a.history, IoT: a.iot, Scheduler: a.scheduler, Tasks: a.tasks, Automations: a.automation, Users: apiKeys, Refresh: refreshStore, Backthink: a.backthink, RAG: ragStore, LLM: a.llm, Channels: a.channels, Plugins: a.plugins, OwnerRegistrar: &ownerRegistrar{path: defaultOwnerPath, owner: a.owner, log: a.log}, AuditStore: auditStore, Streaming: true, Frontend: resolveFrontendFS(a.log), Webhooks: a.channelBridge.WebhookHandlers(), DatabasePath: a.dbPath, DataDir: resolveDataDir(a.cfg), }) if err != nil { return fmt.Errorf("gateway: %w", err) } gw.SetVoice(a.voice) a.gw = gw // assign BEFORE wiring hooks below, which capture a.gw / gw (nil-receiver panic otherwise) // Observability trace sink (design/19 §2): now that the gateway exists, its // ring buffer must be fed by the loop's spans, or the admin trace/metrics // endpoints stay empty. wireGatewayTraceSink(a, gw) if a.channelBridge != nil { // Route the channel bridge's owner-private persisted messages into the same // `message_persisted` broadcast the orchestrator uses, so the Web client mirrors // IM messages with one, uniform frame (design/16 §4.4). a.channelBridge.SetOnPersisted(a.gw.BroadcastPersisted) // design/16 §4.6 §6: a message dropped by the platform's allow/deny // black-or-white lists is recorded in the gateway audit trail so the // rejection is "audit 可查" even though it never reaches the loop. a.channelBridge.SetOnRejected(func(msg adapter.UnifiedMessage, reason string) { a.gw.AuditEvent("bridge:"+msg.Platform, "channel.reject", reason) }) } a.capacitySink = gw a.moodSink = gw // Wire the backthink proactive sinks now that the gateway hub (active-client // route) and the channel bridge (IM fallback) both exist. They were absent when // backthink.New ran (the hub needs the loop and the bridge needs the loop and the // owner profile, all assembled later), so without this the pusher would resolve // RouteNone. Each setter rebuilds the pusher under the service's lifecycle lock. if a.backthink != nil { a.backthink.SetActiveSink(gw) if a.channelBridge != nil { a.backthink.SetIMSink(a.channelBridge) } } return nil }