feat(brandbook): anadir analizador e ingesta automatica de estrategias LinkedIn B2B con IA desde URL

This commit is contained in:
Marcos Fargas
2026-10-15 01:34:24 +02:00
parent e4a605bdbd
commit 0ddd5e5907
3 changed files with 612 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { resolveActiveTenant } from '@/lib/tenant-context';
import { createClient as createAdminClient } from '@supabase/supabase-js';
import { fetchStrategyWebpage, analyzeB2BWithClaude } from '@/lib/b2b-analyzer';
function getAdmin() {
const url = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const key = process.env.SUPABASE_SERVICE_ROLE_KEY!;
return createAdminClient(url, key);
}
export async function POST(req: NextRequest) {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
const apiKeyHeader =
req.headers.get('x-api-key') ||
req.headers.get('authorization')?.replace(/^Bearer\s+/i, '');
const expectedApiKey =
process.env.INTEGRATIONS_API_KEY ||
process.env.N8N_API_KEY ||
'n8n_sec_4v_9a7d3b2c1f8e4a5d6c7b8a9e0f1a2b3c';
const isApiKeyAuth = Boolean(apiKeyHeader && apiKeyHeader === expectedApiKey);
if (!user && !isApiKeyAuth) {
return NextResponse.json({ error: 'No autorizado' }, { status: 401 });
}
let body: any;
try {
body = await req.json();
} catch {
return NextResponse.json({ error: 'JSON inválido' }, { status: 400 });
}
const url = typeof body.url === 'string' ? body.url.trim() : '';
if (!url) {
return NextResponse.json(
{ error: 'Debes proporcionar la URL de la estrategia o documento B2B' },
{ status: 400 }
);
}
// Resolver tenant
let tenantId: string | null = body.tenant_id || null;
if (!tenantId && user) {
const ctx = await resolveActiveTenant(user);
tenantId = ctx.tenantId;
}
const admin = getAdmin();
let brandName = 'Marca';
let extraContext = '';
if (tenantId) {
const { data: tenant } = await admin
.from('tenants')
.select('name, creative_style')
.eq('id', tenantId)
.maybeSingle();
if (tenant?.name) brandName = tenant.name;
const { data: doc } = await admin
.from('documents')
.select('content')
.eq('tenant_id', tenantId)
.eq('type', 'brandbook')
.order('updated_at', { ascending: false })
.limit(1)
.maybeSingle();
if (doc?.content) {
const book =
typeof doc.content === 'string'
? JSON.parse(doc.content)
: doc.content;
extraContext = `Tagline: ${book.tagline || ''} | Elevator pitch: ${
book.elevator_pitch || ''
} | Vertical: ${book.vertical || ''} | Posicionamiento: ${
book.positioning || ''
}`;
}
}
try {
const { text, filename } = await fetchStrategyWebpage(url);
if (!text || text.length < 50) {
return NextResponse.json(
{
error:
'No se pudo extraer contenido suficiente de la URL especificada.',
},
{ status: 400 }
);
}
const result = await analyzeB2BWithClaude(text, brandName, extraContext);
result.source_filename = filename;
result.source_excerpt = text.slice(0, 1000);
return NextResponse.json({
ok: true,
url,
brand_name: brandName,
result,
});
} catch (err) {
return NextResponse.json(
{
error:
err instanceof Error
? err.message
: 'Error al analizar la estrategia con IA',
},
{ status: 500 }
);
}
}