You've already forked agentic-coding-workflow
332 lines
11 KiB
Go
332 lines
11 KiB
Go
// 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
|
|
Attachments []Attachment // 用户可读路径填充;composeHistory 不依赖此字段
|
|
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, limit int) (*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, conversationID uuid.UUID, 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, nextEventID int64) (events []SSEEvent, closed bool, err error)
|
|
}
|