Files
Cyrene_v2/internal/user/owner.go
T
Aska 5a7ee9d772
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(owner): 扫码绑定确认时自动把主人id登记进owner.yaml白名单(热生效)
2026-09-08 02:53:01 +08:00

95 lines
3.5 KiB
Go

// Package user owns the "主人" (owner) profile and account whitelist that the
// channel-layer policy engine uses for identity recognition. Per DevDocs/design/11
// §0, owner identification is structural — channel actor metadata matched against
// this whitelist — and is never driven by prompt text or an LLM judgment.
package user
import "sync"
// OwnerAccount is a single channel-fingerprint that belongs to the owner. The
// policy engine matches channel-layer actor metadata (platform + account id)
// against this list.
type OwnerAccount struct {
Platform string `yaml:"platform"`
AccountID string `yaml:"accountId"`
}
// OwnerProfile is the owner's identity record: a display name, the default
// persona bound to the owner, and the per-channel account whitelist. It is the
// source data for owner recognition; it is not injected into the persona's
// prompt as an authority.
//
// mu guards Accounts so a live profile can be hot-extended (RegisterAccount)
// while the channel bridge's ResolveActor reads it (IsOwner) concurrently. The
// mutex is unexported and carries no YAML tag, so it is invisible to encode and
// is not part of the persisted profile.
type OwnerProfile struct {
mu sync.RWMutex `yaml:"-"`
ID string `yaml:"id"`
DisplayName string `yaml:"displayName"`
DefaultPersona string `yaml:"defaultPersona"`
Accounts []OwnerAccount `yaml:"accounts"`
// IdentityFile is the path to the LLM-visible identity/relationship note
// (persona knowledge, NOT an access-control authority).
IdentityFile string `yaml:"identityFile,omitempty"`
Notes string `yaml:"notes,omitempty"`
}
// IsOwner reports whether the given (platform, accountID) pair belongs to the
// owner according to the whitelist. It is safe for concurrent use with
// RegisterAccount (the bridge reads this on every inbound message while a
// scan-login registration may extend the whitelist).
func (p *OwnerProfile) IsOwner(platform, accountID string) bool {
if p == nil {
return false
}
p.mu.RLock()
defer p.mu.RUnlock()
for _, a := range p.Accounts {
if a.Platform == platform && a.AccountID == accountID {
return true
}
}
return false
}
// RegisterAccount appends (platform, accountID) to the whitelist if it is not
// already present, leaving the profile otherwise untouched. It is the in-memory
// counterpart of RegisterOwnerAccount: the channel bridge holds the very same
// *OwnerProfile, so a scan-login confirmation can extend the whitelist without a
// restart. Duplicate (platform, accountID) pairs are a no-op, matching IsOwner.
func (p *OwnerProfile) RegisterAccount(platform, accountID string) {
if p == nil {
return
}
p.mu.Lock()
defer p.mu.Unlock()
for _, a := range p.Accounts {
if a.Platform == platform && a.AccountID == accountID {
return
}
}
p.Accounts = append(p.Accounts, OwnerAccount{Platform: platform, AccountID: accountID})
}
// AccountIdentifiers returns the distinct channel account ids owned by the
// owner, for diagnostics / listing. Safe for concurrent use with RegisterAccount.
func (p *OwnerProfile) AccountIdentifiers() []string {
if p == nil {
return nil
}
p.mu.RLock()
defer p.mu.RUnlock()
seen := make(map[string]struct{}, len(p.Accounts))
out := make([]string, 0, len(p.Accounts))
for _, a := range p.Accounts {
if _, ok := seen[a.Platform+":"+a.AccountID]; ok {
continue
}
seen[a.Platform+":"+a.AccountID] = struct{}{}
out = append(out, a.Platform+":"+a.AccountID)
}
return out
}