Directorio de proveedores activos: filamentos, componentes LED, packaging y servicios. Gestiona contactos, evalúa reputación y controla plazos de entrega.
—
Directorio
Nombre / Empresa
Categoría
Contacto
Teléfono
Email
Comuna
Rep.
Estado
Plazo
Acc.
🔍 Buscar Proveedor con IA
SUPPLIER SEARCH▼
Rápido:
Nuevo Proveedor
Datos del Proveedor
Haz clic en las categorías para seleccionarlas (puedes elegir varias)
Nuevo Lead / Cliente
Datos del Lead
Análisis LEAD_AGENT
Se guardará en Airtable al crear
Nueva Cotización
Datos
Descripción
Und.
Costo unit.
Precio venta
Costo total
$0
Precio neto (sin IVA)
$0
Margen
—
Descuento %
TOTAL IVA INC. (+19%)
$0
Calculadora Impresión 3D
65%
Total estimado:$0
Centro de Agentes IA
Agentes inteligentes especializados que analizan tus datos en tiempo real y generan insights, reportes automáticos y propuestas de acción para tu negocio.
🔍 SUPPLIER_SEARCH_AGENT
Research IASourcing
Búsquedas rápidas:
🗂️ Cola de Agentes
—
Presiona “Cargar cola” para ver los leads y tareas pendientes (tabla Agent_Queue).
Oficina Virtual
Tu equipo de agentes IA y automatizaciones trabajando en vivo. El estado se actualiza solo cada 45 s a partir de las ejecuciones reales (Agent_Log), la cola de tareas (Agent_Queue) y, si existe, la tabla Automations.
⚠ Sin conexión con Airtable — mostrando los últimos datos conocidos.
🤖 Agentes IA
⚙️ Automatizaciones
🖨️ Impresoras 3D
📊 Analítica de la oficina7–14 días
📡 Actividad en vivo—
Cargando actividad…
Redes Sociales
Planifica, genera y programa contenido. Responde comentarios y DMs con IA, y convierte interacciones en leads de tu CRM.
InstagramLinkedInTikTokFacebook
🚀
Publicados (mes)
—
🗓️
Programados
—
✏️
Borradores
—
💬
Interacciones pend.
—
🎯
Leads desde redes
—
Generador de contenido
IA
Rápidas:
Calendario de contenido
Presiona “Actualizar” para cargar tus publicaciones (tabla Social_Posts de Airtable).
Bandeja de interacciones
—
Comentarios y DMs pendientes de responder (tabla Social_Interactions de Airtable). El COMMUNITY_AGENT sugiere la respuesta y detecta si es un lead.
Métricas de redes
Sin métricas cargadas (tabla Social_Metrics). Aliméntala con Make desde cada red para ver alcance, engagement, clics y leads por plataforma.
⏰ Mejor momento para publicar
♻️ Reciclar contenidotop performers
Tus publicaciones Publicadas con más engagement aparecerán aquí para re-publicarlas con un clic.
¿Cuándo publicar?
✏️ Editar publicación
📈 Reporte semanal de redes
Newsletter
Redacta con IA el newsletter de la empresa, guárdalo como borrador, prográmalo y mide aperturas y clics. La audiencia son tus clientes del CRM suscritos; el envío masivo sale por Resend (Make) y quienes hacen clic se marcan como lead caliente para el vendedor.
Audiencia (con email)
—
Suscritos
—
Borradores
—
Enviadas (mes)
—
Leads calientes
—
Redactar newsletter
IA · NEWSLETTER_AGENT
Rápidas:
Campañas
Presiona “Actualizar” para cargar tus campañas (tabla Newsletter_Campañas de Airtable).
Leads calientes (abrieron / hicieron clic)—
Destinatarios que interactuaron con el newsletter (tabla Newsletter_Envios). Make los marca como lead caliente al hacer clic; aquí los conviertes en una tarea para el vendedor.
Audiencia por rubro
Tu audiencia son los clientes del CRM con email y suscritos. Carga los clientes (botón ↻ del Overview) para ver el desglose por rubro.
Destinatarios del newsletter—
Carga los clientes (botón ↻ del Overview) para ver y gestionar tus destinatarios.
Agregar destinatario
Si el email ya existe en tu CRM, se suscribe a ese cliente en vez de duplicarlo.
👥 Destinatarios de la campaña
—
¿Qué día se envía esta campaña?
✏️ Editar campaña
👁 Vista previa del correo
Máquinas 3D
—
Monitorea en tiempo real el estado de cada impresora 3D: disponible, imprimiendo o en mantención. Gestiona colas de trabajo y recibe alertas de fallos automáticamente.
📡 Monitor en vivo
↻ cargando...
🧠 Agente de impresión 3DSTL / OBJ → G-code · IA + slicer nativo
📦
Arrastra archivos STL, OBJ o 3MF aquí
uno o varios (se acomodan en el plato) · el archivo no sale de tu navegador
Parámetros de laminado (editables)
PERFIL
⚙ Avanzado — modificadores por altura · regiones de soporte
▾
AJUSTES POR PIEZA (relleno/perímetros/soporte distintos por pieza del plato — vacío = global)
MODIFICADORES POR ALTURA (distintos parámetros en un rango de Z)
REGIONES DE SOPORTE (forzar o bloquear soporte en una caja XY·Z)
G-CODE MÁQUINA (inicio/fin para la impresora)
El preset Klipper usa PRINT_START/PRINT_END — funciona solo si esas macros existen en tu printer.cfg. Se guarda por impresora.
Slicer nativo · sin dependencias externas
—
🔧 Calibración (no necesita modelo)
📅 Disponibilidad semanal
Mantenimiento preventivo
Auditoría y mantención 3D
Revisa cada impresora (errores de Klipper, consola, home, malla de cama, mantención) y corrige/calibra con un clic. Las acciones nunca tocan una máquina imprimiendo. La rutina automática de las 9 AM (auditoría + reinicio de firmware + calibración) corre en el printer-bridge del taller.
Pulsa “Auditar ahora” para revisar todas las impresoras.
Analítica de producción
Evento
Disponibilidad del Equipo
—
Vista semanal de disponibilidad de cada miembro del equipo. Registra ausencias, sincroniza con Google Calendar y planifica la carga de trabajo de la semana.
Google Calendar
Evento
Editar Cotización
Descripción
Und.
Costo unit.
Precio venta
Costo total
$0
Total Neto (sin IVA)
$0
Margen
—
Descuento %
TOTAL CON IVA (+19%)
$0
📋 Importar desde cotización anterior
Selecciona una cotización para traer sus ítems, forma de pago, descuento y observaciones al formulario actual.
Clientes›
—
—
📞 Contacto
🏢 Empresa
Financiero
CRM
Notas
🕐 Historial
Facturas emitidas
Editar Cliente
Registrar Factura
—
⬆ Importar DTEs del servidor SII
Editar Proveedor
Configurar Airtable
Pega tu Personal Access Token de Airtable. Necesita scopes: data:records:read + data:records:write
Reportes Ejecutivos
Informes semanales de revenue, conversión y margen operativo. Historial completo para analizar tendencias y tomar decisiones estratégicas.
Historialclic en fila para ver resumen
Semana
Revenue
Pedidos
Margen
Conv.
Estado
Generar Reporte
CEO_AGENT generará el resumen...
Web · thelab.solutions
Gestiona el SEO de tu sitio web: optimiza títulos, meta descriptions y campañas de Google Ads. Usa IA para mejorar el posicionamiento en Google.
Tráfico del sitio web
● 0 en el sitio ahoraGoogle Analytics 4
Visitantes
—
Nuevos
—
primera visita
Sesiones
—
Páginas vistas
—
Duración media
—
por sesión
visitantes por día
DE DÓNDE VIENEN
PÁGINAS MÁS VISTAS
⚠ Datos de ejemplo — configura el endpoint y GA4 para ver datos reales.
Por qué 2 scripts:AdsApp solo existe dentro de Google Ads y no puede desplegarse como App Web. Se necesita un puente.
Script 1 — Apps Script (script.google.com) = el endpoint web:
Crea un proyecto nuevo → pega el código → Desplegar → App Web → Ejecutar como: Yo → Acceso: Cualquier persona → copia la URL al campo Endpoint arriba.
Script 2 — Google Ads Script (ads.google.com → Herramientas → Scripts):
Crea un nuevo script → pega el 2° bloque → actualiza ENDPOINT con la URL del Script 1 → ejecútalo manualmente una vez y programa diario.
Tráfico web — GA4 (opcional): en el editor del Script 1 → menú Servicios (+) → añade Google Analytics Data API → pega el ID numérico de tu propiedad GA4 en GA4_PROPERTY (analytics.google.com → Administrar → Detalles de la propiedad) → Desplegar → Administrar implementaciones → ✏ → Nueva versión. El panel "Tráfico del sitio web" se llena solo.
Script 1 — Apps Script (script.google.com) — el endpoint web
// ── Script 1: Apps Script (script.google.com) ──────────────────────────
// Proyecto nuevo → pega este código → Desplegar → App Web
// Ejecutar como: Yo | Quién tiene acceso: Cualquier persona
var SECRET = 'thelab2025';
// ── Tráfico web (GA4) — opcional ──
// 1) En el editor: menú izquierdo → Servicios (+) → "Google Analytics Data API" → Añadir.
// 2) Pega aquí el ID NUMÉRICO de la propiedad GA4 (analytics.google.com → Administrar → Detalles de la propiedad).
var GA4_PROPERTY = '';
function doPost(e) {
try {
var body = JSON.parse(e.postData.contents);
if (body.secret !== SECRET) return json({ok:false, error:'No autorizado'});
if (body.type === 'mutation') {
var muts = JSON.parse(PropertiesService.getScriptProperties().getProperty('ads_mutations') || '[]');
muts.push({op:body.op, id:body.id||'', data:body.data, timestamp:body.timestamp||new Date().toISOString(), status:'pending'});
PropertiesService.getScriptProperties().setProperty('ads_mutations', JSON.stringify(muts));
return json({ok:true, queued:true, pending:muts.filter(function(m){return m.status==='pending';}).length});
}
if (body.type === 'update_mutations') {
PropertiesService.getScriptProperties().setProperty('ads_mutations', JSON.stringify(body.mutations));
return json({ok:true});
}
PropertiesService.getScriptProperties().setProperty('ads_cache', JSON.stringify({
guardado: new Date().toISOString(),
gasto: body.gasto, impresiones: body.impresiones, clics: body.clics,
conversiones: body.conversiones, valor_conversion: body.valor_conversion,
campanas: body.campanas, terminos: body.terminos || [], keywords: body.keywords || []
}));
return json({ok:true, saved:true});
} catch(err) { return json({ok:false, error:err.message}); }
}
function doGet(e) {
try {
var action = e && e.parameter && e.parameter.action;
if (action === 'mutations') {
var muts = JSON.parse(PropertiesService.getScriptProperties().getProperty('ads_mutations') || '[]');
return json({ok:true, mutations:muts});
}
if (action === 'web') {
return json(webStats(parseInt((e && e.parameter && e.parameter.days) || 30)));
}
var days = parseInt((e && e.parameter && e.parameter.days) || 30);
var raw = PropertiesService.getScriptProperties().getProperty('ads_cache');
if (!raw) return json({ok:false, error:'Sin datos aún — ejecuta el Script 2 en Google Ads primero'});
var data = JSON.parse(raw);
data.ok = true;
data.periodo = days + ' días';
return json(data);
} catch(err) { return json({ok:false, error:err.message}); }
}
// Tráfico del sitio desde GA4 (Analytics Data API). Cachea 30 min; tiempo real 60 s.
function webStats(days) {
try {
if (!GA4_PROPERTY) return {ok:false, web:true, error:'no-config'};
if (typeof AnalyticsData === 'undefined') return {ok:false, web:true, error:'no-service'};
var cache = CacheService.getScriptCache();
var hit = cache.get('web_stats_' + days);
var out = hit ? JSON.parse(hit) : null;
if (!out) {
var prop = 'properties/' + GA4_PROPERTY;
var rango = [{startDate: days + 'daysAgo', endDate: 'today'}];
var res = AnalyticsData.Properties.batchRunReports({requests: [
{dateRanges: rango, metrics: [{name:'activeUsers'},{name:'newUsers'},{name:'sessions'},{name:'screenPageViews'},{name:'averageSessionDuration'}]},
{dateRanges: rango, dimensions: [{name:'date'}], metrics: [{name:'activeUsers'},{name:'sessions'}], orderBys: [{dimension:{dimensionName:'date'}}], limit: 120},
{dateRanges: rango, dimensions: [{name:'sessionDefaultChannelGroup'}], metrics: [{name:'activeUsers'},{name:'sessions'}], orderBys: [{metric:{metricName:'activeUsers'}, desc:true}], limit: 8},
{dateRanges: rango, dimensions: [{name:'pagePath'}], metrics: [{name:'screenPageViews'},{name:'activeUsers'}], orderBys: [{metric:{metricName:'screenPageViews'}, desc:true}], limit: 10}
]}, prop);
var reps = res.reports || [];
var filas = function(rep){ return (rep && rep.rows) || []; };
var mv = function(row, i){ return Number((row.metricValues && row.metricValues[i] && row.metricValues[i].value) || 0); };
var dv = function(row, i){ return (row.dimensionValues && row.dimensionValues[i] && row.dimensionValues[i].value) || ''; };
var tot = filas(reps[0])[0] || {};
out = {
ok: true, web: true,
usuarios: mv(tot,0), nuevos: mv(tot,1), sesiones: mv(tot,2), vistas: mv(tot,3), duracion: mv(tot,4),
dias: filas(reps[1]).map(function(r){ return {fecha: dv(r,0), usuarios: mv(r,0), sesiones: mv(r,1)}; }),
canales: filas(reps[2]).map(function(r){ return {canal: dv(r,0), usuarios: mv(r,0), sesiones: mv(r,1)}; }),
paginas: filas(reps[3]).map(function(r){ return {ruta: dv(r,0), vistas: mv(r,0), usuarios: mv(r,1)}; }),
guardado: new Date().toISOString()
};
cache.put('web_stats_' + days, JSON.stringify(out), 1800);
}
var rt = cache.get('web_rt');
if (rt === null) {
try {
var rres = AnalyticsData.Properties.runRealtimeReport({metrics:[{name:'activeUsers'}]}, 'properties/' + GA4_PROPERTY);
rt = String((rres.rows && rres.rows[0] && rres.rows[0].metricValues[0].value) || 0);
} catch(e2) { rt = '-1'; }
cache.put('web_rt', rt, 60);
}
out.ahora = Number(rt);
return out;
} catch(err) { return {ok:false, web:true, error:err.message}; }
}
function json(obj) {
return ContentService.createTextOutput(JSON.stringify(obj))
.setMimeType(ContentService.MimeType.JSON);
}
// ── Script 2: Google Ads Script ────────────────────────────────────────
// ads.google.com → Herramientas → Scripts → + Nuevo script
// Actualiza ENDPOINT con la URL del Script 1. Programa ejecución: cada hora.
// IMPORTANTE: usa executeInParallel para crear campañas desde MCC.
var ENDPOINT = 'PEGA_AQUI_LA_URL_DEL_SCRIPT_1';
var SECRET = 'thelab2025';
// ID de la cuenta cliente SIN guiones (ej: 7577812099)
var CUSTOMER_ID = 'PEGA_AQUI_EL_CUSTOMER_ID';
// Modo seguro: las campañas nuevas quedan como BORRADOR en
// Google Ads → Acciones masivas → Cargas (revísalas y aplícalas a mano).
// Cuando confirmes que se crean bien, ponlo en false para que se
// apliquen automáticamente. (Solo afecta la CREACIÓN de campañas.)
var PREVIEW_NUEVAS_CAMPANAS = true;
function main() {
var isMCC = typeof AdsManagerApp !== 'undefined';
Logger.log('Contexto: ' + (isMCC ? 'MCC' : 'cuenta directa'));
if (isMCC) {
if (!CUSTOMER_ID || CUSTOMER_ID === 'PEGA_AQUI_EL_CUSTOMER_ID') {
Logger.log('ERROR: Configura CUSTOMER_ID al inicio del script.');
return;
}
var accIt = AdsManagerApp.accounts().withIds([CUSTOMER_ID]).get();
if (!accIt.hasNext()) {
Logger.log('ERROR: Cuenta ' + CUSTOMER_ID + ' no encontrada en este Manager.');
return;
}
// select() permite lectura; las mutaciones van por executeInParallel
AdsManagerApp.select(accIt.next());
Logger.log('MCC: cuenta seleccionada ' + CUSTOMER_ID);
}
// 1. Enviar estadísticas de campañas al Script 1
_enviarStats();
// 2. Obtener mutaciones pendientes
var muts = _fetchMutaciones();
if (!muts) return;
// 3. Aplicar mutaciones
if (isMCC) {
// executeInParallel da contexto nativo de cuenta para aplicar las mutaciones
AdsManagerApp.accounts()
.withIds([CUSTOMER_ID])
.executeInParallel('_aplicarMutaciones', '_finalizarMutaciones', JSON.stringify(muts));
} else {
// Cuenta directa: llamada inmediata
var result = _aplicarMutaciones(JSON.stringify(muts));
_reportarResultados(JSON.parse(result));
}
}
function _enviarStats() {
var campaigns = [];
var t = {gasto:0, impresiones:0, clics:0, conversiones:0, valor_conversion:0};
try {
var report = AdsApp.report(
'SELECT campaign.name, campaign.id, campaign.status, ' +
'campaign_budget.amount_micros, metrics.impressions, metrics.clicks, ' +
'metrics.cost_micros, metrics.conversions, metrics.conversions_value, ' +
'metrics.search_impression_share, metrics.search_budget_lost_impression_share, ' +
'metrics.search_rank_lost_impression_share ' +
"FROM campaign WHERE campaign.status != 'REMOVED' AND segments.date DURING LAST_30_DAYS"
);
var rows = report.rows();
while (rows.hasNext()) {
var row = rows.next();
var g = Math.round(parseInt(row['metrics.cost_micros']||0)/1e6);
var i = parseInt(row['metrics.impressions']||0);
var c = parseInt(row['metrics.clicks']||0);
var cv = parseFloat(row['metrics.conversions']||0);
var vc = parseFloat(row['metrics.conversions_value']||0);
t.gasto+=g; t.impresiones+=i; t.clics+=c; t.conversiones+=cv; t.valor_conversion+=vc;
campaigns.push({
id:row['campaign.id'], nombre:row['campaign.name'], estado:row['campaign.status'],
presupuesto:Math.round(parseInt(row['campaign_budget.amount_micros']||0)/1e6),
gasto:g, impresiones:i, clics:c, conversiones:cv, valor_conversion:vc,
is:parseFloat(row['metrics.search_impression_share']||0),
is_lost_budget:parseFloat(row['metrics.search_budget_lost_impression_share']||0),
is_lost_rank:parseFloat(row['metrics.search_rank_lost_impression_share']||0)
});
}
Logger.log('Campañas leídas: ' + campaigns.length);
} catch(e) { Logger.log('Error leyendo campañas: ' + e.message); return; }
// Términos de búsqueda (los que gastan): el mayor ahorro en Google Ads está aquí
var searchTerms = [];
try {
var stRep = AdsApp.report(
'SELECT search_term_view.search_term, campaign.name, metrics.clicks, ' +
'metrics.cost_micros, metrics.conversions ' +
'FROM search_term_view WHERE segments.date DURING LAST_30_DAYS ' +
'AND metrics.cost_micros > 1000000 ORDER BY metrics.cost_micros DESC LIMIT 40'
);
var stRows = stRep.rows();
while (stRows.hasNext()) {
var sr = stRows.next();
searchTerms.push({
termino: sr['search_term_view.search_term'], campana: sr['campaign.name'],
clics: parseInt(sr['metrics.clicks']||0),
gasto: Math.round(parseInt(sr['metrics.cost_micros']||0)/1e6),
conversiones: parseFloat(sr['metrics.conversions']||0)
});
}
Logger.log('Términos de búsqueda: ' + searchTerms.length);
} catch(e) { Logger.log('Aviso términos de búsqueda: ' + e.message); }
// Palabras clave con Quality Score: dónde mejorar relevancia/landing para bajar el CPC
var keywords = [];
try {
var kwRep = AdsApp.report(
'SELECT ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ' +
'ad_group_criterion.quality_info.quality_score, ' +
'ad_group_criterion.quality_info.creative_quality_score, ' +
'ad_group_criterion.quality_info.post_click_quality_score, ' +
'ad_group_criterion.quality_info.search_predicted_ctr, ' +
'campaign.name, ad_group.name, metrics.impressions, metrics.clicks, ' +
'metrics.cost_micros, metrics.conversions ' +
'FROM keyword_view WHERE segments.date DURING LAST_30_DAYS ' +
"AND ad_group_criterion.status = 'ENABLED' AND metrics.impressions > 0 " +
'ORDER BY metrics.cost_micros DESC LIMIT 60'
);
var kwRows = kwRep.rows();
while (kwRows.hasNext()) {
var kr = kwRows.next();
keywords.push({
kw: kr['ad_group_criterion.keyword.text'],
match: kr['ad_group_criterion.keyword.match_type'],
qs: parseInt(kr['ad_group_criterion.quality_info.quality_score']||0),
qs_anuncio: kr['ad_group_criterion.quality_info.creative_quality_score'],
qs_landing: kr['ad_group_criterion.quality_info.post_click_quality_score'],
qs_ctr: kr['ad_group_criterion.quality_info.search_predicted_ctr'],
campana: kr['campaign.name'], grupo: kr['ad_group.name'],
impresiones: parseInt(kr['metrics.impressions']||0),
clics: parseInt(kr['metrics.clicks']||0),
gasto: Math.round(parseInt(kr['metrics.cost_micros']||0)/1e6),
conversiones: parseFloat(kr['metrics.conversions']||0)
});
}
Logger.log('Palabras clave: ' + keywords.length);
} catch(e) { Logger.log('Aviso palabras clave: ' + e.message); }
try {
UrlFetchApp.fetch(ENDPOINT, {
method:'post', contentType:'application/json', muteHttpExceptions:true,
payload:JSON.stringify({
secret:SECRET, campanas:campaigns, terminos:searchTerms, keywords:keywords,
gasto:t.gasto, impresiones:t.impresiones, clics:t.clics,
conversiones:t.conversiones, valor_conversion:t.valor_conversion
})
});
Logger.log('Stats enviadas OK.');
} catch(e) { Logger.log('Error enviando stats: ' + e.message); }
}
function _fetchMutaciones() {
try {
var resp = UrlFetchApp.fetch(ENDPOINT + '?action=mutations', {muteHttpExceptions:true});
var data = JSON.parse(resp.getContentText());
if (!data.ok || !data.mutations || !data.mutations.length) {
Logger.log('Sin mutaciones pendientes.');
return null;
}
Logger.log('Mutaciones a procesar: ' + data.mutations.length);
return data.mutations;
} catch(e) { Logger.log('Error obteniendo mutaciones: ' + e.message); return null; }
}
// Corre en contexto nativo de cuenta (via executeInParallel o directamente)
// Recibe y devuelve JSON string
function _aplicarMutaciones(mutsJson) {
var muts = JSON.parse(mutsJson);
for (var k = 0; k < muts.length; k++) {
var m = muts[k];
if (m.status !== 'pending') continue;
try {
if (m.op === 'create') {
// Google Ads Scripts NO permite crear campañas con builders
// (AdsApp.newCampaignBuilder no existe). La vía soportada es la carga
// masiva (bulk upload) con el formato CSV del Editor de Google Ads.
var _grupo = 'Grupo 1 - ' + m.data.nombre;
var _kws = (m.data.palabrasClave && m.data.palabrasClave.length) ? m.data.palabrasClave : [];
var _mt = m.data.concordancia || 'FRASE';
function _wrapKw(k){ return (_mt === 'EXACTA') ? ('[' + k + ']') : (_mt === 'AMPLIA') ? k : ('"' + k + '"'); }
var _maxCpc = parseInt(m.data.maxCpc, 10) || 800;
// PLAN B: si la campaña YA EXISTE (cascarón creado a mano en la UI —
// que sí permite responder la pregunta de anuncios políticos UE),
// la COMPLETAMOS con builders nativos (grupo, keywords, RSA,
// negativas, presupuesto) sin pasar por el CSV.
var _existente = _buscarCampana(null, m.data.nombre);
if (_existente) {
_completarCampana(_existente, m, _grupo, _kws, _mt, _maxCpc);
muts[k].status = 'aplicado';
continue;
}
// El CSV de carga masiva solo acepta de forma fiable 'cpc' (CPC manual).
// La estrategia elegida (Maximizar clics/conversiones, tCPA, tROAS) se
// activa a mano en Google Ads al revisar la campaña recién creada.
var _bs = 'cpc';
if (m.data.pujaEstrategia && m.data.pujaEstrategia !== 'MANUAL_CPC') {
Logger.log('Nota: activar la estrategia ' + m.data.pujaEstrategia + ' a mano en la campaña "' + m.data.nombre + '" (el CSV la crea con CPC manual + tope).');
}
var _ad = m.data.anuncio || null;
var _titulos = (_ad && _ad.titulos && _ad.titulos.length) ? _ad.titulos.slice(0, 15) : [];
var _descs = (_ad && _ad.descripciones && _ad.descripciones.length) ? _ad.descripciones.slice(0, 4) : [];
var _conAnuncio = !!(_ad && _ad.finalUrl && _titulos.length >= 3 && _descs.length >= 2);
// 'EU political ads': declaración obligatoria desde sept 2025 (regulación UE)
// aunque la campaña solo sirva en Chile. Sin la columna, la carga entera falla
// con "Falta un valor en 'Anuncios políticos de la UE'".
var _cols = ['Campaign', 'Campaign status', 'Budget', 'Bid Strategy type',
'Campaign type', 'EU political ads', 'Ad group', 'Max CPC', 'Keyword'];
if (_conAnuncio) {
_cols = _cols.concat(['Ad type', 'Final URL', 'Path 1', 'Path 2']);
for (var h = 0; h < _titulos.length; h++) _cols.push('Headline ' + (h + 1));
for (var d = 0; d < _descs.length; d++) _cols.push('Description ' + (d + 1));
}
var _up = AdsApp.bulkUploads().newCsvUpload(_cols, {moneyInMicros: false});
// Fila de campaña (presupuesto y Max CPC en moneda de la cuenta, p. ej. CLP)
_up.append({
'Campaign': m.data.nombre,
'Campaign status': (m.data.estado === 'PAUSED') ? 'Paused' : 'Enabled',
'Budget': m.data.presupuesto,
'Bid Strategy type': _bs,
'Campaign type': 'Search Only',
// Declaración obligatoria (regulación UE): booleano según los docs de
// Ads Scripts — 'false' = no contiene publicidad política de la UE.
'EU political ads': 'false'
});
// Fila de grupo de anuncios
_up.append({'Campaign': m.data.nombre, 'Ad group': _grupo, 'Max CPC': _maxCpc});
// Filas de palabras clave (concordancia segun m.data.concordancia: FRASE / EXACTA / AMPLIA)
for (var kw = 0; kw < _kws.length; kw++) {
_up.append({'Campaign': m.data.nombre, 'Ad group': _grupo, 'Keyword': _wrapKw(_kws[kw])});
}
// Fila de anuncio responsivo de búsqueda (RSA), si hay datos suficientes
if (_conAnuncio) {
var _fila = {'Campaign': m.data.nombre, 'Ad group': _grupo,
'Ad type': 'Responsive search ad', 'Final URL': _ad.finalUrl};
if (_ad.path1) _fila['Path 1'] = _ad.path1;
if (_ad.path2) _fila['Path 2'] = _ad.path2;
for (var hh = 0; hh < _titulos.length; hh++) _fila['Headline ' + (hh + 1)] = _titulos[hh];
for (var dd = 0; dd < _descs.length; dd++) _fila['Description ' + (dd + 1)] = _descs[dd];
_up.append(_fila);
}
_up.forCampaignManagement();
if (PREVIEW_NUEVAS_CAMPANAS) {
_up.preview();
Logger.log('BORRADOR (preview) listo: ' + m.data.nombre + ' — revísalo y aplícalo en ' +
'Google Ads -> Acciones masivas -> Cargas. Pon PREVIEW_NUEVAS_CAMPANAS=false para aplicar solo.');
} else {
_up.apply();
Logger.log('OK creada (bulk upload): ' + m.data.nombre + ' · ' + _kws.length +
' keywords · ' + (_conAnuncio ? 'con anuncio RSA' : 'sin anuncio'));
Logger.log('Si en 3 min no aparece en Campañas (revisa Subidas): crea a mano una campaña de Búsqueda ' +
'vacía con el nombre EXACTO "' + m.data.nombre + '" en la UI, vuelve a guardar la campaña ' +
'desde el dashboard y ejecuta este script: la completará con keywords y anuncio sin usar CSV.');
// Palabras clave negativas (best-effort; si la campana aun no aparece, reintenta el proximo ciclo)
var _negs = (m.data.negativas && m.data.negativas.length) ? m.data.negativas : [];
if (_negs.length) {
try {
var _cIt = AdsApp.campaigns().withCondition("campaign.name = '" + String(m.data.nombre).replace(/'/g, '') + "'").get();
if (_cIt.hasNext()) {
var _cNeg = _cIt.next();
for (var _n = 0; _n < _negs.length; _n++) _cNeg.createNegativeKeyword(_negs[_n]);
Logger.log('OK ' + _negs.length + ' negativos agregados a ' + m.data.nombre);
} else {
Logger.log('Negativos pendientes: la campana aun no aparece — se agregaran a mano o al reintentar');
}
} catch(_eN) { Logger.log('No se pudieron agregar negativos: ' + _eN.message); }
}
}
muts[k].status = 'aplicado';
} else if (m.op === 'edit') {
var camp = _buscarCampana(m.id, m.data && m.data.nombre);
if (camp) {
if (m.data.nombre && camp.setName && camp.getName() !== m.data.nombre) { try { camp.setName(m.data.nombre); } catch(_eN2) {} }
if (m.data.estado === 'ENABLED') camp.enable();
if (m.data.estado === 'PAUSED') camp.pause();
if (m.data.presupuesto) camp.getBudget().setAmount(m.data.presupuesto);
muts[k].status = 'aplicado';
Logger.log('OK editada: ' + m.id);
} else {
muts[k].status = 'error';
muts[k].error = 'Campaña no encontrada: ' + m.id + ' (si es una campaña Smart/inteligente, Scripts no puede editarla — pausala en ads.google.com)';
}
} else if (m.op === 'negative') {
// Agrega palabra clave negativa: a la campaña (por id o nombre) o a nivel cuenta
var term = m.data && m.data.termino; var added = false;
if (term) {
var itN = m.id ? AdsApp.campaigns().withIds([Number(m.id)]).get()
: AdsApp.campaigns().withCondition("campaign.name = '" + (m.data.campana||'').replace(/'/g,"") + "'").get();
if (itN.hasNext()) { itN.next().createNegativeKeyword('[' + term + ']'); added = true; }
}
muts[k].status = added ? 'aplicado' : 'error';
if (!added) muts[k].error = 'No se pudo agregar el negativo (campaña no encontrada)';
Logger.log((added?'OK':'ERR') + ' negativo: ' + term);
} else if (m.op === 'pause_keyword') {
// Pausa la(s) palabra(s) clave que coincidan con el texto (acotado a la campaña si se indica)
var kwt = m.data && m.data.termino; var paused = 0;
if (kwt) {
var sel = AdsApp.keywords()
.withCondition("ad_group_criterion.keyword.text = '" + kwt.replace(/'/g, '') + "'");
if (m.data.campana) sel = sel.withCondition("campaign.name = '" + m.data.campana.replace(/'/g, '') + "'");
var kwIt = sel.get();
while (kwIt.hasNext()) { kwIt.next().pause(); paused++; }
}
muts[k].status = paused > 0 ? 'aplicado' : 'error';
if (paused === 0) muts[k].error = 'Palabra clave no encontrada: ' + kwt;
Logger.log((paused > 0 ? 'OK' : 'ERR') + ' pausar keyword: ' + kwt + ' (' + paused + ')');
} else if (m.op === 'delete') {
var campD = _buscarCampana(m.id, m.data && m.data.nombre);
if (campD) {
campD.pause();
muts[k].status = 'aplicado';
Logger.log('OK pausada: ' + m.id);
} else {
muts[k].status = 'error';
muts[k].error = 'Campaña no encontrada: ' + m.id + ' (si es una campaña Smart/inteligente, Scripts no puede pausarla — hazlo en ads.google.com)';
}
}
} catch(e) {
muts[k].status = 'error';
muts[k].error = e.message;
Logger.log('[Error] op=' + m.op + ' → ' + e.message);
}
}
return JSON.stringify(muts);
}
// Busca una campaña por ID (numérico) o por nombre, en todos los tipos que
// Ads Scripts puede mutar: estándar (Search/Display), Performance Max, Video
// y Shopping. Nota: los reportes GAQL listan también campañas Smart, pero
// esas NO son accesibles para mutación desde Scripts.
function _buscarCampana(id, nombre) {
var factories = [function(){ return AdsApp.campaigns(); }];
if (AdsApp.performanceMaxCampaigns) factories.push(function(){ return AdsApp.performanceMaxCampaigns(); });
if (AdsApp.videoCampaigns) factories.push(function(){ return AdsApp.videoCampaigns(); });
if (AdsApp.shoppingCampaigns) factories.push(function(){ return AdsApp.shoppingCampaigns(); });
var i, it;
var idNum = Number(id);
if (idNum) {
for (i = 0; i < factories.length; i++) {
try { it = factories[i]().withIds([idNum]).get(); if (it.hasNext()) return it.next(); } catch(e) {}
}
}
if (nombre) {
for (i = 0; i < factories.length; i++) {
try {
it = factories[i]().withCondition("campaign.name = '" + String(nombre).replace(/'/g, '') + "'").get();
if (it.hasNext()) return it.next();
} catch(e) {}
}
}
return null;
}
// PLAN B de creación: completa una campaña EXISTENTE (cascarón hecho en la UI)
// con grupo de anuncios, keywords, anuncio RSA, negativas y presupuesto,
// usando builders nativos de Ads Scripts (sin CSV, sin campo político UE).
function _completarCampana(camp, m, grupoNombre, kws, mt, maxCpc) {
try { if (m.data.presupuesto) camp.getBudget().setAmount(m.data.presupuesto); } catch(e) {}
try {
if (m.data.estado === 'PAUSED') camp.pause();
else if (m.data.estado === 'ENABLED') camp.enable();
} catch(e) {}
// Grupo de anuncios: reutiliza si ya existe (idempotente para reintentos)
var ag = null;
try {
var agIt = camp.adGroups().withCondition("ad_group.name = '" + grupoNombre.replace(/'/g, '') + "'").get();
if (agIt.hasNext()) ag = agIt.next();
} catch(e) {}
if (!ag) {
try {
var agOp = camp.newAdGroupBuilder().withName(grupoNombre).withCpc(maxCpc).build();
if (agOp.isSuccessful()) ag = agOp.getResult();
else Logger.log('Grupo de anuncios: ' + agOp.getErrors().join('; '));
} catch(e) { Logger.log('Grupo de anuncios: ' + e.message); }
}
if (!ag) { Logger.log('ERR no se pudo crear/encontrar el grupo en "' + camp.getName() + '"'); return; }
// Palabras clave con la concordancia elegida
var kwOk = 0;
for (var i = 0; i < kws.length; i++) {
var kw = (mt === 'EXACTA') ? ('[' + kws[i] + ']') : (mt === 'AMPLIA') ? kws[i] : ('"' + kws[i] + '"');
try { var kOp = ag.newKeywordBuilder().withText(kw).build(); if (kOp.isSuccessful()) kwOk++; } catch(e) {}
}
// Anuncio responsivo (RSA)
var ad = m.data.anuncio;
var conAd = false;
if (ad && ad.finalUrl && ad.titulos && ad.titulos.length >= 3 && ad.descripciones && ad.descripciones.length >= 2) {
try {
var b = ag.newAd().responsiveSearchAdBuilder()
.withHeadlines(ad.titulos.slice(0, 15))
.withDescriptions(ad.descripciones.slice(0, 4))
.withFinalUrl(ad.finalUrl);
if (ad.path1) b = b.withPath1(ad.path1);
if (ad.path2) b = b.withPath2(ad.path2);
var adOp = b.build();
conAd = adOp.isSuccessful();
if (!conAd) Logger.log('RSA: ' + adOp.getErrors().join('; '));
} catch(e) { Logger.log('RSA: ' + e.message); }
}
// Palabras clave negativas
var negs = (m.data.negativas && m.data.negativas.length) ? m.data.negativas : [];
for (var n = 0; n < negs.length; n++) {
try { camp.createNegativeKeyword(negs[n]); } catch(e) {}
}
Logger.log('OK completada sobre campaña existente "' + camp.getName() + '": ' + kwOk + '/' + kws.length +
' keywords · ' + (conAd ? 'con anuncio RSA' : 'SIN anuncio') + ' · ' + negs.length + ' negativas');
}
// Callback de executeInParallel: recibe array de ManagedAccountStats
function _finalizarMutaciones(results) {
for (var i = 0; i < results.length; i++) {
var r = results[i];
Logger.log('Cuenta ' + r.getCustomerId() + ' → ' + r.getStatus());
if (r.getStatus() === 'OK') {
_reportarResultados(JSON.parse(r.getReturnValue()));
} else {
Logger.log('Error en cuenta: ' + r.getError());
}
}
}
function _reportarResultados(muts) {
try {
UrlFetchApp.fetch(ENDPOINT, {
method:'post', contentType:'application/json', muteHttpExceptions:true,
payload:JSON.stringify({secret:SECRET, type:'update_mutations', mutations:muts})
});
var ok = muts.filter(function(x){return x.status==='aplicado';}).length;
var err = muts.filter(function(x){return x.status==='error';}).length;
Logger.log('Resultado: ' + ok + ' aplicadas, ' + err + ' errores');
} catch(e) { Logger.log('Error reportando resultados: ' + e.message); }
}
⚠Datos desactualizados (más de 24 h). Ejecuta el Script 2 en Google Ads o haz clic en "↺ Actualizar".
Gasto Total
—
Impresiones
—
Clics
—
CTR
—
CPC Prom.
—
Conversiones
—
Costo/Conv.
—
ROAS
—
Ads vs Ventas CRM
Gasto Ads
—
Ingresos CRM
—
ROAS Real
—
ingresos / gasto
Costo por Lead
—
Gasto AdsIngresos CRM
Recomendaciones del Agente
—
Capacidad de Producción vs Campañassemana actual
Ocupación basada en el calendario semanal de máquinas y pedidos activos. Las acciones envían cambios a Google Ads vía la cola de mutaciones.
Campañas Sugeridas—
Cambios pendientes0
Los cambios se aplicarán la próxima vez que el Script 2 se ejecute en Google Ads (cada hora si está programado).
🤖 Piloto automático — propuesta semanal—
El piloto corre cada lunes en el Worker: analiza ventas por línea, capacidad de producción y ROAS-real, y propone ajustes de presupuesto/pausa/creación. Nada se aplica sin aprobar (aquí o desde el email).
Configura el endpoint y haz clic en "Actualizar" para cargar campañas
Para que el botón "Guardar" funcione, debes agregar este código a functions.php de tu tema en WordPress (Apariencia → Editor de temas → functions.php):
Comisiones ganadas, pipeline activo y ventas en proceso
Sueldo Base & Liquidación
Sueldo base mensual (CLP) por integrante. Se guarda localmente.
Liquidación del período seleccionado
Pipeline — Cotizaciones Activas—
N° Cot.
Cliente
Total Neto
Comisión est. 3.5%
Estado
Vto.
Ventas Completadas (Despachadas)—
N° Pedido
Cliente
F. Despacho
Monto Bruto
Monto Neto
Comisión 3.5%
✉️ Correo Corporativo
Bandeja de entrada, redactar y enviar — @thelab.solutions
Cargando...
Selecciona un mensaje para leer
✉️ Contraseña de correo
Ingresa la contraseña de tu cuenta de correo.
(Puede ser distinta a la del dashboard)
Nuevo mensaje
Para
CC
Asunto
✍ Editar Firma
o pega una imagen directamente
🔬 Diagnóstico SEO WordPress
⚠ Requiere snippet PHP en functions.php
Agrégalo en WordPress → Apariencia → Editor de temas → functions.php
Si ya lo agregaste y sigue fallando: 1) Limpia el cache de WordPress (plugin de cache o WP → Ajustes → Cache) 2) Verifica que no haya errores PHP en functions.php (el sitio mostraría pantalla blanca) 3) Si usas tema hijo, agrega el snippet al functions.php del tema hijo
Tras guardar el archivo, vuelve a hacer clic en "Diagnóstico SEO" para verificar.
🔍Esc
Escribe para buscar en todo el dashboard…
Clientes Cotizaciones📦 Pedidos⌘K
Metas de Revenue
—
Guardado en Airtable — disponible en todos los dispositivos.
🇨🇱 Configuración Worker SII / DTE
Conecta directamente con el SII usando el Worker desplegado. El Worker maneja la firma XML, el token SII y la subida del DTE.
Sin configurar
CAF — Certificados de Autorización de Folios
Sube los archivos XML que te entrega el SII para cada tipo de documento. Sin CAF no se pueden emitir documentos.
Factura Electrónica (33)—
Boleta Electrónica (39)—
Nota de Crédito (61)—
Configuración WATI
Sin configurar
Guardado en Airtable — disponible en todos los dispositivos.
Categorías de Proveedores
Agrega o elimina categorías. Los cambios aplican en todos los formularios al instante.
Asistente
Registrar mantención
Máquina
Tipo de mantención
Notas (opcional)
Umbrales de mantención (horas)
h
h
h
h
h
Webcam
📷Sin feed · configura la URL abajo
URL del stream mjpeg de la impresora. Ejemplo: http://IP/webcam/?action=stream
🎛️ Control —
🔁 Cola de impresión
Historial
Configurar alertas
Notificaciones del navegador
Recibe alertas cuando una impresora entra en error o completa una impresión
Webhook (Make.com / Zapier)
Se enviará un POST con la info de la impresora cuando ocurra un error
📲 Alertas por WhatsApp (vía WATI)
Usa la conexión WATI ya configurada. Deja el número con código de país.
Conexión ·
IP de la impresora
IP local de la impresora en tu red. El dashboard se conecta vía Moonraker (puerto 7125).
API Key (opcional)
En OrcaSlicer: Preparar → Impresoras → ⚙ → IP/API Key. En Fluidd/Mainsail: Configuración → API Keys. Se envía como encabezado X-Api-Key.
Gestionar máquinas
#
Modelo
Nombre
IP
Estado
Acciones
+ Agregar máquina
Modelo
IP
N° global
Nueva Campaña
Los cambios se aplicarán en la próxima ejecución del Script 2.
Segmentación y puja
La campaña se crea con CPC manual + el tope indicado (límite del formato de carga). La estrategia elegida se activa en Google Ads al revisar la campaña: Configuración → Ofertas.
El anuncio responsivo es obligatorio para que la campaña se publique. Si lo dejas vacío, la campaña se crea pero no mostrará anuncios.
Eliminar campaña
Esta acción pausará la campaña en Google Ads. Esta operación se aplicará en la próxima ejecución del Script 2.
⬇ Conversiones offline → Google Ads
Genera el CSV con las conversiones reales del CRM (leads calificados y ventas) usando el gclid de cada cliente, para que Google Ads optimice por ganancia real y no por volumen de clics.
Cómo configurarlo en Google Ads (1 vez)
Crea la columna GCLID en la tabla Clientes de Airtable (texto). El worker ya la rellena en cada lead de Google Ads.
En Google Ads → Objetivos → Conversiones → + Acción de conversión → Importar → Subir conversiones desde un archivo (CSV). Crea dos acciones con estos nombres exactos: Lead calificado CRM y Venta CRM. Marca "Venta CRM" como primaria y usa valores.
Genera el CSV acá y súbelo en Conversiones → Cargas → Subir. Repite semanalmente (dentro de los 90 días del clic).
Configuración IA — API Keys
La API key se guarda solo en este navegador (localStorage). No se envía a ningún servidor propio.
💡 Con tu key: GPT-4o analiza la foto → gpt-image-1 genera 3 vistas reales (Frontal, Isométrica, Lateral). Sin key: se usan placeholders Canvas con el nombre del producto.
Ficha Técnica — Propuesta Cliente
Muestras / Productos
KAITHE LAB SOLUTIONS · IA
EN ESPERA
KAISistemas en línea. Puedo navegar el dashboard, consultar pedidos, cotizaciones, clientes y máquinas en tiempo real. ¿En qué le asisto?
📦 Estado pedidos Cotizaciones Máquinas🗞️ Briefing Resumen CEO💰 Cotizar
Notas internas
📡 Modo local — Red del taller
En modo local el dashboard conecta directo a http://IP:7125 sin pasar por el tunnel. Necesitas hacer esto una sola vez en cada impresora:
1 · Edita moonraker.conf en cada impresora
Accede vía SSH o Fluidd/Mainsail y abre ~/printer_data/config/moonraker.conf
3 · Permite contenido HTTP en Chrome (una vez por equipo)
Este dashboard corre en HTTPS y el navegador bloquea las conexiones HTTP a la red local. En Chrome: clic en el candado 🔒 junto a la URL → Configuración de sitios → Contenido no seguro → Permitir. Luego recarga la página.
4 · Activa 📡 Local
Con el botón 📡 Local activo, el dashboard llega directo a cada impresora por http://IP:7125.
⚠ Solo usa modo local dentro de la red del taller. Fuera de ella, desactívalo y usa el tunnel remoto (bridge en el iMac — ver printer-bridge/README.md del repositorio).
📤 Enviar G-code —
Archivo
Subiendo...
🔑 API Key de Anthropic
Los agentes IA necesitan una API key de Anthropic (Claude). Pégala aquí — se guarda solo en este navegador. Puedes crearla en console.anthropic.com.
🦙 Configurar OrcaSlicer
En OrcaSlicer ve a Printer → Physical Printer → Add printer y usa estos datos para cada máquina:
Host type
Moonraker
Puerto
7125
API Key
(dejar vacío)
IPs disponibles
Máquina
Hostname / IP
Puerto
Tipo
💡 También puedes enviar G-code directamente desde el dashboard sin abrir OrcaSlicer — usa el botón 📤 en cada tarjeta de impresora.
📋 Historial de notificaciones
Cargando...
⚙ Preferencias de notificaciones
Tipos de notificación
✉ Correos entrantes
Alerta cuando llegue un nuevo correo
📤 Correos enviados
Confirmar cuando se envíe un correo
⚠ Alertas de negocio
Pedidos atrasados, facturas vencidas, etc.
ℹ Notificaciones generales
Actualizaciones informativas del sistema
Resumen diario por correo
Envío automático
Resumen de alertas críticas (solo admins)
Hora de envío
📊 Incluir análisis CEO_AGENT
Agrega reporte ejecutivo IA al resumen (1 llamada API/día)
Enviar resumen ahora
Envía un resumen inmediato a tu correo
WhatsApp (alertas críticas)
Webhook URL
Webhook de Make.com, Zapier o n8n — recibirá un POST JSON con el mensaje