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

194
src/lib/b2b-analyzer.ts Normal file
View File

@@ -0,0 +1,194 @@
/**
* b2b-analyzer.ts
* Ingesta y análisis automático de documentos y estrategias B2B para LinkedIn (4Ventures)
* Descarga URLs web (HTML/Markdown/Notion) y usa Claude para extraer campos de Brandbook B2B.
*/
export interface B2BAnalysisResult {
linkedin_themes: string[];
linkedin_formats: string[];
thought_leadership: string[];
buyer_roles: string[];
case_study_angles: string[];
linkedin_cta: string[];
abm_notes: string;
editorial_summary?: string;
source_filename?: string;
source_excerpt?: string;
}
function decodeHtml(html: string): string {
return html
.replace(/&/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#039;/g, "'")
.replace(/&#8217;/g, "'")
.replace(/&#8216;/g, "'")
.replace(/&#8220;/g, '"')
.replace(/&#8221;/g, '"')
.replace(/&#8211;/g, '–')
.replace(/&#8212;/g, '—')
.replace(/&nbsp;/g, ' ');
}
export async function fetchStrategyWebpage(url: string): Promise<{ text: string; filename: string }> {
const resp = await fetch(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; 4VenturesStrategyBot/1.0; +https://4ventures.es)',
Accept: 'text/html,application/xhtml+xml,text/plain;q=0.9,*/*;q=0.8',
},
signal: AbortSignal.timeout(20000),
});
if (!resp.ok) {
throw new Error(`Error al acceder a la URL: HTTP ${resp.status}`);
}
const raw = await resp.text();
const clean = raw
.replace(/<script[\s\S]*?<\/script>/gi, '')
.replace(/<style[\s\S]*?<\/style>/gi, '')
.replace(/<noscript[\s\S]*?<\/noscript>/gi, '')
.replace(/<svg[\s\S]*?<\/svg>/gi, '')
.replace(/<\/(p|div|h[1-6]|li|tr)>/gi, '\n\n')
.replace(/<br\s*[\/]?>/gi, '\n');
const text = decodeHtml(clean.replace(/<[^>]+>/g, ' '))
.replace(/[ \t]+/g, ' ')
.replace(/\n\s*\n\s*\n+/g, '\n\n')
.trim();
let filename = 'estrategia-b2b.txt';
try {
const parsed = new URL(url);
const pathSegments = parsed.pathname.split('/').filter(Boolean);
const lastSeg = pathSegments[pathSegments.length - 1];
filename = lastSeg ? (lastSeg.endsWith('.html') ? lastSeg : `${lastSeg}.txt`) : `${parsed.hostname}.txt`;
} catch {
filename = 'estrategia-b2b.txt';
}
return { text, filename };
}
export async function analyzeB2BWithClaude(
content: string,
brandName: string,
extraContext?: string
): Promise<B2BAnalysisResult> {
const apiKey =
process.env.ANTHROPIC_API_KEY ||
process.env.CLAUDE_API_KEY ||
'';
if (!apiKey) {
throw new Error('Falta ANTHROPIC_API_KEY en variables de entorno.');
}
// Limitar el contenido a ~40.000 caracteres para no desbordar tokens
const sample = content.slice(0, 45000);
const prompt = `Eres el Director de Estrategia B2B y LinkedIn de la agencia 4Ventures.
Tu tarea es analizar el siguiente documento o página estratégica de contenidos de la marca "${brandName}" y extraer los campos estructurados necesarios para configurar el Brandbook / Kit de Marca B2B.
Contexto previo de la marca: ${extraContext || 'Sin contexto adicional'}
--- DOCUMENTO O ESTRATEGIA B2B ---
${sample}
--- FIN DOCUMENTO ---
Extrae y estructura la información en un único objeto JSON válido (sin formato Markdown adicional, solo el JSON puro) con las siguientes claves exactas:
{
"editorial_summary": "Resumen conciso de 2-3 frases de la visión estratégica y posicionamiento de la marca en LinkedIn.",
"linkedin_themes": [
"Pilar o tema recurrente de contenido B2B 1",
"Pilar o tema recurrente de contenido B2B 2",
"Pilar o tema recurrente de contenido B2B 3",
"Pilar o tema recurrente de contenido B2B 4",
"Pilar o tema recurrente de contenido B2B 5"
],
"linkedin_formats": [
"Regla de formato o copy 1 (ej. Formato vertical 4:5, fondo claro institucional)",
"Regla de formato o copy 2 (ej. Gancho detén-scroll de 2 líneas)",
"Regla de formato o copy 3 (ej. Enlaces siempre en el primer comentario)",
"Regla de formato o copy 4 (ej. Carruseles PDF para procesos)",
"Regla de formato o copy 5 (ej. Hashtags de nicho)"
],
"thought_leadership": [
"Línea de autoridad o pensamiento sectorial 1",
"Línea de autoridad o pensamiento sectorial 2",
"Línea de autoridad o pensamiento sectorial 3",
"Línea de autoridad o pensamiento sectorial 4"
],
"buyer_roles": [
"Cargo decisor 1 con su dolor o motivación principal",
"Cargo decisor 2 con su dolor o motivación principal",
"Cargo decisor 3 con su dolor o motivación principal",
"Cargo decisor 4 con su dolor o motivación principal"
],
"case_study_angles": [
"Ángulo o caso de éxito sectorial 1",
"Ángulo o caso de éxito sectorial 2",
"Ángulo o caso de éxito sectorial 3",
"Ángulo o caso de éxito sectorial 4"
],
"linkedin_cta": [
"Llamada a la acción de baja fricción 1 (ej. Pre-diagnóstico gratuito en 1er comentario)",
"Llamada a la acción de baja fricción 2 (ej. Escríbenos por DM)",
"Llamada a la acción de baja fricción 3",
"Llamada a la acción de baja fricción 4"
],
"abm_notes": "Párrafo detallado con la estrategia ABM: universo de cuentas objetivo, sectores prioritarios, disparadores de compra (triggers) y protocolo de contacto."
}
Instrucciones:
1. Sé extremadamente fiel y específico a los datos, mercados, números E, normativas, países, dolores y métricas que aparecen en el documento.
2. Cada elemento de los arrays debe ser una frase o directriz clara y accionable.
3. El resultado debe ser EXCLUSIVAMENTE el JSON válido.`;
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
'content-type': 'application/json',
},
body: JSON.stringify({
model: 'claude-haiku-4-5-20251001',
max_tokens: 3000,
messages: [{ role: 'user', content: prompt }],
}),
signal: AbortSignal.timeout(30000),
});
if (!response.ok) {
const errText = await response.text();
throw new Error(`Claude API error ${response.status}: ${errText}`);
}
const data = await response.json();
const text = (data.content || [])
.map((c: any) => (c.type === 'text' ? c.text : ''))
.join('');
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
throw new Error('Claude no devolvió un JSON reconocible');
}
const parsed = JSON.parse(jsonMatch[0]);
return {
linkedin_themes: Array.isArray(parsed.linkedin_themes) ? parsed.linkedin_themes : [],
linkedin_formats: Array.isArray(parsed.linkedin_formats) ? parsed.linkedin_formats : [],
thought_leadership: Array.isArray(parsed.thought_leadership) ? parsed.thought_leadership : [],
buyer_roles: Array.isArray(parsed.buyer_roles) ? parsed.buyer_roles : [],
case_study_angles: Array.isArray(parsed.case_study_angles) ? parsed.case_study_angles : [],
linkedin_cta: Array.isArray(parsed.linkedin_cta) ? parsed.linkedin_cta : [],
abm_notes: typeof parsed.abm_notes === 'string' ? parsed.abm_notes : '',
editorial_summary: typeof parsed.editorial_summary === 'string' ? parsed.editorial_summary : '',
};
}