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
|
|||
|
|
}
|