You've already forked agentic-coding-workflow
feat(chat): domain types + service interfaces + cross-module Reader interfaces
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user