feat(calendario): opciones de generación, regenerar y publish IG/FB

Cantidades posts/stories/reels, días, mix mismo día, instrucciones,
regenerar marcadas, texto por slide/guión reel, stories interactivas
y horarios IG/FB separados o iguales.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-10-15 16:36:45 +02:00
parent 39b1f8ec02
commit 3b57cdce53
10 changed files with 791 additions and 120 deletions

View File

@@ -190,13 +190,32 @@ export async function processDuePipelinePublishes(opts?: {
// Publish IG then FB in two steps, persisting each Meta id immediately
// so a crash / FB failure cannot create a duplicate IG on retry.
const channels = normalizeChannels(item.channels);
// Horarios por canal opcionales en notes: __4v_sched__:{"instagram":"...","facebook":"..."}
let channelSched: { instagram?: string; facebook?: string } = {};
const schedMatch = String(item.notes || '').match(
/__4v_sched__:(\{[^}\n]+\})/
);
if (schedMatch) {
try {
channelSched = JSON.parse(schedMatch[1]) as typeof channelSched;
} catch {
channelSched = {};
}
}
const nowMs = Date.now();
const channelDue = (ch: 'instagram' | 'facebook') => {
const iso = channelSched[ch];
if (!iso) return true; // usa scheduled_at global (ya filtrado)
const t = Date.parse(iso);
return !Number.isNaN(t) && t <= nowMs;
};
const errors: string[] = [];
let igMediaId: string | null = item.meta_ig_media_id || null;
let fbPostId: string | null = item.meta_fb_post_id || null;
let igPermalink: string | null = null;
let fbPermalink: string | null = null;
if (channels.includes('instagram') && !igMediaId) {
if (channels.includes('instagram') && !igMediaId && channelDue('instagram')) {
const ig = await publishPipelineItem(item, { skipFb: true });
if (ig.sanitizedCaption && ig.sanitizedCaption !== (item.caption || '')) {
await patchItem(base, headers, item.id, {
@@ -217,7 +236,12 @@ export async function processDuePipelinePublishes(opts?: {
}
}
if (channels.includes('facebook') && !fbPostId && errors.length === 0) {
if (
channels.includes('facebook') &&
!fbPostId &&
errors.length === 0 &&
channelDue('facebook')
) {
const fb = await publishPipelineItem(
{ ...item, meta_ig_media_id: igMediaId },
{ skipIg: true }
@@ -235,8 +259,28 @@ export async function processDuePipelinePublishes(opts?: {
}
}
const wantIg = channels.includes('instagram');
const wantFb = channels.includes('facebook');
const wantIg = channels.includes('instagram') && channelDue('instagram');
const wantFb = channels.includes('facebook') && channelDue('facebook');
// Si un canal aún no toca por horario, no marcar fallo: reintentará el cron.
const igPending =
channels.includes('instagram') && !channelDue('instagram') && !igMediaId;
const fbPending =
channels.includes('facebook') && !channelDue('facebook') && !fbPostId;
if ((igPending || fbPending) && errors.length === 0) {
await patchItem(base, headers, item.id, {
publish_status: 'queued',
publish_error: null,
meta_ig_media_id: igMediaId,
meta_fb_post_id: fbPostId,
});
results.push({
id: item.id,
title: item.title,
ok: true,
detail: 'partial_wait_channel_schedule',
});
continue;
}
const igOk = !wantIg || Boolean(igMediaId);
const fbOk = !wantFb || Boolean(fbPostId);
const ok = igOk && fbOk && errors.length === 0;