Files
Memoh/internal/chat/types.go
T
Ran 6acdd191c7 Squashed commit of the following:
commit bcdb026ae43e4f95d0b2c4f9bd440a2df9d6b514
Author: Ran <16112591+chen-ran@users.noreply.github.com>
Date:   Thu Feb 12 17:10:32 2026 +0800

    chore: update DEVELOPMENT.md

commit 30281742ef
Merge: ca5c6a1 5b05f13
Author: BBQ <bbq@BBQdeMacBook-Air.local>
Date:   Thu Feb 12 15:49:17 2026 +0800

    merge(github/main): integrate fx dependency injection framework

    Merge upstream fx refactor and adapt all services to use go.uber.org/fx
    for dependency injection. Resolve conflicts in main.go, server.go,
    and service constructors while preserving our domain model changes.

    - Fix telegram adapter panic on shutdown (double close channel)
    - Fix feishu adapter processing messages after stop
    - Increase directory lookup timeout from 2s to 5s

commit ca5c6a1866
Author: BBQ <bbq@BBQdeMacBook-Air.local>
Date:   Thu Feb 12 15:33:09 2026 +0800

    refactor(core): restructure conversation, channel and message domains

    - Rename chat module to conversation with flow-based architecture
    - Move channelidentities into channel/identities subpackage
    - Add channel/route for routing logic
    - Add message service with event hub
    - Add MCP providers: container, directory, schedule
    - Refactor Feishu/Telegram adapters with directory and stream support
    - Add platform management page and channel badges in web UI
    - Update database schema for conversations, messages and channel routes
    - Add @memoh/shared package for cross-package type definitions

commit 75e2ef0467
Merge: d99ba38 01cb6c8
Author: BBQ <bbq@BBQdeMacBook-Air.local>
Date:   Thu Feb 12 14:45:49 2026 +0800

    merge(github): merge github/main, resolve index.ts URL conflict

    Keep our defensive absolute-URL check in createAuthFetcher.

commit d99ba38b7d
Merge: 860e20f 35ce7d1
Author: BBQ <bbq@BBQdeMacBook-Air.local>
Date:   Thu Feb 12 05:20:18 2026 +0800

    merge(github): merge github/main, keep our code and docs/spec

commit 860e20fe70
Author: BBQ <bbq@BBQdeMacBook-Air.local>
Date:   Wed Feb 11 22:13:27 2026 +0800

    docs(docs): add concepts and style guides for VitePress site

    - Add concepts: identity-and-binding, index (en/zh)
    - Add style: terminology (en/zh)
    - Update index and zh/index
    - Update .vitepress/config.ts

commit a75fdb8040
Author: BBQ <bbq@BBQdeMacBook-Air.local>
Date:   Wed Feb 11 17:37:16 2026 +0800

    refactor(mcp): standardize unified tool gateway on go-sdk

    Split business executors from federation sources and migrate unified tool/federation transports to the official go-sdk for stricter MCP compliance and safer session lifecycle handling. Add targeted regression tests for accept compatibility, initialization retries, pending cleanup, and include updated swagger artifacts.

commit 02b33c8e85
Author: BBQ <bbq@BBQdeMacBook-Air.local>
Date:   Wed Feb 11 15:42:21 2026 +0800

    refactor(core): finalize user-centric identity and policy cleanup

    Unify auth and chat identity semantics around user_id, enforce personal-bot owner-only authorization, and remove legacy compatibility branches in integration tests.

commit 06e8619a37
Author: BBQ <bbq@BBQdeMacBook-Air.local>
Date:   Wed Feb 11 14:47:03 2026 +0800

    refactor(core): migrate channel identity and binding across app

    Align channel identity and bind flow across backend and app-facing layers, including generated swagger artifacts and package lock updates while excluding docs content changes.
2026-02-12 17:13:03 +08:00

270 lines
9.2 KiB
Go

// Package conversation orchestrates interactions with the agent gateway, including
// synchronous and streaming responses, scheduled triggers, messages, and memory storage.
package conversation
import (
"encoding/json"
"strings"
"time"
)
// Chat kind constants.
const (
KindDirect = "direct"
KindGroup = "group"
KindThread = "thread"
)
// Participant role constants.
const (
RoleOwner = "owner"
RoleAdmin = "admin"
RoleMember = "member"
)
// Chat list access mode constants.
const (
AccessModeParticipant = "participant"
AccessModeChannelIdentityObserved = "channel_identity_observed"
)
// Chat is the first-class conversation container.
type Chat struct {
ID string `json:"id"`
BotID string `json:"bot_id"`
Kind string `json:"kind"`
ParentChatID string `json:"parent_chat_id,omitempty"`
Title string `json:"title,omitempty"`
CreatedBy string `json:"created_by"`
Metadata map[string]any `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ChatListItem is a chat entry with access context for list rendering.
type ChatListItem struct {
ID string `json:"id"`
BotID string `json:"bot_id"`
Kind string `json:"kind"`
ParentChatID string `json:"parent_chat_id,omitempty"`
Title string `json:"title,omitempty"`
CreatedBy string `json:"created_by"`
Metadata map[string]any `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
AccessMode string `json:"access_mode"`
ParticipantRole string `json:"participant_role,omitempty"`
LastObservedAt *time.Time `json:"last_observed_at,omitempty"`
}
// ChatReadAccess is the resolved access context for reading chat content.
type ChatReadAccess struct {
AccessMode string
ParticipantRole string
LastObservedAt *time.Time
}
// Participant represents a chat member.
type Participant struct {
ChatID string `json:"chat_id"`
UserID string `json:"user_id"`
Role string `json:"role"`
JoinedAt time.Time `json:"joined_at"`
}
// Settings holds per-chat configuration.
type Settings struct {
ChatID string `json:"chat_id"`
ModelID string `json:"model_id,omitempty"`
}
// Route maps external channel conversations to a chat.
type Route struct {
ID string `json:"id"`
ChatID string `json:"chat_id"`
BotID string `json:"bot_id"`
Platform string `json:"platform"`
ChannelConfigID string `json:"channel_config_id,omitempty"`
ConversationID string `json:"conversation_id"`
ThreadID string `json:"thread_id,omitempty"`
ReplyTarget string `json:"reply_target,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Message represents a single persisted bot message.
type Message struct {
ID string `json:"id"`
BotID string `json:"bot_id"`
RouteID string `json:"route_id,omitempty"`
SenderChannelIdentityID string `json:"sender_channel_identity_id,omitempty"`
SenderUserID string `json:"sender_user_id,omitempty"`
Platform string `json:"platform,omitempty"`
ExternalMessageID string `json:"external_message_id,omitempty"`
SourceReplyToMessageID string `json:"source_reply_to_message_id,omitempty"`
Role string `json:"role"`
Content json.RawMessage `json:"content"`
Metadata map[string]any `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
// CreateRequest is the input for creating a bot-scoped conversation container.
type CreateRequest struct {
Kind string `json:"kind"`
Title string `json:"title,omitempty"`
ParentChatID string `json:"parent_chat_id,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
}
// UpdateSettingsRequest is the input for updating chat settings.
type UpdateSettingsRequest struct {
ModelID *string `json:"model_id,omitempty"`
}
// ResolveChatResult is returned by ResolveChat.
type ResolveChatResult struct {
ChatID string
RouteID string
Created bool
}
// ModelMessage is the canonical message format exchanged with the agent gateway.
// Aligned with Vercel AI SDK ModelMessage structure.
type ModelMessage struct {
Role string `json:"role"`
Content json.RawMessage `json:"content,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
Name string `json:"name,omitempty"`
}
// TextContent extracts the plain text from the message content.
// If content is a string, it returns it directly.
// If content is an array of parts, it joins all text-type parts.
func (m ModelMessage) TextContent() string {
if len(m.Content) == 0 {
return ""
}
var s string
if err := json.Unmarshal(m.Content, &s); err == nil {
return s
}
var parts []ContentPart
if err := json.Unmarshal(m.Content, &parts); err == nil {
texts := make([]string, 0, len(parts))
for _, p := range parts {
if strings.TrimSpace(p.Text) != "" {
texts = append(texts, p.Text)
}
}
return strings.Join(texts, "\n")
}
return ""
}
// ContentParts parses the content as an array of ContentPart.
// Returns nil if the content is a plain string or not parseable.
func (m ModelMessage) ContentParts() []ContentPart {
if len(m.Content) == 0 {
return nil
}
var parts []ContentPart
if err := json.Unmarshal(m.Content, &parts); err != nil {
return nil
}
return parts
}
// HasContent reports whether the message carries non-empty content or tool calls.
func (m ModelMessage) HasContent() bool {
if strings.TrimSpace(m.TextContent()) != "" {
return true
}
if len(m.ContentParts()) > 0 {
return true
}
return len(m.ToolCalls) > 0
}
// NewTextContent creates a json.RawMessage from a plain string.
func NewTextContent(text string) json.RawMessage {
data, _ := json.Marshal(text)
return data
}
// ContentPart represents one element of a multi-part message content.
type ContentPart struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
URL string `json:"url,omitempty"`
Styles []string `json:"styles,omitempty"`
Language string `json:"language,omitempty"`
ChannelIdentityID string `json:"channel_identity_id,omitempty"`
Emoji string `json:"emoji,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
}
// HasValue reports whether the content part carries a meaningful value.
func (p ContentPart) HasValue() bool {
return strings.TrimSpace(p.Text) != "" ||
strings.TrimSpace(p.URL) != "" ||
strings.TrimSpace(p.Emoji) != ""
}
// ToolCall represents a function/tool invocation in an assistant message.
type ToolCall struct {
ID string `json:"id,omitempty"`
Type string `json:"type"`
Function ToolCallFunction `json:"function"`
}
// ToolCallFunction holds the name and serialized arguments of a tool call.
type ToolCallFunction struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
}
// ChatRequest is the input for Chat and StreamChat.
type ChatRequest struct {
BotID string `json:"-"`
ChatID string `json:"-"`
Token string `json:"-"`
UserID string `json:"-"`
SourceChannelIdentityID string `json:"-"`
ContainerID string `json:"-"`
DisplayName string `json:"-"`
RouteID string `json:"-"`
ChatToken string `json:"-"`
ExternalMessageID string `json:"-"`
UserMessagePersisted bool `json:"-"`
Query string `json:"query"`
Model string `json:"model,omitempty"`
Provider string `json:"provider,omitempty"`
MaxContextLoadTime int `json:"max_context_load_time,omitempty"`
Language string `json:"language,omitempty"`
Channels []string `json:"channels,omitempty"`
CurrentChannel string `json:"current_channel,omitempty"`
Messages []ModelMessage `json:"messages,omitempty"`
Skills []string `json:"skills,omitempty"`
AllowedActions []string `json:"allowed_actions,omitempty"`
}
// ChatResponse is the output of a non-streaming chat call.
type ChatResponse struct {
Messages []ModelMessage `json:"messages"`
Skills []string `json:"skills,omitempty"`
Model string `json:"model,omitempty"`
Provider string `json:"provider,omitempty"`
}
// StreamChunk is a raw JSON chunk from the streaming response.
type StreamChunk = json.RawMessage
// AssistantOutput holds extracted assistant content for downstream consumers.
type AssistantOutput struct {
Content string
Parts []ContentPart
}