Pipeline completo de publicação no LinkedIn: evaluator → redator → editor → art → director → publisher - Seed com 37 posts em _sugestoes.md - Sorteio de formato com N=3 bloqueados (format-history) - Reciclagem mensal de posts com rotação de formato - Revisão via Telegram com chat livre (Gemini 2.5 Flash) - Publicação via LinkedIn API (OAuth2) - Makefile com targets para Windows/Linux/ARM64 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
170 lines
5.5 KiB
Go
170 lines
5.5 KiB
Go
package state
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
// ─── Status constants ─────────────────────────────────────────────────────────
|
|
|
|
const (
|
|
StatusWaitingEvaluator = "waiting_evaluator"
|
|
StatusWaitingRedator = "waiting_redator"
|
|
StatusWaitingEditor = "waiting_editor"
|
|
StatusWaitingArt = "waiting_art"
|
|
StatusWaitingDirector = "waiting_director"
|
|
StatusWaitingPublisher = "waiting_publisher"
|
|
StatusPublished = "published"
|
|
StatusRejected = "rejected"
|
|
|
|
EtapaDone = "done"
|
|
EtapaPending = "pending"
|
|
EtapaWaiting = "waiting"
|
|
)
|
|
|
|
// ─── Schema ───────────────────────────────────────────────────────────────────
|
|
|
|
type Etapas struct {
|
|
Evaluator string `json:"evaluator"`
|
|
Redator string `json:"redator"`
|
|
Editor string `json:"editor"`
|
|
Art string `json:"art"`
|
|
Director string `json:"director"`
|
|
Publisher string `json:"publisher"`
|
|
}
|
|
|
|
type Aprovacao struct {
|
|
Aprovado bool `json:"aprovado"`
|
|
Timestamp time.Time `json:"timestamp,omitempty"`
|
|
Ciclos int `json:"ciclos,omitempty"`
|
|
}
|
|
|
|
type Aprovacoes struct {
|
|
Tema Aprovacao `json:"tema"`
|
|
Texto Aprovacao `json:"texto"`
|
|
Imagens Aprovacao `json:"imagens"`
|
|
Final Aprovacao `json:"final,omitempty"`
|
|
}
|
|
|
|
type PostState struct {
|
|
Slug string `json:"slug"`
|
|
Categoria string `json:"categoria"`
|
|
Status string `json:"status"`
|
|
Formato string `json:"formato"`
|
|
TemaEscolhido string `json:"tema_escolhido"`
|
|
TrendReferencia string `json:"trend_referencia"`
|
|
FunilTag string `json:"funil_tag,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
Etapas Etapas `json:"etapas"`
|
|
Aprovacoes Aprovacoes `json:"aprovacoes"`
|
|
PollingActive bool `json:"polling_active,omitempty"`
|
|
DirectorMessageID int `json:"director_message_id,omitempty"`
|
|
LinkedInPostID string `json:"linkedin_post_id,omitempty"`
|
|
PublishedAt *time.Time `json:"published_at,omitempty"`
|
|
}
|
|
|
|
// ─── I/O ──────────────────────────────────────────────────────────────────────
|
|
|
|
// LoadState reads work/state.json relative to postPath.
|
|
// postPath is the post root: <workspace>/<categoria>/<slug>
|
|
func LoadState(postPath string) (*PostState, error) {
|
|
path := stateFilePath(postPath)
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ler state.json (%s): %w", path, err)
|
|
}
|
|
var s PostState
|
|
if err := json.Unmarshal(data, &s); err != nil {
|
|
return nil, fmt.Errorf("parsear state.json: %w", err)
|
|
}
|
|
return &s, nil
|
|
}
|
|
|
|
// SaveState writes work/state.json atomically (tmp + rename).
|
|
// Updates UpdatedAt automatically.
|
|
func SaveState(postPath string, s *PostState) error {
|
|
s.UpdatedAt = time.Now()
|
|
|
|
data, err := json.MarshalIndent(s, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("serializar state: %w", err)
|
|
}
|
|
|
|
workDir := filepath.Join(postPath, "work")
|
|
if err := os.MkdirAll(workDir, 0755); err != nil {
|
|
return fmt.Errorf("criar work/: %w", err)
|
|
}
|
|
|
|
path := stateFilePath(postPath)
|
|
tmp := path + ".tmp"
|
|
|
|
if err := os.WriteFile(tmp, data, 0644); err != nil {
|
|
return fmt.Errorf("escrever tmp: %w", err)
|
|
}
|
|
if err := os.Rename(tmp, path); err != nil {
|
|
os.Remove(tmp)
|
|
return fmt.Errorf("rename tmp→state.json: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ─── Methods ──────────────────────────────────────────────────────────────────
|
|
|
|
// SetEtapa updates a specific agent's step status and bumps UpdatedAt.
|
|
func (s *PostState) SetEtapa(agente, status string) {
|
|
s.UpdatedAt = time.Now()
|
|
switch agente {
|
|
case "evaluator":
|
|
s.Etapas.Evaluator = status
|
|
case "redator":
|
|
s.Etapas.Redator = status
|
|
case "editor":
|
|
s.Etapas.Editor = status
|
|
case "art":
|
|
s.Etapas.Art = status
|
|
case "director":
|
|
s.Etapas.Director = status
|
|
case "publisher":
|
|
s.Etapas.Publisher = status
|
|
}
|
|
}
|
|
|
|
// IsStatus reports whether the current Status matches expected.
|
|
func (s *PostState) IsStatus(expected string) bool {
|
|
return s.Status == expected
|
|
}
|
|
|
|
// ─── Constructor ──────────────────────────────────────────────────────────────
|
|
|
|
func NewPostState(slug, categoria, formato, tema, trend string) *PostState {
|
|
now := time.Now()
|
|
return &PostState{
|
|
Slug: slug,
|
|
Categoria: categoria,
|
|
Status: StatusWaitingEvaluator,
|
|
Formato: formato,
|
|
TemaEscolhido: tema,
|
|
TrendReferencia: trend,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
Etapas: Etapas{
|
|
Evaluator: EtapaPending,
|
|
Redator: EtapaPending,
|
|
Editor: EtapaPending,
|
|
Art: EtapaPending,
|
|
Director: EtapaPending,
|
|
Publisher: EtapaPending,
|
|
},
|
|
}
|
|
}
|
|
|
|
// ─── Internal ─────────────────────────────────────────────────────────────────
|
|
|
|
func stateFilePath(postPath string) string {
|
|
return filepath.Join(postPath, "work", "state.json")
|
|
}
|