Harden Meta publish against dirty captions and IG duplicates.
Strip production notes before posting, persist each network id immediately, refuse IG republish if delete fails, and place FB photos via feed for better timeline visibility. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -8,6 +8,8 @@ import {
|
||||
publishPipelineItem,
|
||||
type PipelinePublishRow,
|
||||
} from '@/lib/meta-publish';
|
||||
import { normalizeChannels } from '@/lib/pipeline-schedule';
|
||||
import { sanitizePublishCaption } from '@/lib/publish-caption';
|
||||
|
||||
const SELECT =
|
||||
'id,tenant_id,title,format,status,scheduled_at,channels,publish_status,publish_attempts,cover_path,cover_mime,slide_paths,caption,notes,meta_ig_media_id,meta_fb_post_id,updated_at';
|
||||
@@ -156,7 +158,8 @@ export async function processDuePipelinePublishes(opts?: {
|
||||
|
||||
const results: DuePublishResult[] = [];
|
||||
|
||||
for (const item of slice) {
|
||||
for (const due of slice) {
|
||||
let item: typeof due = due;
|
||||
const attempts = (item.publish_attempts || 0) + 1;
|
||||
const lock = await patchItem(base, headers, item.id, {
|
||||
publish_status: 'publishing',
|
||||
@@ -174,42 +177,103 @@ export async function processDuePipelinePublishes(opts?: {
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = await publishPipelineItem(item);
|
||||
if (!result.ok) {
|
||||
// Sanitize + persist clean caption before hitting Meta.
|
||||
const scrub = sanitizePublishCaption(item.caption || item.title || '', {
|
||||
maxLen: 6000,
|
||||
});
|
||||
const cleanCaption = scrub.caption || item.title || '';
|
||||
if (scrub.changed && cleanCaption !== (item.caption || '')) {
|
||||
await patchItem(base, headers, item.id, { caption: cleanCaption || null });
|
||||
item = { ...item, caption: cleanCaption };
|
||||
}
|
||||
|
||||
// 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);
|
||||
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) {
|
||||
const ig = await publishPipelineItem(item, { skipFb: true });
|
||||
if (ig.sanitizedCaption && ig.sanitizedCaption !== (item.caption || '')) {
|
||||
await patchItem(base, headers, item.id, {
|
||||
caption: ig.sanitizedCaption,
|
||||
});
|
||||
item = { ...item, caption: ig.sanitizedCaption };
|
||||
}
|
||||
if (ig.igMediaId) {
|
||||
igMediaId = ig.igMediaId;
|
||||
igPermalink = ig.igPermalink || null;
|
||||
await patchItem(base, headers, item.id, {
|
||||
meta_ig_media_id: igMediaId,
|
||||
});
|
||||
item = { ...item, meta_ig_media_id: igMediaId };
|
||||
}
|
||||
if (!ig.ok) {
|
||||
errors.push(ig.error || 'IG falló');
|
||||
}
|
||||
}
|
||||
|
||||
if (channels.includes('facebook') && !fbPostId && errors.length === 0) {
|
||||
const fb = await publishPipelineItem(
|
||||
{ ...item, meta_ig_media_id: igMediaId },
|
||||
{ skipIg: true }
|
||||
);
|
||||
if (fb.fbPostId) {
|
||||
fbPostId = fb.fbPostId;
|
||||
fbPermalink = fb.fbPermalink || null;
|
||||
await patchItem(base, headers, item.id, {
|
||||
meta_fb_post_id: fbPostId,
|
||||
});
|
||||
item = { ...item, meta_fb_post_id: fbPostId };
|
||||
}
|
||||
if (!fb.ok) {
|
||||
errors.push(fb.error || 'FB falló');
|
||||
}
|
||||
}
|
||||
|
||||
const wantIg = channels.includes('instagram');
|
||||
const wantFb = channels.includes('facebook');
|
||||
const igOk = !wantIg || Boolean(igMediaId);
|
||||
const fbOk = !wantFb || Boolean(fbPostId);
|
||||
const ok = igOk && fbOk && errors.length === 0;
|
||||
|
||||
if (!ok) {
|
||||
await patchItem(base, headers, item.id, {
|
||||
publish_status: 'failed',
|
||||
publish_error: result.error || 'Error Meta',
|
||||
meta_ig_media_id: result.igMediaId || item.meta_ig_media_id || null,
|
||||
meta_fb_post_id: result.fbPostId || item.meta_fb_post_id || null,
|
||||
publish_error: errors.join(' · ') || 'Error Meta',
|
||||
meta_ig_media_id: igMediaId,
|
||||
meta_fb_post_id: fbPostId,
|
||||
caption: cleanCaption || null,
|
||||
});
|
||||
results.push({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
ok: false,
|
||||
detail: result.error || 'publish failed',
|
||||
detail: errors.join(' · ') || 'publish failed',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const notes = mergePermalinkNotes(
|
||||
item.notes,
|
||||
result.igPermalink,
|
||||
result.fbPermalink
|
||||
);
|
||||
const notes = mergePermalinkNotes(item.notes, igPermalink, fbPermalink);
|
||||
await patchItem(base, headers, item.id, {
|
||||
publish_status: 'published',
|
||||
publish_error: null,
|
||||
published_at: new Date().toISOString(),
|
||||
status: 'publicado',
|
||||
meta_ig_media_id: result.igMediaId || null,
|
||||
meta_fb_post_id: result.fbPostId || null,
|
||||
meta_ig_media_id: igMediaId,
|
||||
meta_fb_post_id: fbPostId,
|
||||
caption: cleanCaption || null,
|
||||
notes,
|
||||
});
|
||||
results.push({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
ok: true,
|
||||
detail: `published ig=${result.igMediaId || '-'} fb=${result.fbPostId || '-'}`,
|
||||
detail: `published ig=${igMediaId || '-'} fb=${fbPostId || '-'}`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user