feat(identity): add unified user identity with canonical platform:id format

Introduce SenderInfo struct and pkg/identity package to standardize user
identification across all channels. Each channel now constructs structured
sender info (platform, platformID, canonicalID, username, displayName)
instead of ad-hoc string IDs. Allow-list matching supports all legacy
formats (numeric ID, @username, id|username) plus the new canonical
"platform:id" format. Session key resolution also handles canonical
peerIDs for backward-compatible identity link matching.
This commit is contained in:
Hoshina
2026-02-23 06:56:48 +08:00
parent f645e9a377
commit 56d80373eb
20 changed files with 742 additions and 34 deletions
+9
View File
@@ -163,6 +163,15 @@ func resolveLinkedPeerID(identityLinks map[string][]string, channel, peerID stri
scopedCandidate := fmt.Sprintf("%s:%s", channel, strings.ToLower(peerID))
candidates[scopedCandidate] = true
}
// If peerID is already in canonical "platform:id" format, also add the
// bare ID part as a candidate for backward compatibility with identity_links
// that use raw IDs (e.g. "123" instead of "telegram:123").
if idx := strings.Index(rawCandidate, ":"); idx > 0 && idx < len(rawCandidate)-1 {
bareID := rawCandidate[idx+1:]
candidates[bareID] = true
}
if len(candidates) == 0 {
return ""
}
+45
View File
@@ -115,6 +115,51 @@ func TestBuildAgentPeerSessionKey_IdentityLink(t *testing.T) {
}
}
func TestResolveLinkedPeerID_CanonicalPeerID(t *testing.T) {
// When peerID is already in canonical "platform:id" format,
// it should match identity_links that use the bare ID.
links := map[string][]string{
"john": {"123"},
}
got := resolveLinkedPeerID(links, "telegram", "telegram:123")
if got != "john" {
t.Errorf("resolveLinkedPeerID with canonical peerID = %q, want %q", got, "john")
}
}
func TestResolveLinkedPeerID_CanonicalInLinks(t *testing.T) {
// When identity_links contain canonical IDs and peerID is canonical too
links := map[string][]string{
"john": {"telegram:123", "discord:456"},
}
got := resolveLinkedPeerID(links, "telegram", "telegram:123")
if got != "john" {
t.Errorf("resolveLinkedPeerID canonical in links = %q, want %q", got, "john")
}
}
func TestResolveLinkedPeerID_BarePeerIDMatchesCanonicalLink(t *testing.T) {
// When peerID is bare "123" and links have "telegram:123",
// the scoped candidate "telegram:123" should match.
links := map[string][]string{
"john": {"telegram:123"},
}
got := resolveLinkedPeerID(links, "telegram", "123")
if got != "john" {
t.Errorf("resolveLinkedPeerID bare peer matches canonical link = %q, want %q", got, "john")
}
}
func TestResolveLinkedPeerID_NoMatch(t *testing.T) {
links := map[string][]string{
"john": {"telegram:123"},
}
got := resolveLinkedPeerID(links, "discord", "999")
if got != "" {
t.Errorf("resolveLinkedPeerID no match = %q, want empty", got)
}
}
func TestParseAgentSessionKey_Valid(t *testing.T) {
parsed := ParseAgentSessionKey("agent:sales:telegram:direct:user123")
if parsed == nil {