From ca5a018cd4cc009e585ae5b3de35a911ace3eacb Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Mon, 4 May 2026 09:49:30 +0800 Subject: [PATCH] feat(chat): domain types + service interfaces + cross-module Reader interfaces --- internal/chat/domain.go | 330 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 internal/chat/domain.go diff --git a/internal/chat/domain.go b/internal/chat/domain.go new file mode 100644 index 0000000..4a0b919 --- /dev/null +++ b/internal/chat/domain.go @@ -0,0 +1,330 @@ +// Package chat 承载 Chat 模块的领域类型、Service 接口与对外暴露给其他业务模块的只读 Reader 接口。 +// +// 跨模块约束(Platform spec §4.3):其他业务模块(如 mcp)只能通过本文件暴露的 +// MessageReader / ConversationReader 等 interface 横向依赖;不允许直接 import +// repository.go / service_*.go。 +package chat + +import ( + "context" + "time" + + "github.com/google/uuid" + + "github.com/yan1h/agent-coding-workflow/internal/infra/llm" +) + +// MessageRole 与 DB CHECK 约束一致。 +type MessageRole string + +const ( + RoleUser MessageRole = "user" + RoleAssistant MessageRole = "assistant" +) + +// MessageStatus 是 messages.status 枚举。 +type MessageStatus string + +const ( + StatusPending MessageStatus = "pending" + StatusOK MessageStatus = "ok" + StatusError MessageStatus = "error" + StatusCancelled MessageStatus = "cancelled" +) + +// TemplateScope 区分系统/用户模板。 +type TemplateScope string + +const ( + TemplateScopeSystem TemplateScope = "system" + TemplateScopeUser TemplateScope = "user" +) + +// LLMProvider 与 DB CHECK 一致。 +type LLMProvider string + +const ( + ProviderAnthropic LLMProvider = "anthropic" + ProviderOpenAI LLMProvider = "openai" + ProviderGemini LLMProvider = "gemini" +) + +// ModelCapabilities 描述模型能力,用于附件 capability 校验与前端能力提示。 +type ModelCapabilities struct { + Image bool `json:"image"` + PDF bool `json:"pdf"` + Audio bool `json:"audio"` + Reasoning bool `json:"reasoning"` + Tools bool `json:"tools"` +} + +// LLMEndpoint 是一个 LLM 端点(provider + base_url + 加密 api_key)。 +type LLMEndpoint struct { + ID uuid.UUID + Provider LLMProvider + DisplayName string + BaseURL string + APIKeyEncrypted []byte + CreatedBy uuid.UUID + CreatedAt, UpdatedAt time.Time +} + +// LLMModel 是 endpoint 下的具体模型。 +type LLMModel struct { + ID uuid.UUID + EndpointID uuid.UUID + ModelID string + DisplayName string + Capabilities ModelCapabilities + ContextWindow int + MaxOutputTokens int + PromptPricePerMillionUSD float64 + CompletionPricePerMillionUSD float64 + ThinkingPricePerMillionUSD float64 + IsTitleGenerator bool + Enabled bool + SortOrder int + CreatedAt, UpdatedAt time.Time +} + +// PromptTemplate 是 system prompt 模板。 +type PromptTemplate struct { + ID uuid.UUID + Name, Content string + Scope TemplateScope + OwnerID *uuid.UUID + CreatedAt, UpdatedAt time.Time +} + +// Conversation 是对话聚合根。Mounting 字段全部 NULLABLE 实现双轨制。 +type Conversation struct { + ID uuid.UUID + UserID uuid.UUID + ProjectID *uuid.UUID + WorkspaceID *uuid.UUID + IssueID *uuid.UUID + ModelID uuid.UUID + SystemPrompt string + Title *string + TitleGeneratedAt *time.Time + CreatedAt, UpdatedAt time.Time + DeletedAt *time.Time +} + +// Attachment 是 message 的附件(user_id 必填,message_id 上传后未关联时为 nil)。 +type Attachment struct { + ID uuid.UUID + UserID uuid.UUID + MessageID *int64 // nil = orphan + Filename string + MimeType string + SizeBytes int64 + SHA256 string + StoragePath string + CreatedAt time.Time +} + +// Message 是一轮对话消息。BIGSERIAL ID。 +type Message struct { + ID int64 + ConversationID uuid.UUID + Role MessageRole + Content string + Thinking *string + ToolCalls []byte // JSONB raw bytes; mcp 预留,MVP 始终 nil + Status MessageStatus + ErrorMessage *string + PromptTokens *int + CompletionTokens *int + ThinkingTokens *int + CreatedAt, UpdatedAt time.Time +} + +// UsageRecord 是 llm_usage 表一行。 +type UsageRecord struct { + ID int64 + ConversationID uuid.UUID + MessageID int64 + UserID uuid.UUID + EndpointID uuid.UUID + ModelID uuid.UUID + PromptTokens int + CompletionTokens int + ThinkingTokens int + CostUSD float64 + CreatedAt time.Time +} + +// ===== 横向暴露给其他业务模块的只读接口(mcp 用) ===== + +// MessageReader 暴露只读消息查询,供 mcp 模块按用户上下文返回最近消息。 +type MessageReader interface { + ListRecentMessages(ctx context.Context, userID uuid.UUID, limit int) ([]Message, error) +} + +// ConversationReader 暴露只读会话查询。 +type ConversationReader interface { + ListConversationsByUser(ctx context.Context, userID uuid.UUID, limit int) ([]Conversation, error) +} + +// ===== Service 接口(在 service_*.go 实现) ===== + +// ConversationService — 会话 CRUD + 标题生成 hook。 +type ConversationService interface { + Create(ctx context.Context, userID uuid.UUID, in CreateConversationInput) (*Conversation, error) + List(ctx context.Context, userID uuid.UUID, filter ConversationFilter) ([]Conversation, error) + Get(ctx context.Context, userID, id uuid.UUID) (*Conversation, []Message, error) + UpdateTitle(ctx context.Context, userID, id uuid.UUID, title string) error + SoftDelete(ctx context.Context, userID, id uuid.UUID) error +} + +// CreateConversationInput 是创建会话的入参。 +type CreateConversationInput struct { + ModelID uuid.UUID + TemplateID *uuid.UUID + SystemPrompt *string // 优先级:自定义 > 模板内容 > 空字符串 + ProjectID *uuid.UUID + WorkspaceID *uuid.UUID + IssueID *uuid.UUID +} + +// ConversationFilter 列表过滤参数。 +type ConversationFilter struct { + ProjectID *uuid.UUID + WorkspaceID *uuid.UUID + IssueID *uuid.UUID + TitleQuery string + BeforeID *uuid.UUID // cursor + Limit int +} + +// MessageService — 发消息 + 流接收 + 取消 + 重试。 +type MessageService interface { + Send(ctx context.Context, userID, conversationID uuid.UUID, content string, attachmentIDs []uuid.UUID) (userMsgID, assistantMsgID int64, err error) + Stream(ctx context.Context, userID, conversationID uuid.UUID, sinceMessageID int64) (<-chan SSEEvent, error) + Cancel(ctx context.Context, userID uuid.UUID, messageID int64) error + Retry(ctx context.Context, userID uuid.UUID, messageID int64) (newAssistantMsgID int64, err error) + ListMessages(ctx context.Context, userID, conversationID uuid.UUID, beforeID *int64, limit int) ([]Message, error) +} + +// SSEEvent 是 chat handler 下行的 SSE 事件中间表示。 +type SSEEvent struct { + ID int64 // 自增;客户端用作 ?since= + Event string // "message_meta" | "text_delta" | "thinking_delta" | "usage" | "done" | "error" + Data any // 序列化为 JSON 的 payload +} + +// TemplateService — prompt_templates CRUD。 +type TemplateService interface { + List(ctx context.Context, userID uuid.UUID) ([]PromptTemplate, error) + Create(ctx context.Context, userID uuid.UUID, name, content string, scope TemplateScope) (*PromptTemplate, error) + Update(ctx context.Context, userID, id uuid.UUID, name, content string) error + Delete(ctx context.Context, userID, id uuid.UUID) error +} + +// EndpointService — admin LLM endpoints / models CRUD(管理员守卫上层处理)。 +type EndpointService interface { + ListEndpoints(ctx context.Context) ([]LLMEndpoint, error) + CreateEndpoint(ctx context.Context, userID uuid.UUID, in CreateEndpointInput) (*LLMEndpoint, error) + UpdateEndpoint(ctx context.Context, id uuid.UUID, in UpdateEndpointInput) error + DeleteEndpoint(ctx context.Context, id uuid.UUID) error + TestEndpoint(ctx context.Context, id uuid.UUID) error // ping 上游 + + ListModels(ctx context.Context, onlyEnabled bool) ([]LLMModel, error) + CreateModel(ctx context.Context, in CreateModelInput) (*LLMModel, error) + UpdateModel(ctx context.Context, id uuid.UUID, in UpdateModelInput) error + DeleteModel(ctx context.Context, id uuid.UUID) error +} + +// CreateEndpointInput 创建 endpoint 入参。 +type CreateEndpointInput struct { + Provider LLMProvider + DisplayName string + BaseURL string + APIKey string // 明文,service 内 AES-GCM 加密 +} + +// UpdateEndpointInput 更新 endpoint 入参;nil 字段保持不变。 +type UpdateEndpointInput struct { + DisplayName *string + BaseURL *string + APIKey *string // nil = 不修改 +} + +// CreateModelInput 创建 model 入参。 +type CreateModelInput struct { + EndpointID uuid.UUID + ModelID, DisplayName string + Capabilities ModelCapabilities + ContextWindow, MaxOutputTokens int + PromptPrice, CompletionPrice, ThinkingPrice float64 + IsTitleGenerator bool + Enabled bool + SortOrder int +} + +// UpdateModelInput 更新 model 入参;nil 字段保持不变。 +type UpdateModelInput struct { + DisplayName *string + Capabilities *ModelCapabilities + ContextWindow *int + MaxOutputTokens *int + PromptPrice *float64 + CompletionPrice *float64 + ThinkingPrice *float64 + IsTitleGenerator *bool + Enabled *bool + SortOrder *int +} + +// UsageService — 写入 + 汇总查询。 +type UsageService interface { + Insert(ctx context.Context, in UsageRecord) error + SummarizeByUser(ctx context.Context, from, to time.Time) ([]UserUsageRow, error) + SummarizeByModel(ctx context.Context, from, to time.Time) ([]ModelUsageRow, error) + SummarizeByDay(ctx context.Context, from, to time.Time) ([]DayUsageRow, error) + UserDaily(ctx context.Context, userID uuid.UUID, from, to time.Time) ([]DayUsageRow, error) +} + +// UserUsageRow 按用户聚合的用量。 +type UserUsageRow struct { + UserID uuid.UUID + PromptTokens, CompletionTokens, ThinkingTokens int + CostUSD float64 +} + +// ModelUsageRow 按模型聚合的用量。 +type ModelUsageRow struct { + ModelID uuid.UUID + PromptTokens, CompletionTokens, ThinkingTokens int + CostUSD float64 +} + +// DayUsageRow 按天聚合的用量。 +type DayUsageRow struct { + Day time.Time + PromptTokens, CompletionTokens, ThinkingTokens int + CostUSD float64 +} + +// ===== 流式控制接口(service_message 用) ===== + +// StreamerHub 暴露给 message service 的流控制能力。 +type StreamerHub interface { + Start(ctx context.Context, p StreamParams) error + Cancel(messageID int64) bool + Subscribe(messageID int64) (Subscription, bool) +} + +// StreamParams 是启动一条流所需的所有上下文。 +type StreamParams struct { + UserID, ConversationID, EndpointID, ModelID uuid.UUID + AssistantMessageID int64 + Request llm.Request + ModelMeta LLMModel +} + +// Subscription 是订阅者读取 ring buffer 的句柄。 +type Subscription interface { + Next(ctx context.Context, lastEventID int64) (events []SSEEvent, closed bool, err error) +}