289 lines
13 KiB
Go
289 lines
13 KiB
Go
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")
|
||
ErrBadURL = errors.New("invalid external url")
|
||
ErrFormulaRemove = errors.New("generateFromFormula removed")
|
||
)
|
||
|
||
const (
|
||
PersonaEmpty = "empty"
|
||
PersonaAnalyzing = "analyzing"
|
||
PersonaReady = "ready"
|
||
|
||
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"
|
||
|
||
MentionPending = "pending"
|
||
MentionReplied = "replied"
|
||
MentionSkipped = "skipped"
|
||
)
|
||
|
||
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 {
|
||
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"`
|
||
// 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 {
|
||
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"`
|
||
}
|
||
|
||
type Play struct {
|
||
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"`
|
||
}
|
||
|
||
// Outbox
|
||
type OutboxStep struct {
|
||
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"`
|
||
// 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"`
|
||
}
|
||
|
||
type OutboxBundle struct {
|
||
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"`
|
||
// 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_tag;1~50 字,不可含 . &)
|
||
TopicTag string
|
||
// ImageURLs — 公開可抓的 http(s) 圖;空=純文字 TEXT
|
||
ImageURLs []string
|
||
}
|
||
|
||
type PublishResult struct {
|
||
MediaID string
|
||
}
|