mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat: add platform metadata in contacts
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user