Add RRSS auto-generation via the Ads Claude/Freepik/Canva pipeline.
Pipeline pieces can be created manually or enqueued with channel=rrss, using brandbook, calendar copy and RRSS history; n8n writes results back to creative_pipeline. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
264
src/app/api/creative-pipeline/generate/route.ts
Normal file
264
src/app/api/creative-pipeline/generate/route.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { isAgencyRole, userRole } from '@/lib/agency';
|
||||
import { resolveActiveTenant } from '@/lib/tenant-context';
|
||||
import { dispatchN8nWebhookAsync } from '@/lib/creatives-webhooks';
|
||||
import {
|
||||
BRANDBOOK_DOC_TYPE,
|
||||
brandbookToPrompt,
|
||||
parseBrandbook,
|
||||
} from '@/lib/brandbook';
|
||||
import {
|
||||
buildRrssGenerationBrief,
|
||||
generationRequestNoteMarker,
|
||||
isRrssPipelineFormat,
|
||||
type RrssPipelineFormat,
|
||||
} from '@/lib/rrss-generation';
|
||||
import { createClient as createAdminClient } from '@supabase/supabase-js';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
function admin() {
|
||||
return createAdminClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY!
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST — create pipeline piece + generation_request (channel=rrss) in one shot.
|
||||
* Body: { title, format, month?, caption?, notes?, pillar?, calendar_piece_id?,
|
||||
* calendar_piece?, scheduled_at?, channels?, variants_requested? }
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
const supabase = await createClient();
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
if (!isAgencyRole(userRole(user))) {
|
||||
return NextResponse.json({ error: 'Solo agencia' }, { status: 403 });
|
||||
}
|
||||
|
||||
const ctx = await resolveActiveTenant(user);
|
||||
if (!ctx.tenantId || ctx.portfolioWide) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Selecciona un cliente en Contexto' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
let body: Record<string, unknown>;
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'JSON inválido' }, { status: 400 });
|
||||
}
|
||||
|
||||
const title = String(body.title || '').trim();
|
||||
const format = body.format as RrssPipelineFormat;
|
||||
if (!title) {
|
||||
return NextResponse.json({ error: 'title requerido' }, { status: 400 });
|
||||
}
|
||||
if (!isRrssPipelineFormat(format)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'format debe ser post|story|reel|carrusel' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const db = admin();
|
||||
const tenantId = ctx.tenantId;
|
||||
|
||||
const { data: tenant } = await db
|
||||
.from('tenants')
|
||||
.select('id, slug, name')
|
||||
.eq('id', tenantId)
|
||||
.maybeSingle();
|
||||
|
||||
// Recent pipeline for continuity
|
||||
const { data: recentRows } = await db
|
||||
.from('creative_pipeline')
|
||||
.select('title, format, caption')
|
||||
.eq('tenant_id', tenantId)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(12);
|
||||
|
||||
// Brandbook
|
||||
let brandbookPrompt: string | null = null;
|
||||
const { data: brandDoc } = await db
|
||||
.from('documents')
|
||||
.select('content')
|
||||
.eq('tenant_id', tenantId)
|
||||
.eq('type', BRANDBOOK_DOC_TYPE)
|
||||
.order('updated_at', { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
if (brandDoc?.content) {
|
||||
try {
|
||||
const raw =
|
||||
typeof brandDoc.content === 'string'
|
||||
? JSON.parse(brandDoc.content)
|
||||
: brandDoc.content;
|
||||
const book = parseBrandbook(raw);
|
||||
if (book) brandbookPrompt = brandbookToPrompt(book);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
// Social cache (optional)
|
||||
let socialHistory: unknown = null;
|
||||
const { data: integs } = await db
|
||||
.from('integrations')
|
||||
.select('provider, meta')
|
||||
.eq('tenant_id', tenantId)
|
||||
.in('provider', ['instagram', 'facebook']);
|
||||
if (integs?.length) {
|
||||
socialHistory = integs.map((i) => ({
|
||||
provider: i.provider,
|
||||
cache: (i.meta as { social_cache?: unknown } | null)?.social_cache || null,
|
||||
}));
|
||||
}
|
||||
|
||||
const calendarPieceId =
|
||||
typeof body.calendar_piece_id === 'string' ? body.calendar_piece_id : null;
|
||||
const calendarPiece =
|
||||
body.calendar_piece && typeof body.calendar_piece === 'object'
|
||||
? (body.calendar_piece as Record<string, unknown>)
|
||||
: null;
|
||||
|
||||
const caption =
|
||||
typeof body.caption === 'string' ? body.caption.trim() || null : null;
|
||||
const pillar =
|
||||
typeof body.pillar === 'string' ? body.pillar.trim() || null : null;
|
||||
const month =
|
||||
typeof body.month === 'string'
|
||||
? body.month
|
||||
: typeof calendarPiece?.date === 'string'
|
||||
? String(calendarPiece.date).slice(0, 7)
|
||||
: null;
|
||||
const notesBase =
|
||||
typeof body.notes === 'string' ? body.notes.trim() : pillar ? `Pilar: ${pillar}` : '';
|
||||
|
||||
const { data: item, error: insErr } = await db
|
||||
.from('creative_pipeline')
|
||||
.insert({
|
||||
tenant_id: tenantId,
|
||||
title,
|
||||
format,
|
||||
month,
|
||||
status: 'borrador',
|
||||
caption,
|
||||
notes: notesBase || null,
|
||||
calendar_piece_id: calendarPieceId,
|
||||
scheduled_at:
|
||||
typeof body.scheduled_at === 'string' ? body.scheduled_at : null,
|
||||
channels: Array.isArray(body.channels)
|
||||
? body.channels
|
||||
: ['instagram', 'facebook'],
|
||||
created_by: user.id,
|
||||
})
|
||||
.select('*')
|
||||
.single();
|
||||
|
||||
if (insErr || !item) {
|
||||
return NextResponse.json(
|
||||
{ error: insErr?.message || 'No se pudo crear la pieza' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const brief = buildRrssGenerationBrief({
|
||||
tenantId,
|
||||
clientSlug: tenant?.slug || 'cliente',
|
||||
clientName: tenant?.name || ctx.label || 'Cliente',
|
||||
format,
|
||||
pipelineId: item.id,
|
||||
calendarPieceId,
|
||||
title,
|
||||
caption,
|
||||
notes: notesBase,
|
||||
pillar,
|
||||
month,
|
||||
scheduledAt:
|
||||
typeof body.scheduled_at === 'string' ? body.scheduled_at : null,
|
||||
calendarPiece: calendarPiece
|
||||
? {
|
||||
id: calendarPieceId,
|
||||
title: String(calendarPiece.title || title),
|
||||
date: calendarPiece.date ? String(calendarPiece.date) : null,
|
||||
time: calendarPiece.time ? String(calendarPiece.time) : null,
|
||||
format: calendarPiece.format ? String(calendarPiece.format) : format,
|
||||
pillar: calendarPiece.pillar
|
||||
? String(calendarPiece.pillar)
|
||||
: pillar,
|
||||
hook: calendarPiece.hook ? String(calendarPiece.hook) : null,
|
||||
body: calendarPiece.body
|
||||
? String(calendarPiece.body)
|
||||
: caption,
|
||||
cta: calendarPiece.cta ? String(calendarPiece.cta) : null,
|
||||
hashtags: calendarPiece.hashtags
|
||||
? String(calendarPiece.hashtags)
|
||||
: null,
|
||||
platform: calendarPiece.platform
|
||||
? String(calendarPiece.platform)
|
||||
: 'instagram',
|
||||
}
|
||||
: null,
|
||||
recentPipeline: (recentRows || []).map((r) => ({
|
||||
title: r.title,
|
||||
format: r.format,
|
||||
caption: r.caption,
|
||||
})),
|
||||
brandbookPrompt,
|
||||
socialHistory,
|
||||
variantsRequested: Number(body.variants_requested || 2),
|
||||
});
|
||||
|
||||
const { data: gr, error: grErr } = await db
|
||||
.from('generation_requests')
|
||||
.insert({
|
||||
tenant_id: tenantId,
|
||||
reason: 'calendario',
|
||||
source_creative_id: null,
|
||||
brief,
|
||||
status: 'pendiente',
|
||||
})
|
||||
.select('*')
|
||||
.single();
|
||||
|
||||
if (grErr || !gr) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: grErr?.message || 'Pieza creada pero falló la cola Claude',
|
||||
item,
|
||||
},
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
|
||||
const marker = generationRequestNoteMarker(gr.id);
|
||||
const notesWithMarker = [notesBase, marker, 'Auto: Claude + Freepik + Canva']
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
await db
|
||||
.from('creative_pipeline')
|
||||
.update({ notes: notesWithMarker, updated_at: new Date().toISOString() })
|
||||
.eq('id', item.id)
|
||||
.eq('tenant_id', tenantId);
|
||||
|
||||
dispatchN8nWebhookAsync('generation_request.created', {
|
||||
generation_request: { ...gr, client_id: tenantId },
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
item: { ...item, notes: notesWithMarker },
|
||||
generation_request: gr,
|
||||
message:
|
||||
'Pieza en cola. n8n/Claude generará cover+caption y escribirá en el pipeline.',
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,10 @@ import { isAgencyRole, userRole } from '@/lib/agency';
|
||||
import { resolveActiveTenant } from '@/lib/tenant-context';
|
||||
import { dispatchN8nWebhookAsync } from '@/lib/creatives-webhooks';
|
||||
import { staticAdBriefAspectFields } from '@/lib/creatives-aspect-ratios';
|
||||
import {
|
||||
isRrssPipelineFormat,
|
||||
rrssBriefAspectFields,
|
||||
} from '@/lib/rrss-generation';
|
||||
import { createClient as createAdminClient } from '@supabase/supabase-js';
|
||||
|
||||
function getAdmin() {
|
||||
@@ -145,23 +149,48 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
const avoidAngles = body.avoid_angles || (creativeAngle ? [creativeAngle] : []);
|
||||
|
||||
const format = body.format || 'estatico';
|
||||
const channel =
|
||||
body.channel === 'rrss' || body.brief?.channel === 'rrss' ? 'rrss' : 'ads';
|
||||
const format =
|
||||
body.format ||
|
||||
(channel === 'rrss' ? 'post' : 'estatico');
|
||||
|
||||
const aspectFields =
|
||||
channel === 'rrss' && isRrssPipelineFormat(format)
|
||||
? rrssBriefAspectFields(format)
|
||||
: staticAdBriefAspectFields(format);
|
||||
|
||||
const brief = {
|
||||
tenant_id: tenantId,
|
||||
client_id: tenantId,
|
||||
client_slug: tenant?.slug || 'cliente',
|
||||
reason: body.reason || 'manual',
|
||||
source_creative_id: body.source_creative_id || null,
|
||||
client_name: tenant?.name || null,
|
||||
channel,
|
||||
reason: body.reason || (channel === 'rrss' ? 'calendario' : 'manual'),
|
||||
source_creative_id:
|
||||
channel === 'rrss' ? null : body.source_creative_id || null,
|
||||
source_pipeline_id: body.source_pipeline_id || null,
|
||||
calendar_piece_id: body.calendar_piece_id || null,
|
||||
format,
|
||||
...staticAdBriefAspectFields(format),
|
||||
variants_requested: Number(body.variants_requested || 3),
|
||||
campaign_name: campaignName,
|
||||
adset_id: adsetId,
|
||||
audience,
|
||||
product,
|
||||
landing_url: landingUrl,
|
||||
skill_format: body.skill_format || format,
|
||||
...aspectFields,
|
||||
variants_requested: Number(
|
||||
body.variants_requested || (channel === 'rrss' ? 2 : 3)
|
||||
),
|
||||
campaign_name: channel === 'rrss' ? null : campaignName,
|
||||
adset_id: channel === 'rrss' ? null : adsetId,
|
||||
audience: channel === 'rrss' ? null : audience,
|
||||
product: channel === 'rrss' ? null : product,
|
||||
landing_url: channel === 'rrss' ? null : landingUrl,
|
||||
avoid_angles: avoidAngles,
|
||||
title: body.title || null,
|
||||
caption: body.caption || null,
|
||||
pillar: body.pillar || null,
|
||||
month: body.month || null,
|
||||
scheduled_at: body.scheduled_at || null,
|
||||
calendar_piece: body.calendar_piece || null,
|
||||
context: body.context || null,
|
||||
write_back: body.write_back || null,
|
||||
notes: body.notes || null,
|
||||
};
|
||||
|
||||
@@ -181,21 +210,23 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
|
||||
// Registrar en creative_events
|
||||
await admin.from('creative_events').insert({
|
||||
tenant_id: tenantId,
|
||||
creative_id: brief.source_creative_id,
|
||||
event_type: 'generation_requested',
|
||||
actor: user ? `user:${user.id}` : 'agency',
|
||||
from_status: null,
|
||||
to_status: null,
|
||||
payload: {
|
||||
generation_request_id: created.id,
|
||||
reason: brief.reason,
|
||||
variants_requested: brief.variants_requested,
|
||||
},
|
||||
visibility: 'agency',
|
||||
});
|
||||
// Registrar en creative_events (solo Ads: FK a creatives)
|
||||
if (channel === 'ads' && brief.source_creative_id) {
|
||||
await admin.from('creative_events').insert({
|
||||
tenant_id: tenantId,
|
||||
creative_id: brief.source_creative_id,
|
||||
event_type: 'generation_requested',
|
||||
actor: user ? `user:${user.id}` : 'agency',
|
||||
from_status: null,
|
||||
to_status: null,
|
||||
payload: {
|
||||
generation_request_id: created.id,
|
||||
reason: brief.reason,
|
||||
variants_requested: brief.variants_requested,
|
||||
},
|
||||
visibility: 'agency',
|
||||
});
|
||||
}
|
||||
|
||||
const enrichedRequest = { ...created, client_id: created.tenant_id };
|
||||
dispatchN8nWebhookAsync('generation_request.created', {
|
||||
|
||||
240
src/app/api/integrations/creative-pipeline/route.ts
Normal file
240
src/app/api/integrations/creative-pipeline/route.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { authenticateIntegrationRequest } from '@/lib/integrations-auth';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { ASSETS_BUCKET, serviceSupabase } from '@/lib/assets';
|
||||
import {
|
||||
parseGenerationRequestIdFromNotes,
|
||||
stripGenerationRequestMarker,
|
||||
} from '@/lib/rrss-generation';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
function admin() {
|
||||
return createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY!
|
||||
);
|
||||
}
|
||||
|
||||
async function downloadToStorage(opts: {
|
||||
tenantId: string;
|
||||
pipelineId: string;
|
||||
url: string;
|
||||
filename: string;
|
||||
}): Promise<string | null> {
|
||||
const svc = serviceSupabase();
|
||||
if (!svc) return null;
|
||||
try {
|
||||
const r = await fetch(opts.url, { cache: 'no-store' });
|
||||
if (!r.ok) return null;
|
||||
const buf = Buffer.from(await r.arrayBuffer());
|
||||
const contentType = r.headers.get('content-type') || 'image/jpeg';
|
||||
const ext = contentType.includes('png')
|
||||
? 'png'
|
||||
: contentType.includes('webp')
|
||||
? 'webp'
|
||||
: 'jpg';
|
||||
const path = `${opts.tenantId}/pipeline/${opts.pipelineId}/${opts.filename}.${ext}`;
|
||||
const up = await svc.storage.from(ASSETS_BUCKET).upload(path, buf, {
|
||||
contentType,
|
||||
upsert: true,
|
||||
});
|
||||
if (up.error) return null;
|
||||
return path;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST — n8n write-back for RRSS auto-generation.
|
||||
* Body: {
|
||||
* generation_request_id?,
|
||||
* pipeline_id?,
|
||||
* asset_url?, // final or Freepik raw
|
||||
* stories_asset_url?,
|
||||
* caption?,
|
||||
* hook?, support?,
|
||||
* slide_urls?: string[],
|
||||
* status?: 'borrador'|'revision_agencia',
|
||||
* mark_request?: 'completada'|'fallida',
|
||||
* error?: string
|
||||
* }
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
let body: Record<string, unknown>;
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 });
|
||||
}
|
||||
|
||||
const auth = await authenticateIntegrationRequest(req, {
|
||||
requireTenant: true,
|
||||
parsedBody: body,
|
||||
});
|
||||
if (!auth.ok) return auth.errorResponse!;
|
||||
|
||||
const tenantId = auth.tenantId!;
|
||||
const db = admin();
|
||||
|
||||
const grId =
|
||||
typeof body.generation_request_id === 'string'
|
||||
? body.generation_request_id
|
||||
: null;
|
||||
|
||||
let pipelineId =
|
||||
typeof body.pipeline_id === 'string' ? body.pipeline_id : null;
|
||||
|
||||
if (!pipelineId && grId) {
|
||||
const { data: gr } = await db
|
||||
.from('generation_requests')
|
||||
.select('brief')
|
||||
.eq('id', grId)
|
||||
.eq('tenant_id', tenantId)
|
||||
.maybeSingle();
|
||||
const brief = (gr?.brief || {}) as {
|
||||
source_pipeline_id?: string;
|
||||
write_back?: { pipeline_id?: string };
|
||||
};
|
||||
pipelineId =
|
||||
brief.source_pipeline_id || brief.write_back?.pipeline_id || null;
|
||||
}
|
||||
|
||||
if (!pipelineId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'pipeline_id or generation_request_id with source_pipeline_id required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { data: item } = await db
|
||||
.from('creative_pipeline')
|
||||
.select('id, notes, caption, format')
|
||||
.eq('id', pipelineId)
|
||||
.eq('tenant_id', tenantId)
|
||||
.maybeSingle();
|
||||
|
||||
if (!item) {
|
||||
return NextResponse.json({ error: 'Pipeline piece not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const assetUrl =
|
||||
typeof body.asset_url === 'string'
|
||||
? body.asset_url
|
||||
: typeof body.stories_asset_url === 'string'
|
||||
? body.stories_asset_url
|
||||
: null;
|
||||
|
||||
let coverPath: string | null = null;
|
||||
if (assetUrl) {
|
||||
if (assetUrl.startsWith('http')) {
|
||||
coverPath = await downloadToStorage({
|
||||
tenantId,
|
||||
pipelineId,
|
||||
url: assetUrl,
|
||||
filename: 'cover-ai',
|
||||
});
|
||||
// Fallback: store URL so UI can still show something via <img src>
|
||||
if (!coverPath) coverPath = assetUrl;
|
||||
} else {
|
||||
coverPath = assetUrl;
|
||||
}
|
||||
}
|
||||
|
||||
const slideUrls = Array.isArray(body.slide_urls)
|
||||
? body.slide_urls.filter((u): u is string => typeof u === 'string')
|
||||
: [];
|
||||
const slidePaths: string[] = [];
|
||||
for (let i = 0; i < slideUrls.length; i++) {
|
||||
const u = slideUrls[i];
|
||||
if (u.startsWith('http')) {
|
||||
const p = await downloadToStorage({
|
||||
tenantId,
|
||||
pipelineId,
|
||||
url: u,
|
||||
filename: `slide-${i + 1}-ai`,
|
||||
});
|
||||
slidePaths.push(p || u);
|
||||
} else {
|
||||
slidePaths.push(u);
|
||||
}
|
||||
}
|
||||
|
||||
const caption =
|
||||
typeof body.caption === 'string'
|
||||
? body.caption
|
||||
: [body.hook, body.support, body.primary_text]
|
||||
.filter((x) => typeof x === 'string' && x.trim())
|
||||
.join('\n\n') || item.caption;
|
||||
|
||||
const status =
|
||||
body.status === 'revision_agencia' || body.status === 'borrador'
|
||||
? body.status
|
||||
: coverPath
|
||||
? 'revision_agencia'
|
||||
: 'borrador';
|
||||
|
||||
const cleanNotes = stripGenerationRequestMarker(item.notes);
|
||||
const grMarker = grId || parseGenerationRequestIdFromNotes(item.notes);
|
||||
const notes = [
|
||||
cleanNotes,
|
||||
grMarker ? `[[generation_request:${grMarker}]]` : null,
|
||||
coverPath ? 'Generado: Claude + Freepik + Canva' : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
|
||||
const patch: Record<string, unknown> = {
|
||||
notes,
|
||||
updated_at: new Date().toISOString(),
|
||||
status,
|
||||
};
|
||||
if (coverPath) {
|
||||
patch.cover_path = coverPath;
|
||||
patch.cover_mime = coverPath.includes('.png')
|
||||
? 'image/png'
|
||||
: coverPath.includes('.webp')
|
||||
? 'image/webp'
|
||||
: 'image/jpeg';
|
||||
}
|
||||
if (caption) patch.caption = caption;
|
||||
if (slidePaths.length) patch.slide_paths = slidePaths;
|
||||
|
||||
const { data: updated, error } = await db
|
||||
.from('creative_pipeline')
|
||||
.update(patch)
|
||||
.eq('id', pipelineId)
|
||||
.eq('tenant_id', tenantId)
|
||||
.select('*')
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
|
||||
if (grId) {
|
||||
const mark =
|
||||
body.mark_request === 'fallida'
|
||||
? 'fallida'
|
||||
: body.error
|
||||
? 'fallida'
|
||||
: 'completada';
|
||||
await db
|
||||
.from('generation_requests')
|
||||
.update({
|
||||
status: mark,
|
||||
completed_at: new Date().toISOString(),
|
||||
error: typeof body.error === 'string' ? body.error : null,
|
||||
result: {
|
||||
pipeline_id: pipelineId,
|
||||
cover_path: coverPath,
|
||||
notes: 'rrss write-back',
|
||||
},
|
||||
})
|
||||
.eq('id', grId)
|
||||
.eq('tenant_id', tenantId);
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true, item: updated });
|
||||
}
|
||||
@@ -36,7 +36,14 @@ export async function GET(req: NextRequest) {
|
||||
}
|
||||
|
||||
const slug = (tenant.slug || 'bps').toLowerCase().trim();
|
||||
const formatParam = (req.nextUrl.searchParams.get('format') || 'estatico').toLowerCase().trim();
|
||||
// Prefer explicit format; RRSS briefs also send skill_format=post|story|reel|carrusel
|
||||
const formatParam = (
|
||||
req.nextUrl.searchParams.get('skill_format') ||
|
||||
req.nextUrl.searchParams.get('format') ||
|
||||
'estatico'
|
||||
)
|
||||
.toLowerCase()
|
||||
.trim();
|
||||
|
||||
// 2. Obtener brandbook de documents (fuente central de verdad: Zona de Marca)
|
||||
const { data: brandDoc } = await adminClient
|
||||
|
||||
Reference in New Issue
Block a user