feat: add platform metadata in contacts

This commit is contained in:
Acbox
2026-02-20 22:19:15 +08:00
parent c9d96d9da3
commit 1a78ba3f53
8 changed files with 133 additions and 4 deletions
+5
View File
@@ -87,6 +87,11 @@ UPDATE bot_channel_routes
SET default_reply_target = sqlc.arg(reply_target), updated_at = now()
WHERE id = sqlc.arg(id);
-- name: UpdateChatRouteMetadata :exec
UPDATE bot_channel_routes
SET metadata = sqlc.arg(metadata), updated_at = now()
WHERE id = sqlc.arg(id);
-- name: DeleteChatRoute :exec
DELETE FROM bot_channel_routes
WHERE id = $1;
+30
View File
@@ -176,6 +176,7 @@ func (p *ChannelInboundProcessor) HandleInbound(ctx context.Context, cfg channel
if p.routeResolver == nil {
return fmt.Errorf("route resolver not configured")
}
routeMetadata := buildRouteMetadata(msg, identity)
resolved, err := p.routeResolver.ResolveConversation(ctx, route.ResolveInput{
BotID: identity.BotID,
Platform: msg.Channel.String(),
@@ -185,6 +186,7 @@ func (p *ChannelInboundProcessor) HandleInbound(ctx context.Context, cfg channel
ChannelIdentityID: identity.UserID,
ChannelConfigID: identity.ChannelConfigID,
ReplyTarget: strings.TrimSpace(msg.ReplyTarget),
Metadata: routeMetadata,
})
if err != nil {
return fmt.Errorf("resolve route conversation: %w", err)
@@ -1788,3 +1790,31 @@ func parseAttachmentDelta(raw json.RawMessage) []channel.Attachment {
}
return attachments
}
// buildRouteMetadata extracts user/conversation information for route metadata persistence.
func buildRouteMetadata(msg channel.InboundMessage, identity InboundIdentity) map[string]any {
m := make(map[string]any)
if v := strings.TrimSpace(identity.DisplayName); v != "" {
m["sender_display_name"] = v
}
if v := strings.TrimSpace(msg.Sender.SubjectID); v != "" {
m["sender_id"] = v
}
if v := strings.TrimSpace(msg.Conversation.Name); v != "" {
m["conversation_name"] = v
}
for k, v := range msg.Sender.Attributes {
v = strings.TrimSpace(v)
if v == "" {
continue
}
switch k {
case "username":
m["sender_username"] = v
}
}
return m
}
+51
View File
@@ -149,6 +149,22 @@ func (s *DBService) UpdateReplyTarget(ctx context.Context, routeID, replyTarget
})
}
// UpdateMetadata replaces the route metadata.
func (s *DBService) UpdateMetadata(ctx context.Context, routeID string, metadata map[string]any) error {
pgID, err := dbpkg.ParseUUID(routeID)
if err != nil {
return err
}
data, err := json.Marshal(nonNilMap(metadata))
if err != nil {
return fmt.Errorf("marshal route metadata: %w", err)
}
return s.queries.UpdateChatRouteMetadata(ctx, sqlc.UpdateChatRouteMetadataParams{
ID: pgID,
Metadata: data,
})
}
// ResolveConversation finds or creates a conversation route for an inbound message.
func (s *DBService) ResolveConversation(ctx context.Context, input ResolveInput) (ResolveConversationResult, error) {
route, err := s.Find(ctx, input.BotID, input.Platform, input.ConversationID, input.ThreadID)
@@ -169,6 +185,12 @@ func (s *DBService) ResolveConversation(ctx context.Context, input ResolveInput)
s.logger.Warn("update route reply target failed", slog.Any("error", updateErr))
}
}
if len(input.Metadata) > 0 && metadataChanged(route.Metadata, input.Metadata) {
merged := mergeMetadata(route.Metadata, input.Metadata)
if updateErr := s.UpdateMetadata(ctx, route.ID, merged); updateErr != nil && s.logger != nil {
s.logger.Warn("update route metadata failed", slog.Any("error", updateErr))
}
}
pgConversationID, parseErr := dbpkg.ParseUUID(route.ChatID)
if parseErr != nil {
return ResolveConversationResult{}, fmt.Errorf("parse route conversation id: %w", parseErr)
@@ -217,6 +239,7 @@ func (s *DBService) ResolveConversation(ctx context.Context, input ResolveInput)
ThreadID: input.ThreadID,
ConversationType: input.ConversationType,
ReplyTarget: input.ReplyTarget,
Metadata: input.Metadata,
})
if err != nil {
// Concurrent insert race: another goroutine created the same route between
@@ -342,3 +365,31 @@ func parseJSONMap(data []byte) map[string]any {
}
return m
}
// metadataChanged returns true when any key in incoming differs from existing.
func metadataChanged(existing, incoming map[string]any) bool {
for k, v := range incoming {
old, ok := existing[k]
if !ok {
return true
}
oldJSON, _ := json.Marshal(old)
newJSON, _ := json.Marshal(v)
if string(oldJSON) != string(newJSON) {
return true
}
}
return false
}
// mergeMetadata merges incoming keys into existing, preserving keys not in incoming.
func mergeMetadata(existing, incoming map[string]any) map[string]any {
merged := make(map[string]any, len(existing)+len(incoming))
for k, v := range existing {
merged[k] = v
}
for k, v := range incoming {
merged[k] = v
}
return merged
}
+2
View File
@@ -51,6 +51,7 @@ type ResolveInput struct {
ChannelIdentityID string
ChannelConfigID string
ReplyTarget string
Metadata map[string]any
}
// Resolver defines the route resolution behavior used by inbound routing.
@@ -67,4 +68,5 @@ type Service interface {
List(ctx context.Context, chatID string) ([]Route, error)
Delete(ctx context.Context, routeID string) error
UpdateReplyTarget(ctx context.Context, routeID, replyTarget string) error
UpdateMetadata(ctx context.Context, routeID string, metadata map[string]any) error
}
+16
View File
@@ -296,6 +296,22 @@ func (q *Queries) ListChatRoutes(ctx context.Context, chatID pgtype.UUID) ([]Lis
return items, nil
}
const updateChatRouteMetadata = `-- name: UpdateChatRouteMetadata :exec
UPDATE bot_channel_routes
SET metadata = $1, updated_at = now()
WHERE id = $2
`
type UpdateChatRouteMetadataParams struct {
Metadata []byte `json:"metadata"`
ID pgtype.UUID `json:"id"`
}
func (q *Queries) UpdateChatRouteMetadata(ctx context.Context, arg UpdateChatRouteMetadataParams) error {
_, err := q.db.Exec(ctx, updateChatRouteMetadata, arg.Metadata, arg.ID)
return err
}
const updateChatRouteReplyTarget = `-- name: UpdateChatRouteReplyTarget :exec
UPDATE bot_channel_routes
SET default_reply_target = $1, updated_at = now()
+4 -4
View File
@@ -133,7 +133,7 @@ SELECT
m.bot_id,
m.route_id,
m.sender_channel_identity_id,
COALESCE(ci.user_id, m.sender_account_user_id) AS sender_user_id,
m.sender_account_user_id AS sender_user_id,
m.channel_type AS platform,
m.source_message_id AS external_message_id,
m.source_reply_to_message_id,
@@ -211,7 +211,7 @@ SELECT
m.bot_id,
m.route_id,
m.sender_channel_identity_id,
COALESCE(ci.user_id, m.sender_account_user_id) AS sender_user_id,
m.sender_account_user_id AS sender_user_id,
m.channel_type AS platform,
m.source_message_id AS external_message_id,
m.source_reply_to_message_id,
@@ -296,7 +296,7 @@ SELECT
m.bot_id,
m.route_id,
m.sender_channel_identity_id,
COALESCE(ci.user_id, m.sender_account_user_id) AS sender_user_id,
m.sender_account_user_id AS sender_user_id,
m.channel_type AS platform,
m.source_message_id AS external_message_id,
m.source_reply_to_message_id,
@@ -379,7 +379,7 @@ SELECT
m.bot_id,
m.route_id,
m.sender_channel_identity_id,
COALESCE(ci.user_id, m.sender_account_user_id) AS sender_user_id,
m.sender_account_user_id AS sender_user_id,
m.channel_type AS platform,
m.source_message_id AS external_message_id,
m.source_reply_to_message_id,
+17
View File
@@ -186,6 +186,23 @@ type McpConnection struct {
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type MediaAsset struct {
ID pgtype.UUID `json:"id"`
BotID pgtype.UUID `json:"bot_id"`
StorageProviderID pgtype.UUID `json:"storage_provider_id"`
ContentHash string `json:"content_hash"`
MediaType string `json:"media_type"`
Mime string `json:"mime"`
SizeBytes int64 `json:"size_bytes"`
StorageKey string `json:"storage_key"`
OriginalName pgtype.Text `json:"original_name"`
Width pgtype.Int4 `json:"width"`
Height pgtype.Int4 `json:"height"`
DurationMs pgtype.Int8 `json:"duration_ms"`
Metadata []byte `json:"metadata"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
type Model struct {
ID pgtype.UUID `json:"id"`
ModelID string `json:"model_id"`
@@ -85,6 +85,14 @@ func (p *Executor) CallTool(ctx context.Context, session mcpgw.ToolSessionContex
"last_active": r.UpdatedAt.Format("2006-01-02T15:04:05Z"),
}
if len(r.Metadata) > 0 {
if v, ok := r.Metadata["conversation_name"].(string); ok && v != "" {
entry["display_name"] = v
} else if v, ok := r.Metadata["sender_display_name"].(string); ok && v != "" {
entry["display_name"] = v
}
if v, ok := r.Metadata["sender_username"].(string); ok && v != "" {
entry["username"] = v
}
entry["metadata"] = r.Metadata
}
contacts = append(contacts, entry)