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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user