thread-master/apps/backend/internal/module/studio/domain/domain.go

294 lines
13 KiB
Go
Raw Permalink Normal View History

2026-07-13 01:15:30 +00:00
package domain
import (
"errors"
"time"
)
var (
ErrNotFound = errors.New("studio not found")
ErrForbidden = errors.New("studio forbidden")
ErrValidation = errors.New("studio validation")
ErrNoAccount = errors.New("no usable threads account")
ErrIllegalStatus = errors.New("illegal outbox status")
ErrNotDue = errors.New("step not due")
ErrRootBlocked = errors.New("root not published; reply blocked")
2026-07-15 15:23:59 +00:00
ErrLeaseLost = errors.New("outbox step lease lost")
2026-07-13 01:15:30 +00:00
ErrBadURL = errors.New("invalid external url")
ErrFormulaRemove = errors.New("generateFromFormula removed")
)
const (
2026-07-15 15:23:59 +00:00
PersonaEmpty = "empty"
PersonaAnalyzing = "analyzing"
PersonaReady = "ready"
2026-07-13 01:15:30 +00:00
PlayDraft = "draft"
PlayReady = "ready"
PlayScheduling = "scheduling"
PlayActive = "active"
PlayCompleted = "completed"
PlayPartial = "partial_failed"
PlayArchived = "archived"
StepRoot = "root"
StepReply = "reply"
OBScheduling = "scheduling"
OBActive = "active"
OBCompleted = "completed"
OBPartial = "partial_failed"
OBCancelled = "cancelled"
StepDraft = "draft"
StepScheduled = "scheduled"
StepPublishing = "publishing"
StepPublished = "published"
StepFailed = "failed"
StepCancelled = "cancelled"
StepBlocked = "blocked"
2026-07-15 15:23:59 +00:00
MentionPending = "pending"
MentionReplied = "replied"
MentionSkipped = "skipped"
2026-07-13 01:15:30 +00:00
)
func NowNano() int64 { return time.Now().UTC().UnixNano() }
// Persona — PE domain
type Persona struct {
ID string `bson:"_id" json:"id"`
OwnerUID int64 `bson:"owner_uid" json:"owner_uid"`
Name string `bson:"name" json:"name"`
Brief string `bson:"brief" json:"brief"`
Status string `bson:"status" json:"status"`
Style PersonaStyle `bson:"style" json:"style"`
Guard PersonaGuard `bson:"guard" json:"guard"`
Voice string `bson:"voice,omitempty" json:"voice,omitempty"`
Notes string `bson:"notes,omitempty" json:"notes,omitempty"`
CreatedAt int64 `bson:"created_at" json:"created_at"`
UpdatedAt int64 `bson:"updated_at" json:"updated_at"`
}
type PersonaStyle struct {
2026-07-15 15:23:59 +00:00
Draft PersonaDraft `bson:"draft" json:"draft"`
DraftText string `bson:"draft_text" json:"draft_text"`
Source string `bson:"source" json:"source"` // manual|benchmark|seed
BenchmarkUsername string `bson:"benchmark_username,omitempty" json:"benchmark_username,omitempty"`
SourceLabel string `bson:"source_label,omitempty" json:"source_label,omitempty"`
SampleCount int `bson:"sample_count" json:"sample_count"`
AnalyzedAt int64 `bson:"analyzed_at,omitempty" json:"analyzed_at,omitempty"`
SamplePreviews []string `bson:"sample_previews,omitempty" json:"sample_previews,omitempty"`
2026-07-13 01:15:30 +00:00
// Dimensions — 8D 風格摘要(分析後必填,前端「分析」頁卡片)
Dimensions map[string]StyleDimension `bson:"dimensions,omitempty" json:"dimensions,omitempty"`
}
// StyleDimension — 單一 8D 維度
type StyleDimension struct {
Summary string `bson:"summary" json:"summary"`
Evidence []string `bson:"evidence,omitempty" json:"evidence,omitempty"`
}
type PersonaDraft struct {
Identity string `bson:"identity" json:"identity"`
Tone string `bson:"tone" json:"tone"`
Audience string `bson:"audience" json:"audience"`
Hooks string `bson:"hooks" json:"hooks"`
LanguageFingerprint string `bson:"language_fingerprint" json:"language_fingerprint"`
Rhythm string `bson:"rhythm" json:"rhythm"`
Punctuation string `bson:"punctuation" json:"punctuation"`
ContentPatterns string `bson:"content_patterns" json:"content_patterns"`
KnowledgeTranslation string `bson:"knowledge_translation" json:"knowledge_translation"`
CtaStyle string `bson:"cta_style" json:"cta_style"`
Examples string `bson:"examples" json:"examples"`
Avoid string `bson:"avoid" json:"avoid"`
}
type PersonaGuard struct {
Avoid []string `bson:"avoid" json:"avoid"`
MaxChars int `bson:"max_chars" json:"max_chars"`
BanAiTone bool `bson:"ban_ai_tone" json:"ban_ai_tone"`
}
// ActivePersona stores per-owner active id
type ActivePersona struct {
OwnerUID int64 `bson:"_id" json:"owner_uid"`
PersonaID string `bson:"persona_id" json:"persona_id"`
UpdatedAt int64 `bson:"updated_at" json:"updated_at"`
}
// Play — PL domain
type ExternalTarget struct {
URL string `bson:"url" json:"url"`
RawURL string `bson:"raw_url,omitempty" json:"raw_url,omitempty"`
Shortcode string `bson:"shortcode,omitempty" json:"shortcode,omitempty"`
AuthorUsername string `bson:"author_username,omitempty" json:"author_username,omitempty"`
TextPreview string `bson:"text_preview,omitempty" json:"text_preview,omitempty"`
MediaID string `bson:"media_id,omitempty" json:"media_id,omitempty"`
ResolvedAt int64 `bson:"resolved_at,omitempty" json:"resolved_at,omitempty"`
}
type PlayStep struct {
2026-07-15 15:23:59 +00:00
ID string `bson:"id" json:"id"`
SortOrder int `bson:"sort_order" json:"sort_order"`
Kind string `bson:"kind" json:"kind"`
AccountID string `bson:"account_id" json:"account_id"`
Text string `bson:"text" json:"text"`
DelayFromPreviousSec int `bson:"delay_from_previous_sec" json:"delay_from_previous_sec"`
PersonaID string `bson:"persona_id,omitempty" json:"persona_id,omitempty"`
BrandID string `bson:"brand_id,omitempty" json:"brand_id,omitempty"`
ImageURLs []string `bson:"image_urls,omitempty" json:"image_urls,omitempty"`
2026-07-13 01:15:30 +00:00
}
type Play struct {
2026-07-15 15:23:59 +00:00
ID string `bson:"_id" json:"id"`
OwnerUID int64 `bson:"owner_uid" json:"owner_uid"`
Title string `bson:"title" json:"title"`
Topic string `bson:"topic" json:"topic"`
Status string `bson:"status" json:"status"`
LeadAccountID string `bson:"lead_account_id" json:"lead_account_id"`
CastAccountIDs []string `bson:"cast_account_ids" json:"cast_account_ids"`
TargetOwnPostID string `bson:"target_own_post_id,omitempty" json:"target_own_post_id,omitempty"`
TargetExternal *ExternalTarget `bson:"target_external,omitempty" json:"target_external,omitempty"`
Steps []PlayStep `bson:"steps" json:"steps"`
ScheduleStartAt int64 `bson:"schedule_start_at" json:"schedule_start_at"`
IntervalMinutes int `bson:"interval_minutes,omitempty" json:"interval_minutes,omitempty"`
CreatedAt int64 `bson:"created_at" json:"created_at"`
UpdatedAt int64 `bson:"updated_at" json:"updated_at"`
2026-07-13 01:15:30 +00:00
}
// Outbox
type OutboxStep struct {
2026-07-15 15:23:59 +00:00
ID string `bson:"id" json:"id"`
StepID string `bson:"step_id" json:"step_id"`
SortOrder int `bson:"sort_order" json:"sort_order"`
Kind string `bson:"kind" json:"kind"`
AccountID string `bson:"account_id" json:"account_id"`
Text string `bson:"text" json:"text"`
2026-07-13 01:15:30 +00:00
// TopicTag — 主貼可用的 Threads 話題標籤
TopicTag string `bson:"topic_tag,omitempty" json:"topic_tag,omitempty"`
// ImageURLs — 公開 https 圖網址Meta image_url 可抓;勿存 data URL
ImageURLs []string `bson:"image_urls,omitempty" json:"image_urls,omitempty"`
Status string `bson:"status" json:"status"`
Error string `bson:"error,omitempty" json:"error,omitempty"`
ScheduledAt int64 `bson:"scheduled_at,omitempty" json:"scheduled_at,omitempty"`
PublishedAt int64 `bson:"published_at,omitempty" json:"published_at,omitempty"`
// MediaID after successful publish (for reply chain)
MediaID string `bson:"media_id,omitempty" json:"media_id,omitempty"`
// ReplyTo is parent media id (external or previous root)
ReplyTo string `bson:"reply_to,omitempty" json:"reply_to,omitempty"`
2026-07-15 15:23:59 +00:00
// Lease fields are internal worker fencing state. A unique owner is used for
// every claim so an expired publisher cannot finalize a reclaimed step.
LeaseOwner string `bson:"lease_owner,omitempty" json:"-"`
LeaseExpiresAt int64 `bson:"lease_expires_at,omitempty" json:"-"`
2026-07-13 01:15:30 +00:00
}
type OutboxBundle struct {
2026-07-15 15:23:59 +00:00
ID string `bson:"_id" json:"id"`
OwnerUID int64 `bson:"owner_uid" json:"owner_uid"`
PlayID string `bson:"play_id" json:"play_id"`
Title string `bson:"title" json:"title"`
Status string `bson:"status" json:"status"`
Steps []OutboxStep `bson:"steps" json:"steps"`
2026-07-13 01:15:30 +00:00
// Root reply target for under-post schemes
ReplyToMediaID string `bson:"reply_to_media_id,omitempty" json:"reply_to_media_id,omitempty"`
CreatedAt int64 `bson:"created_at" json:"created_at"`
UpdatedAt int64 `bson:"updated_at" json:"updated_at"`
}
// OwnPost / Mention
type OwnPostReply struct {
ID string `bson:"id" json:"id"`
Username string `bson:"username" json:"username"`
Text string `bson:"text" json:"text"`
CreatedAt int64 `bson:"created_at" json:"created_at"`
LikeCount int `bson:"like_count,omitempty" json:"like_count,omitempty"`
ReplyStatus string `bson:"reply_status,omitempty" json:"reply_status,omitempty"`
RepliedBy string `bson:"replied_by,omitempty" json:"replied_by,omitempty"`
RepliedAt int64 `bson:"replied_at,omitempty" json:"replied_at,omitempty"`
ParentReplyID string `bson:"parent_reply_id,omitempty" json:"parent_reply_id,omitempty"`
IsMine bool `bson:"is_mine,omitempty" json:"is_mine,omitempty"`
}
type OwnPost struct {
ID string `bson:"_id" json:"id"`
OwnerUID int64 `bson:"owner_uid" json:"owner_uid"`
AccountID string `bson:"account_id" json:"account_id"`
MediaID string `bson:"media_id,omitempty" json:"media_id,omitempty"`
Text string `bson:"text" json:"text"`
MediaType string `bson:"media_type" json:"media_type"`
MediaURL string `bson:"media_url,omitempty" json:"media_url,omitempty"`
ThumbnailURL string `bson:"thumbnail_url,omitempty" json:"thumbnail_url,omitempty"`
Permalink string `bson:"permalink,omitempty" json:"permalink,omitempty"`
Shortcode string `bson:"shortcode,omitempty" json:"shortcode,omitempty"`
TopicTag string `bson:"topic_tag,omitempty" json:"topic_tag,omitempty"`
LikeCount int `bson:"like_count" json:"like_count"`
ReplyCount int `bson:"reply_count" json:"reply_count"`
RepostCount int `bson:"repost_count" json:"repost_count"`
QuoteCount int `bson:"quote_count" json:"quote_count"`
ViewCount int `bson:"view_count" json:"view_count"`
ShareCount int `bson:"share_count" json:"share_count"`
InsightsStatus string `bson:"insights_status,omitempty" json:"insights_status,omitempty"`
FormulaSummary string `bson:"formula_summary,omitempty" json:"formula_summary,omitempty"`
Insight string `bson:"insight,omitempty" json:"insight,omitempty"`
FormulaDetail string `bson:"formula_detail,omitempty" json:"formula_detail,omitempty"`
Replies []OwnPostReply `bson:"replies" json:"replies"`
PublishedAt int64 `bson:"published_at" json:"published_at"`
}
type Mention struct {
ID string `bson:"_id" json:"id"`
OwnerUID int64 `bson:"owner_uid" json:"owner_uid"`
AccountID string `bson:"account_id" json:"account_id"`
MediaID string `bson:"media_id,omitempty" json:"media_id,omitempty"` // Threads media id回覆 reply_to
FromUsername string `bson:"from_username" json:"from_username"`
Text string `bson:"text" json:"text"`
ContextSnippet string `bson:"context_snippet" json:"context_snippet"`
Permalink string `bson:"permalink,omitempty" json:"permalink,omitempty"`
RootPostID string `bson:"root_post_id,omitempty" json:"root_post_id,omitempty"`
ParentID string `bson:"parent_id,omitempty" json:"parent_id,omitempty"`
Status string `bson:"status" json:"status"`
DraftText string `bson:"draft_text,omitempty" json:"draft_text,omitempty"`
CreatedAt int64 `bson:"created_at" json:"created_at"`
}
type SyncMeta struct {
OwnerUID int64 `bson:"_id" json:"owner_uid"`
LastSyncedAt int64 `bson:"last_synced_at" json:"last_synced_at"`
}
type ViralAnalysis struct {
Hooks string `json:"hooks"`
Structure string `json:"structure"`
Emotion string `json:"emotion"`
Summary string `json:"summary"`
CTA string `json:"cta,omitempty"`
Copyable string `json:"copyable,omitempty"` // 可複製要點
Risks string `json:"risks,omitempty"` // 風險/禁忌
}
// PersonaPreview — 試產主貼 + 回文(真 LLM + 可選新聞話題)
type PersonaPreview struct {
Topic string `json:"topic"`
TopicSource string `json:"topic_source"` // news | manual | fallback
PostText string `json:"post_text"`
ReplyText string `json:"reply_text"`
Notes string `json:"notes,omitempty"`
}
// PublishRequest for transport (must be called for real publish).
type PublishRequest struct {
AccessToken string
AccountID string
Text string
ReplyTo string // empty = root post
// TopicTag — Threads 話題標籤API topic_tag150 字,不可含 . &
TopicTag string
// ImageURLs — 公開可抓的 http(s) 圖;空=純文字 TEXT
ImageURLs []string
}
type PublishResult struct {
MediaID string
}