feat(channel/session): 渠道↔会话映射 + 主人识别(actor元数据+白名单,不读消息内容) + 群聊隔离 + 主动推送策略(iLink24h/OneBot限频)
This commit is contained in:
1 parent
3ce9489f8e
commit
7adaf490c7
7 files changed
+1133
No files matched your search
@@ -0,0 +1,85 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/restrict"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/user"
|
||||
)
|
||||
|
||||
// ResolveActor turns a normalized inbound message into the strongly typed actor
|
||||
// metadata used by every downstream permission decision. It is the structural
|
||||
// owner-recognition rule from design/11 §0: the owner is decided by matching
|
||||
// channel-level identity (platform + account id) against the user.OwnerProfile
|
||||
// whitelist (or by honoring a channel-level owner tag), and never by reading the
|
||||
// message text. A message whose body says "I am the owner" is not the owner
|
||||
// unless it also matches the whitelist.
|
||||
func ResolveActor(msg adapter.UnifiedMessage, owner *user.OwnerProfile) adapter.Actor {
|
||||
actor := adapter.Actor{
|
||||
Role: msg.Sender.Role,
|
||||
UserID: firstNonEmpty(msg.Sender.UserID, msg.SenderID),
|
||||
DisplayName: firstNonEmpty(msg.Sender.DisplayName, msg.SenderName),
|
||||
IsGroup: msg.Sender.IsGroup || msg.IsGroup(),
|
||||
GroupID: firstNonEmpty(msg.Sender.GroupID, msg.GroupID),
|
||||
GroupName: firstNonEmpty(msg.Sender.GroupName, msg.GroupName),
|
||||
}
|
||||
|
||||
// Structural owner resolution: the whitelist is authoritative, and a
|
||||
// channel-level owner tag (from authenticated login binding) is also
|
||||
// structural. Neither depends on msg.Content.
|
||||
if (owner != nil && owner.IsOwner(msg.Platform, msg.SenderID)) || actor.Role == adapter.RoleOwner {
|
||||
actor.Role = adapter.RoleOwner
|
||||
} else if !actor.Role.Valid() {
|
||||
// No channel-tagged role and not the owner: default to a member rather
|
||||
// than leaving the role ambiguous, which is the conservative choice.
|
||||
actor.Role = adapter.RoleMember
|
||||
}
|
||||
return actor.Resolve()
|
||||
}
|
||||
|
||||
// EnvFor maps a resolved actor to the restrict.ActorEnv used to gate tools.
|
||||
// Group chats always map to group_member (isolated, restricted) even when the
|
||||
// owner is the speaker (design/02 §3: owner group speech stays in the isolated
|
||||
// group context). A non-group owner maps to owner (full access); a non-group
|
||||
// non-owner maps to stranger.
|
||||
func EnvFor(actor adapter.Actor) restrict.ActorEnv {
|
||||
if actor.IsGroup {
|
||||
return restrict.ActorGroupMember
|
||||
}
|
||||
if actor.IsOwner() {
|
||||
return restrict.ActorOwner
|
||||
}
|
||||
return restrict.ActorStranger
|
||||
}
|
||||
|
||||
// IdentifyContext returns the restrict.ActorEnv for a message from the channel
|
||||
// metadata it carries (msg.Sender), inferring the group flag from the channel
|
||||
// type when the sender is not already tagged. This is the environment the policy
|
||||
// engine is bound to before any tool call.
|
||||
func IdentifyContext(msg adapter.UnifiedMessage) restrict.ActorEnv {
|
||||
actor := msg.Sender.Resolve()
|
||||
if msg.IsGroup() {
|
||||
actor.IsGroup = true
|
||||
}
|
||||
return EnvFor(actor)
|
||||
}
|
||||
|
||||
// Env returns the restrict actor environment for the session. It mirrors
|
||||
// EnvFor: group chats are group_member regardless of membership, owner private
|
||||
// is owner, and anything else is stranger.
|
||||
func (s Session) Env() restrict.ActorEnv {
|
||||
if s.IsGroup {
|
||||
return restrict.ActorGroupMember
|
||||
}
|
||||
if s.Owner {
|
||||
return restrict.ActorOwner
|
||||
}
|
||||
return restrict.ActorStranger
|
||||
}
|
||||
|
||||
// firstNonEmpty returns a when a is non-empty, otherwise b.
|
||||
func firstNonEmpty(a, b string) string {
|
||||
if a != "" {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in new issue
Block a user