feat(owner): 扫码绑定确认时自动把主人id登记进owner.yaml白名单(热生效)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

This commit is contained in:
Aska committed 2026-09-08 02:53:01 +08:00
1 parent 0aa387f83c
commit 5a7ee9d772
7 files changed
+487 -65

No files matched your search

+58 -24
View File
@@ -114,6 +114,40 @@ type capacityEmitter interface {
BroadcastCapacityAwaken(sessionID, fromState, toState, tool string)
}
// ownerRegistrar implements gateway.OwnerRegistrar: it persists a channel account
// id into the owner whitelist (configs/users/owner.yaml) AND hot-updates the live
// owner profile the channel bridge holds, so a weixin scan-login confirmation is
// recognized as the owner immediately — no restart, no manual edit. path and the
// live profile are captured at wiring time: the boot-loaded *user.OwnerProfile is
// the same pointer the bridge resolves actors against, so RegisterAccount on it is
// visible to ResolveActor without touching the bridge.
type ownerRegistrar struct {
path string
owner *user.OwnerProfile
log *slog.Logger
}
func (r *ownerRegistrar) RegisterChannelOwner(_ context.Context, platform, accountID string) error {
if strings.TrimSpace(accountID) == "" {
return nil
}
if err := user.RegisterOwnerAccount(r.path, platform, accountID); err != nil {
return fmt.Errorf("register owner %s:%s: %w", platform, accountID, err)
}
// Hot-effect: extend the live profile the bridge already points at. If the
// profile failed to load at boot (r.owner nil) the persist above still lands
// on disk and is recognized after a restart; log so the operator knows the
// hot path was unavailable.
if r.owner != nil {
r.owner.RegisterAccount(platform, accountID)
} else {
r.log.Warn("owner registrar: no live profile, whitelist persisted to disk only (restart to take effect)",
"platform", platform, "accountId", accountID)
}
r.log.Info("owner whitelist registered", "platform", platform, "accountId", accountID)
return nil
}
// newApp assembles the full Cyrene runtime from cfg and log. It opens the
// database, migrates the schema, wires the persona registry, model adapter, tool
// dispatcher, memory stack, agent loop, orchestration, execution, IoT, channel
@@ -625,29 +659,30 @@ func (a *app) wireFrontend() error {
// 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,
Backthink: a.backthink,
RAG: ragStore,
LLM: a.llm,
Channels: a.channels,
AuditStore: auditStore,
Streaming: true,
Frontend: resolveFrontendFS(a.log),
Webhooks: a.channelBridge.WebhookHandlers(),
DatabasePath: a.dbPath,
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,
Backthink: a.backthink,
RAG: ragStore,
LLM: a.llm,
Channels: a.channels,
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,
})
if err != nil {
return fmt.Errorf("gateway: %w", err)
@@ -668,7 +703,6 @@ func (a *app) wireFrontend() error {
}
a.capacitySink = 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