95 lines
3.5 KiB
Go
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
|
|
}
|