ponch-mcp-server 1.0.70 → 1.0.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1877 -472
- package/dist/index.js.map +1 -1
- package/dist/prompts/system/en/martin_base.md +67 -0
- package/dist/prompts/system/es/martin_base.md +68 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -133,6 +133,14 @@ var Session = class {
|
|
|
133
133
|
get userName() {
|
|
134
134
|
return this._authContext.userName;
|
|
135
135
|
}
|
|
136
|
+
/** uid del usuario logueado. Necesario para audit log (211.1 HITO 6). */
|
|
137
|
+
get userId() {
|
|
138
|
+
return this._authContext.userId;
|
|
139
|
+
}
|
|
140
|
+
/** Rol del usuario (admin/encargado/empleado/super_admin). 211.1 HITO 6 */
|
|
141
|
+
get rol() {
|
|
142
|
+
return this._authContext.rol;
|
|
143
|
+
}
|
|
136
144
|
setContext(tenantId, brandId) {
|
|
137
145
|
if (this._authContext.mode !== "cowork") {
|
|
138
146
|
throw new Error("set_context solo disponible en Modo Cowork");
|
|
@@ -408,27 +416,26 @@ function registerContextTools(server, session) {
|
|
|
408
416
|
brandId: import_zod.z.string().describe('ID de la brand (ej: "ponch")')
|
|
409
417
|
},
|
|
410
418
|
async ({ tenantId, brandId }) => {
|
|
411
|
-
const
|
|
412
|
-
if (!
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
+
const brand = await readDoc(`tenants/${tenantId}/marketing_config`, brandId);
|
|
420
|
+
if (!brand) {
|
|
421
|
+
const brandsSnap = await getAdminDb().collection("tenants").doc(tenantId).collection("marketing_config").get();
|
|
422
|
+
if (brandsSnap.empty) {
|
|
423
|
+
return {
|
|
424
|
+
content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Tenant "${tenantId}" no tiene marketing_config` }) }]
|
|
425
|
+
};
|
|
426
|
+
}
|
|
419
427
|
return {
|
|
420
428
|
content: [{
|
|
421
429
|
type: "text",
|
|
422
430
|
text: JSON.stringify({
|
|
423
431
|
ok: false,
|
|
424
432
|
error: `Brand "${brandId}" no existe en tenant "${tenantId}"`,
|
|
425
|
-
brandsDisponibles:
|
|
433
|
+
brandsDisponibles: brandsSnap.docs.map((d) => d.id)
|
|
426
434
|
})
|
|
427
435
|
}]
|
|
428
436
|
};
|
|
429
437
|
}
|
|
430
438
|
session.setContext(tenantId, brandId);
|
|
431
|
-
const brand = brands[brandId];
|
|
432
439
|
return {
|
|
433
440
|
content: [{
|
|
434
441
|
type: "text",
|
|
@@ -522,7 +529,7 @@ function registerContextTools(server, session) {
|
|
|
522
529
|
}
|
|
523
530
|
|
|
524
531
|
// src/tools/core.ts
|
|
525
|
-
var
|
|
532
|
+
var import_zod26 = require("zod");
|
|
526
533
|
|
|
527
534
|
// ../packages/marketing-schemas/dist/index.js
|
|
528
535
|
var import_zod2 = require("zod");
|
|
@@ -537,6 +544,18 @@ var import_zod10 = require("zod");
|
|
|
537
544
|
var import_zod11 = require("zod");
|
|
538
545
|
var import_zod12 = require("zod");
|
|
539
546
|
var import_zod13 = require("zod");
|
|
547
|
+
var import_zod14 = require("zod");
|
|
548
|
+
var import_zod15 = require("zod");
|
|
549
|
+
var import_zod16 = require("zod");
|
|
550
|
+
var import_zod17 = require("zod");
|
|
551
|
+
var import_zod18 = require("zod");
|
|
552
|
+
var import_zod19 = require("zod");
|
|
553
|
+
var import_zod20 = require("zod");
|
|
554
|
+
var import_zod21 = require("zod");
|
|
555
|
+
var import_zod22 = require("zod");
|
|
556
|
+
var import_zod23 = require("zod");
|
|
557
|
+
var import_zod24 = require("zod");
|
|
558
|
+
var import_zod25 = require("zod");
|
|
540
559
|
var MARKETING_CONSTRAINTS = {
|
|
541
560
|
// ═══════════════════════════════════════════════════════════
|
|
542
561
|
// AUDIT — Legacy Content Audit (killer feature)
|
|
@@ -662,23 +681,24 @@ var MARKETING_CONSTRAINTS = {
|
|
|
662
681
|
var SOURCE_PLATFORMS = ["shopify", "wordpress", "webonly"];
|
|
663
682
|
var SourceSchema = import_zod2.z.enum(SOURCE_PLATFORMS);
|
|
664
683
|
var FeaturedImageSchema = import_zod2.z.object({
|
|
665
|
-
url: import_zod2.z.string().min(
|
|
666
|
-
altText: import_zod2.z.string().
|
|
684
|
+
url: import_zod2.z.string().min(1, "URL de la imagen (no vacia)"),
|
|
685
|
+
altText: import_zod2.z.string().nullable().optional(),
|
|
667
686
|
width: import_zod2.z.number().int().positive().nullable().optional(),
|
|
668
687
|
height: import_zod2.z.number().int().positive().nullable().optional()
|
|
669
688
|
}).strict();
|
|
670
689
|
var SeoFieldsSchema = import_zod2.z.object({
|
|
671
|
-
metaTitle: import_zod2.z.string().
|
|
672
|
-
metaDescription: import_zod2.z.string().
|
|
690
|
+
metaTitle: import_zod2.z.string().nullable().optional(),
|
|
691
|
+
metaDescription: import_zod2.z.string().nullable().optional(),
|
|
692
|
+
ogDescription: import_zod2.z.string().nullable().optional(),
|
|
673
693
|
focusKeyword: import_zod2.z.string().nullable().optional(),
|
|
674
694
|
canonicalUrl: import_zod2.z.string().nullable().optional()
|
|
675
695
|
}).strict();
|
|
676
|
-
var
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
/** Vertex multimodalembedding@001 — 1408 dims imagen */
|
|
680
|
-
image: import_zod2.z.array(import_zod2.z.number()).length(1408, "embedding imagen debe ser array de 1408 floats (Vertex multimodalembedding@001)").nullable().optional()
|
|
696
|
+
var ExtractedHeadingsSchema = import_zod2.z.object({
|
|
697
|
+
h1: import_zod2.z.string().nullable(),
|
|
698
|
+
h2s: import_zod2.z.array(import_zod2.z.string()).default([])
|
|
681
699
|
}).strict();
|
|
700
|
+
var EMBEDDING_TEXT = import_zod2.z.array(import_zod2.z.number()).length(1408, "embedding texto debe ser array de 1408 floats (Vertex multimodalembedding@001)").nullable().optional();
|
|
701
|
+
var EMBEDDING_IMAGE = import_zod2.z.array(import_zod2.z.number()).length(1408, "embedding imagen debe ser array de 1408 floats (Vertex multimodalembedding@001)").nullable().optional();
|
|
682
702
|
var AdapterMetaSchema = import_zod2.z.object({
|
|
683
703
|
/** WP: estructura de permalinks detectada. Ej: "/%postname%/" */
|
|
684
704
|
permalinkStructure: import_zod2.z.string().nullable().optional(),
|
|
@@ -692,12 +712,13 @@ var AdapterMetaSchema = import_zod2.z.object({
|
|
|
692
712
|
shopifyBlogId: import_zod2.z.string().nullable().optional()
|
|
693
713
|
}).strict().nullable().optional();
|
|
694
714
|
var ArticleSchema = import_zod2.z.object({
|
|
715
|
+
schemaVersion: import_zod2.z.literal(1).default(1),
|
|
695
716
|
source: SourceSchema,
|
|
696
717
|
platformId: import_zod2.z.string().min(1, "ID de la plataforma origen (Shopify GID o WP post ID como string)"),
|
|
697
718
|
handle: import_zod2.z.string().min(1, "Slug/handle del articulo"),
|
|
698
|
-
title: import_zod2.z.string().min(
|
|
699
|
-
url: import_zod2.z.string().min(
|
|
700
|
-
content: import_zod2.z.string()
|
|
719
|
+
title: import_zod2.z.string().min(1, "Titulo del articulo (no vacio)"),
|
|
720
|
+
url: import_zod2.z.string().min(1, "URL del articulo (no vacia)"),
|
|
721
|
+
content: import_zod2.z.string(),
|
|
701
722
|
excerpt: import_zod2.z.string().nullable().optional(),
|
|
702
723
|
featuredImage: FeaturedImageSchema.nullable().optional(),
|
|
703
724
|
seo: SeoFieldsSchema,
|
|
@@ -709,33 +730,63 @@ var ArticleSchema = import_zod2.z.object({
|
|
|
709
730
|
name: import_zod2.z.string().min(1),
|
|
710
731
|
url: import_zod2.z.string().nullable().optional()
|
|
711
732
|
}).strict().nullable().optional(),
|
|
712
|
-
|
|
733
|
+
/**
|
|
734
|
+
* Blog asociado del articulo (Shopify blog / WP custom post type).
|
|
735
|
+
* Top-level porque es metadata estructural — UI agrupa articles por blog,
|
|
736
|
+
* brandBriefBuilder enriquece prompts con blog context, etc.
|
|
737
|
+
* Sesion 211.1 hito 5b.4 fix.
|
|
738
|
+
*/
|
|
739
|
+
blogId: import_zod2.z.string().nullable().optional(),
|
|
740
|
+
blogHandle: import_zod2.z.string().nullable().optional(),
|
|
741
|
+
blogTitle: import_zod2.z.string().nullable().optional(),
|
|
742
|
+
embeddingText: EMBEDDING_TEXT,
|
|
743
|
+
embeddingImage: EMBEDDING_IMAGE,
|
|
713
744
|
_adapterMeta: AdapterMetaSchema
|
|
714
745
|
}).strict();
|
|
715
746
|
var PageSchema = import_zod2.z.object({
|
|
747
|
+
schemaVersion: import_zod2.z.literal(1).default(1),
|
|
716
748
|
source: SourceSchema,
|
|
717
749
|
platformId: import_zod2.z.string().min(1),
|
|
718
750
|
handle: import_zod2.z.string().min(1),
|
|
719
|
-
title: import_zod2.z.string().min(
|
|
720
|
-
url: import_zod2.z.string().min(
|
|
721
|
-
content: import_zod2.z.string()
|
|
751
|
+
title: import_zod2.z.string().min(1),
|
|
752
|
+
url: import_zod2.z.string().min(1),
|
|
753
|
+
content: import_zod2.z.string(),
|
|
722
754
|
seo: SeoFieldsSchema,
|
|
755
|
+
/** H1 + H2s pre-extraidos del HTML (sesion 211.1 — usado por brandBriefBuilder sin re-parsear). */
|
|
756
|
+
extractedHeadings: ExtractedHeadingsSchema.nullable().optional(),
|
|
757
|
+
/** Primer parrafo plano extraido del content HTML (sesion 211.1). */
|
|
758
|
+
firstParagraph: import_zod2.z.string().nullable().optional(),
|
|
723
759
|
featuredImage: FeaturedImageSchema.nullable().optional(),
|
|
724
760
|
publishedAt: import_zod2.z.string().nullable().optional(),
|
|
725
761
|
updatedAt: import_zod2.z.string().nullable().optional(),
|
|
726
|
-
|
|
762
|
+
embeddingText: EMBEDDING_TEXT,
|
|
763
|
+
embeddingImage: EMBEDDING_IMAGE,
|
|
727
764
|
_adapterMeta: AdapterMetaSchema
|
|
728
765
|
}).strict();
|
|
729
766
|
var PriceSchema = import_zod2.z.object({
|
|
730
767
|
amount: import_zod2.z.number().nonnegative(),
|
|
731
768
|
currencyCode: import_zod2.z.string().length(3, "currencyCode debe ser ISO 4217 (ej: USD, MXN, AUD)")
|
|
732
769
|
}).strict();
|
|
770
|
+
var VariantSchema = import_zod2.z.object({
|
|
771
|
+
platformId: import_zod2.z.string().min(1, "ID de la variante en la plataforma origen (Shopify GID variant, WP variation ID)"),
|
|
772
|
+
sku: import_zod2.z.string().nullable().optional(),
|
|
773
|
+
title: import_zod2.z.string().nullable().optional(),
|
|
774
|
+
price: PriceSchema.nullable().optional(),
|
|
775
|
+
compareAtPrice: PriceSchema.nullable().optional(),
|
|
776
|
+
available: import_zod2.z.boolean().nullable().optional(),
|
|
777
|
+
inventoryQuantity: import_zod2.z.number().int().nullable().optional(),
|
|
778
|
+
/** Mapa de opciones: { Color: 'Rojo', Talla: 'M' } — formato platform-agnostic */
|
|
779
|
+
options: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.string()).optional(),
|
|
780
|
+
image: FeaturedImageSchema.nullable().optional(),
|
|
781
|
+
position: import_zod2.z.number().int().nullable().optional()
|
|
782
|
+
}).strict();
|
|
733
783
|
var ProductSchema = import_zod2.z.object({
|
|
784
|
+
schemaVersion: import_zod2.z.literal(1).default(1),
|
|
734
785
|
source: SourceSchema,
|
|
735
786
|
platformId: import_zod2.z.string().min(1),
|
|
736
787
|
handle: import_zod2.z.string().min(1),
|
|
737
|
-
title: import_zod2.z.string().min(
|
|
738
|
-
url: import_zod2.z.string().min(
|
|
788
|
+
title: import_zod2.z.string().min(1),
|
|
789
|
+
url: import_zod2.z.string().min(1),
|
|
739
790
|
description: import_zod2.z.string(),
|
|
740
791
|
descriptionHtml: import_zod2.z.string().nullable().optional(),
|
|
741
792
|
vendor: import_zod2.z.string().nullable().optional(),
|
|
@@ -746,27 +797,44 @@ var ProductSchema = import_zod2.z.object({
|
|
|
746
797
|
price: PriceSchema.nullable().optional(),
|
|
747
798
|
compareAtPrice: PriceSchema.nullable().optional(),
|
|
748
799
|
available: import_zod2.z.boolean().nullable().optional(),
|
|
800
|
+
/** Variantes del producto (sesion 211.1 — Shopify variants, WP variations). */
|
|
801
|
+
variants: import_zod2.z.array(VariantSchema).default([]),
|
|
749
802
|
seo: SeoFieldsSchema,
|
|
750
803
|
collections: import_zod2.z.array(import_zod2.z.string()).optional(),
|
|
751
804
|
publishedAt: import_zod2.z.string().nullable().optional(),
|
|
752
805
|
updatedAt: import_zod2.z.string().nullable().optional(),
|
|
753
|
-
|
|
806
|
+
embeddingText: EMBEDDING_TEXT,
|
|
807
|
+
embeddingImage: EMBEDDING_IMAGE,
|
|
754
808
|
_adapterMeta: AdapterMetaSchema
|
|
755
809
|
}).strict();
|
|
810
|
+
var COLLECTION_TYPES = ["shopify_custom", "shopify_smart", "wp_category", "canonical"];
|
|
811
|
+
var CollectionTypeSchema = import_zod2.z.enum(COLLECTION_TYPES);
|
|
756
812
|
var CollectionSchema = import_zod2.z.object({
|
|
813
|
+
schemaVersion: import_zod2.z.literal(1).default(1),
|
|
757
814
|
source: SourceSchema,
|
|
758
815
|
platformId: import_zod2.z.string().min(1),
|
|
759
816
|
handle: import_zod2.z.string().min(1),
|
|
760
|
-
title: import_zod2.z.string().min(
|
|
761
|
-
url: import_zod2.z.string().min(
|
|
817
|
+
title: import_zod2.z.string().min(1),
|
|
818
|
+
url: import_zod2.z.string().min(1),
|
|
762
819
|
description: import_zod2.z.string().nullable().optional(),
|
|
763
820
|
descriptionHtml: import_zod2.z.string().nullable().optional(),
|
|
764
821
|
featuredImage: FeaturedImageSchema.nullable().optional(),
|
|
765
822
|
productIds: import_zod2.z.array(import_zod2.z.string()).optional(),
|
|
823
|
+
/**
|
|
824
|
+
* Cantidad de productos en la coleccion. Sesion 211.1 5b.3 fix.
|
|
825
|
+
* Shopify lo provee via GraphQL (productsCount). WordPress lo calcularia del
|
|
826
|
+
* category. Webonly: null. UI usa para "X productos" badge.
|
|
827
|
+
*/
|
|
828
|
+
productsCount: import_zod2.z.number().int().nonnegative().nullable().optional(),
|
|
829
|
+
/**
|
|
830
|
+
* Tipo de coleccion en la plataforma origen. Sesion 211.1 5b.3 fix.
|
|
831
|
+
*/
|
|
832
|
+
collectionType: CollectionTypeSchema.nullable().optional(),
|
|
766
833
|
seo: SeoFieldsSchema,
|
|
767
834
|
publishedAt: import_zod2.z.string().nullable().optional(),
|
|
768
835
|
updatedAt: import_zod2.z.string().nullable().optional(),
|
|
769
|
-
|
|
836
|
+
embeddingText: EMBEDDING_TEXT,
|
|
837
|
+
embeddingImage: EMBEDDING_IMAGE,
|
|
770
838
|
_adapterMeta: AdapterMetaSchema
|
|
771
839
|
}).strict();
|
|
772
840
|
var TONO_NARRATIVO_PROMPTS = {
|
|
@@ -863,6 +931,7 @@ var VisualRulesSchema = import_zod3.z.object({
|
|
|
863
931
|
customNegatives: import_zod3.z.array(import_zod3.z.string()).default([])
|
|
864
932
|
}).passthrough();
|
|
865
933
|
var BrandBriefSchema = import_zod3.z.object({
|
|
934
|
+
schemaVersion: import_zod3.z.literal(1).default(1),
|
|
866
935
|
descripcionCorta: import_zod3.z.string().min(1, "descripcionCorta es obligatorio"),
|
|
867
936
|
segmento: SegmentoEnum,
|
|
868
937
|
industria: IndustriaSchema.optional(),
|
|
@@ -1049,6 +1118,7 @@ var SemrushUsageSchema = import_zod4.z.object({
|
|
|
1049
1118
|
historial: import_zod4.z.array(SemrushUsageHistorialItemSchema).optional()
|
|
1050
1119
|
}).passthrough();
|
|
1051
1120
|
var ConfigSchema = import_zod4.z.object({
|
|
1121
|
+
schemaVersion: import_zod4.z.literal(1).default(1),
|
|
1052
1122
|
id: import_zod4.z.string().min(1, "id es obligatorio"),
|
|
1053
1123
|
tenantId: import_zod4.z.string().min(1, "tenantId es obligatorio"),
|
|
1054
1124
|
brands: import_zod4.z.record(import_zod4.z.string(), BrandConfigSchema).optional(),
|
|
@@ -1111,6 +1181,7 @@ var SemanaCalendarioSchema = import_zod5.z.object({
|
|
|
1111
1181
|
items: import_zod5.z.array(CalendarioItemSchema).optional()
|
|
1112
1182
|
}).passthrough();
|
|
1113
1183
|
var CalendarioSchema = import_zod5.z.object({
|
|
1184
|
+
schemaVersion: import_zod5.z.literal(1).default(1),
|
|
1114
1185
|
id: import_zod5.z.string().min(1, "id es obligatorio"),
|
|
1115
1186
|
tenantId: import_zod5.z.string().min(1, "tenantId es obligatorio"),
|
|
1116
1187
|
brandId: import_zod5.z.string().min(1, "brandId es obligatorio"),
|
|
@@ -1135,6 +1206,7 @@ var CollectionSuggestionInputSchema = import_zod6.z.object({
|
|
|
1135
1206
|
}).passthrough();
|
|
1136
1207
|
var CollectionSuggestionsInputArraySchema = import_zod6.z.array(CollectionSuggestionInputSchema).min(1, "Debe enviar al menos 1 sugerencia");
|
|
1137
1208
|
var CollectionSuggestionStoredSchema = CollectionSuggestionInputSchema.extend({
|
|
1209
|
+
schemaVersion: import_zod6.z.literal(1).default(1),
|
|
1138
1210
|
estado: import_zod6.z.enum(["pendiente", "rechazado", "aplicado_parcial", "aplicado_total"]),
|
|
1139
1211
|
generadoAt: import_zod6.z.union([
|
|
1140
1212
|
import_zod6.z.string(),
|
|
@@ -1339,6 +1411,7 @@ var MediaVarianteSchema = import_zod7.z.object({
|
|
|
1339
1411
|
mediaExternalId: import_zod7.z.string().nullable().optional()
|
|
1340
1412
|
}).passthrough();
|
|
1341
1413
|
var ContenidoSchema = import_zod7.z.object({
|
|
1414
|
+
schemaVersion: import_zod7.z.literal(1).default(1),
|
|
1342
1415
|
id: import_zod7.z.string().min(1, "id es obligatorio"),
|
|
1343
1416
|
tenantId: import_zod7.z.string().min(1, "tenantId es obligatorio"),
|
|
1344
1417
|
brandId: import_zod7.z.string().min(1, "brandId es obligatorio"),
|
|
@@ -1366,6 +1439,11 @@ var ContenidoSchema = import_zod7.z.object({
|
|
|
1366
1439
|
externalId: import_zod7.z.string().nullable().optional()
|
|
1367
1440
|
}).passthrough();
|
|
1368
1441
|
function buildContenido(input) {
|
|
1442
|
+
if (!input.languageCode) {
|
|
1443
|
+
throw new Error(
|
|
1444
|
+
`buildContenido requiere languageCode expl\xEDcito en input. Resolver antes de invocar el builder.`
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1369
1447
|
return ContenidoSchema.parse({
|
|
1370
1448
|
id: input.id,
|
|
1371
1449
|
tenantId: input.tenantId,
|
|
@@ -1373,7 +1451,7 @@ function buildContenido(input) {
|
|
|
1373
1451
|
plataforma: input.plataforma ?? null,
|
|
1374
1452
|
tipo: input.tipo,
|
|
1375
1453
|
keyword: input.keyword ?? null,
|
|
1376
|
-
languageCode: input.languageCode
|
|
1454
|
+
languageCode: input.languageCode,
|
|
1377
1455
|
estado: input.estado ?? "borrador",
|
|
1378
1456
|
fotoId: input.fotoId ?? null,
|
|
1379
1457
|
mediaUrl: input.mediaUrl ?? null,
|
|
@@ -1435,6 +1513,7 @@ var FotoLinkManualSchema = import_zod8.z.object({
|
|
|
1435
1513
|
title: import_zod8.z.string()
|
|
1436
1514
|
}).nullable();
|
|
1437
1515
|
var FotoSchema = import_zod8.z.object({
|
|
1516
|
+
schemaVersion: import_zod8.z.literal(1).default(1),
|
|
1438
1517
|
id: import_zod8.z.string().min(1, "id es obligatorio"),
|
|
1439
1518
|
tenantId: import_zod8.z.string().min(1, "tenantId es obligatorio"),
|
|
1440
1519
|
brandId: import_zod8.z.string().min(1, "brandId es obligatorio"),
|
|
@@ -1510,6 +1589,7 @@ var FotoBriefingNecesidadSchema = import_zod9.z.object({
|
|
|
1510
1589
|
fechaCubierta: import_zod9.z.unknown().nullable().optional()
|
|
1511
1590
|
});
|
|
1512
1591
|
var FotoBriefingSchema = import_zod9.z.object({
|
|
1592
|
+
schemaVersion: import_zod9.z.literal(1).default(1),
|
|
1513
1593
|
id: import_zod9.z.string().min(1),
|
|
1514
1594
|
tenantId: import_zod9.z.string().min(1),
|
|
1515
1595
|
brandId: import_zod9.z.string().min(1),
|
|
@@ -1619,6 +1699,7 @@ var PrioridadMesSchema = import_zod10.z.object({
|
|
|
1619
1699
|
foco: import_zod10.z.string().optional()
|
|
1620
1700
|
}).passthrough();
|
|
1621
1701
|
var ColeccionesPriorizadasSchema = import_zod10.z.object({
|
|
1702
|
+
schemaVersion: import_zod10.z.literal(1).default(1),
|
|
1622
1703
|
grupos: import_zod10.z.array(GrupoColeccionesSchema),
|
|
1623
1704
|
prioridadesMes: import_zod10.z.record(import_zod10.z.string(), PrioridadMesSchema).optional()
|
|
1624
1705
|
}).passthrough();
|
|
@@ -1650,6 +1731,7 @@ var BlogEntrySchema = import_zod10.z.object({
|
|
|
1650
1731
|
totalArticulos: import_zod10.z.number().int().nonnegative()
|
|
1651
1732
|
}).passthrough();
|
|
1652
1733
|
var BlogStrategySchema = import_zod10.z.object({
|
|
1734
|
+
schemaVersion: import_zod10.z.literal(1).default(1),
|
|
1653
1735
|
blogs: import_zod10.z.array(BlogEntrySchema).min(1, "Al menos 1 blog configurado"),
|
|
1654
1736
|
defaultBlogId: import_zod10.z.string().min(1),
|
|
1655
1737
|
defaultBlogHandle: import_zod10.z.string().min(1),
|
|
@@ -1835,6 +1917,7 @@ var EmbeddedDocBase = {
|
|
|
1835
1917
|
ultimoUpdate: import_zod11.z.unknown().nullable().optional()
|
|
1836
1918
|
};
|
|
1837
1919
|
var ShopifyProductDocSchema = import_zod11.z.object({
|
|
1920
|
+
schemaVersion: import_zod11.z.literal(1).default(1),
|
|
1838
1921
|
...EmbeddedDocBase,
|
|
1839
1922
|
title: import_zod11.z.string(),
|
|
1840
1923
|
handle: import_zod11.z.string(),
|
|
@@ -1850,6 +1933,7 @@ var ShopifyProductDocSchema = import_zod11.z.object({
|
|
|
1850
1933
|
collectionHandles: import_zod11.z.array(import_zod11.z.string()).default([])
|
|
1851
1934
|
}).passthrough();
|
|
1852
1935
|
var ShopifyCollectionDocSchema = import_zod11.z.object({
|
|
1936
|
+
schemaVersion: import_zod11.z.literal(1).default(1),
|
|
1853
1937
|
...EmbeddedDocBase,
|
|
1854
1938
|
title: import_zod11.z.string(),
|
|
1855
1939
|
handle: import_zod11.z.string(),
|
|
@@ -1866,6 +1950,7 @@ var ShopifyInlineImageSchema = import_zod11.z.object({
|
|
|
1866
1950
|
alt: import_zod11.z.string().nullable()
|
|
1867
1951
|
});
|
|
1868
1952
|
var ShopifyArticleDocSchema = import_zod11.z.object({
|
|
1953
|
+
schemaVersion: import_zod11.z.literal(1).default(1),
|
|
1869
1954
|
...EmbeddedDocBase,
|
|
1870
1955
|
title: import_zod11.z.string(),
|
|
1871
1956
|
handle: import_zod11.z.string(),
|
|
@@ -1882,6 +1967,7 @@ var ShopifyArticleDocSchema = import_zod11.z.object({
|
|
|
1882
1967
|
published_at: import_zod11.z.string().nullable()
|
|
1883
1968
|
}).passthrough();
|
|
1884
1969
|
var ShopifyPageDocSchema = import_zod11.z.object({
|
|
1970
|
+
schemaVersion: import_zod11.z.literal(1).default(1),
|
|
1885
1971
|
...EmbeddedDocBase,
|
|
1886
1972
|
title: import_zod11.z.string(),
|
|
1887
1973
|
handle: import_zod11.z.string(),
|
|
@@ -1905,6 +1991,7 @@ var SitePageContentSchema = import_zod12.z.object({
|
|
|
1905
1991
|
fetchedAt: import_zod12.z.string()
|
|
1906
1992
|
}).passthrough();
|
|
1907
1993
|
var SiteContentSchema = import_zod12.z.object({
|
|
1994
|
+
schemaVersion: import_zod12.z.literal(1).default(1),
|
|
1908
1995
|
id: import_zod12.z.string().min(1),
|
|
1909
1996
|
tenantId: import_zod12.z.string().min(1),
|
|
1910
1997
|
brandId: import_zod12.z.string().min(1),
|
|
@@ -1984,6 +2071,7 @@ var DETECTED_CONFLICT_TYPES = [
|
|
|
1984
2071
|
];
|
|
1985
2072
|
var CONFLICT_SEVERITIES = ["info", "warning", "error"];
|
|
1986
2073
|
var SuggestedActionSchema = import_zod13.z.object({
|
|
2074
|
+
schemaVersion: import_zod13.z.literal(1).default(1),
|
|
1987
2075
|
id: import_zod13.z.enum(ALLOWED_ACTION_IDS),
|
|
1988
2076
|
labelKey: import_zod13.z.string().regex(/^marketing\.actions\./, {
|
|
1989
2077
|
message: 'labelKey debe empezar con "marketing.actions."'
|
|
@@ -2000,6 +2088,456 @@ var DetectedConflictSchema = import_zod13.z.object({
|
|
|
2000
2088
|
message: import_zod13.z.string().max(500),
|
|
2001
2089
|
relatedItems: import_zod13.z.array(import_zod13.z.unknown()).optional()
|
|
2002
2090
|
});
|
|
2091
|
+
var TenantStatusEnum = import_zod14.z.enum([
|
|
2092
|
+
"active",
|
|
2093
|
+
"trial",
|
|
2094
|
+
"suspended",
|
|
2095
|
+
"cancelled",
|
|
2096
|
+
"pending_payment"
|
|
2097
|
+
]);
|
|
2098
|
+
var TrialInfoSchema = import_zod14.z.object({
|
|
2099
|
+
inicioTrial: import_zod14.z.unknown().nullable(),
|
|
2100
|
+
finTrial: import_zod14.z.unknown().nullable()
|
|
2101
|
+
}).strict().nullable().optional();
|
|
2102
|
+
var SuspensionInfoSchema = import_zod14.z.object({
|
|
2103
|
+
fecha: import_zod14.z.unknown(),
|
|
2104
|
+
motivo: import_zod14.z.string(),
|
|
2105
|
+
gracePeriodDias: import_zod14.z.number(),
|
|
2106
|
+
finGracePeriod: import_zod14.z.unknown()
|
|
2107
|
+
}).strict().nullable().optional();
|
|
2108
|
+
var CancellationInfoSchema = import_zod14.z.object({
|
|
2109
|
+
fecha: import_zod14.z.unknown(),
|
|
2110
|
+
motivo: import_zod14.z.string(),
|
|
2111
|
+
iniciadaPor: import_zod14.z.enum(["user", "admin", "system"]),
|
|
2112
|
+
gracePeriodDias: import_zod14.z.number(),
|
|
2113
|
+
finGracePeriod: import_zod14.z.unknown()
|
|
2114
|
+
}).strict().nullable().optional();
|
|
2115
|
+
var TenantStatusFieldsSchema = import_zod14.z.object({
|
|
2116
|
+
schemaVersion: import_zod14.z.literal(1).default(1),
|
|
2117
|
+
status: TenantStatusEnum.default("active"),
|
|
2118
|
+
trial: TrialInfoSchema,
|
|
2119
|
+
suspension: SuspensionInfoSchema,
|
|
2120
|
+
cancellation: CancellationInfoSchema
|
|
2121
|
+
}).passthrough();
|
|
2122
|
+
var AuditActorSchema = import_zod15.z.object({
|
|
2123
|
+
type: import_zod15.z.enum(["user", "martin", "system", "cf_scheduled", "webhook"]),
|
|
2124
|
+
uid: import_zod15.z.string().nullable(),
|
|
2125
|
+
nombre: import_zod15.z.string(),
|
|
2126
|
+
metadata: import_zod15.z.record(import_zod15.z.string(), import_zod15.z.any()).optional()
|
|
2127
|
+
}).strict();
|
|
2128
|
+
var AuditChangesSchema = import_zod15.z.object({
|
|
2129
|
+
before: import_zod15.z.any().nullable(),
|
|
2130
|
+
after: import_zod15.z.any().nullable()
|
|
2131
|
+
}).strict();
|
|
2132
|
+
var AuditLogEntrySchema = import_zod15.z.object({
|
|
2133
|
+
schemaVersion: import_zod15.z.literal(1).default(1),
|
|
2134
|
+
id: import_zod15.z.string(),
|
|
2135
|
+
tenantId: import_zod15.z.string(),
|
|
2136
|
+
brandId: import_zod15.z.string().nullable(),
|
|
2137
|
+
// Quién
|
|
2138
|
+
actor: AuditActorSchema,
|
|
2139
|
+
// Qué
|
|
2140
|
+
action: import_zod15.z.string().min(1, 'action can\xF3nico es obligatorio (ej. "contenido.descartar")'),
|
|
2141
|
+
targetType: import_zod15.z.string(),
|
|
2142
|
+
targetPath: import_zod15.z.string(),
|
|
2143
|
+
targetId: import_zod15.z.string(),
|
|
2144
|
+
// Cómo
|
|
2145
|
+
changes: AuditChangesSchema,
|
|
2146
|
+
// Contexto
|
|
2147
|
+
motivo: import_zod15.z.string().nullable(),
|
|
2148
|
+
conversacionId: import_zod15.z.string().nullable(),
|
|
2149
|
+
// Resultado
|
|
2150
|
+
status: import_zod15.z.enum(["success", "error", "partial"]),
|
|
2151
|
+
errorMessage: import_zod15.z.string().nullable(),
|
|
2152
|
+
durationMs: import_zod15.z.number().nonnegative(),
|
|
2153
|
+
// Cuándo (Firestore serverTimestamp)
|
|
2154
|
+
timestamp: import_zod15.z.unknown(),
|
|
2155
|
+
// Origen técnico (debugging, NO mostrar al usuario)
|
|
2156
|
+
origenTecnico: import_zod15.z.object({
|
|
2157
|
+
cfName: import_zod15.z.string().optional(),
|
|
2158
|
+
helperName: import_zod15.z.string().optional(),
|
|
2159
|
+
requestId: import_zod15.z.string().optional()
|
|
2160
|
+
}).optional()
|
|
2161
|
+
}).strict();
|
|
2162
|
+
function buildAuditLogEntry(input) {
|
|
2163
|
+
return AuditLogEntrySchema.parse(input);
|
|
2164
|
+
}
|
|
2165
|
+
var PrecioRegionSchema = import_zod16.z.object({
|
|
2166
|
+
mensual: import_zod16.z.number().nonnegative(),
|
|
2167
|
+
anual: import_zod16.z.number().nonnegative(),
|
|
2168
|
+
moneda: import_zod16.z.string().length(3, "moneda debe ser ISO 4217 (USD, MXN, AUD, etc.)")
|
|
2169
|
+
}).strict();
|
|
2170
|
+
var MembresiaAddonSchema = import_zod16.z.object({
|
|
2171
|
+
schemaVersion: import_zod16.z.literal(1).default(1),
|
|
2172
|
+
id: import_zod16.z.string().min(1),
|
|
2173
|
+
nombre: import_zod16.z.string().min(1),
|
|
2174
|
+
descripcion: import_zod16.z.string(),
|
|
2175
|
+
activo: import_zod16.z.boolean(),
|
|
2176
|
+
/** Diccionario de límites a incrementar. Ej: { brandsIncluidas: 1 } */
|
|
2177
|
+
incrementa: import_zod16.z.record(import_zod16.z.string(), import_zod16.z.number()),
|
|
2178
|
+
/** Precio default (si la región del tenant no tiene override) */
|
|
2179
|
+
precio: PrecioRegionSchema,
|
|
2180
|
+
/** Precios por región: { MX, US, AR, ES, AU, ... } */
|
|
2181
|
+
preciosPorRegion: import_zod16.z.record(import_zod16.z.string(), PrecioRegionSchema).optional(),
|
|
2182
|
+
/** Planes a los que se puede aplicar. Ej: ['pro', 'enterprise'] */
|
|
2183
|
+
aplicableA: import_zod16.z.array(import_zod16.z.string()),
|
|
2184
|
+
/** Máximo de unidades por tenant. null = sin tope */
|
|
2185
|
+
maxPorTenant: import_zod16.z.number().int().nullable(),
|
|
2186
|
+
fechaCreacion: import_zod16.z.string()
|
|
2187
|
+
}).strict();
|
|
2188
|
+
var AddonActivoSchema = import_zod16.z.object({
|
|
2189
|
+
addonId: import_zod16.z.string(),
|
|
2190
|
+
cantidad: import_zod16.z.number().int().positive(),
|
|
2191
|
+
fechaCompra: import_zod16.z.unknown(),
|
|
2192
|
+
/** null = suscripción mensual continua */
|
|
2193
|
+
finVigencia: import_zod16.z.unknown().nullable(),
|
|
2194
|
+
estado: import_zod16.z.enum(["active", "cancelled"])
|
|
2195
|
+
}).strict();
|
|
2196
|
+
var TipoMemoriaEnum = import_zod17.z.enum([
|
|
2197
|
+
"filtro",
|
|
2198
|
+
"preferencia",
|
|
2199
|
+
"patron",
|
|
2200
|
+
"aprendido",
|
|
2201
|
+
"delegacion"
|
|
2202
|
+
]);
|
|
2203
|
+
var StatusMemoriaEnum = import_zod17.z.enum([
|
|
2204
|
+
"explicito",
|
|
2205
|
+
"explicito_confirmado",
|
|
2206
|
+
"inferido",
|
|
2207
|
+
"archivado"
|
|
2208
|
+
]);
|
|
2209
|
+
var CategoriaMemoriaEnum = import_zod17.z.enum([
|
|
2210
|
+
"compras",
|
|
2211
|
+
"produccion",
|
|
2212
|
+
"dispatch",
|
|
2213
|
+
"ventas",
|
|
2214
|
+
"marketing",
|
|
2215
|
+
"operacion",
|
|
2216
|
+
"personal",
|
|
2217
|
+
"delegacion"
|
|
2218
|
+
]);
|
|
2219
|
+
var OrigenMemoriaSchema = import_zod17.z.object({
|
|
2220
|
+
tipo: import_zod17.z.enum(["conversacion", "feedback_card", "inferido_automatico"]),
|
|
2221
|
+
conversacionId: import_zod17.z.string().nullable(),
|
|
2222
|
+
/** Si vino de feedback long-press en un card */
|
|
2223
|
+
cardId: import_zod17.z.string().nullable(),
|
|
2224
|
+
/** Razones que la generaron (para debugging + transparencia) */
|
|
2225
|
+
rationale: import_zod17.z.array(import_zod17.z.string()).optional()
|
|
2226
|
+
}).strict();
|
|
2227
|
+
var MartinMemoriaSchema = import_zod17.z.object({
|
|
2228
|
+
schemaVersion: import_zod17.z.literal(1).default(1),
|
|
2229
|
+
id: import_zod17.z.string().min(1, "id es obligatorio"),
|
|
2230
|
+
tenantId: import_zod17.z.string().min(1, "tenantId es obligatorio"),
|
|
2231
|
+
/** Usuario dueño de la memoria */
|
|
2232
|
+
uid: import_zod17.z.string().min(1, "uid es obligatorio"),
|
|
2233
|
+
tipo: TipoMemoriaEnum,
|
|
2234
|
+
categoria: CategoriaMemoriaEnum,
|
|
2235
|
+
contenido: import_zod17.z.string().min(3, "contenido debe tener al menos 3 chars").max(500, "contenido max 500 chars"),
|
|
2236
|
+
origen: OrigenMemoriaSchema,
|
|
2237
|
+
status: StatusMemoriaEnum.default("explicito"),
|
|
2238
|
+
/** Veces que Martin la usó */
|
|
2239
|
+
aplicacionesCount: import_zod17.z.number().int().nonnegative().default(0),
|
|
2240
|
+
ultimaAplicacionAt: import_zod17.z.string().datetime().nullable().default(null),
|
|
2241
|
+
createdAt: import_zod17.z.string().datetime(),
|
|
2242
|
+
updatedAt: import_zod17.z.string().datetime()
|
|
2243
|
+
}).strict();
|
|
2244
|
+
var TipoRutinaEnum = import_zod18.z.enum([
|
|
2245
|
+
"reporte",
|
|
2246
|
+
"recordatorio",
|
|
2247
|
+
"accion_delegada",
|
|
2248
|
+
"publicacion",
|
|
2249
|
+
"compra_sugerida"
|
|
2250
|
+
]);
|
|
2251
|
+
var FrecuenciaRutinaEnum = import_zod18.z.enum([
|
|
2252
|
+
"diaria",
|
|
2253
|
+
"semanal",
|
|
2254
|
+
"quincenal",
|
|
2255
|
+
"mensual",
|
|
2256
|
+
"trimestral",
|
|
2257
|
+
"puntual"
|
|
2258
|
+
]);
|
|
2259
|
+
var StatusRutinaEnum = import_zod18.z.enum([
|
|
2260
|
+
"activa",
|
|
2261
|
+
"pausada",
|
|
2262
|
+
"archivada",
|
|
2263
|
+
"completada"
|
|
2264
|
+
]);
|
|
2265
|
+
var ConfigRutinaSchema = import_zod18.z.object({
|
|
2266
|
+
/** Día semana 0=dom, 6=sab. Para semanal/quincenal */
|
|
2267
|
+
diaSemana: import_zod18.z.number().int().min(0).max(6).nullable(),
|
|
2268
|
+
/** Día mes 1-31. Para mensual/trimestral */
|
|
2269
|
+
diaMes: import_zod18.z.number().int().min(1).max(31).nullable(),
|
|
2270
|
+
/** Hora "HH:MM" en la timezone del tenant (heredada de tenants/{tid}.zonaHoraria) */
|
|
2271
|
+
hora: import_zod18.z.string().regex(/^\d{2}:\d{2}$/, "hora debe ser formato HH:MM (24h)"),
|
|
2272
|
+
/** Si frecuencia=puntual, fecha exacta ISO con offset */
|
|
2273
|
+
fechaPuntual: import_zod18.z.string().datetime({ offset: true }).nullable()
|
|
2274
|
+
}).strict();
|
|
2275
|
+
var AccionRutinaSchema = import_zod18.z.object({
|
|
2276
|
+
/** Nombre del tool MCP a invocar */
|
|
2277
|
+
tool: import_zod18.z.string().min(1),
|
|
2278
|
+
/** Parámetros para el tool */
|
|
2279
|
+
params: import_zod18.z.record(import_zod18.z.string(), import_zod18.z.unknown())
|
|
2280
|
+
}).strict();
|
|
2281
|
+
var OrigenRutinaSchema = import_zod18.z.object({
|
|
2282
|
+
tipo: import_zod18.z.enum(["conversacion", "feedback_card", "configurada_explicito"]),
|
|
2283
|
+
conversacionId: import_zod18.z.string().nullable(),
|
|
2284
|
+
cardId: import_zod18.z.string().nullable()
|
|
2285
|
+
}).strict();
|
|
2286
|
+
var MartinRutinaSchema = import_zod18.z.object({
|
|
2287
|
+
schemaVersion: import_zod18.z.literal(1).default(1),
|
|
2288
|
+
id: import_zod18.z.string().min(1),
|
|
2289
|
+
tenantId: import_zod18.z.string().min(1),
|
|
2290
|
+
/** Quién la creó */
|
|
2291
|
+
uidCreador: import_zod18.z.string().min(1),
|
|
2292
|
+
/** A quién va dirigida (puede ser otro empleado) */
|
|
2293
|
+
uidDestinatario: import_zod18.z.string().min(1),
|
|
2294
|
+
tipo: TipoRutinaEnum,
|
|
2295
|
+
frecuencia: FrecuenciaRutinaEnum,
|
|
2296
|
+
config: ConfigRutinaSchema,
|
|
2297
|
+
accion: AccionRutinaSchema,
|
|
2298
|
+
origen: OrigenRutinaSchema,
|
|
2299
|
+
status: StatusRutinaEnum.default("activa"),
|
|
2300
|
+
ejecucionesCount: import_zod18.z.number().int().nonnegative().default(0),
|
|
2301
|
+
ultimaEjecucionAt: import_zod18.z.string().datetime().nullable().default(null),
|
|
2302
|
+
/** Calculado para optimizar query del cron */
|
|
2303
|
+
proximaEjecucionAt: import_zod18.z.string().datetime(),
|
|
2304
|
+
createdAt: import_zod18.z.string().datetime(),
|
|
2305
|
+
updatedAt: import_zod18.z.string().datetime()
|
|
2306
|
+
}).strict();
|
|
2307
|
+
var MenuItemSchema = import_zod19.z.lazy(
|
|
2308
|
+
() => import_zod19.z.object({
|
|
2309
|
+
title: import_zod19.z.string().min(1, "Texto visible del item del menu"),
|
|
2310
|
+
url: import_zod19.z.string().min(1, "URL del item (puede ser absoluta o relativa)"),
|
|
2311
|
+
/** Tipo de destino: collection / page / product / catalog / blog / article / etc. */
|
|
2312
|
+
type: import_zod19.z.string().nullable().optional(),
|
|
2313
|
+
/** ID del recurso al que apunta el item (collection ID, page ID, etc.) */
|
|
2314
|
+
resourceId: import_zod19.z.string().nullable().optional(),
|
|
2315
|
+
/** Sub-items recursivos (sub-menus). Default array vacio. */
|
|
2316
|
+
items: import_zod19.z.array(MenuItemSchema).default([])
|
|
2317
|
+
}).strict()
|
|
2318
|
+
);
|
|
2319
|
+
var MENU_ASSIGNMENTS = ["header", "footer", "unassigned"];
|
|
2320
|
+
var MenuAssignmentSchema = import_zod19.z.enum(MENU_ASSIGNMENTS);
|
|
2321
|
+
var MenuSchema = import_zod19.z.object({
|
|
2322
|
+
schemaVersion: import_zod19.z.literal(1).default(1),
|
|
2323
|
+
source: SourceSchema,
|
|
2324
|
+
platformId: import_zod19.z.string().min(1, "ID del menu en la plataforma origen"),
|
|
2325
|
+
handle: import_zod19.z.string().min(1, "Handle/slug del menu"),
|
|
2326
|
+
title: import_zod19.z.string().min(1, "Titulo del menu"),
|
|
2327
|
+
/** Donde aparece el menu en el sitio */
|
|
2328
|
+
assignedTo: MenuAssignmentSchema.default("unassigned"),
|
|
2329
|
+
items: import_zod19.z.array(MenuItemSchema).default([]),
|
|
2330
|
+
updatedAt: import_zod19.z.string().nullable().optional()
|
|
2331
|
+
}).strict();
|
|
2332
|
+
var SITE_META_SOURCES = ["shopify", "wordpress", "webonly"];
|
|
2333
|
+
var SiteMetaSourceSchema = import_zod20.z.enum(SITE_META_SOURCES);
|
|
2334
|
+
var ScrapedPageSchema = import_zod20.z.object({
|
|
2335
|
+
url: import_zod20.z.string().min(1, "URL de la pagina scrappeada (no vacia)"),
|
|
2336
|
+
title: import_zod20.z.string().nullable().optional(),
|
|
2337
|
+
metaDescription: import_zod20.z.string().nullable().optional(),
|
|
2338
|
+
ogTitle: import_zod20.z.string().nullable().optional(),
|
|
2339
|
+
ogDescription: import_zod20.z.string().nullable().optional(),
|
|
2340
|
+
h1: import_zod20.z.string().nullable().optional(),
|
|
2341
|
+
h2s: import_zod20.z.array(import_zod20.z.string()).default([]),
|
|
2342
|
+
firstParagraph: import_zod20.z.string().nullable().optional(),
|
|
2343
|
+
canonicalUrl: import_zod20.z.string().nullable().optional(),
|
|
2344
|
+
fetchedAt: import_zod20.z.string()
|
|
2345
|
+
}).strict();
|
|
2346
|
+
var SiteMetaSchema = import_zod20.z.object({
|
|
2347
|
+
schemaVersion: import_zod20.z.literal(1).default(1),
|
|
2348
|
+
source: SiteMetaSourceSchema,
|
|
2349
|
+
/** Hostname del shop (ej: "atteyo-com.myshopify.com" o "atteyo.com") */
|
|
2350
|
+
shop: import_zod20.z.string().min(1, "Hostname del shop"),
|
|
2351
|
+
fetchedAt: import_zod20.z.string(),
|
|
2352
|
+
sitemapFound: import_zod20.z.boolean(),
|
|
2353
|
+
urlsDiscovered: import_zod20.z.number().int().nonnegative(),
|
|
2354
|
+
urlsScraped: import_zod20.z.number().int().nonnegative(),
|
|
2355
|
+
/** Homepage parseada (puede ser null si fall el fetch). */
|
|
2356
|
+
homepage: ScrapedPageSchema.nullable().optional(),
|
|
2357
|
+
/** Paginas extra detectadas via sitemap.xml: about, faq, contact, terms, privacy, etc. */
|
|
2358
|
+
extraPages: import_zod20.z.record(import_zod20.z.string(), ScrapedPageSchema).default({}),
|
|
2359
|
+
/** Texto combinado de homepage + extras para embedding texto. */
|
|
2360
|
+
combinedText: import_zod20.z.string().nullable().optional(),
|
|
2361
|
+
/** Embedding 1408d Vertex (texto). */
|
|
2362
|
+
embeddingText: import_zod20.z.array(import_zod20.z.number()).length(EMBEDDING_DIMS).nullable().optional(),
|
|
2363
|
+
/** Embedding 1408d Vertex (imagen — opcional). */
|
|
2364
|
+
embeddingImage: import_zod20.z.array(import_zod20.z.number()).length(EMBEDDING_DIMS).nullable().optional(),
|
|
2365
|
+
embeddingModelo: import_zod20.z.string().nullable().optional(),
|
|
2366
|
+
/** Firestore Timestamp / Date / string ISO — flexible. */
|
|
2367
|
+
embeddingFecha: import_zod20.z.unknown().nullable().optional()
|
|
2368
|
+
}).strict();
|
|
2369
|
+
var GSC_CHANGE_TYPES = [
|
|
2370
|
+
"publish",
|
|
2371
|
+
"urlChange",
|
|
2372
|
+
"bodyRewrite",
|
|
2373
|
+
"metaEdit",
|
|
2374
|
+
"altText",
|
|
2375
|
+
"internalLinks"
|
|
2376
|
+
];
|
|
2377
|
+
var GscChangeTypeSchema = import_zod21.z.enum(GSC_CHANGE_TYPES);
|
|
2378
|
+
var GSC_PRIORITIES = ["high", "medium", "low", "skip"];
|
|
2379
|
+
var GscPrioritySchema = import_zod21.z.enum(GSC_PRIORITIES);
|
|
2380
|
+
var GSC_STATUSES = ["pending", "flushed", "error", "skipped"];
|
|
2381
|
+
var GscStatusSchema = import_zod21.z.enum(GSC_STATUSES);
|
|
2382
|
+
var GscQueueItemSchema = import_zod21.z.object({
|
|
2383
|
+
schemaVersion: import_zod21.z.literal(1).default(1),
|
|
2384
|
+
id: import_zod21.z.string().min(1),
|
|
2385
|
+
tenantId: import_zod21.z.string().min(1),
|
|
2386
|
+
brandId: import_zod21.z.string().min(1),
|
|
2387
|
+
url: import_zod21.z.string().url("URL absoluta del recurso a notificar a GSC"),
|
|
2388
|
+
changeType: GscChangeTypeSchema,
|
|
2389
|
+
priority: GscPrioritySchema,
|
|
2390
|
+
/** Firestore Timestamp / Date / ISO — set por enqueueGscPing helper. */
|
|
2391
|
+
enqueuedAt: import_zod21.z.unknown(),
|
|
2392
|
+
status: GscStatusSchema,
|
|
2393
|
+
/** Set cuando flushGscQueue procesa el item (status='flushed' o 'skipped'). */
|
|
2394
|
+
flushedAt: import_zod21.z.unknown().nullable(),
|
|
2395
|
+
errorMessage: import_zod21.z.string().nullable()
|
|
2396
|
+
}).strict();
|
|
2397
|
+
var BlogSchema = import_zod22.z.object({
|
|
2398
|
+
schemaVersion: import_zod22.z.literal(1).default(1),
|
|
2399
|
+
source: SourceSchema,
|
|
2400
|
+
platformId: import_zod22.z.string().min(1, "ID del blog en la plataforma origen (Shopify blog ID)"),
|
|
2401
|
+
handle: import_zod22.z.string().min(1, "Slug/handle del blog"),
|
|
2402
|
+
title: import_zod22.z.string().min(1, "Titulo del blog"),
|
|
2403
|
+
/** Si los articles aceptan comentarios. Shopify: 'no' | 'moderate' | 'yes' */
|
|
2404
|
+
commentable: import_zod22.z.string().nullable().optional(),
|
|
2405
|
+
feedburner: import_zod22.z.string().nullable().optional(),
|
|
2406
|
+
tags: import_zod22.z.string().nullable().optional(),
|
|
2407
|
+
templateSuffix: import_zod22.z.string().nullable().optional(),
|
|
2408
|
+
/** Conteo de articles publicados (derivado al fetch — no oficial Shopify). */
|
|
2409
|
+
articlesCount: import_zod22.z.number().int().nonnegative().nullable().optional(),
|
|
2410
|
+
publishedAt: import_zod22.z.string().nullable().optional(),
|
|
2411
|
+
updatedAt: import_zod22.z.string().nullable().optional(),
|
|
2412
|
+
createdAt: import_zod22.z.string().nullable().optional()
|
|
2413
|
+
}).strict();
|
|
2414
|
+
var LocationSchema = import_zod23.z.object({
|
|
2415
|
+
schemaVersion: import_zod23.z.literal(1).default(1),
|
|
2416
|
+
source: SourceSchema,
|
|
2417
|
+
platformId: import_zod23.z.string().min(1, "ID de la location en la plataforma origen"),
|
|
2418
|
+
name: import_zod23.z.string().min(1, "Nombre de la location"),
|
|
2419
|
+
address1: import_zod23.z.string().nullable().optional(),
|
|
2420
|
+
address2: import_zod23.z.string().nullable().optional(),
|
|
2421
|
+
city: import_zod23.z.string().nullable().optional(),
|
|
2422
|
+
province: import_zod23.z.string().nullable().optional(),
|
|
2423
|
+
provinceCode: import_zod23.z.string().nullable().optional(),
|
|
2424
|
+
zip: import_zod23.z.string().nullable().optional(),
|
|
2425
|
+
country: import_zod23.z.string().nullable().optional(),
|
|
2426
|
+
countryCode: import_zod23.z.string().nullable().optional(),
|
|
2427
|
+
countryName: import_zod23.z.string().nullable().optional(),
|
|
2428
|
+
phone: import_zod23.z.string().nullable().optional(),
|
|
2429
|
+
active: import_zod23.z.boolean().nullable().optional(),
|
|
2430
|
+
legacy: import_zod23.z.boolean().nullable().optional(),
|
|
2431
|
+
createdAt: import_zod23.z.string().nullable().optional(),
|
|
2432
|
+
updatedAt: import_zod23.z.string().nullable().optional()
|
|
2433
|
+
}).strict();
|
|
2434
|
+
var StaffMemberSchema = import_zod24.z.object({
|
|
2435
|
+
schemaVersion: import_zod24.z.literal(1).default(1),
|
|
2436
|
+
source: SourceSchema,
|
|
2437
|
+
platformId: import_zod24.z.string().min(1, "ID del staff member en la plataforma origen (Shopify GID o numeric ID)"),
|
|
2438
|
+
/** Shopify GraphQL devuelve "name" combinado (ya no first_name/last_name separados). */
|
|
2439
|
+
name: import_zod24.z.string().min(1, "Nombre del staff member"),
|
|
2440
|
+
email: import_zod24.z.string().nullable().optional(),
|
|
2441
|
+
/** Shopify GraphQL: isShopOwner indica si es el dueno principal. */
|
|
2442
|
+
isShopOwner: import_zod24.z.boolean().nullable().optional(),
|
|
2443
|
+
active: import_zod24.z.boolean().nullable().optional(),
|
|
2444
|
+
accountType: import_zod24.z.string().nullable().optional(),
|
|
2445
|
+
role: import_zod24.z.string().nullable().optional(),
|
|
2446
|
+
locale: import_zod24.z.string().nullable().optional()
|
|
2447
|
+
}).strict();
|
|
2448
|
+
var ShopInfoSchema = import_zod25.z.object({
|
|
2449
|
+
shopId: import_zod25.z.union([import_zod25.z.string(), import_zod25.z.number()]).nullable().optional(),
|
|
2450
|
+
name: import_zod25.z.string().nullable().optional(),
|
|
2451
|
+
email: import_zod25.z.string().nullable().optional(),
|
|
2452
|
+
description: import_zod25.z.string().nullable().optional(),
|
|
2453
|
+
currency: import_zod25.z.string().nullable().optional(),
|
|
2454
|
+
moneyFormat: import_zod25.z.string().nullable().optional(),
|
|
2455
|
+
timezone: import_zod25.z.string().nullable().optional(),
|
|
2456
|
+
country: import_zod25.z.string().nullable().optional(),
|
|
2457
|
+
countryCode: import_zod25.z.string().nullable().optional(),
|
|
2458
|
+
province: import_zod25.z.string().nullable().optional(),
|
|
2459
|
+
city: import_zod25.z.string().nullable().optional(),
|
|
2460
|
+
address1: import_zod25.z.string().nullable().optional(),
|
|
2461
|
+
phone: import_zod25.z.string().nullable().optional(),
|
|
2462
|
+
domain: import_zod25.z.string().nullable().optional(),
|
|
2463
|
+
myshopifyDomain: import_zod25.z.string().nullable().optional(),
|
|
2464
|
+
planName: import_zod25.z.string().nullable().optional(),
|
|
2465
|
+
planDisplayName: import_zod25.z.string().nullable().optional(),
|
|
2466
|
+
weightUnit: import_zod25.z.string().nullable().optional(),
|
|
2467
|
+
primaryLocale: import_zod25.z.string().nullable().optional(),
|
|
2468
|
+
createdAt: import_zod25.z.string().nullable().optional(),
|
|
2469
|
+
updatedAt: import_zod25.z.string().nullable().optional()
|
|
2470
|
+
}).strict().nullable();
|
|
2471
|
+
var ThemeSchema = import_zod25.z.object({
|
|
2472
|
+
id: import_zod25.z.union([import_zod25.z.string(), import_zod25.z.number()]).nullable().optional(),
|
|
2473
|
+
name: import_zod25.z.string().nullable().optional(),
|
|
2474
|
+
role: import_zod25.z.string().nullable().optional(),
|
|
2475
|
+
/** Mapa de menus asignados al tema (header, footer). */
|
|
2476
|
+
themeMenus: import_zod25.z.object({
|
|
2477
|
+
header: import_zod25.z.array(import_zod25.z.object({ handle: import_zod25.z.string() }).passthrough()).default([]),
|
|
2478
|
+
footer: import_zod25.z.array(import_zod25.z.object({ handle: import_zod25.z.string() }).passthrough()).default([])
|
|
2479
|
+
}).strict().nullable().optional(),
|
|
2480
|
+
themeMenuHandle: import_zod25.z.string().nullable().optional(),
|
|
2481
|
+
/** Settings del header section (sub-objeto del theme settings). */
|
|
2482
|
+
headerSettings: import_zod25.z.unknown().nullable().optional(),
|
|
2483
|
+
footerSettings: import_zod25.z.unknown().nullable().optional()
|
|
2484
|
+
}).strict().nullable();
|
|
2485
|
+
var OrdersSummarySchema = import_zod25.z.object({
|
|
2486
|
+
totalOrders: import_zod25.z.number().int().nonnegative().default(0),
|
|
2487
|
+
dateRange: import_zod25.z.object({
|
|
2488
|
+
from: import_zod25.z.string(),
|
|
2489
|
+
to: import_zod25.z.string()
|
|
2490
|
+
}).strict().nullable().optional(),
|
|
2491
|
+
topSkus: import_zod25.z.array(import_zod25.z.object({
|
|
2492
|
+
sku: import_zod25.z.string(),
|
|
2493
|
+
title: import_zod25.z.string().nullable().optional(),
|
|
2494
|
+
quantity: import_zod25.z.number().nonnegative(),
|
|
2495
|
+
revenue: import_zod25.z.number().nonnegative()
|
|
2496
|
+
}).strict()).default([]),
|
|
2497
|
+
avgOrderValue: import_zod25.z.number().nonnegative().default(0),
|
|
2498
|
+
ordersPerDay: import_zod25.z.number().nonnegative().default(0),
|
|
2499
|
+
totalRevenue: import_zod25.z.number().nonnegative().default(0),
|
|
2500
|
+
financialStatusBreakdown: import_zod25.z.record(import_zod25.z.string(), import_zod25.z.number().nonnegative()).default({})
|
|
2501
|
+
}).strict().nullable();
|
|
2502
|
+
var CustomersSummarySchema = import_zod25.z.object({
|
|
2503
|
+
totalCustomers: import_zod25.z.number().int().nonnegative().default(0),
|
|
2504
|
+
topCustomers: import_zod25.z.array(import_zod25.z.object({
|
|
2505
|
+
id: import_zod25.z.union([import_zod25.z.string(), import_zod25.z.number()]).nullable().optional(),
|
|
2506
|
+
name: import_zod25.z.string().nullable().optional(),
|
|
2507
|
+
ordersCount: import_zod25.z.number().int().nonnegative().nullable().optional(),
|
|
2508
|
+
totalSpent: import_zod25.z.union([import_zod25.z.string(), import_zod25.z.number()]).nullable().optional(),
|
|
2509
|
+
tags: import_zod25.z.string().nullable().optional(),
|
|
2510
|
+
createdAt: import_zod25.z.string().nullable().optional()
|
|
2511
|
+
}).strict()).default([])
|
|
2512
|
+
}).strict().nullable();
|
|
2513
|
+
var SnapshotCountsSchema = import_zod25.z.object({
|
|
2514
|
+
products: import_zod25.z.number().int().nonnegative().default(0),
|
|
2515
|
+
collections: import_zod25.z.number().int().nonnegative().default(0),
|
|
2516
|
+
articles: import_zod25.z.number().int().nonnegative().default(0),
|
|
2517
|
+
pages: import_zod25.z.number().int().nonnegative().default(0),
|
|
2518
|
+
menus: import_zod25.z.number().int().nonnegative().default(0),
|
|
2519
|
+
blogs: import_zod25.z.number().int().nonnegative().default(0),
|
|
2520
|
+
locations: import_zod25.z.number().int().nonnegative().default(0),
|
|
2521
|
+
staffMembers: import_zod25.z.number().int().nonnegative().default(0),
|
|
2522
|
+
siteMeta: import_zod25.z.number().int().nonnegative().default(0)
|
|
2523
|
+
}).strict();
|
|
2524
|
+
var SnapshotMetaSchema = import_zod25.z.object({
|
|
2525
|
+
schemaVersion: import_zod25.z.literal(1).default(1),
|
|
2526
|
+
source: SourceSchema,
|
|
2527
|
+
brandId: import_zod25.z.string().min(1),
|
|
2528
|
+
/** Hostname del shop (atteyo.com / atteyo-com.myshopify.com). */
|
|
2529
|
+
shop: import_zod25.z.string().min(1),
|
|
2530
|
+
shopName: import_zod25.z.string().nullable().optional(),
|
|
2531
|
+
/** Timestamp de la ultima sync exitosa. Firestore Timestamp/Date/string ISO. */
|
|
2532
|
+
syncedAt: import_zod25.z.unknown().nullable().optional(),
|
|
2533
|
+
/** ID del ultimo import que populo este snapshot. */
|
|
2534
|
+
lastImportId: import_zod25.z.string().nullable().optional(),
|
|
2535
|
+
counts: SnapshotCountsSchema,
|
|
2536
|
+
shopInfo: ShopInfoSchema,
|
|
2537
|
+
theme: ThemeSchema,
|
|
2538
|
+
ordersSummary: OrdersSummarySchema,
|
|
2539
|
+
customersSummary: CustomersSummarySchema
|
|
2540
|
+
}).strict();
|
|
2003
2541
|
|
|
2004
2542
|
// src/tools/core.ts
|
|
2005
2543
|
function registerCoreTools(server, session) {
|
|
@@ -2007,19 +2545,18 @@ function registerCoreTools(server, session) {
|
|
|
2007
2545
|
"get_business_summary",
|
|
2008
2546
|
"Resumen general del negocio: metricas clave, contenido pendiente, estado de conexiones, alertas.",
|
|
2009
2547
|
{
|
|
2010
|
-
brandId:
|
|
2548
|
+
brandId: import_zod26.z.string().optional().describe("ID de la brand. Si no se pasa, usa la brand del contexto.")
|
|
2011
2549
|
},
|
|
2012
2550
|
async ({ brandId: inputBrandId }) => {
|
|
2013
2551
|
const tenantId = session.requireTenant();
|
|
2014
2552
|
const brandId = inputBrandId ?? session.brandId;
|
|
2015
|
-
const config = await readDoc("marketing_config", tenantId);
|
|
2016
|
-
if (!config) {
|
|
2017
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: "No hay marketing_config para este tenant" }) }] };
|
|
2018
|
-
}
|
|
2019
|
-
const brands = config.brands ?? {};
|
|
2020
2553
|
if (!brandId) {
|
|
2021
|
-
const
|
|
2022
|
-
|
|
2554
|
+
const brandsList = await queryByTenant(session, `tenants/${tenantId}/marketing_config`);
|
|
2555
|
+
if (brandsList.length === 0) {
|
|
2556
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "No hay marketing_config para este tenant" }) }] };
|
|
2557
|
+
}
|
|
2558
|
+
const brandListSummary = brandsList.map((b) => ({
|
|
2559
|
+
id: b.id,
|
|
2023
2560
|
nombre: b.nombre,
|
|
2024
2561
|
dominio: b.dominio
|
|
2025
2562
|
}));
|
|
@@ -2028,13 +2565,13 @@ function registerCoreTools(server, session) {
|
|
|
2028
2565
|
type: "text",
|
|
2029
2566
|
text: JSON.stringify({
|
|
2030
2567
|
mensaje: "No hay brand seleccionada. Brands disponibles:",
|
|
2031
|
-
brands:
|
|
2568
|
+
brands: brandListSummary,
|
|
2032
2569
|
instruccion: "Usa set_context o pasa brandId al tool."
|
|
2033
2570
|
})
|
|
2034
2571
|
}]
|
|
2035
2572
|
};
|
|
2036
2573
|
}
|
|
2037
|
-
const brand =
|
|
2574
|
+
const brand = await readDoc(`tenants/${tenantId}/marketing_config`, brandId);
|
|
2038
2575
|
if (!brand) {
|
|
2039
2576
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Brand "${brandId}" no existe` }) }] };
|
|
2040
2577
|
}
|
|
@@ -2084,7 +2621,7 @@ function registerCoreTools(server, session) {
|
|
|
2084
2621
|
"get_pending_actions",
|
|
2085
2622
|
"Lista todo lo que necesita atencion: contenido por aprobar, fotos sin procesar.",
|
|
2086
2623
|
{
|
|
2087
|
-
brandId:
|
|
2624
|
+
brandId: import_zod26.z.string().optional().describe("ID de la brand")
|
|
2088
2625
|
},
|
|
2089
2626
|
async ({ brandId: inputBrandId }) => {
|
|
2090
2627
|
const tenantId = session.requireTenant();
|
|
@@ -2099,9 +2636,7 @@ function registerCoreTools(server, session) {
|
|
|
2099
2636
|
{ field: "estado", op: "==", value: ESTADO_FOTO.NUEVA }
|
|
2100
2637
|
])
|
|
2101
2638
|
]);
|
|
2102
|
-
const
|
|
2103
|
-
const brands = config?.brands ?? {};
|
|
2104
|
-
const brand = brands[brandId];
|
|
2639
|
+
const brand = await readDoc(`tenants/${tenantId}/marketing_config`, brandId);
|
|
2105
2640
|
const seo = brand?.seoSnapshot;
|
|
2106
2641
|
const result = {
|
|
2107
2642
|
contenidoPorAprobar: contenidoPendiente.map((c) => ({
|
|
@@ -2128,9 +2663,9 @@ function registerCoreTools(server, session) {
|
|
|
2128
2663
|
"execute_action",
|
|
2129
2664
|
"Ejecuta una accion sobre contenido o foto: aprobar, rechazar, descartar foto, usar foto tal cual.",
|
|
2130
2665
|
{
|
|
2131
|
-
accion:
|
|
2132
|
-
targetId:
|
|
2133
|
-
motivo:
|
|
2666
|
+
accion: import_zod26.z.enum(["aprobar", "rechazar", "descartar_foto", "usar_foto_tal_cual"]).describe("Accion a ejecutar"),
|
|
2667
|
+
targetId: import_zod26.z.string().describe("ID del contenido o foto"),
|
|
2668
|
+
motivo: import_zod26.z.string().optional().describe("Motivo del rechazo (requerido para rechazar)")
|
|
2134
2669
|
},
|
|
2135
2670
|
async ({ accion, targetId, motivo }) => {
|
|
2136
2671
|
session.requireTenant();
|
|
@@ -2191,17 +2726,31 @@ function registerCoreTools(server, session) {
|
|
|
2191
2726
|
}
|
|
2192
2727
|
|
|
2193
2728
|
// src/tools/marketing.ts
|
|
2194
|
-
var
|
|
2729
|
+
var import_zod37 = require("zod");
|
|
2195
2730
|
|
|
2196
2731
|
// ../packages/marketing-business-logic/dist/index.js
|
|
2197
2732
|
var import_firebase_admin2 = require("firebase-admin");
|
|
2198
2733
|
var import_firebase_admin3 = require("firebase-admin");
|
|
2734
|
+
var import_zod27 = require("zod");
|
|
2735
|
+
var import_zod28 = require("zod");
|
|
2736
|
+
var import_zod29 = require("zod");
|
|
2199
2737
|
var import_firebase_admin4 = require("firebase-admin");
|
|
2200
2738
|
var import_firebase_admin5 = require("firebase-admin");
|
|
2201
2739
|
var import_firebase_admin6 = require("firebase-admin");
|
|
2740
|
+
var import_zod30 = require("zod");
|
|
2741
|
+
var import_zod31 = require("zod");
|
|
2202
2742
|
var import_firebase_admin7 = require("firebase-admin");
|
|
2743
|
+
var import_zod32 = require("zod");
|
|
2203
2744
|
var import_firebase_admin8 = require("firebase-admin");
|
|
2204
2745
|
var import_firebase_admin9 = require("firebase-admin");
|
|
2746
|
+
var import_firestore3 = require("firebase-admin/firestore");
|
|
2747
|
+
var import_firestore4 = require("firebase-admin/firestore");
|
|
2748
|
+
var import_firestore5 = require("firebase-admin/firestore");
|
|
2749
|
+
var import_firestore6 = require("firebase-admin/firestore");
|
|
2750
|
+
var import_zod33 = require("zod");
|
|
2751
|
+
var import_firestore7 = require("firebase-admin/firestore");
|
|
2752
|
+
var import_zod34 = require("zod");
|
|
2753
|
+
var import_firestore8 = require("firebase-admin/firestore");
|
|
2205
2754
|
var RULE_NEGATIVES = {
|
|
2206
2755
|
allowFaces: "no people, no faces, no hands",
|
|
2207
2756
|
allowProductTransform: "no distorted products, no warped objects",
|
|
@@ -2351,14 +2900,9 @@ async function brandBriefWriter(input) {
|
|
|
2351
2900
|
hint: "Revisa el ejemplo y corrige el campo indicado en path. Manda objeto/array nativo, NO string JSON."
|
|
2352
2901
|
};
|
|
2353
2902
|
}
|
|
2354
|
-
const
|
|
2355
|
-
const
|
|
2356
|
-
if (!
|
|
2357
|
-
return { ok: false, error: "No hay marketing_config" };
|
|
2358
|
-
}
|
|
2359
|
-
const config = configSnap.data();
|
|
2360
|
-
const brands = config?.brands ?? {};
|
|
2361
|
-
if (!brands[brandId]) {
|
|
2903
|
+
const brandRef = db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId);
|
|
2904
|
+
const brandSnap = await brandRef.get();
|
|
2905
|
+
if (!brandSnap.exists) {
|
|
2362
2906
|
return { ok: false, error: `Brand "${brandId}" no encontrada` };
|
|
2363
2907
|
}
|
|
2364
2908
|
const brief = validation.parsed;
|
|
@@ -2370,10 +2914,14 @@ async function brandBriefWriter(input) {
|
|
|
2370
2914
|
validadoAt: null,
|
|
2371
2915
|
_validatedFields: brief._validatedFields ?? {}
|
|
2372
2916
|
};
|
|
2373
|
-
await
|
|
2374
|
-
|
|
2917
|
+
await brandRef.set({
|
|
2918
|
+
brandBrief: briefWithMeta,
|
|
2919
|
+
id: brandId,
|
|
2920
|
+
brandId,
|
|
2921
|
+
tenantId,
|
|
2922
|
+
schemaVersion: 1,
|
|
2375
2923
|
updatedAt: import_firebase_admin2.firestore.FieldValue.serverTimestamp()
|
|
2376
|
-
});
|
|
2924
|
+
}, { merge: true });
|
|
2377
2925
|
return {
|
|
2378
2926
|
ok: true,
|
|
2379
2927
|
mensaje: `Brand Brief guardado para "${brandId}". El tenant puede validar los campos en Config > Mi Negocio.`
|
|
@@ -2381,14 +2929,9 @@ async function brandBriefWriter(input) {
|
|
|
2381
2929
|
}
|
|
2382
2930
|
async function save(input) {
|
|
2383
2931
|
const { db, tenantId, brandId, plan } = input;
|
|
2384
|
-
const
|
|
2385
|
-
const
|
|
2386
|
-
if (!
|
|
2387
|
-
return { ok: false, error: "No hay marketing_config" };
|
|
2388
|
-
}
|
|
2389
|
-
const config = configSnap.data();
|
|
2390
|
-
const brands = config.brands ?? {};
|
|
2391
|
-
if (!brands[brandId]) {
|
|
2932
|
+
const brandRef = db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId);
|
|
2933
|
+
const brandSnap = await brandRef.get();
|
|
2934
|
+
if (!brandSnap.exists) {
|
|
2392
2935
|
return { ok: false, error: `Brand "${brandId}" no encontrada` };
|
|
2393
2936
|
}
|
|
2394
2937
|
const { blogStrategy, ...planSinBlogStrategy } = plan;
|
|
@@ -2397,18 +2940,15 @@ async function save(input) {
|
|
|
2397
2940
|
fechaCreacion: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2398
2941
|
creadoPorId: "mcp-cowork"
|
|
2399
2942
|
};
|
|
2400
|
-
|
|
2401
|
-
updatedBrands[brandId] = {
|
|
2402
|
-
...brands[brandId],
|
|
2943
|
+
await brandRef.set({
|
|
2403
2944
|
plan: planWithMeta,
|
|
2404
|
-
...blogStrategy ? { blogStrategy } : {}
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
id: config.id,
|
|
2945
|
+
...blogStrategy ? { blogStrategy } : {},
|
|
2946
|
+
id: brandId,
|
|
2947
|
+
brandId,
|
|
2408
2948
|
tenantId,
|
|
2409
|
-
|
|
2949
|
+
schemaVersion: 1,
|
|
2410
2950
|
updatedAt: import_firebase_admin3.firestore.FieldValue.serverTimestamp()
|
|
2411
|
-
});
|
|
2951
|
+
}, { merge: true });
|
|
2412
2952
|
return {
|
|
2413
2953
|
ok: true,
|
|
2414
2954
|
mensaje: `Plan de marketing guardado para brand "${brandId}"` + (blogStrategy ? " (incluye blogStrategy)" : "")
|
|
@@ -2416,16 +2956,12 @@ async function save(input) {
|
|
|
2416
2956
|
}
|
|
2417
2957
|
async function updateField(input) {
|
|
2418
2958
|
const { db, tenantId, brandId, field, value } = input;
|
|
2419
|
-
const
|
|
2420
|
-
const
|
|
2421
|
-
if (!
|
|
2422
|
-
return { ok: false, error: "No hay marketing_config" };
|
|
2423
|
-
}
|
|
2424
|
-
const config = configSnap.data();
|
|
2425
|
-
const brands = config.brands ?? {};
|
|
2426
|
-
if (!brands[brandId]) {
|
|
2959
|
+
const brandRef = db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId);
|
|
2960
|
+
const brandSnap = await brandRef.get();
|
|
2961
|
+
if (!brandSnap.exists) {
|
|
2427
2962
|
return { ok: false, error: `Brand "${brandId}" no encontrada` };
|
|
2428
2963
|
}
|
|
2964
|
+
const brandData = brandSnap.data();
|
|
2429
2965
|
let parsedValue = value;
|
|
2430
2966
|
if (typeof value === "string") {
|
|
2431
2967
|
const trimmed = value.trim();
|
|
@@ -2448,27 +2984,24 @@ async function updateField(input) {
|
|
|
2448
2984
|
hint: "Manda el valor como objeto/array nativo, NO como string JSON. Usa el ejemplo arriba como referencia exacta del shape esperado."
|
|
2449
2985
|
};
|
|
2450
2986
|
}
|
|
2451
|
-
const
|
|
2987
|
+
const updatePayload = {
|
|
2988
|
+
id: brandId,
|
|
2989
|
+
brandId,
|
|
2990
|
+
tenantId,
|
|
2991
|
+
schemaVersion: 1,
|
|
2992
|
+
updatedAt: import_firebase_admin3.firestore.FieldValue.serverTimestamp()
|
|
2993
|
+
};
|
|
2452
2994
|
if (field === "blogStrategy") {
|
|
2453
|
-
|
|
2454
|
-
...brands[brandId],
|
|
2455
|
-
blogStrategy: validation.parsed
|
|
2456
|
-
};
|
|
2995
|
+
updatePayload.blogStrategy = validation.parsed;
|
|
2457
2996
|
} else {
|
|
2458
|
-
const currentPlan =
|
|
2459
|
-
|
|
2997
|
+
const currentPlan = brandData.plan ?? {};
|
|
2998
|
+
updatePayload.plan = {
|
|
2460
2999
|
...currentPlan,
|
|
2461
3000
|
[field]: validation.parsed,
|
|
2462
3001
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2463
3002
|
};
|
|
2464
|
-
updatedBrands[brandId] = { ...brands[brandId], plan: updatedPlan };
|
|
2465
3003
|
}
|
|
2466
|
-
await
|
|
2467
|
-
id: config.id,
|
|
2468
|
-
tenantId,
|
|
2469
|
-
brands: updatedBrands,
|
|
2470
|
-
updatedAt: import_firebase_admin3.firestore.FieldValue.serverTimestamp()
|
|
2471
|
-
});
|
|
3004
|
+
await brandRef.set(updatePayload, { merge: true });
|
|
2472
3005
|
return {
|
|
2473
3006
|
ok: true,
|
|
2474
3007
|
mensaje: `Campo "${field}" actualizado en el plan de "${brandId}"`
|
|
@@ -2478,6 +3011,225 @@ var planWriter = {
|
|
|
2478
3011
|
save,
|
|
2479
3012
|
updateField
|
|
2480
3013
|
};
|
|
3014
|
+
var DisabledReasonCodeSchema = import_zod29.z.enum([
|
|
3015
|
+
"paywall",
|
|
3016
|
+
// Acción requiere upgrade de plan
|
|
3017
|
+
"oauth_missing",
|
|
3018
|
+
// Falta conectar servicio externo (Shopify, GBP, etc.)
|
|
3019
|
+
"data_missing",
|
|
3020
|
+
// Falta información necesaria (brand brief, foto, etc.)
|
|
3021
|
+
"plan_not_includes",
|
|
3022
|
+
// Plan actual no incluye la feature
|
|
3023
|
+
"feature_disabled",
|
|
3024
|
+
// Feature deshabilitada admin-side para este tenant
|
|
3025
|
+
"other"
|
|
3026
|
+
// Caso explícito fuera del enum. Detail obligatorio.
|
|
3027
|
+
]);
|
|
3028
|
+
var DisabledOutputSchema = import_zod29.z.object({
|
|
3029
|
+
disabled: import_zod29.z.literal(true),
|
|
3030
|
+
code: DisabledReasonCodeSchema,
|
|
3031
|
+
/**
|
|
3032
|
+
* Variables para interpolar en el template del mensaje (`{key}` → value).
|
|
3033
|
+
* Ejemplo: { service: 'Shopify' } para `oauth_missing`.
|
|
3034
|
+
* Para `other`, requiere al menos { detail: '...' } para que el template
|
|
3035
|
+
* '{detail}' interpole.
|
|
3036
|
+
*/
|
|
3037
|
+
detail: import_zod29.z.record(import_zod29.z.string(), import_zod29.z.string()).optional()
|
|
3038
|
+
});
|
|
3039
|
+
var TEMPLATES = {
|
|
3040
|
+
es: {
|
|
3041
|
+
paywall: "Esta acci\xF3n requiere actualizar tu plan.",
|
|
3042
|
+
oauth_missing: "Necesitas conectar {service} primero.",
|
|
3043
|
+
data_missing: "Falta informaci\xF3n: {detail}",
|
|
3044
|
+
plan_not_includes: "Tu plan no incluye {feature}",
|
|
3045
|
+
feature_disabled: "Esta funci\xF3n est\xE1 deshabilitada para tu cuenta.",
|
|
3046
|
+
other: "{detail}"
|
|
3047
|
+
},
|
|
3048
|
+
en: {
|
|
3049
|
+
paywall: "This action requires upgrading your plan.",
|
|
3050
|
+
oauth_missing: "You need to connect {service} first.",
|
|
3051
|
+
data_missing: "Missing information: {detail}",
|
|
3052
|
+
plan_not_includes: "Your plan doesn't include {feature}",
|
|
3053
|
+
feature_disabled: "This feature is disabled for your account.",
|
|
3054
|
+
other: "{detail}"
|
|
3055
|
+
}
|
|
3056
|
+
};
|
|
3057
|
+
function getDisabledMessage(code, locale, vars) {
|
|
3058
|
+
if (!(locale in TEMPLATES)) {
|
|
3059
|
+
throw new Error(
|
|
3060
|
+
`Locale '${locale}' no soportado en disabledMessages. Locales activos: ${Object.keys(TEMPLATES).join(", ")}.`
|
|
3061
|
+
);
|
|
3062
|
+
}
|
|
3063
|
+
const template = TEMPLATES[locale][code];
|
|
3064
|
+
if (!template) {
|
|
3065
|
+
throw new Error(
|
|
3066
|
+
`Disabled code '${code}' no tiene template definido para locale '${locale}'.`
|
|
3067
|
+
);
|
|
3068
|
+
}
|
|
3069
|
+
return template.replace(/\{(\w+)\}/g, (_match, key) => {
|
|
3070
|
+
if (!vars || !(key in vars)) {
|
|
3071
|
+
throw new Error(
|
|
3072
|
+
`getDisabledMessage: variable '${key}' del template '${code}' (${locale}) no provista en vars. Helper debe pasar { ${key}: '...' } en detail.`
|
|
3073
|
+
);
|
|
3074
|
+
}
|
|
3075
|
+
return vars[key];
|
|
3076
|
+
});
|
|
3077
|
+
}
|
|
3078
|
+
var SideEffectEnum = import_zod28.z.enum([
|
|
3079
|
+
"reads_firestore",
|
|
3080
|
+
"writes_firestore",
|
|
3081
|
+
"updates_calendar_slot",
|
|
3082
|
+
"updates_brand_config",
|
|
3083
|
+
"modifies_quota_state",
|
|
3084
|
+
"spends_credits",
|
|
3085
|
+
"consumes_ai_tokens",
|
|
3086
|
+
"sends_email",
|
|
3087
|
+
"publishes_external"
|
|
3088
|
+
]);
|
|
3089
|
+
var WRITER_EFFECTS = [
|
|
3090
|
+
"writes_firestore",
|
|
3091
|
+
"updates_calendar_slot",
|
|
3092
|
+
"updates_brand_config",
|
|
3093
|
+
"modifies_quota_state",
|
|
3094
|
+
"publishes_external"
|
|
3095
|
+
];
|
|
3096
|
+
var SummaryTemplateSchema = import_zod28.z.custom((val) => typeof val === "function", {
|
|
3097
|
+
message: "martinSummaryTemplate debe ser funci\xF3n (input, output, locale) => string"
|
|
3098
|
+
});
|
|
3099
|
+
var ConfirmationTemplateSchema = import_zod28.z.custom((val) => typeof val === "function", {
|
|
3100
|
+
message: "martinConfirmationTemplate debe ser funci\xF3n (input, locale) => string"
|
|
3101
|
+
});
|
|
3102
|
+
var ExtractTargetPathSchema = import_zod28.z.custom((val) => typeof val === "function", {
|
|
3103
|
+
message: "extractTargetPath debe ser funci\xF3n (input, output) => string"
|
|
3104
|
+
});
|
|
3105
|
+
var ExtractChangesSchema = import_zod28.z.custom((val) => typeof val === "function", {
|
|
3106
|
+
message: "extractChanges debe ser funci\xF3n (input, output) => { before, after }"
|
|
3107
|
+
});
|
|
3108
|
+
var MartinContractSchema = import_zod28.z.object({
|
|
3109
|
+
// Schema versioning (Dim 4 backbone)
|
|
3110
|
+
schemaVersion: import_zod28.z.literal(1).default(1),
|
|
3111
|
+
// Identidad
|
|
3112
|
+
name: import_zod28.z.string().min(1).regex(/^[a-z][a-z0-9_]*$/, "snake_case requerido (ej: descartar_contenido)"),
|
|
3113
|
+
description: import_zod28.z.string().min(10),
|
|
3114
|
+
// Validación I/O del helper
|
|
3115
|
+
paramsSchema: import_zod28.z.instanceof(import_zod28.z.ZodType),
|
|
3116
|
+
outputSchema: import_zod28.z.instanceof(import_zod28.z.ZodType),
|
|
3117
|
+
// Reglas de comportamiento
|
|
3118
|
+
requiresConfirmation: import_zod28.z.boolean(),
|
|
3119
|
+
requiresDoubleConfirmation: import_zod28.z.boolean().optional(),
|
|
3120
|
+
destructive: import_zod28.z.boolean(),
|
|
3121
|
+
affectsPublication: import_zod28.z.boolean(),
|
|
3122
|
+
affectsExternal: import_zod28.z.boolean(),
|
|
3123
|
+
// Presentación al usuario (i18n)
|
|
3124
|
+
martinSummaryTemplate: SummaryTemplateSchema,
|
|
3125
|
+
martinConfirmationTemplate: ConfirmationTemplateSchema.optional(),
|
|
3126
|
+
// Auditoría
|
|
3127
|
+
auditAction: import_zod28.z.string().regex(/^[a-z][a-z0-9_]*\.[a-z][a-z0-9_.]*$/, "formato dominio.accion (ej: contenido.descartar)"),
|
|
3128
|
+
extractTargetPath: ExtractTargetPathSchema.optional(),
|
|
3129
|
+
extractChanges: ExtractChangesSchema.optional(),
|
|
3130
|
+
// Governance
|
|
3131
|
+
quotasConsumed: import_zod28.z.array(import_zod28.z.string()),
|
|
3132
|
+
requiredRoles: import_zod28.z.array(import_zod28.z.string()).min(1),
|
|
3133
|
+
sideEffects: import_zod28.z.array(SideEffectEnum),
|
|
3134
|
+
// Disabled state declaration (HITO 6).
|
|
3135
|
+
// Si el helper PUEDE retornar { disabled: true, code }, el author
|
|
3136
|
+
// declara aquí los códigos posibles. El wrapper valida en runtime
|
|
3137
|
+
// que el code retornado por el helper esté declarado — si no, throws.
|
|
3138
|
+
// Sin esto declarado, el wrapper trata cualquier code como inválido.
|
|
3139
|
+
disabledReasonCodes: import_zod28.z.array(DisabledReasonCodeSchema).optional()
|
|
3140
|
+
}).superRefine((contract, ctx) => {
|
|
3141
|
+
const isWriter = contract.sideEffects.some((e) => WRITER_EFFECTS.includes(e));
|
|
3142
|
+
if (isWriter && !contract.extractTargetPath) {
|
|
3143
|
+
ctx.addIssue({
|
|
3144
|
+
code: "custom",
|
|
3145
|
+
message: `Contract "${contract.name}" tiene side effects de escritura \u2014 extractTargetPath es obligatorio`,
|
|
3146
|
+
path: ["extractTargetPath"]
|
|
3147
|
+
});
|
|
3148
|
+
}
|
|
3149
|
+
if (isWriter && !contract.extractChanges) {
|
|
3150
|
+
ctx.addIssue({
|
|
3151
|
+
code: "custom",
|
|
3152
|
+
message: `Contract "${contract.name}" tiene side effects de escritura \u2014 extractChanges es obligatorio`,
|
|
3153
|
+
path: ["extractChanges"]
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
if (contract.requiresConfirmation && !contract.martinConfirmationTemplate) {
|
|
3157
|
+
ctx.addIssue({
|
|
3158
|
+
code: "custom",
|
|
3159
|
+
message: `Contract "${contract.name}" requiere confirmaci\xF3n \u2014 martinConfirmationTemplate es obligatorio`,
|
|
3160
|
+
path: ["martinConfirmationTemplate"]
|
|
3161
|
+
});
|
|
3162
|
+
}
|
|
3163
|
+
if (contract.requiresDoubleConfirmation && !contract.requiresConfirmation) {
|
|
3164
|
+
ctx.addIssue({
|
|
3165
|
+
code: "custom",
|
|
3166
|
+
message: `Contract "${contract.name}" requiere doble confirmaci\xF3n pero requiresConfirmation es false`,
|
|
3167
|
+
path: ["requiresDoubleConfirmation"]
|
|
3168
|
+
});
|
|
3169
|
+
}
|
|
3170
|
+
if (contract.destructive && !contract.requiresConfirmation) {
|
|
3171
|
+
ctx.addIssue({
|
|
3172
|
+
code: "custom",
|
|
3173
|
+
message: `Contract "${contract.name}" es destructivo pero NO requiere confirmaci\xF3n`,
|
|
3174
|
+
path: ["requiresConfirmation"]
|
|
3175
|
+
});
|
|
3176
|
+
}
|
|
3177
|
+
});
|
|
3178
|
+
var ParamsSchema = import_zod27.z.object({
|
|
3179
|
+
tenantId: import_zod27.z.string().min(1),
|
|
3180
|
+
brandId: import_zod27.z.string().min(1),
|
|
3181
|
+
/** Plan completo generado por Claude. Puede incluir blogStrategy. */
|
|
3182
|
+
plan: import_zod27.z.record(import_zod27.z.string(), import_zod27.z.unknown())
|
|
3183
|
+
});
|
|
3184
|
+
var OutputSchema = import_zod27.z.discriminatedUnion("ok", [
|
|
3185
|
+
import_zod27.z.object({
|
|
3186
|
+
ok: import_zod27.z.literal(true),
|
|
3187
|
+
mensaje: import_zod27.z.string()
|
|
3188
|
+
}),
|
|
3189
|
+
import_zod27.z.object({
|
|
3190
|
+
ok: import_zod27.z.literal(false),
|
|
3191
|
+
error: import_zod27.z.string(),
|
|
3192
|
+
detalle: import_zod27.z.unknown().optional(),
|
|
3193
|
+
path: import_zod27.z.string().optional(),
|
|
3194
|
+
recibido: import_zod27.z.unknown().optional(),
|
|
3195
|
+
ejemplo_correcto: import_zod27.z.unknown().optional(),
|
|
3196
|
+
hint: import_zod27.z.string().optional()
|
|
3197
|
+
})
|
|
3198
|
+
]);
|
|
3199
|
+
var rawContract = {
|
|
3200
|
+
name: "save_marketing_plan",
|
|
3201
|
+
description: "Guarda el plan de marketing de una brand. Si el plan incluye blogStrategy, la extrae al nivel brand (no dentro de plan).",
|
|
3202
|
+
paramsSchema: ParamsSchema,
|
|
3203
|
+
outputSchema: OutputSchema,
|
|
3204
|
+
// No es destructivo (sobrescribe plan completo, pero plan se regenera fácil),
|
|
3205
|
+
// no afecta publicación, no llama externo.
|
|
3206
|
+
requiresConfirmation: false,
|
|
3207
|
+
destructive: false,
|
|
3208
|
+
affectsPublication: false,
|
|
3209
|
+
affectsExternal: false,
|
|
3210
|
+
martinSummaryTemplate: (input, output, locale) => {
|
|
3211
|
+
if (!output.ok) {
|
|
3212
|
+
if (locale === "en") return `I couldn't save the plan: ${output.error}`;
|
|
3213
|
+
return `No pude guardar el plan: ${output.error}`;
|
|
3214
|
+
}
|
|
3215
|
+
if (locale === "en") return `I saved the marketing plan for ${input.brandId}.`;
|
|
3216
|
+
return `Guard\xE9 el plan de marketing de ${input.brandId}.`;
|
|
3217
|
+
},
|
|
3218
|
+
// Auditoría — OBLIGATORIO porque writes_firestore + updates_brand_config.
|
|
3219
|
+
auditAction: "marketing.plan.guardar",
|
|
3220
|
+
extractTargetPath: (input) => `tenants/${input.tenantId}/marketing_config/${input.brandId}`,
|
|
3221
|
+
extractChanges: (input, output) => ({
|
|
3222
|
+
before: null,
|
|
3223
|
+
// El helper no lee el plan previo en save (solo merge).
|
|
3224
|
+
after: output.ok ? { plan: input.plan } : null
|
|
3225
|
+
}),
|
|
3226
|
+
quotasConsumed: [],
|
|
3227
|
+
requiredRoles: ["admin", "encargado"],
|
|
3228
|
+
sideEffects: ["writes_firestore", "updates_brand_config"]
|
|
3229
|
+
};
|
|
3230
|
+
var planWriterSaveContract = MartinContractSchema.parse(
|
|
3231
|
+
rawContract
|
|
3232
|
+
);
|
|
2481
3233
|
async function collectionSuggestionsWriter(input) {
|
|
2482
3234
|
const { db, tenantId, brandId, suggestions } = input;
|
|
2483
3235
|
const validation = validateCollectionSuggestions(suggestions);
|
|
@@ -2493,18 +3245,13 @@ async function collectionSuggestionsWriter(input) {
|
|
|
2493
3245
|
};
|
|
2494
3246
|
}
|
|
2495
3247
|
const validSuggestions = validation.parsed;
|
|
2496
|
-
const
|
|
2497
|
-
const
|
|
2498
|
-
if (!
|
|
2499
|
-
return { ok: false, error: "marketing_config no existe" };
|
|
2500
|
-
}
|
|
2501
|
-
const config = configSnap.data();
|
|
2502
|
-
const brands = config.brands ?? {};
|
|
2503
|
-
const brand = brands[brandId];
|
|
2504
|
-
if (!brand) {
|
|
3248
|
+
const brandRef = db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId);
|
|
3249
|
+
const brandSnap = await brandRef.get();
|
|
3250
|
+
if (!brandSnap.exists) {
|
|
2505
3251
|
return { ok: false, error: `Brand ${brandId} no encontrada` };
|
|
2506
3252
|
}
|
|
2507
|
-
const
|
|
3253
|
+
const brandData = brandSnap.data();
|
|
3254
|
+
const existing = brandData.collectionSuggestions ?? {};
|
|
2508
3255
|
const updated = { ...existing };
|
|
2509
3256
|
for (const s of validSuggestions) {
|
|
2510
3257
|
const id = String(s.collectionId);
|
|
@@ -2522,9 +3269,14 @@ async function collectionSuggestionsWriter(input) {
|
|
|
2522
3269
|
generadoPor: "mcp-cowork"
|
|
2523
3270
|
};
|
|
2524
3271
|
}
|
|
2525
|
-
await
|
|
2526
|
-
|
|
2527
|
-
|
|
3272
|
+
await brandRef.set({
|
|
3273
|
+
collectionSuggestions: updated,
|
|
3274
|
+
id: brandId,
|
|
3275
|
+
brandId,
|
|
3276
|
+
tenantId,
|
|
3277
|
+
schemaVersion: 1,
|
|
3278
|
+
updatedAt: import_firebase_admin4.firestore.FieldValue.serverTimestamp()
|
|
3279
|
+
}, { merge: true });
|
|
2528
3280
|
return {
|
|
2529
3281
|
ok: true,
|
|
2530
3282
|
saved: validSuggestions.length,
|
|
@@ -2631,7 +3383,7 @@ async function contenidoUpdater(input) {
|
|
|
2631
3383
|
estado,
|
|
2632
3384
|
calendarioItemRef
|
|
2633
3385
|
} = input;
|
|
2634
|
-
const docRef = db.
|
|
3386
|
+
const docRef = db.doc(`tenants/${tenantId}/marketing_contenido/${contenidoId}`);
|
|
2635
3387
|
const snap = await docRef.get();
|
|
2636
3388
|
if (!snap.exists) {
|
|
2637
3389
|
return { ok: false, error: `Contenido ${contenidoId} no existe` };
|
|
@@ -2691,12 +3443,29 @@ async function contenidoUpdater(input) {
|
|
|
2691
3443
|
};
|
|
2692
3444
|
}
|
|
2693
3445
|
var CAMPOS_SEMANTICOS = ["keyword", "tema", "plataforma", "tipo"];
|
|
3446
|
+
function mapContenidoEstadoToSlotEstado(contenidoEstado) {
|
|
3447
|
+
switch (contenidoEstado) {
|
|
3448
|
+
case "pendiente_aprobacion":
|
|
3449
|
+
case "generado":
|
|
3450
|
+
case "editado":
|
|
3451
|
+
return "generado";
|
|
3452
|
+
case "aprobado":
|
|
3453
|
+
return "aprobado";
|
|
3454
|
+
case "publicado":
|
|
3455
|
+
return "publicado";
|
|
3456
|
+
case "rechazado":
|
|
3457
|
+
return "rechazado";
|
|
3458
|
+
// borrador + error_publicacion + estados desconocidos → no tocar slot
|
|
3459
|
+
default:
|
|
3460
|
+
return null;
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
2694
3463
|
async function calendarSlotUpdater(input) {
|
|
2695
3464
|
const { db, tenantId, brandId, mes, semana, slotIndex, cambios, accionContenidoExistente } = input;
|
|
2696
3465
|
if (slotIndex < 0) {
|
|
2697
3466
|
return { ok: false, error: "slotIndex no puede ser negativo" };
|
|
2698
3467
|
}
|
|
2699
|
-
const calQuery = await db.collection("
|
|
3468
|
+
const calQuery = await db.collection("tenants").doc(tenantId).collection("marketing_calendario").where("brandId", "==", brandId).where("mes", "==", mes).limit(1).get();
|
|
2700
3469
|
if (calQuery.empty) {
|
|
2701
3470
|
return { ok: false, error: "Calendario no encontrado" };
|
|
2702
3471
|
}
|
|
@@ -2733,7 +3502,7 @@ async function calendarSlotUpdater(input) {
|
|
|
2733
3502
|
}
|
|
2734
3503
|
let contenidosADescartar = [];
|
|
2735
3504
|
if (oldContenidoRef && (accionContenidoExistente === "descartar" || !tocaSemantica)) {
|
|
2736
|
-
const contenidoQuery = await db.collection("
|
|
3505
|
+
const contenidoQuery = await db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).where("calendarioItemRef", "==", oldContenidoRef).limit(10).get();
|
|
2737
3506
|
contenidosADescartar = contenidoQuery.docs.filter((c) => {
|
|
2738
3507
|
const data = c.data();
|
|
2739
3508
|
return data.estado !== "descartado" && data.estado !== ESTADO_CONTENIDO.PUBLICADO;
|
|
@@ -2790,6 +3559,18 @@ async function calendarSlotUpdater(input) {
|
|
|
2790
3559
|
targetRef: `semana:${targetSemanaNum}:slot:${targetSlotIdx}`
|
|
2791
3560
|
};
|
|
2792
3561
|
}
|
|
3562
|
+
let movedSlotEstado = null;
|
|
3563
|
+
if (moveTarget && oldContenidoRef) {
|
|
3564
|
+
try {
|
|
3565
|
+
const contenidoSnap = await db.doc(`tenants/${tenantId}/marketing_contenido/${oldContenidoRef}`).get();
|
|
3566
|
+
if (contenidoSnap.exists) {
|
|
3567
|
+
const estadoContenido = contenidoSnap.data().estado;
|
|
3568
|
+
movedSlotEstado = mapContenidoEstadoToSlotEstado(estadoContenido);
|
|
3569
|
+
}
|
|
3570
|
+
} catch {
|
|
3571
|
+
movedSlotEstado = null;
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
2793
3574
|
let resultAction;
|
|
2794
3575
|
let resultSlotIndex = slotIndex;
|
|
2795
3576
|
let movedTo;
|
|
@@ -2825,10 +3606,14 @@ async function calendarSlotUpdater(input) {
|
|
|
2825
3606
|
};
|
|
2826
3607
|
const targetSemana = freshSemanas[moveTarget.targetSemanaNum - 1];
|
|
2827
3608
|
const targetItems = targetSemana.items ?? [];
|
|
3609
|
+
const targetSlotActual = targetItems[moveTarget.targetSlotIdx];
|
|
3610
|
+
const targetEstadoActual = targetSlotActual.estado;
|
|
3611
|
+
const shouldUpdateEstado = movedSlotEstado !== null && targetEstadoActual !== "publicado";
|
|
2828
3612
|
targetItems[moveTarget.targetSlotIdx] = {
|
|
2829
|
-
...
|
|
3613
|
+
...targetSlotActual,
|
|
2830
3614
|
contenidoRef: oldContenidoRef,
|
|
2831
|
-
...oldFotoIdAsignada ? { fotoIdAsignada: oldFotoIdAsignada } : {}
|
|
3615
|
+
...oldFotoIdAsignada ? { fotoIdAsignada: oldFotoIdAsignada } : {},
|
|
3616
|
+
...shouldUpdateEstado ? { estado: movedSlotEstado } : {}
|
|
2832
3617
|
};
|
|
2833
3618
|
targetSemana.items = targetItems;
|
|
2834
3619
|
freshSemanas[moveTarget.targetSemanaNum - 1] = targetSemana;
|
|
@@ -2850,7 +3635,7 @@ async function calendarSlotUpdater(input) {
|
|
|
2850
3635
|
});
|
|
2851
3636
|
if (moveTarget && oldContenidoRef) {
|
|
2852
3637
|
try {
|
|
2853
|
-
await db.
|
|
3638
|
+
await db.doc(`tenants/${tenantId}/marketing_contenido/${oldContenidoRef}`).update({
|
|
2854
3639
|
calendarioItemRef: moveTarget.targetRef
|
|
2855
3640
|
});
|
|
2856
3641
|
} catch (err) {
|
|
@@ -2949,6 +3734,134 @@ async function handleNuevoSlot(args) {
|
|
|
2949
3734
|
descartados: 0
|
|
2950
3735
|
};
|
|
2951
3736
|
}
|
|
3737
|
+
var ParamsSchema2 = import_zod30.z.object({
|
|
3738
|
+
tenantId: import_zod30.z.string().min(1),
|
|
3739
|
+
brandId: import_zod30.z.string().min(1),
|
|
3740
|
+
mes: import_zod30.z.string().regex(/^\d{4}-\d{2}$/, "Formato YYYY-MM"),
|
|
3741
|
+
semana: import_zod30.z.number().int().min(1).max(5),
|
|
3742
|
+
slotIndex: import_zod30.z.number().int().min(0),
|
|
3743
|
+
cambios: import_zod30.z.record(import_zod30.z.string(), import_zod30.z.unknown()),
|
|
3744
|
+
accionContenidoExistente: import_zod30.z.string().optional()
|
|
3745
|
+
});
|
|
3746
|
+
var OutputSchema2 = import_zod30.z.discriminatedUnion("ok", [
|
|
3747
|
+
import_zod30.z.object({
|
|
3748
|
+
ok: import_zod30.z.literal(true),
|
|
3749
|
+
action: import_zod30.z.enum(["added", "updated", "nuevo_slot", "moved"]),
|
|
3750
|
+
slotIndex: import_zod30.z.number().int(),
|
|
3751
|
+
descartados: import_zod30.z.number().int(),
|
|
3752
|
+
movedTo: import_zod30.z.string().optional()
|
|
3753
|
+
}),
|
|
3754
|
+
import_zod30.z.object({
|
|
3755
|
+
ok: import_zod30.z.literal(false),
|
|
3756
|
+
error: import_zod30.z.string(),
|
|
3757
|
+
code: import_zod30.z.enum(["ACCION_CONTENIDO_EXISTENTE_REQUIRED", "MOVE_TARGET_OCCUPIED", "MOVE_TARGET_INVALID"]).optional(),
|
|
3758
|
+
opciones: import_zod30.z.array(import_zod30.z.string()).optional()
|
|
3759
|
+
})
|
|
3760
|
+
]);
|
|
3761
|
+
var rawContract2 = {
|
|
3762
|
+
name: "update_calendar_slot",
|
|
3763
|
+
description: "Modifica o agrega un slot del calendario editorial. Si slotIndex >= items.length agrega un slot nuevo. Si el slot ten\xEDa contenidoRef previo y los cambios tocan campos sem\xE1nticos (keyword/tema/plataforma/tipo), exige accionContenidoExistente con 4 opciones: descartar | mover:semana:N:slot:M | nuevo_slot | mantener.",
|
|
3764
|
+
paramsSchema: ParamsSchema2,
|
|
3765
|
+
outputSchema: OutputSchema2,
|
|
3766
|
+
requiresConfirmation: false,
|
|
3767
|
+
destructive: false,
|
|
3768
|
+
// Mutación, pero reversible (puede deshacerse cambiando el slot).
|
|
3769
|
+
affectsPublication: false,
|
|
3770
|
+
affectsExternal: false,
|
|
3771
|
+
martinSummaryTemplate: (input, output, locale) => {
|
|
3772
|
+
if (!output.ok) {
|
|
3773
|
+
if (locale === "en") return `I couldn't update the slot: ${output.error}`;
|
|
3774
|
+
return `No pude actualizar el slot: ${output.error}`;
|
|
3775
|
+
}
|
|
3776
|
+
const verb = {
|
|
3777
|
+
added: locale === "en" ? "added" : "agregu\xE9",
|
|
3778
|
+
updated: locale === "en" ? "updated" : "actualic\xE9",
|
|
3779
|
+
nuevo_slot: locale === "en" ? "created a new slot" : "cre\xE9 un slot nuevo",
|
|
3780
|
+
moved: locale === "en" ? "moved" : "mov\xED"
|
|
3781
|
+
}[output.action];
|
|
3782
|
+
if (locale === "en") {
|
|
3783
|
+
return `I ${verb} the slot in week ${input.semana} (${input.mes}).`;
|
|
3784
|
+
}
|
|
3785
|
+
return `${verb[0].toUpperCase() + verb.slice(1)} el slot en la semana ${input.semana} (${input.mes}).`;
|
|
3786
|
+
},
|
|
3787
|
+
auditAction: "marketing.calendario.slot.actualizar",
|
|
3788
|
+
extractTargetPath: (input) => `tenants/${input.tenantId}/marketing_calendario/(brand=${input.brandId},mes=${input.mes}).semana=${input.semana}.slot=${input.slotIndex}`,
|
|
3789
|
+
extractChanges: (input, output) => ({
|
|
3790
|
+
before: null,
|
|
3791
|
+
// Helper no devuelve estado previo del slot — captura el cambio aplicado.
|
|
3792
|
+
after: output.ok ? {
|
|
3793
|
+
action: output.action,
|
|
3794
|
+
cambios: input.cambios,
|
|
3795
|
+
accionContenidoExistente: input.accionContenidoExistente ?? null,
|
|
3796
|
+
descartados: output.descartados,
|
|
3797
|
+
movedTo: output.movedTo ?? null
|
|
3798
|
+
} : null
|
|
3799
|
+
}),
|
|
3800
|
+
quotasConsumed: [],
|
|
3801
|
+
requiredRoles: ["admin", "encargado"],
|
|
3802
|
+
sideEffects: ["writes_firestore", "updates_calendar_slot"]
|
|
3803
|
+
};
|
|
3804
|
+
var calendarSlotUpdaterContract = MartinContractSchema.parse(
|
|
3805
|
+
rawContract2
|
|
3806
|
+
);
|
|
3807
|
+
async function getCalendar(input) {
|
|
3808
|
+
const { db, tenantId, brandId, mes } = input;
|
|
3809
|
+
const snap = await db.collection("tenants").doc(tenantId).collection("marketing_calendario").where("brandId", "==", brandId).where("mes", "==", mes).limit(1).get();
|
|
3810
|
+
if (snap.empty) {
|
|
3811
|
+
return {
|
|
3812
|
+
ok: true,
|
|
3813
|
+
mes,
|
|
3814
|
+
brandId,
|
|
3815
|
+
calendario: null,
|
|
3816
|
+
mensaje: "No hay calendario para este mes. Genera un plan de marketing primero."
|
|
3817
|
+
};
|
|
3818
|
+
}
|
|
3819
|
+
const doc = snap.docs[0];
|
|
3820
|
+
return {
|
|
3821
|
+
ok: true,
|
|
3822
|
+
mes,
|
|
3823
|
+
brandId,
|
|
3824
|
+
calendario: { id: doc.id, ...doc.data() }
|
|
3825
|
+
};
|
|
3826
|
+
}
|
|
3827
|
+
var ParamsSchema3 = import_zod31.z.object({
|
|
3828
|
+
tenantId: import_zod31.z.string().min(1),
|
|
3829
|
+
brandId: import_zod31.z.string().min(1),
|
|
3830
|
+
mes: import_zod31.z.string().regex(/^\d{4}-\d{2}$/, "Formato YYYY-MM")
|
|
3831
|
+
});
|
|
3832
|
+
var OutputSchema3 = import_zod31.z.object({
|
|
3833
|
+
ok: import_zod31.z.literal(true),
|
|
3834
|
+
mes: import_zod31.z.string(),
|
|
3835
|
+
brandId: import_zod31.z.string(),
|
|
3836
|
+
calendario: import_zod31.z.record(import_zod31.z.string(), import_zod31.z.unknown()).nullable(),
|
|
3837
|
+
mensaje: import_zod31.z.string().optional()
|
|
3838
|
+
});
|
|
3839
|
+
var rawContract3 = {
|
|
3840
|
+
name: "get_calendar",
|
|
3841
|
+
description: "Lee el calendario editorial del mes para una brand. Retorna semanas con items planificados por plataforma.",
|
|
3842
|
+
paramsSchema: ParamsSchema3,
|
|
3843
|
+
outputSchema: OutputSchema3,
|
|
3844
|
+
requiresConfirmation: false,
|
|
3845
|
+
destructive: false,
|
|
3846
|
+
affectsPublication: false,
|
|
3847
|
+
affectsExternal: false,
|
|
3848
|
+
martinSummaryTemplate: (input, output, locale) => {
|
|
3849
|
+
if (!output.calendario) {
|
|
3850
|
+
if (locale === "en") return `No calendar for ${output.mes}.`;
|
|
3851
|
+
return `No hay calendario para ${output.mes}.`;
|
|
3852
|
+
}
|
|
3853
|
+
if (locale === "en") return `Here's the ${output.mes} calendar for ${input.brandId}.`;
|
|
3854
|
+
return `Aqu\xED est\xE1 el calendario de ${output.mes} para ${input.brandId}.`;
|
|
3855
|
+
},
|
|
3856
|
+
// AUDITA SIEMPRE — regla A5. Lectura no muta, changes son null/null.
|
|
3857
|
+
auditAction: "marketing.calendario.leer",
|
|
3858
|
+
quotasConsumed: [],
|
|
3859
|
+
requiredRoles: ["admin", "encargado", "empleado"],
|
|
3860
|
+
sideEffects: ["reads_firestore"]
|
|
3861
|
+
};
|
|
3862
|
+
var getCalendarContract = MartinContractSchema.parse(
|
|
3863
|
+
rawContract3
|
|
3864
|
+
);
|
|
2952
3865
|
var PLATAFORMA_A_FORMATO = {
|
|
2953
3866
|
gbp: "gbp_4_3",
|
|
2954
3867
|
shopify_blog: "blog_3_2",
|
|
@@ -2957,7 +3870,7 @@ var PLATAFORMA_A_FORMATO = {
|
|
|
2957
3870
|
};
|
|
2958
3871
|
async function photoAssigner(input) {
|
|
2959
3872
|
const { db, tenantId, brandId, contenidoRef, fotoId, calendarioItemRef } = input;
|
|
2960
|
-
const contenidoRefDoc = db.
|
|
3873
|
+
const contenidoRefDoc = db.doc(`tenants/${tenantId}/marketing_contenido/${contenidoRef}`);
|
|
2961
3874
|
const contenidoSnap = await contenidoRefDoc.get();
|
|
2962
3875
|
if (!contenidoSnap.exists) {
|
|
2963
3876
|
return { ok: false, error: `Contenido ${contenidoRef} no encontrado` };
|
|
@@ -2966,7 +3879,7 @@ async function photoAssigner(input) {
|
|
|
2966
3879
|
if (contenido.tenantId !== tenantId) {
|
|
2967
3880
|
return { ok: false, error: "Contenido no pertenece a este tenant" };
|
|
2968
3881
|
}
|
|
2969
|
-
const fotoQuery = await db.collection("
|
|
3882
|
+
const fotoQuery = await db.collection("tenants").doc(tenantId).collection("marketing_fotos").where("id", "==", fotoId).limit(1).get();
|
|
2970
3883
|
if (fotoQuery.empty) {
|
|
2971
3884
|
return { ok: false, error: `Foto ${fotoId} no encontrada` };
|
|
2972
3885
|
}
|
|
@@ -2998,7 +3911,7 @@ async function photoAssigner(input) {
|
|
|
2998
3911
|
if (match) {
|
|
2999
3912
|
const semanaNum = parseInt(match[1], 10);
|
|
3000
3913
|
const slotIdx = parseInt(match[2], 10);
|
|
3001
|
-
const calQuery = await db.collection("
|
|
3914
|
+
const calQuery = await db.collection("tenants").doc(tenantId).collection("marketing_calendario").where("brandId", "==", brandId).limit(12).get();
|
|
3002
3915
|
for (const calDoc of calQuery.docs) {
|
|
3003
3916
|
const cal = calDoc.data();
|
|
3004
3917
|
const semanas = cal.semanas ?? [];
|
|
@@ -3055,101 +3968,87 @@ async function photoAssigner(input) {
|
|
|
3055
3968
|
formato
|
|
3056
3969
|
};
|
|
3057
3970
|
}
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
const config = configSnap.data();
|
|
3061
|
-
const brands = config?.brands ?? {};
|
|
3062
|
-
const brand = brands[brandId];
|
|
3063
|
-
const canalId = brand?.canalId;
|
|
3064
|
-
if (!canalId) return null;
|
|
3065
|
-
const canalSnap = await db.collection("canales_venta").doc(canalId).get();
|
|
3066
|
-
const canal = canalSnap.data();
|
|
3067
|
-
const app = canal?.app ?? {};
|
|
3068
|
-
return app.lastImportId ?? null;
|
|
3971
|
+
function findPageByHeuristic(pages, pattern) {
|
|
3972
|
+
return pages.find((p) => pattern.test(p.title || "")) || pages.find((p) => pattern.test(p.handle || "")) || null;
|
|
3069
3973
|
}
|
|
3974
|
+
var ABOUT_PATTERN = /about|nosotros|sobre|quien|historia|story|filosof|esencia/i;
|
|
3975
|
+
var FAQ_PATTERN = /faq|preguntas|frecuent|frequent|questions|ayuda|soporte/i;
|
|
3070
3976
|
async function brandBriefBuilder(input) {
|
|
3071
3977
|
const { db, tenantId, brandId } = input;
|
|
3072
|
-
const configSnap = await db.collection("
|
|
3978
|
+
const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
|
|
3073
3979
|
if (!configSnap.exists) {
|
|
3074
|
-
return { ok: false, error: "No hay marketing_config" };
|
|
3075
|
-
}
|
|
3076
|
-
const config = configSnap.data();
|
|
3077
|
-
const brands = config.brands ?? {};
|
|
3078
|
-
const brand = brands[brandId];
|
|
3079
|
-
if (!brand) {
|
|
3080
3980
|
return { ok: false, error: `Brand "${brandId}" no encontrada` };
|
|
3081
3981
|
}
|
|
3082
|
-
const
|
|
3083
|
-
|
|
3982
|
+
const brand = configSnap.data();
|
|
3983
|
+
const [snapshotMetaSnap, productsSnap, collectionsSnap, pagesSnap, siteMetaSnap, tenantSnap] = await Promise.all([
|
|
3984
|
+
db.doc(`tenants/${tenantId}/marketing_snapshots/${brandId}`).get().catch(() => null),
|
|
3985
|
+
db.collection(`tenants/${tenantId}/marketing_snapshots/${brandId}/products`).get().catch(() => null),
|
|
3986
|
+
db.collection(`tenants/${tenantId}/marketing_snapshots/${brandId}/collections`).get().catch(() => null),
|
|
3987
|
+
db.collection(`tenants/${tenantId}/marketing_snapshots/${brandId}/pages`).get().catch(() => null),
|
|
3988
|
+
db.doc(`tenants/${tenantId}/marketing_site_meta/${brandId}`).get().catch(() => null),
|
|
3084
3989
|
db.collection("tenants").doc(tenantId).get()
|
|
3085
3990
|
]);
|
|
3086
3991
|
const tenantDoc = tenantSnap.data();
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
const
|
|
3101
|
-
const
|
|
3102
|
-
const
|
|
3103
|
-
|
|
3992
|
+
const snapshotMeta = snapshotMetaSnap?.exists ? snapshotMetaSnap.data() : null;
|
|
3993
|
+
const lastImportId = snapshotMeta?.lastImportId || null;
|
|
3994
|
+
const shopInfo = snapshotMeta?.shopInfo || null;
|
|
3995
|
+
const ordersSummary = snapshotMeta?.ordersSummary || null;
|
|
3996
|
+
const shopOrders = ordersSummary ? {
|
|
3997
|
+
totalOrders: ordersSummary.totalOrders,
|
|
3998
|
+
averageOrderValue: ordersSummary.avgOrderValue,
|
|
3999
|
+
ordersPerDay: ordersSummary.ordersPerDay,
|
|
4000
|
+
totalRevenue: ordersSummary.totalRevenue,
|
|
4001
|
+
topSkus: ordersSummary.topSkus,
|
|
4002
|
+
dateRange: ordersSummary.dateRange
|
|
4003
|
+
} : null;
|
|
4004
|
+
const shopProducts = (productsSnap?.docs || []).map((doc) => {
|
|
4005
|
+
const p = doc.data();
|
|
4006
|
+
const firstVariant = p.variants?.[0];
|
|
4007
|
+
const firstVariantPrice = firstVariant?.price?.amount;
|
|
4008
|
+
return {
|
|
3104
4009
|
title: p.title,
|
|
3105
|
-
product_type: p.
|
|
3106
|
-
tags: p.tags,
|
|
3107
|
-
price_min: p.
|
|
3108
|
-
price_max:
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
4010
|
+
product_type: p.productType,
|
|
4011
|
+
tags: Array.isArray(p.tags) ? p.tags.join(", ") : p.tags,
|
|
4012
|
+
price_min: p.price?.amount ?? firstVariantPrice ?? null,
|
|
4013
|
+
price_max: null,
|
|
4014
|
+
// canonical no tiene price_max separado, derivable de variants si necesita
|
|
4015
|
+
status: p.available !== false ? "active" : "draft"
|
|
4016
|
+
};
|
|
4017
|
+
});
|
|
4018
|
+
const shopCollections = (collectionsSnap?.docs || []).map((doc) => {
|
|
4019
|
+
const c = doc.data();
|
|
4020
|
+
return {
|
|
4021
|
+
id: c.platformId,
|
|
3113
4022
|
title: c.title,
|
|
3114
4023
|
handle: c.handle,
|
|
3115
|
-
products_count:
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
const faqEn = sitePages["/pages/faq"] || {};
|
|
3144
|
-
const faqEs = sitePages["/pages/preguntas-frecuentes"] || {};
|
|
3145
|
-
const sitePayload = siteContentDoc ? {
|
|
3146
|
-
homeTitle: home.title || null,
|
|
3147
|
-
homeMeta: home.metaDescription || null,
|
|
3148
|
-
homeH1: home.h1 || null,
|
|
3149
|
-
homeOgDescription: home.ogDescription || null,
|
|
3150
|
-
aboutFirstParagraph: aboutEn.firstParagraph || aboutEs.firstParagraph || null,
|
|
3151
|
-
aboutH1: aboutEn.h1 || aboutEs.h1 || null,
|
|
3152
|
-
faqQuestions: faqEn.h2s || faqEs.h2s || null
|
|
4024
|
+
products_count: null,
|
|
4025
|
+
// adapter v2 no calcula products_count por collection todavia
|
|
4026
|
+
collectionType: "canonical"
|
|
4027
|
+
};
|
|
4028
|
+
});
|
|
4029
|
+
const siteMetaDoc = siteMetaSnap?.exists ? siteMetaSnap.data() : null;
|
|
4030
|
+
const homepage = siteMetaDoc?.homepage || null;
|
|
4031
|
+
const extraPagesMeta = siteMetaDoc?.extraPages || {};
|
|
4032
|
+
const allPagesCanonical = (pagesSnap?.docs || []).map((doc) => {
|
|
4033
|
+
const p = doc.data();
|
|
4034
|
+
return {
|
|
4035
|
+
handle: p.handle || "",
|
|
4036
|
+
title: p.title || "",
|
|
4037
|
+
firstParagraph: p.firstParagraph || null,
|
|
4038
|
+
h1: p.extractedHeadings?.h1 || null,
|
|
4039
|
+
h2s: p.extractedHeadings?.h2s || []
|
|
4040
|
+
};
|
|
4041
|
+
});
|
|
4042
|
+
const aboutPage = findPageByHeuristic(allPagesCanonical, ABOUT_PATTERN);
|
|
4043
|
+
const faqPage = findPageByHeuristic(allPagesCanonical, FAQ_PATTERN);
|
|
4044
|
+
const sitePayload = siteMetaDoc || homepage ? {
|
|
4045
|
+
homeTitle: homepage?.title || null,
|
|
4046
|
+
homeMeta: homepage?.metaDescription || null,
|
|
4047
|
+
homeH1: homepage?.h1 || null,
|
|
4048
|
+
homeOgDescription: homepage?.ogDescription || null,
|
|
4049
|
+
aboutFirstParagraph: aboutPage?.firstParagraph || null,
|
|
4050
|
+
aboutH1: aboutPage?.h1 || null,
|
|
4051
|
+
faqQuestions: faqPage?.h2s?.length ? faqPage.h2s : null
|
|
3153
4052
|
} : null;
|
|
3154
4053
|
const seo = brand.seoSnapshot;
|
|
3155
4054
|
const gbpPerfiles = brand.gbpPerfiles ?? [];
|
|
@@ -3182,14 +4081,20 @@ async function brandBriefBuilder(input) {
|
|
|
3182
4081
|
}
|
|
3183
4082
|
const topTags = Object.entries(tagCount).sort(([, a], [, b]) => b - a).slice(0, 20).map(([tag, count]) => ({ tag, count }));
|
|
3184
4083
|
const instruccionBase = `Genera un brandBrief completo para "${brand.nombre ?? brandId}". Pre-llena TODOS los campos bas\xE1ndote en los datos a continuaci\xF3n. El 80% debe ser inferido autom\xE1ticamente. Marca lo que el tenant debe validar. Guarda con save_brand_brief.`;
|
|
3185
|
-
const instruccionSiteContent = sitePayload ? " Tienes site_content REAL scrapeado del sitio en datosDisponibles.sitePayload. Usa: homeMeta y homeOgDescription para descripcionCorta; aboutFirstParagraph para propuestaDeValor (texto real, no inventes); faqQuestions para inferir mensajesClave y dudas frecuentes; aboutH1 para inferir tono y posicionamiento. Si algun campo es null, infierelo de Shopify + GBP." : ' No hay site_content disponible (sitePayload es null). Recomienda al tenant correr "Sincronizar Shopify" con modo full para mejor calidad del brief.';
|
|
4084
|
+
const instruccionSiteContent = sitePayload ? " Tienes site_content REAL scrapeado del sitio en datosDisponibles.sitePayload. Usa: homeMeta y homeOgDescription para descripcionCorta; aboutFirstParagraph para propuestaDeValor (texto real, no inventes); faqQuestions para inferir mensajesClave y dudas frecuentes; aboutH1 para inferir tono y posicionamiento. Si algun campo es null, intenta identificar la pagina correcta en datosDisponibles.availablePages (titles + handles) y usa ese contenido \u2014 la heuristica regex puede haber fallado para un tenant con nombres creativos. Si igual no aparece, infierelo de Shopify + GBP." : ' No hay site_content disponible (sitePayload es null). Recomienda al tenant correr "Sincronizar Shopify" con modo full para mejor calidad del brief.';
|
|
4085
|
+
const availablePages = allPagesCanonical.map((p) => ({
|
|
4086
|
+
handle: p.handle,
|
|
4087
|
+
title: p.title,
|
|
4088
|
+
h1: p.h1
|
|
4089
|
+
}));
|
|
3186
4090
|
const payload = {
|
|
3187
4091
|
instruccion: instruccionBase + instruccionSiteContent,
|
|
3188
|
-
|
|
4092
|
+
_lastImportId: lastImportId,
|
|
3189
4093
|
_schema: BRAND_BRIEF_SCHEMA_HINT,
|
|
3190
4094
|
datosDisponibles: {
|
|
3191
4095
|
shopInfo,
|
|
3192
4096
|
sitePayload,
|
|
4097
|
+
availablePages,
|
|
3193
4098
|
precioStats,
|
|
3194
4099
|
productTypes: typeCount,
|
|
3195
4100
|
topTags,
|
|
@@ -3250,11 +4155,51 @@ var BRAND_BRIEF_SCHEMA_HINT = {
|
|
|
3250
4155
|
escenas: [{ id: string, nombre: string, promptHint: string }]
|
|
3251
4156
|
}`
|
|
3252
4157
|
};
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
4158
|
+
var ParamsSchema4 = import_zod32.z.object({
|
|
4159
|
+
tenantId: import_zod32.z.string().min(1),
|
|
4160
|
+
brandId: import_zod32.z.string().min(1)
|
|
4161
|
+
});
|
|
4162
|
+
var OutputSchema4 = import_zod32.z.discriminatedUnion("ok", [
|
|
4163
|
+
import_zod32.z.object({
|
|
4164
|
+
ok: import_zod32.z.literal(true),
|
|
4165
|
+
/** Payload con instrucción + datos del negocio para que Claude genere el brief. */
|
|
4166
|
+
payload: import_zod32.z.record(import_zod32.z.string(), import_zod32.z.unknown())
|
|
4167
|
+
}),
|
|
4168
|
+
import_zod32.z.object({
|
|
4169
|
+
ok: import_zod32.z.literal(false),
|
|
4170
|
+
error: import_zod32.z.string()
|
|
4171
|
+
})
|
|
4172
|
+
]);
|
|
4173
|
+
var rawContract4 = {
|
|
4174
|
+
name: "generate_brand_brief",
|
|
4175
|
+
description: "Prepara los datos del negocio (Shopify, SEO, GBP, site_content scrape, brand config, ubicaciones del tenant) para que el sistema genere un Brand Brief pre-llenado. NO escribe el brief \u2014 s\xF3lo arma el payload.",
|
|
4176
|
+
paramsSchema: ParamsSchema4,
|
|
4177
|
+
outputSchema: OutputSchema4,
|
|
4178
|
+
// Lectura pura, sin side effects de escritura ni publicación.
|
|
4179
|
+
requiresConfirmation: false,
|
|
4180
|
+
destructive: false,
|
|
4181
|
+
affectsPublication: false,
|
|
4182
|
+
affectsExternal: false,
|
|
4183
|
+
martinSummaryTemplate: (_input, output, locale) => {
|
|
4184
|
+
if (!output.ok) {
|
|
4185
|
+
if (locale === "en") return `I couldn't gather the data: ${output.error}`;
|
|
4186
|
+
return `No pude reunir los datos: ${output.error}`;
|
|
4187
|
+
}
|
|
4188
|
+
if (locale === "en") return `I gathered the data for the brief.`;
|
|
4189
|
+
return `Reun\xED los datos para el brief.`;
|
|
4190
|
+
},
|
|
4191
|
+
auditAction: "marketing.brand_brief.preparar",
|
|
4192
|
+
// No extractTargetPath/extractChanges — no es writer.
|
|
4193
|
+
quotasConsumed: [],
|
|
4194
|
+
requiredRoles: ["admin", "encargado"],
|
|
4195
|
+
sideEffects: ["reads_firestore"]
|
|
4196
|
+
};
|
|
4197
|
+
var brandBriefBuilderContract = MartinContractSchema.parse(
|
|
4198
|
+
rawContract4
|
|
4199
|
+
);
|
|
4200
|
+
async function resolveLastImportId(db, tenantId, brandId) {
|
|
4201
|
+
const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
|
|
4202
|
+
const brand = configSnap.data();
|
|
3258
4203
|
const canalId = brand?.canalId;
|
|
3259
4204
|
if (!canalId) return null;
|
|
3260
4205
|
const canalSnap = await db.collection("canales_venta").doc(canalId).get();
|
|
@@ -3264,16 +4209,11 @@ async function resolveLastImportId2(db, tenantId, brandId) {
|
|
|
3264
4209
|
}
|
|
3265
4210
|
async function marketingPlanBuilder(input) {
|
|
3266
4211
|
const { db, tenantId, brandId } = input;
|
|
3267
|
-
const configSnap = await db.collection("
|
|
4212
|
+
const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
|
|
3268
4213
|
if (!configSnap.exists) {
|
|
3269
|
-
return { ok: false, error: "No hay marketing_config" };
|
|
3270
|
-
}
|
|
3271
|
-
const config = configSnap.data();
|
|
3272
|
-
const brands = config.brands ?? {};
|
|
3273
|
-
const brand = brands[brandId];
|
|
3274
|
-
if (!brand) {
|
|
3275
4214
|
return { ok: false, error: `Brand "${brandId}" no encontrada` };
|
|
3276
4215
|
}
|
|
4216
|
+
const brand = configSnap.data();
|
|
3277
4217
|
if (!brand.seoSnapshot) {
|
|
3278
4218
|
return {
|
|
3279
4219
|
ok: false,
|
|
@@ -3282,9 +4222,9 @@ async function marketingPlanBuilder(input) {
|
|
|
3282
4222
|
}
|
|
3283
4223
|
const productosQ = await db.collection("productos").where("tenantId", "==", tenantId).limit(100).get();
|
|
3284
4224
|
const productos = productosQ.docs.map((d) => d.data());
|
|
3285
|
-
const histQ = await db.collection("
|
|
4225
|
+
const histQ = await db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).where("estado", "==", ESTADO_CONTENIDO.PUBLICADO).limit(20).get();
|
|
3286
4226
|
const historial = histQ.docs.map((d) => d.data());
|
|
3287
|
-
const lastImportId = await
|
|
4227
|
+
const lastImportId = await resolveLastImportId(db, tenantId, brandId);
|
|
3288
4228
|
let colecciones = [];
|
|
3289
4229
|
let shopifyBlogs = [];
|
|
3290
4230
|
if (lastImportId) {
|
|
@@ -3459,13 +4399,11 @@ async function contenidoWriter(input) {
|
|
|
3459
4399
|
calendarioItemRef,
|
|
3460
4400
|
linkPipeline
|
|
3461
4401
|
} = input;
|
|
3462
|
-
const configSnap = await db.collection("
|
|
3463
|
-
const
|
|
3464
|
-
const brands = config?.brands ?? {};
|
|
3465
|
-
const brand = brands[brandId];
|
|
4402
|
+
const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
|
|
4403
|
+
const brand = configSnap.exists ? configSnap.data() : void 0;
|
|
3466
4404
|
const frecuencia = brand?.frecuencia ?? {};
|
|
3467
4405
|
const estadosQueOcupanSlot = ["aprobado", "publicado"];
|
|
3468
|
-
const contenidoQ = await db.collection("
|
|
4406
|
+
const contenidoQ = await db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).where("plataforma", "==", plataforma).limit(100).get();
|
|
3469
4407
|
const contenidoEstaSemana = contenidoQ.docs.map((d2) => ({ id: d2.id, ...d2.data() }));
|
|
3470
4408
|
const now = /* @__PURE__ */ new Date();
|
|
3471
4409
|
const startOfWeek = new Date(now);
|
|
@@ -3498,7 +4436,7 @@ async function contenidoWriter(input) {
|
|
|
3498
4436
|
(c) => c.calendarioItemRef === calendarioItemRef && c.estado !== "descartado"
|
|
3499
4437
|
);
|
|
3500
4438
|
for (const existente of existentes) {
|
|
3501
|
-
await db.
|
|
4439
|
+
await db.doc(`tenants/${tenantId}/marketing_contenido/${existente.id}`).update({
|
|
3502
4440
|
estado: "descartado",
|
|
3503
4441
|
rechazadoMotivo: "Reemplazado por contenido nuevo para el mismo slot",
|
|
3504
4442
|
rechazadoAt: import_firebase_admin8.firestore.FieldValue.serverTimestamp()
|
|
@@ -3542,6 +4480,11 @@ async function contenidoWriter(input) {
|
|
|
3542
4480
|
}
|
|
3543
4481
|
}
|
|
3544
4482
|
const id = buildGenId();
|
|
4483
|
+
if (!languageCode) {
|
|
4484
|
+
throw new Error(
|
|
4485
|
+
`contenidoWriter requiere languageCode expl\xEDcito. Resolver desde brand.idiomas.primario o tenant.idiomaPrincipal antes de invocar.`
|
|
4486
|
+
);
|
|
4487
|
+
}
|
|
3545
4488
|
const contenido = buildContenido({
|
|
3546
4489
|
id,
|
|
3547
4490
|
tenantId,
|
|
@@ -3549,7 +4492,7 @@ async function contenidoWriter(input) {
|
|
|
3549
4492
|
plataforma,
|
|
3550
4493
|
tipo: tipo ?? "post",
|
|
3551
4494
|
keyword: keyword ?? null,
|
|
3552
|
-
languageCode
|
|
4495
|
+
languageCode,
|
|
3553
4496
|
estado: ESTADO_CONTENIDO.PENDIENTE_APROBACION,
|
|
3554
4497
|
fotoId: fotoId ?? null,
|
|
3555
4498
|
mediaUrl: null,
|
|
@@ -3559,14 +4502,14 @@ async function contenidoWriter(input) {
|
|
|
3559
4502
|
creadoPorId: "mcp-cowork",
|
|
3560
4503
|
origen: "ai_assisted"
|
|
3561
4504
|
});
|
|
3562
|
-
const contenidoRef = db.
|
|
4505
|
+
const contenidoRef = db.doc(`tenants/${tenantId}/marketing_contenido/${id}`);
|
|
3563
4506
|
let slotResolved = null;
|
|
3564
4507
|
if (calendarioItemRef) {
|
|
3565
4508
|
const match = calendarioItemRef.match(/^semana:(\d+):slot:(\d+)$/);
|
|
3566
4509
|
if (match) {
|
|
3567
4510
|
const semanaNum = parseInt(match[1], 10);
|
|
3568
4511
|
const slotIdx = parseInt(match[2], 10);
|
|
3569
|
-
const calQ = await db.collection("
|
|
4512
|
+
const calQ = await db.collection("tenants").doc(tenantId).collection("marketing_calendario").where("brandId", "==", brandId).limit(12).get();
|
|
3570
4513
|
for (const calDoc of calQ.docs) {
|
|
3571
4514
|
const cal = calDoc.data();
|
|
3572
4515
|
const semanas = cal.semanas ?? [];
|
|
@@ -3715,14 +4658,12 @@ async function weeklyContentBuilder(input) {
|
|
|
3715
4658
|
const now = /* @__PURE__ */ new Date();
|
|
3716
4659
|
const targetMes = now.toISOString().slice(0, 7);
|
|
3717
4660
|
const targetSemana = semana ?? Math.ceil(now.getDate() / 7);
|
|
3718
|
-
const configSnap = await db.collection("
|
|
4661
|
+
const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
|
|
3719
4662
|
if (!configSnap.exists) {
|
|
3720
|
-
return { ok: false, error: "
|
|
4663
|
+
return { ok: false, error: `Brand "${brandId}" no encontrada` };
|
|
3721
4664
|
}
|
|
3722
|
-
const
|
|
3723
|
-
const
|
|
3724
|
-
const brand = brands[brandId];
|
|
3725
|
-
const calQuery = await db.collection("marketing_calendario").where("tenantId", "==", tenantId).where("brandId", "==", brandId).where("mes", "==", targetMes).limit(1).get();
|
|
4665
|
+
const brand = configSnap.data();
|
|
4666
|
+
const calQuery = await db.collection("tenants").doc(tenantId).collection("marketing_calendario").where("brandId", "==", brandId).where("mes", "==", targetMes).limit(1).get();
|
|
3726
4667
|
let calendario = calQuery.empty ? null : { id: calQuery.docs[0].id, ...calQuery.docs[0].data() };
|
|
3727
4668
|
if (!calendario) {
|
|
3728
4669
|
const calId = `${tenantId}_${brandId}_${targetMes}`;
|
|
@@ -3737,14 +4678,14 @@ async function weeklyContentBuilder(input) {
|
|
|
3737
4678
|
creadoPorId: "mcp-cowork",
|
|
3738
4679
|
updatedAt: null
|
|
3739
4680
|
};
|
|
3740
|
-
await db.
|
|
4681
|
+
await db.doc(`tenants/${tenantId}/marketing_calendario/${calId}`).set(calDoc);
|
|
3741
4682
|
calendario = { ...calDoc, semanas };
|
|
3742
4683
|
}
|
|
3743
4684
|
const semanasArr = calendario.semanas ?? [];
|
|
3744
4685
|
const semanaData = semanasArr[targetSemana - 1] ?? null;
|
|
3745
4686
|
const slotsExistentes = semanaData?.items ?? [];
|
|
3746
4687
|
if (targetModo === "planificar") {
|
|
3747
|
-
const histQ2 = await db.collection("
|
|
4688
|
+
const histQ2 = await db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).limit(20).get();
|
|
3748
4689
|
const historial2 = histQ2.docs.map((d) => d.data());
|
|
3749
4690
|
return {
|
|
3750
4691
|
ok: true,
|
|
@@ -3802,9 +4743,9 @@ async function weeklyContentBuilder(input) {
|
|
|
3802
4743
|
}
|
|
3803
4744
|
};
|
|
3804
4745
|
}
|
|
3805
|
-
const fotosQ = await db.collection("
|
|
4746
|
+
const fotosQ = await db.collection("tenants").doc(tenantId).collection("marketing_fotos").where("brandId", "==", brandId).where("estado", "==", ESTADO_FOTO.EDITADA).limit(20).get();
|
|
3806
4747
|
const fotosDisponibles = fotosQ.docs.map((d) => d.data());
|
|
3807
|
-
const histQ = await db.collection("
|
|
4748
|
+
const histQ = await db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).limit(20).get();
|
|
3808
4749
|
const historial = histQ.docs.map((d) => d.data());
|
|
3809
4750
|
const hasBlogSlots = slotsParaGenerar.some((s) => s.plataforma === "shopify_blog");
|
|
3810
4751
|
let blogJIT = null;
|
|
@@ -3812,7 +4753,7 @@ async function weeklyContentBuilder(input) {
|
|
|
3812
4753
|
const blogStrategy = brand.blogStrategy;
|
|
3813
4754
|
const blogs = blogStrategy?.blogs ?? [];
|
|
3814
4755
|
const idiomas = brand.idiomas;
|
|
3815
|
-
const articulosQ = await db.collection("
|
|
4756
|
+
const articulosQ = await db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).where("plataforma", "==", "shopify_blog").where("estado", "==", ESTADO_CONTENIDO.PUBLICADO).limit(20).get();
|
|
3816
4757
|
const articulosExistentes = articulosQ.docs.map(
|
|
3817
4758
|
(d) => d.data()
|
|
3818
4759
|
);
|
|
@@ -3848,7 +4789,15 @@ async function weeklyContentBuilder(input) {
|
|
|
3848
4789
|
userId: blogStrategy?.defaultAuthorId ?? null,
|
|
3849
4790
|
name: blogStrategy?.defaultAuthorName ?? null
|
|
3850
4791
|
},
|
|
3851
|
-
languageCode:
|
|
4792
|
+
languageCode: (() => {
|
|
4793
|
+
const lc = idiomas?.primario;
|
|
4794
|
+
if (!lc) {
|
|
4795
|
+
throw new Error(
|
|
4796
|
+
`weeklyContentBuilder requiere brand.idiomas.primario. Configurar antes de generar weekly content.`
|
|
4797
|
+
);
|
|
4798
|
+
}
|
|
4799
|
+
return lc;
|
|
4800
|
+
})()
|
|
3852
4801
|
});
|
|
3853
4802
|
}
|
|
3854
4803
|
blogJIT = {
|
|
@@ -3981,10 +4930,8 @@ async function contentFinder(input) {
|
|
|
3981
4930
|
const textThreshold = deps.thresholds?.text ?? DEFAULT_TEXT_THRESHOLD;
|
|
3982
4931
|
const imageThreshold = deps.thresholds?.image ?? DEFAULT_IMAGE_THRESHOLD;
|
|
3983
4932
|
const conflictThreshold = deps.thresholds?.conflictSimilarity ?? DEFAULT_CONFLICT_THRESHOLD;
|
|
3984
|
-
const configSnap = await db.collection("
|
|
3985
|
-
const
|
|
3986
|
-
const brands = config?.brands ?? {};
|
|
3987
|
-
const brand = brands[brandId] ?? null;
|
|
4933
|
+
const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
|
|
4934
|
+
const brand = configSnap.exists ? configSnap.data() : null;
|
|
3988
4935
|
const brandBrief = brand?.brandBrief ?? null;
|
|
3989
4936
|
const visualRules = brandBrief?.visualRules ?? {};
|
|
3990
4937
|
const plan = brand?.plan ?? null;
|
|
@@ -4041,22 +4988,22 @@ async function contentFinder(input) {
|
|
|
4041
4988
|
const productsLimit = include.products ? collFilter ? limit.products * 4 : limit.products * 2 : 0;
|
|
4042
4989
|
const [productsRaw, collectionsRaw, articlesRaw, pagesRaw] = await Promise.all([
|
|
4043
4990
|
searchCollection(
|
|
4044
|
-
"
|
|
4991
|
+
"products",
|
|
4045
4992
|
include.products && limit.products > 0,
|
|
4046
4993
|
productsLimit
|
|
4047
4994
|
),
|
|
4048
4995
|
searchCollection(
|
|
4049
|
-
"
|
|
4996
|
+
"collections",
|
|
4050
4997
|
include.collections && limit.collections > 0,
|
|
4051
4998
|
limit.collections * 2
|
|
4052
4999
|
),
|
|
4053
5000
|
searchCollection(
|
|
4054
|
-
"
|
|
5001
|
+
"articles",
|
|
4055
5002
|
include.articles && limit.articles > 0,
|
|
4056
5003
|
limit.articles * 2
|
|
4057
5004
|
),
|
|
4058
5005
|
searchCollection(
|
|
4059
|
-
"
|
|
5006
|
+
"pages",
|
|
4060
5007
|
include.pages && limit.pages > 0,
|
|
4061
5008
|
limit.pages * 2
|
|
4062
5009
|
)
|
|
@@ -4161,16 +5108,11 @@ async function slotAssetFinder(input) {
|
|
|
4161
5108
|
const limit = input.limit ?? 5;
|
|
4162
5109
|
const photoThreshold = deps.thresholds?.photos ?? DEFAULT_PHOTO_THRESHOLD;
|
|
4163
5110
|
const shopifyThreshold = deps.thresholds?.shopify ?? DEFAULT_SHOPIFY_THRESHOLD;
|
|
4164
|
-
const configSnap = await db.collection("
|
|
5111
|
+
const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
|
|
4165
5112
|
if (!configSnap.exists) {
|
|
4166
|
-
return { ok: false, error: `Brand "${brandId}" no encontrada (no marketing_config)` };
|
|
4167
|
-
}
|
|
4168
|
-
const config = configSnap.data();
|
|
4169
|
-
const brands = config.brands ?? {};
|
|
4170
|
-
const brand = brands[brandId] ?? null;
|
|
4171
|
-
if (!brand) {
|
|
4172
5113
|
return { ok: false, error: `Brand "${brandId}" no encontrada` };
|
|
4173
5114
|
}
|
|
5115
|
+
const brand = configSnap.data();
|
|
4174
5116
|
const brandBrief = brand.brandBrief ?? null;
|
|
4175
5117
|
const visualRules = brandBrief?.visualRules ?? {};
|
|
4176
5118
|
const plan = brand.plan ?? null;
|
|
@@ -4196,7 +5138,7 @@ async function slotAssetFinder(input) {
|
|
|
4196
5138
|
try {
|
|
4197
5139
|
const fetchLimit = bloqueoProducto && temporada?.coleccion?.handle ? limit * 4 : limit;
|
|
4198
5140
|
const nearest = await deps.findNearestInCollection({
|
|
4199
|
-
collection: "
|
|
5141
|
+
collection: "products",
|
|
4200
5142
|
tenantId,
|
|
4201
5143
|
brandId,
|
|
4202
5144
|
queryEmbedding: queryEmbedding ?? void 0,
|
|
@@ -4298,16 +5240,11 @@ function cosineSimilarity(a, b) {
|
|
|
4298
5240
|
async function canvaTemplateSelector(input) {
|
|
4299
5241
|
const { db, tenantId, brandId, plataforma, tipoContenido, keyword, deps } = input;
|
|
4300
5242
|
const threshold = deps.threshold ?? DEFAULT_SIMILARITY_THRESHOLD;
|
|
4301
|
-
const configSnap = await db.collection("
|
|
5243
|
+
const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
|
|
4302
5244
|
if (!configSnap.exists) {
|
|
4303
5245
|
return { plantillaId: null, motivo: "brand_no_encontrada" };
|
|
4304
5246
|
}
|
|
4305
|
-
const
|
|
4306
|
-
const brands = config.brands ?? {};
|
|
4307
|
-
const brand = brands[brandId] ?? null;
|
|
4308
|
-
if (!brand) {
|
|
4309
|
-
return { plantillaId: null, motivo: "brand_no_encontrada" };
|
|
4310
|
-
}
|
|
5247
|
+
const brand = configSnap.data();
|
|
4311
5248
|
const canva = brand.canva ?? {};
|
|
4312
5249
|
if (!canva.connected || !Array.isArray(canva.templates) || canva.templates.length === 0) {
|
|
4313
5250
|
return {
|
|
@@ -4465,7 +5402,7 @@ var ERROR_MESSAGES = {
|
|
|
4465
5402
|
en: () => `External infrastructure failure. You can retry ONCE. If it persists, report the error and try another photo.`
|
|
4466
5403
|
}
|
|
4467
5404
|
};
|
|
4468
|
-
function instruccionesParaError(code, details, lang
|
|
5405
|
+
function instruccionesParaError(code, details, lang) {
|
|
4469
5406
|
const messages = ERROR_MESSAGES[code];
|
|
4470
5407
|
if (!messages) {
|
|
4471
5408
|
return lang === "en" ? `Unknown error. Report code=${code} and details to the tech team.` : `Error desconocido. Reporta el code=${code} y detalles al equipo tecnico.`;
|
|
@@ -4474,16 +5411,14 @@ function instruccionesParaError(code, details, lang = "es") {
|
|
|
4474
5411
|
}
|
|
4475
5412
|
async function photoDirectorPlan(input) {
|
|
4476
5413
|
const { db, tenantId, fotoId, deps } = input;
|
|
4477
|
-
const fotoQ = await db.collection("
|
|
5414
|
+
const fotoQ = await db.collection("tenants").doc(tenantId).collection("marketing_fotos").where("id", "==", fotoId).limit(1).get();
|
|
4478
5415
|
if (fotoQ.empty) {
|
|
4479
5416
|
return { ok: false, code: "FOTO_NOT_FOUND", error: `Foto ${fotoId} no encontrada` };
|
|
4480
5417
|
}
|
|
4481
5418
|
const foto = fotoQ.docs[0].data();
|
|
4482
5419
|
const brandId = foto.brandId;
|
|
4483
|
-
const configSnap = await db.collection("
|
|
4484
|
-
const
|
|
4485
|
-
const brands = config?.brands ?? {};
|
|
4486
|
-
const brand = brands[brandId] ?? null;
|
|
5420
|
+
const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
|
|
5421
|
+
const brand = configSnap.exists ? configSnap.data() : null;
|
|
4487
5422
|
const brandBrief = (brand?.brandBrief ?? null) || {};
|
|
4488
5423
|
const visualRules = brandBrief.visualRules ?? {};
|
|
4489
5424
|
const catalogoVisual = brandBrief.catalogoVisual;
|
|
@@ -4910,15 +5845,12 @@ Campos comunes de save_generated_content:
|
|
|
4910
5845
|
tipo: string (del slot)
|
|
4911
5846
|
`;
|
|
4912
5847
|
async function buildTenantContext(db, tenantId, brandId) {
|
|
4913
|
-
const configSnap = await db.collection("
|
|
5848
|
+
const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
|
|
4914
5849
|
if (!configSnap.exists) return "";
|
|
4915
|
-
const
|
|
4916
|
-
const brands = config.brands ?? {};
|
|
4917
|
-
const brand = brands[brandId];
|
|
4918
|
-
if (!brand) return "";
|
|
5850
|
+
const brand = configSnap.data();
|
|
4919
5851
|
const [fotosQ, contenidoQ, productosQ] = await Promise.all([
|
|
4920
|
-
db.collection("
|
|
4921
|
-
db.collection("
|
|
5852
|
+
db.collection("tenants").doc(tenantId).collection("marketing_fotos").where("brandId", "==", brandId).where("estado", "==", ESTADO_FOTO.EDITADA).limit(10).get(),
|
|
5853
|
+
db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).where("estado", "==", ESTADO_CONTENIDO.PUBLICADO).limit(10).get(),
|
|
4922
5854
|
db.collection("productos").where("tenantId", "==", tenantId).limit(50).get()
|
|
4923
5855
|
]);
|
|
4924
5856
|
const fotosEditadas = fotosQ.docs.map((d) => d.data());
|
|
@@ -4943,7 +5875,12 @@ Dominio: ${brand.dominio ?? "sin dominio"}
|
|
|
4943
5875
|
ctx += `Tono: ${plan?.tonoMarca ?? "profesional y cercano"}
|
|
4944
5876
|
`;
|
|
4945
5877
|
if (idiomas) {
|
|
4946
|
-
|
|
5878
|
+
if (!idiomas.primario) {
|
|
5879
|
+
throw new Error(
|
|
5880
|
+
`systemPromptBuilder requiere idiomas.primario. Brand sin idioma configurado \u2014 completar marketing_config.brands[bId].idiomas.`
|
|
5881
|
+
);
|
|
5882
|
+
}
|
|
5883
|
+
ctx += `Idioma principal: ${idiomas.primario}
|
|
4947
5884
|
`;
|
|
4948
5885
|
if (idiomas.distribucion) {
|
|
4949
5886
|
ctx += `Distribucion de idiomas: ${JSON.stringify(idiomas.distribucion)}
|
|
@@ -5122,10 +6059,8 @@ async function buildSystemPrompt(input) {
|
|
|
5122
6059
|
const p2 = PARTE_2_REGLAS.replace("{{SHAPES_BLOCK}}", shapesBlock);
|
|
5123
6060
|
return p1 + p2;
|
|
5124
6061
|
}
|
|
5125
|
-
const configSnap = await db.collection("
|
|
5126
|
-
const
|
|
5127
|
-
const brands = config?.brands ?? {};
|
|
5128
|
-
const brand = brands[brandId];
|
|
6062
|
+
const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
|
|
6063
|
+
const brand = configSnap.exists ? configSnap.data() : null;
|
|
5129
6064
|
let prompt = PARTE_1_IDENTIDAD.replace(
|
|
5130
6065
|
"{{brand_nombre}}",
|
|
5131
6066
|
brand?.nombre ?? brandId
|
|
@@ -5137,6 +6072,451 @@ async function buildSystemPrompt(input) {
|
|
|
5137
6072
|
}
|
|
5138
6073
|
return prompt;
|
|
5139
6074
|
}
|
|
6075
|
+
async function writeAuditLog(input) {
|
|
6076
|
+
try {
|
|
6077
|
+
const db = (0, import_firestore3.getFirestore)();
|
|
6078
|
+
const eventId = `evt_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
6079
|
+
const entryInput = {
|
|
6080
|
+
schemaVersion: 1,
|
|
6081
|
+
id: eventId,
|
|
6082
|
+
tenantId: input.tenantId,
|
|
6083
|
+
brandId: input.brandId ?? null,
|
|
6084
|
+
actor: input.actor,
|
|
6085
|
+
action: input.action,
|
|
6086
|
+
targetType: input.targetType ?? "",
|
|
6087
|
+
targetPath: input.targetPath ?? "",
|
|
6088
|
+
targetId: input.targetId ?? "",
|
|
6089
|
+
changes: input.changes ?? { before: null, after: null },
|
|
6090
|
+
motivo: input.motivo ?? null,
|
|
6091
|
+
conversacionId: input.conversacionId ?? null,
|
|
6092
|
+
status: input.status ?? "success",
|
|
6093
|
+
errorMessage: input.errorMessage ?? null,
|
|
6094
|
+
durationMs: input.durationMs ?? 0,
|
|
6095
|
+
timestamp: import_firestore3.FieldValue.serverTimestamp()
|
|
6096
|
+
};
|
|
6097
|
+
if (input.origenTecnico !== void 0) {
|
|
6098
|
+
entryInput.origenTecnico = input.origenTecnico;
|
|
6099
|
+
}
|
|
6100
|
+
const entry = buildAuditLogEntry(entryInput);
|
|
6101
|
+
await db.doc(`tenants/${input.tenantId}/audit_log/${eventId}`).set(entry);
|
|
6102
|
+
} catch (err) {
|
|
6103
|
+
console.error("[auditLog] Failed to write:", err, {
|
|
6104
|
+
action: input.action,
|
|
6105
|
+
tenantId: input.tenantId
|
|
6106
|
+
});
|
|
6107
|
+
}
|
|
6108
|
+
}
|
|
6109
|
+
async function getEffectiveLimits(tenantId) {
|
|
6110
|
+
const db = (0, import_firestore4.getFirestore)();
|
|
6111
|
+
const tenantSnap = await db.doc(`tenants/${tenantId}`).get();
|
|
6112
|
+
if (!tenantSnap.exists) {
|
|
6113
|
+
throw new Error(`Tenant ${tenantId} no existe`);
|
|
6114
|
+
}
|
|
6115
|
+
const tenant = tenantSnap.data();
|
|
6116
|
+
const membresiaId = tenant.membresiaId;
|
|
6117
|
+
if (!membresiaId) {
|
|
6118
|
+
throw new Error(`Tenant ${tenantId} no tiene membresiaId`);
|
|
6119
|
+
}
|
|
6120
|
+
const membresiaSnap = await db.doc(`membresias/${membresiaId}`).get();
|
|
6121
|
+
if (!membresiaSnap.exists) {
|
|
6122
|
+
throw new Error(`Membresia ${membresiaId} no existe`);
|
|
6123
|
+
}
|
|
6124
|
+
const membresia = membresiaSnap.data();
|
|
6125
|
+
const membresiaLimites = membresia.limites ?? {};
|
|
6126
|
+
const limits = {
|
|
6127
|
+
weeklyContentsPerMonth: membresiaLimites.weeklyContentsPerMonth ?? null,
|
|
6128
|
+
fotosPorMes: membresiaLimites.fotosPorMes ?? null,
|
|
6129
|
+
auditOptimizationsPerMonth: membresiaLimites.auditOptimizationsPerMonth ?? null,
|
|
6130
|
+
aiCostBudgetUSD: membresiaLimites.aiCostBudgetUSD ?? null,
|
|
6131
|
+
brandsIncluidas: membresiaLimites.brandsIncluidas ?? null,
|
|
6132
|
+
usuarios: membresiaLimites.usuarios ?? null,
|
|
6133
|
+
ubicaciones: membresiaLimites.ubicaciones ?? null,
|
|
6134
|
+
productos: membresiaLimites.productos ?? null,
|
|
6135
|
+
insumos: membresiaLimites.insumos ?? null,
|
|
6136
|
+
ordenesMes: membresiaLimites.ordenesMes ?? null,
|
|
6137
|
+
almacenamientoGB: membresiaLimites.almacenamientoGB ?? null,
|
|
6138
|
+
creditosMes: membresiaLimites.creditosMes ?? null
|
|
6139
|
+
};
|
|
6140
|
+
const addonsActivos = tenant.addonsActivos ?? [];
|
|
6141
|
+
const now = Date.now();
|
|
6142
|
+
const activeAddons = addonsActivos.filter((a) => {
|
|
6143
|
+
if (a.estado !== "active") return false;
|
|
6144
|
+
const finVigencia = a.finVigencia;
|
|
6145
|
+
if (finVigencia && typeof finVigencia.toMillis === "function" && finVigencia.toMillis() < now) {
|
|
6146
|
+
return false;
|
|
6147
|
+
}
|
|
6148
|
+
return true;
|
|
6149
|
+
});
|
|
6150
|
+
if (activeAddons.length === 0) return limits;
|
|
6151
|
+
const addonDocs = await Promise.all(
|
|
6152
|
+
activeAddons.map(
|
|
6153
|
+
(a) => db.doc(`membresia_addons/${a.addonId}`).get()
|
|
6154
|
+
)
|
|
6155
|
+
);
|
|
6156
|
+
for (let i = 0; i < activeAddons.length; i++) {
|
|
6157
|
+
const addonSnap = addonDocs[i];
|
|
6158
|
+
if (!addonSnap.exists) continue;
|
|
6159
|
+
const addon = addonSnap.data();
|
|
6160
|
+
const cantidad = activeAddons[i].cantidad;
|
|
6161
|
+
for (const [key, increment] of Object.entries(addon.incrementa ?? {})) {
|
|
6162
|
+
const k = key;
|
|
6163
|
+
if (limits[k] === null) continue;
|
|
6164
|
+
limits[k] = (limits[k] ?? 0) + increment * cantidad;
|
|
6165
|
+
}
|
|
6166
|
+
}
|
|
6167
|
+
return limits;
|
|
6168
|
+
}
|
|
6169
|
+
async function checkQuota(tenantId, quotaName) {
|
|
6170
|
+
const limits = await getEffectiveLimits(tenantId);
|
|
6171
|
+
const limit = limits[quotaName];
|
|
6172
|
+
if (limit === null) {
|
|
6173
|
+
return { ok: true, limit: null, current: 0, remaining: null };
|
|
6174
|
+
}
|
|
6175
|
+
const current = await getCurrentUsage(tenantId, quotaName);
|
|
6176
|
+
const remaining = Math.max(0, limit - current);
|
|
6177
|
+
const ok = current < limit;
|
|
6178
|
+
return {
|
|
6179
|
+
ok,
|
|
6180
|
+
limit,
|
|
6181
|
+
current,
|
|
6182
|
+
remaining,
|
|
6183
|
+
reason: ok ? void 0 : `Alcanzaste el l\xEDmite de ${quotaName}: ${current}/${limit}`
|
|
6184
|
+
};
|
|
6185
|
+
}
|
|
6186
|
+
async function getCurrentUsage(_tenantId, _quotaName) {
|
|
6187
|
+
return 0;
|
|
6188
|
+
}
|
|
6189
|
+
async function resolveTenantIdioma(tenantId) {
|
|
6190
|
+
const db = (0, import_firestore5.getFirestore)();
|
|
6191
|
+
const tenantSnap = await db.doc(`tenants/${tenantId}`).get();
|
|
6192
|
+
if (!tenantSnap.exists) {
|
|
6193
|
+
throw new Error(`Tenant ${tenantId} no existe`);
|
|
6194
|
+
}
|
|
6195
|
+
const data = tenantSnap.data();
|
|
6196
|
+
const idioma = data.idiomaPrincipal ?? data.idioma;
|
|
6197
|
+
if (!idioma) {
|
|
6198
|
+
throw new Error(
|
|
6199
|
+
`Tenant ${tenantId} sin idiomaPrincipal configurado. Completar tenants/${tenantId}.idiomaPrincipal antes de invocar helpers que generen contenido.`
|
|
6200
|
+
);
|
|
6201
|
+
}
|
|
6202
|
+
return idioma;
|
|
6203
|
+
}
|
|
6204
|
+
var MESSAGES = {
|
|
6205
|
+
es: {
|
|
6206
|
+
generic: "Tuve un problema. Intent\xE9moslo de nuevo en un momento.",
|
|
6207
|
+
quota_exceeded: "Alcanzaste el l\xEDmite mensual.",
|
|
6208
|
+
not_found: "No lo encontr\xE9. \xBFPodr\xEDas verificar el dato?",
|
|
6209
|
+
permission: "No tengo permiso para hacer eso desde tu cuenta.",
|
|
6210
|
+
timeout: "Esto est\xE1 tardando m\xE1s de lo normal. \xBFLo intento de nuevo?"
|
|
6211
|
+
},
|
|
6212
|
+
en: {
|
|
6213
|
+
generic: "I had a problem. Let's try again in a moment.",
|
|
6214
|
+
quota_exceeded: "You reached the monthly limit.",
|
|
6215
|
+
not_found: "I couldn't find it. Could you verify the data?",
|
|
6216
|
+
permission: "I don't have permission to do that from your account.",
|
|
6217
|
+
timeout: "This is taking longer than usual. Should I try again?"
|
|
6218
|
+
}
|
|
6219
|
+
};
|
|
6220
|
+
function martinSafeError(err, locale) {
|
|
6221
|
+
if (!(locale in MESSAGES)) {
|
|
6222
|
+
throw new Error(
|
|
6223
|
+
`Locale '${locale}' no soportado en martinSafeError. Locales activos: ${Object.keys(MESSAGES).join(", ")}. Agregar traducciones antes de usar.`
|
|
6224
|
+
);
|
|
6225
|
+
}
|
|
6226
|
+
const msgs = MESSAGES[locale];
|
|
6227
|
+
const e = err;
|
|
6228
|
+
const message = e?.message ?? "";
|
|
6229
|
+
const code = e?.code ?? "";
|
|
6230
|
+
if (/quota|límite|limit/i.test(message)) return msgs.quota_exceeded;
|
|
6231
|
+
if (/not.found|no.encontrado|no existe/i.test(message)) return msgs.not_found;
|
|
6232
|
+
if (/permission|permiso/i.test(message)) return msgs.permission;
|
|
6233
|
+
if (code === "deadline-exceeded" || /timeout/i.test(message)) return msgs.timeout;
|
|
6234
|
+
return msgs.generic;
|
|
6235
|
+
}
|
|
6236
|
+
var MESSAGES2 = {
|
|
6237
|
+
es: {
|
|
6238
|
+
denied_role: "No tengo permiso para hacer eso desde tu cuenta. P\xEDdelo al encargado o admin que te autorice.",
|
|
6239
|
+
input_invalido: "Falta algo o est\xE1 mal. \xBFLo intentamos de nuevo?",
|
|
6240
|
+
confirm_default: "\xBFConfirmas?"
|
|
6241
|
+
},
|
|
6242
|
+
en: {
|
|
6243
|
+
denied_role: "I don't have permission to do that from your account. Ask the manager or admin to authorize it.",
|
|
6244
|
+
input_invalido: "Something is missing or off. Could you try again?",
|
|
6245
|
+
confirm_default: "Are you sure?"
|
|
6246
|
+
}
|
|
6247
|
+
};
|
|
6248
|
+
function getWrapperMessage(key, locale) {
|
|
6249
|
+
if (!(locale in MESSAGES2)) {
|
|
6250
|
+
throw new Error(
|
|
6251
|
+
`Locale '${locale}' no soportado en getWrapperMessage. Locales activos: ${Object.keys(MESSAGES2).join(", ")}. Agregar traducciones antes de usar.`
|
|
6252
|
+
);
|
|
6253
|
+
}
|
|
6254
|
+
const msg = MESSAGES2[locale][key];
|
|
6255
|
+
if (!msg) {
|
|
6256
|
+
throw new Error(
|
|
6257
|
+
`Mensaje '${key}' no definido en wrapperMessages para locale '${locale}'.`
|
|
6258
|
+
);
|
|
6259
|
+
}
|
|
6260
|
+
return msg;
|
|
6261
|
+
}
|
|
6262
|
+
function hasRequiredRole(userRol, required) {
|
|
6263
|
+
if (required.includes("*")) return true;
|
|
6264
|
+
return required.includes(userRol);
|
|
6265
|
+
}
|
|
6266
|
+
function wrapWithContract(contract, helper) {
|
|
6267
|
+
return async function wrappedTool(input, ctx) {
|
|
6268
|
+
const startMs = Date.now();
|
|
6269
|
+
const locale = ctx.user.idiomaPreferido;
|
|
6270
|
+
if (!hasRequiredRole(ctx.user.rol, contract.requiredRoles)) {
|
|
6271
|
+
await writeAuditLog({
|
|
6272
|
+
tenantId: ctx.tenantId,
|
|
6273
|
+
brandId: ctx.brandId ?? null,
|
|
6274
|
+
actor: { type: "martin", uid: ctx.user.uid, nombre: ctx.user.nombre },
|
|
6275
|
+
action: contract.auditAction,
|
|
6276
|
+
motivo: `Acceso denegado \u2014 rol '${ctx.user.rol}' no autorizado`,
|
|
6277
|
+
conversacionId: ctx.conversacionId ?? null,
|
|
6278
|
+
status: "error",
|
|
6279
|
+
errorMessage: `Required: ${contract.requiredRoles.join(",")}, got: ${ctx.user.rol}`,
|
|
6280
|
+
durationMs: Date.now() - startMs
|
|
6281
|
+
});
|
|
6282
|
+
return {
|
|
6283
|
+
text: getWrapperMessage("denied_role", locale),
|
|
6284
|
+
structuredOutput: null,
|
|
6285
|
+
state: "denied_role"
|
|
6286
|
+
};
|
|
6287
|
+
}
|
|
6288
|
+
const parseResult = contract.paramsSchema.safeParse(input);
|
|
6289
|
+
if (!parseResult.success) {
|
|
6290
|
+
await writeAuditLog({
|
|
6291
|
+
tenantId: ctx.tenantId,
|
|
6292
|
+
brandId: ctx.brandId ?? null,
|
|
6293
|
+
actor: { type: "martin", uid: ctx.user.uid, nombre: ctx.user.nombre },
|
|
6294
|
+
action: contract.auditAction,
|
|
6295
|
+
motivo: "Input inv\xE1lido \u2014 Zod parse failed",
|
|
6296
|
+
conversacionId: ctx.conversacionId ?? null,
|
|
6297
|
+
status: "error",
|
|
6298
|
+
errorMessage: `Input shape inv\xE1lido: ${parseResult.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`,
|
|
6299
|
+
durationMs: Date.now() - startMs
|
|
6300
|
+
});
|
|
6301
|
+
return {
|
|
6302
|
+
text: getWrapperMessage("input_invalido", locale),
|
|
6303
|
+
structuredOutput: null,
|
|
6304
|
+
state: "error"
|
|
6305
|
+
};
|
|
6306
|
+
}
|
|
6307
|
+
const parsedInput = parseResult.data;
|
|
6308
|
+
if (contract.requiresConfirmation && !ctx.confirmationGranted) {
|
|
6309
|
+
const confirmMsg = contract.martinConfirmationTemplate ? contract.martinConfirmationTemplate(parsedInput, locale) : getWrapperMessage("confirm_default", locale);
|
|
6310
|
+
return {
|
|
6311
|
+
text: confirmMsg,
|
|
6312
|
+
structuredOutput: null,
|
|
6313
|
+
state: "pending_confirmation"
|
|
6314
|
+
};
|
|
6315
|
+
}
|
|
6316
|
+
if (contract.requiresDoubleConfirmation && !ctx.doubleConfirmationGranted) {
|
|
6317
|
+
const msg = contract.martinConfirmationTemplate ? contract.martinConfirmationTemplate(parsedInput, locale) : getWrapperMessage("confirm_default", locale);
|
|
6318
|
+
return {
|
|
6319
|
+
text: msg,
|
|
6320
|
+
structuredOutput: null,
|
|
6321
|
+
state: "pending_double_confirmation"
|
|
6322
|
+
};
|
|
6323
|
+
}
|
|
6324
|
+
for (const quotaName of contract.quotasConsumed) {
|
|
6325
|
+
const check = await checkQuota(
|
|
6326
|
+
ctx.tenantId,
|
|
6327
|
+
quotaName
|
|
6328
|
+
);
|
|
6329
|
+
if (!check.ok) {
|
|
6330
|
+
await writeAuditLog({
|
|
6331
|
+
tenantId: ctx.tenantId,
|
|
6332
|
+
brandId: ctx.brandId ?? null,
|
|
6333
|
+
actor: { type: "martin", uid: ctx.user.uid, nombre: ctx.user.nombre },
|
|
6334
|
+
action: contract.auditAction,
|
|
6335
|
+
motivo: `Quota excedida \u2014 ${quotaName}: ${check.current}/${check.limit}`,
|
|
6336
|
+
conversacionId: ctx.conversacionId ?? null,
|
|
6337
|
+
status: "error",
|
|
6338
|
+
errorMessage: check.reason ?? "quota exceeded",
|
|
6339
|
+
durationMs: Date.now() - startMs
|
|
6340
|
+
});
|
|
6341
|
+
return {
|
|
6342
|
+
text: martinSafeError(new Error(check.reason ?? "quota"), locale),
|
|
6343
|
+
structuredOutput: null,
|
|
6344
|
+
state: "quota_exceeded"
|
|
6345
|
+
};
|
|
6346
|
+
}
|
|
6347
|
+
}
|
|
6348
|
+
let output;
|
|
6349
|
+
try {
|
|
6350
|
+
output = await helper(parsedInput);
|
|
6351
|
+
contract.outputSchema.parse(output);
|
|
6352
|
+
} catch (err) {
|
|
6353
|
+
await writeAuditLog({
|
|
6354
|
+
tenantId: ctx.tenantId,
|
|
6355
|
+
brandId: ctx.brandId ?? null,
|
|
6356
|
+
actor: { type: "martin", uid: ctx.user.uid, nombre: ctx.user.nombre },
|
|
6357
|
+
action: contract.auditAction,
|
|
6358
|
+
motivo: "Acci\xF3n solicitada v\xEDa Martin",
|
|
6359
|
+
conversacionId: ctx.conversacionId ?? null,
|
|
6360
|
+
status: "error",
|
|
6361
|
+
errorMessage: err?.message ?? "unknown",
|
|
6362
|
+
durationMs: Date.now() - startMs
|
|
6363
|
+
});
|
|
6364
|
+
return {
|
|
6365
|
+
text: martinSafeError(err, locale),
|
|
6366
|
+
structuredOutput: null,
|
|
6367
|
+
state: "error"
|
|
6368
|
+
};
|
|
6369
|
+
}
|
|
6370
|
+
const maybeDisabled = output;
|
|
6371
|
+
if (maybeDisabled.disabled === true) {
|
|
6372
|
+
const code = maybeDisabled.code;
|
|
6373
|
+
if (!code) {
|
|
6374
|
+
throw new Error(
|
|
6375
|
+
`Wrapper: helper "${contract.name}" retorn\xF3 disabled:true pero sin 'code'. Helper debe retornar { disabled:true, code, detail? }.`
|
|
6376
|
+
);
|
|
6377
|
+
}
|
|
6378
|
+
if (contract.disabledReasonCodes && !contract.disabledReasonCodes.includes(code)) {
|
|
6379
|
+
throw new Error(
|
|
6380
|
+
`Wrapper: helper "${contract.name}" retorn\xF3 disabled code "${code}" que NO est\xE1 declarado en contract.disabledReasonCodes [${contract.disabledReasonCodes.join(",")}]. Actualizar el contract antes de que el helper retorne c\xF3digos nuevos.`
|
|
6381
|
+
);
|
|
6382
|
+
}
|
|
6383
|
+
const detail = maybeDisabled.detail;
|
|
6384
|
+
const disabledText = getDisabledMessage(code, locale, detail);
|
|
6385
|
+
await writeAuditLog({
|
|
6386
|
+
tenantId: ctx.tenantId,
|
|
6387
|
+
brandId: ctx.brandId ?? null,
|
|
6388
|
+
actor: { type: "martin", uid: ctx.user.uid, nombre: ctx.user.nombre },
|
|
6389
|
+
action: contract.auditAction,
|
|
6390
|
+
motivo: `disabled \u2014 ${code}${detail ? `: ${JSON.stringify(detail)}` : ""}`,
|
|
6391
|
+
conversacionId: ctx.conversacionId ?? null,
|
|
6392
|
+
status: "partial",
|
|
6393
|
+
durationMs: Date.now() - startMs
|
|
6394
|
+
});
|
|
6395
|
+
return {
|
|
6396
|
+
text: disabledText,
|
|
6397
|
+
structuredOutput: output,
|
|
6398
|
+
state: "disabled",
|
|
6399
|
+
disabledReason: code
|
|
6400
|
+
};
|
|
6401
|
+
}
|
|
6402
|
+
const targetPath = contract.extractTargetPath ? contract.extractTargetPath(parsedInput, output) : "";
|
|
6403
|
+
const changes = contract.extractChanges ? contract.extractChanges(parsedInput, output) : { before: null, after: null };
|
|
6404
|
+
await writeAuditLog({
|
|
6405
|
+
tenantId: ctx.tenantId,
|
|
6406
|
+
brandId: ctx.brandId ?? null,
|
|
6407
|
+
actor: { type: "martin", uid: ctx.user.uid, nombre: ctx.user.nombre },
|
|
6408
|
+
action: contract.auditAction,
|
|
6409
|
+
targetPath,
|
|
6410
|
+
changes,
|
|
6411
|
+
motivo: "Acci\xF3n solicitada v\xEDa Martin",
|
|
6412
|
+
conversacionId: ctx.conversacionId ?? null,
|
|
6413
|
+
status: "success",
|
|
6414
|
+
durationMs: Date.now() - startMs
|
|
6415
|
+
});
|
|
6416
|
+
const summary = contract.martinSummaryTemplate(parsedInput, output, locale);
|
|
6417
|
+
return {
|
|
6418
|
+
text: summary,
|
|
6419
|
+
structuredOutput: output,
|
|
6420
|
+
state: "success"
|
|
6421
|
+
};
|
|
6422
|
+
};
|
|
6423
|
+
}
|
|
6424
|
+
var RecordarMemoriaParamsSchema = import_zod33.z.object({
|
|
6425
|
+
tipo: TipoMemoriaEnum,
|
|
6426
|
+
categoria: CategoriaMemoriaEnum,
|
|
6427
|
+
contenido: import_zod33.z.string().min(3).max(500)
|
|
6428
|
+
});
|
|
6429
|
+
var RecordarMemoriaOutputSchema = import_zod33.z.object({
|
|
6430
|
+
memoriaId: import_zod33.z.string(),
|
|
6431
|
+
status: import_zod33.z.literal("creada")
|
|
6432
|
+
});
|
|
6433
|
+
var OlvidarMemoriaParamsSchema = import_zod33.z.object({
|
|
6434
|
+
memoriaId: import_zod33.z.string(),
|
|
6435
|
+
motivo: import_zod33.z.string().optional()
|
|
6436
|
+
});
|
|
6437
|
+
var OlvidarMemoriaOutputSchema = import_zod33.z.object({
|
|
6438
|
+
status: import_zod33.z.literal("archivada")
|
|
6439
|
+
});
|
|
6440
|
+
var ConfigInputSchema = import_zod34.z.object({
|
|
6441
|
+
diaSemana: import_zod34.z.number().int().min(0).max(6).nullable(),
|
|
6442
|
+
diaMes: import_zod34.z.number().int().min(1).max(31).nullable(),
|
|
6443
|
+
hora: import_zod34.z.string().regex(/^\d{2}:\d{2}$/),
|
|
6444
|
+
// Timezone NO viene en input — hereda de tenants/{tid}.zonaHoraria (audit fix 2).
|
|
6445
|
+
fechaPuntual: import_zod34.z.string().datetime({ offset: true }).nullable()
|
|
6446
|
+
}).strict();
|
|
6447
|
+
var AccionInputSchema = import_zod34.z.object({
|
|
6448
|
+
tool: import_zod34.z.string().min(1),
|
|
6449
|
+
params: import_zod34.z.record(import_zod34.z.string(), import_zod34.z.unknown())
|
|
6450
|
+
}).strict();
|
|
6451
|
+
var ProgramarRutinaParamsSchema = import_zod34.z.object({
|
|
6452
|
+
tipo: TipoRutinaEnum,
|
|
6453
|
+
frecuencia: FrecuenciaRutinaEnum,
|
|
6454
|
+
config: ConfigInputSchema,
|
|
6455
|
+
accion: AccionInputSchema,
|
|
6456
|
+
uidDestinatario: import_zod34.z.string()
|
|
6457
|
+
});
|
|
6458
|
+
var ProgramarRutinaOutputSchema = import_zod34.z.object({
|
|
6459
|
+
rutinaId: import_zod34.z.string(),
|
|
6460
|
+
proximaEjecucionAt: import_zod34.z.string().datetime()
|
|
6461
|
+
});
|
|
6462
|
+
var PausarRutinaParamsSchema = import_zod34.z.object({
|
|
6463
|
+
rutinaId: import_zod34.z.string(),
|
|
6464
|
+
motivo: import_zod34.z.string().optional()
|
|
6465
|
+
});
|
|
6466
|
+
var PausarRutinaOutputSchema = import_zod34.z.object({
|
|
6467
|
+
status: import_zod34.z.literal("pausada")
|
|
6468
|
+
});
|
|
6469
|
+
var ArchivarRutinaParamsSchema = import_zod34.z.object({
|
|
6470
|
+
rutinaId: import_zod34.z.string(),
|
|
6471
|
+
motivo: import_zod34.z.string().optional()
|
|
6472
|
+
});
|
|
6473
|
+
var ArchivarRutinaOutputSchema = import_zod34.z.object({
|
|
6474
|
+
status: import_zod34.z.literal("archivada")
|
|
6475
|
+
});
|
|
6476
|
+
var ListarRutinasParamsSchema = import_zod34.z.object({
|
|
6477
|
+
uid: import_zod34.z.string().optional()
|
|
6478
|
+
});
|
|
6479
|
+
var ListarRutinasOutputSchema = import_zod34.z.object({
|
|
6480
|
+
rutinas: import_zod34.z.array(MartinRutinaSchema)
|
|
6481
|
+
});
|
|
6482
|
+
|
|
6483
|
+
// src/tools/martinContext.ts
|
|
6484
|
+
function buildMartinContext(session, brandId, opts = {}) {
|
|
6485
|
+
return {
|
|
6486
|
+
tenantId: session.requireTenant(),
|
|
6487
|
+
brandId,
|
|
6488
|
+
user: {
|
|
6489
|
+
uid: session.userId ?? "cowork-admin",
|
|
6490
|
+
nombre: session.userName ?? "Cowork Admin",
|
|
6491
|
+
rol: session.rol ?? "super_admin",
|
|
6492
|
+
idiomaPreferido: "es"
|
|
6493
|
+
// TODO HITO 8: leer usuarios/{uid}.idiomaPreferido
|
|
6494
|
+
},
|
|
6495
|
+
conversacionId: opts.conversacionId ?? null,
|
|
6496
|
+
confirmationGranted: opts.confirmationGranted ?? true,
|
|
6497
|
+
doubleConfirmationGranted: opts.doubleConfirmationGranted ?? false
|
|
6498
|
+
};
|
|
6499
|
+
}
|
|
6500
|
+
async function dispatchWithContract(args) {
|
|
6501
|
+
const { contract, helper, callable, input, ctx } = args;
|
|
6502
|
+
if (getMode() === "admin") {
|
|
6503
|
+
const db = getAdminDb();
|
|
6504
|
+
const wrapped = wrapWithContract(
|
|
6505
|
+
contract,
|
|
6506
|
+
async (i) => helper({ ...i, db })
|
|
6507
|
+
);
|
|
6508
|
+
return wrapped(input, ctx);
|
|
6509
|
+
}
|
|
6510
|
+
return callable({
|
|
6511
|
+
...input,
|
|
6512
|
+
_martinContext: {
|
|
6513
|
+
conversacionId: ctx.conversacionId ?? null,
|
|
6514
|
+
confirmationGranted: ctx.confirmationGranted === true,
|
|
6515
|
+
doubleConfirmationGranted: ctx.doubleConfirmationGranted === true,
|
|
6516
|
+
userIdiomaPreferido: ctx.user.idiomaPreferido
|
|
6517
|
+
}
|
|
6518
|
+
});
|
|
6519
|
+
}
|
|
5140
6520
|
|
|
5141
6521
|
// src/services/marketingHelperCallables.ts
|
|
5142
6522
|
var import_app3 = require("firebase/app");
|
|
@@ -5156,6 +6536,9 @@ async function callCF(name, input) {
|
|
|
5156
6536
|
const result = await fn(input);
|
|
5157
6537
|
return result.data;
|
|
5158
6538
|
}
|
|
6539
|
+
function callGetCalendar(input) {
|
|
6540
|
+
return callCF("marketingGetCalendarCallable", input);
|
|
6541
|
+
}
|
|
5159
6542
|
function callBrandBriefWriter(input) {
|
|
5160
6543
|
return callCF("marketingBrandBriefWriterCallable", input);
|
|
5161
6544
|
}
|
|
@@ -5206,7 +6589,7 @@ function callPhotoDirectorExecute(input) {
|
|
|
5206
6589
|
}
|
|
5207
6590
|
|
|
5208
6591
|
// src/tools/marketing/photos.ts
|
|
5209
|
-
var
|
|
6592
|
+
var import_zod36 = require("zod");
|
|
5210
6593
|
|
|
5211
6594
|
// src/services/marketingEmbeddings.ts
|
|
5212
6595
|
var import_google_auth_library = require("google-auth-library");
|
|
@@ -5292,7 +6675,7 @@ var MARKETING_THRESHOLDS = {
|
|
|
5292
6675
|
gbp: 0.4
|
|
5293
6676
|
},
|
|
5294
6677
|
content: {
|
|
5295
|
-
// find_content_for_topic busca text→text en
|
|
6678
|
+
// find_content_for_topic busca text→text en products / collections /
|
|
5296
6679
|
// articles / pages. Scores reales de top-10 andan 0.45-0.50, umbral 0.35
|
|
5297
6680
|
// deja pasar los relevantes sin ser laxo.
|
|
5298
6681
|
text: 0.35,
|
|
@@ -5379,12 +6762,12 @@ async function findNearestInCollection(params) {
|
|
|
5379
6762
|
);
|
|
5380
6763
|
}
|
|
5381
6764
|
const db = getAdminDb();
|
|
5382
|
-
const
|
|
6765
|
+
const FieldValue3 = import_firebase_admin.default.firestore.FieldValue;
|
|
5383
6766
|
const fetchLimit = extraFilters.length > 0 ? Math.max(limit * 4, 20) : limit;
|
|
5384
6767
|
const baseQ = db.collection(collection).where("tenantId", "==", tenantId).where("brandId", "==", brandId);
|
|
5385
6768
|
const q = baseQ.findNearest({
|
|
5386
6769
|
vectorField,
|
|
5387
|
-
queryVector:
|
|
6770
|
+
queryVector: FieldValue3.vector(queryEmbedding),
|
|
5388
6771
|
limit: fetchLimit,
|
|
5389
6772
|
distanceMeasure: "COSINE",
|
|
5390
6773
|
distanceResultField: "_distance"
|
|
@@ -5474,7 +6857,7 @@ async function findNearestInCollectionWithOverride(params) {
|
|
|
5474
6857
|
}
|
|
5475
6858
|
|
|
5476
6859
|
// src/tools/marketing/content.ts
|
|
5477
|
-
var
|
|
6860
|
+
var import_zod35 = require("zod");
|
|
5478
6861
|
var _logOverride = null;
|
|
5479
6862
|
async function logToMcpLogs(entry) {
|
|
5480
6863
|
if (_logOverride) return _logOverride(entry);
|
|
@@ -5487,22 +6870,22 @@ async function logToMcpLogs(entry) {
|
|
|
5487
6870
|
} catch {
|
|
5488
6871
|
}
|
|
5489
6872
|
}
|
|
5490
|
-
var IncludeSchema =
|
|
5491
|
-
products:
|
|
5492
|
-
collections:
|
|
5493
|
-
articles:
|
|
5494
|
-
pages:
|
|
6873
|
+
var IncludeSchema = import_zod35.z.object({
|
|
6874
|
+
products: import_zod35.z.boolean().default(true),
|
|
6875
|
+
collections: import_zod35.z.boolean().default(true),
|
|
6876
|
+
articles: import_zod35.z.boolean().default(true),
|
|
6877
|
+
pages: import_zod35.z.boolean().default(false)
|
|
5495
6878
|
}).default({
|
|
5496
6879
|
products: true,
|
|
5497
6880
|
collections: true,
|
|
5498
6881
|
articles: true,
|
|
5499
6882
|
pages: false
|
|
5500
6883
|
});
|
|
5501
|
-
var LimitSchema =
|
|
5502
|
-
products:
|
|
5503
|
-
collections:
|
|
5504
|
-
articles:
|
|
5505
|
-
pages:
|
|
6884
|
+
var LimitSchema = import_zod35.z.object({
|
|
6885
|
+
products: import_zod35.z.number().int().min(0).max(20).default(5),
|
|
6886
|
+
collections: import_zod35.z.number().int().min(0).max(10).default(3),
|
|
6887
|
+
articles: import_zod35.z.number().int().min(0).max(20).default(5),
|
|
6888
|
+
pages: import_zod35.z.number().int().min(0).max(10).default(2)
|
|
5506
6889
|
}).default({ products: 5, collections: 3, articles: 5, pages: 2 });
|
|
5507
6890
|
async function findContentForTopicHandler(input, session) {
|
|
5508
6891
|
const tenantId = session.requireTenant();
|
|
@@ -5588,13 +6971,13 @@ MODOS (parametro mode):
|
|
|
5588
6971
|
|
|
5589
6972
|
Cuando uses hybrid: inspecciona el campo \`modesFound\` en los resultados. 2 = el doc aparecio en ambos modos (alta confianza). 1 = aparecio solo en text (problema visual) o solo en image (problema SEO). Esos "solo image" son candidatos perfectos para recomendar optimizacion al tenant.`,
|
|
5590
6973
|
{
|
|
5591
|
-
brandId:
|
|
5592
|
-
contexto:
|
|
5593
|
-
fecha:
|
|
6974
|
+
brandId: import_zod35.z.string().optional().describe("ID de la brand"),
|
|
6975
|
+
contexto: import_zod35.z.string().min(1).describe("Parrafo, keyword o intencion"),
|
|
6976
|
+
fecha: import_zod35.z.string().describe("Fecha del contenido en ISO YYYY-MM-DD"),
|
|
5594
6977
|
include: IncludeSchema.optional(),
|
|
5595
6978
|
limit: LimitSchema.optional(),
|
|
5596
|
-
diversidad:
|
|
5597
|
-
mode:
|
|
6979
|
+
diversidad: import_zod35.z.boolean().default(true),
|
|
6980
|
+
mode: import_zod35.z.enum(["text", "hybrid"]).default("text").describe("Modo de busqueda: 'text' (rapido, 1 query) o 'hybrid' (text+image via RRF, 2x queries, rescata SEO debil)")
|
|
5598
6981
|
},
|
|
5599
6982
|
async ({ brandId: inputBrandId, contexto, fecha, include, limit, diversidad, mode: mode2 }) => {
|
|
5600
6983
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
@@ -5698,11 +7081,11 @@ REGLAS:
|
|
|
5698
7081
|
|
|
5699
7082
|
USAR: antes de generar contenido de cualquier slot del calendario.`,
|
|
5700
7083
|
{
|
|
5701
|
-
brandId:
|
|
5702
|
-
keyword:
|
|
5703
|
-
plataforma:
|
|
5704
|
-
fecha:
|
|
5705
|
-
limit:
|
|
7084
|
+
brandId: import_zod36.z.string().optional().describe("ID de la brand"),
|
|
7085
|
+
keyword: import_zod36.z.string().describe("Keyword del slot"),
|
|
7086
|
+
plataforma: import_zod36.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino del slot"),
|
|
7087
|
+
fecha: import_zod36.z.string().describe("Fecha del slot en ISO YYYY-MM-DD"),
|
|
7088
|
+
limit: import_zod36.z.number().int().min(1).max(10).default(5)
|
|
5706
7089
|
},
|
|
5707
7090
|
async ({ brandId: inputBrandId, keyword, plataforma, fecha, limit }) => {
|
|
5708
7091
|
const tenantId = session.requireTenant();
|
|
@@ -5743,15 +7126,17 @@ DESPUES de ver la foto, decide:
|
|
|
5743
7126
|
|
|
5744
7127
|
Luego llama execute_photo_edit con tu analisis y prompt.`,
|
|
5745
7128
|
{
|
|
5746
|
-
fotoId:
|
|
7129
|
+
fotoId: import_zod36.z.string().describe("ID de la foto")
|
|
5747
7130
|
},
|
|
5748
7131
|
async ({ fotoId }) => {
|
|
5749
7132
|
const tenantId = session.requireTenant();
|
|
7133
|
+
const lang = await resolveTenantIdioma(tenantId);
|
|
5750
7134
|
const result = getMode() === "admin" ? await photoDirectorPlan({
|
|
5751
7135
|
db: getAdminDb(),
|
|
5752
7136
|
tenantId,
|
|
5753
7137
|
fotoId,
|
|
5754
|
-
deps: { compressImageForTransport }
|
|
7138
|
+
deps: { compressImageForTransport },
|
|
7139
|
+
lang
|
|
5755
7140
|
}) : await callPhotoDirectorPlan({ tenantId, fotoId });
|
|
5756
7141
|
if (!result.ok) {
|
|
5757
7142
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
@@ -5774,16 +7159,18 @@ Retorna la foto editada para que la revises. Si no te gusta:
|
|
|
5774
7159
|
|
|
5775
7160
|
Si estrategia era 'tal_cual', pasa acciones=['none'] \u2014 no se edita pero si se genera thumbnail y embedding con tus tags.`,
|
|
5776
7161
|
{
|
|
5777
|
-
fotoId:
|
|
5778
|
-
prompt:
|
|
5779
|
-
acciones:
|
|
5780
|
-
descripcion:
|
|
5781
|
-
tipo:
|
|
5782
|
-
tagsPrimarios:
|
|
5783
|
-
tagsSecundarios:
|
|
5784
|
-
tagsContexto:
|
|
7162
|
+
fotoId: import_zod36.z.string(),
|
|
7163
|
+
prompt: import_zod36.z.string().nullable().describe("Prompt en ingles para Gemini Image Edit. null si tal_cual"),
|
|
7164
|
+
acciones: import_zod36.z.array(import_zod36.z.enum(["edit_background", "none"])),
|
|
7165
|
+
descripcion: import_zod36.z.string().describe("Descripcion semantica en espanol"),
|
|
7166
|
+
tipo: import_zod36.z.string().nullable().describe("Tipo del catalogoVisual del tenant"),
|
|
7167
|
+
tagsPrimarios: import_zod36.z.array(import_zod36.z.string()),
|
|
7168
|
+
tagsSecundarios: import_zod36.z.array(import_zod36.z.string()),
|
|
7169
|
+
tagsContexto: import_zod36.z.array(import_zod36.z.string())
|
|
5785
7170
|
},
|
|
5786
7171
|
async ({ fotoId, prompt, acciones, descripcion, tipo, tagsPrimarios, tagsSecundarios, tagsContexto }) => {
|
|
7172
|
+
const tenantId = session.requireTenant();
|
|
7173
|
+
const lang = await resolveTenantIdioma(tenantId);
|
|
5787
7174
|
if (getMode() === "admin") {
|
|
5788
7175
|
const executePhotoEditAdapter = async (payload) => {
|
|
5789
7176
|
const cfUrl = process.env.MARKETING_PHOTO_EDIT_CF_URL || "https://us-central1-atteyo-ops.cloudfunctions.net/marketingExecutePhotoEdit";
|
|
@@ -5807,7 +7194,8 @@ Si estrategia era 'tal_cual', pasa acciones=['none'] \u2014 no se edita pero si
|
|
|
5807
7194
|
tagsPrimarios,
|
|
5808
7195
|
tagsSecundarios,
|
|
5809
7196
|
tagsContexto,
|
|
5810
|
-
deps: { compressImageForTransport, executePhotoEdit: executePhotoEditAdapter }
|
|
7197
|
+
deps: { compressImageForTransport, executePhotoEdit: executePhotoEditAdapter },
|
|
7198
|
+
lang
|
|
5811
7199
|
});
|
|
5812
7200
|
if (!result2.ok) {
|
|
5813
7201
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
@@ -5821,7 +7209,7 @@ Si estrategia era 'tal_cual', pasa acciones=['none'] \u2014 no se edita pero si
|
|
|
5821
7209
|
return { content: contentArray2 };
|
|
5822
7210
|
}
|
|
5823
7211
|
const result = await callPhotoDirectorExecute({
|
|
5824
|
-
tenantId
|
|
7212
|
+
tenantId,
|
|
5825
7213
|
fotoId,
|
|
5826
7214
|
prompt,
|
|
5827
7215
|
acciones,
|
|
@@ -5855,7 +7243,7 @@ Retorna:
|
|
|
5855
7243
|
- creditos: { balance, planId, periodEnd, costoPhotoEdit }
|
|
5856
7244
|
- _instrucciones: que hacer segun el estado`,
|
|
5857
7245
|
{
|
|
5858
|
-
fotoId:
|
|
7246
|
+
fotoId: import_zod36.z.string().describe("ID de la foto")
|
|
5859
7247
|
},
|
|
5860
7248
|
async ({ fotoId }) => {
|
|
5861
7249
|
const tenantId = session.requireTenant();
|
|
@@ -5898,7 +7286,7 @@ Retorna:
|
|
|
5898
7286
|
};
|
|
5899
7287
|
}
|
|
5900
7288
|
}
|
|
5901
|
-
const creditsDoc = await readDoc(
|
|
7289
|
+
const creditsDoc = await readDoc(`tenants/${tenantId}/brand_credits`, brandId);
|
|
5902
7290
|
const balance = creditsDoc?.balance ?? 0;
|
|
5903
7291
|
const planId = creditsDoc?.planId ?? null;
|
|
5904
7292
|
const periodEnd = creditsDoc?.periodEnd ?? null;
|
|
@@ -5955,11 +7343,11 @@ Retorna:
|
|
|
5955
7343
|
"find_products_for_content",
|
|
5956
7344
|
`[DEPRECATED 179.5] Wrapper interno de find_content_for_topic. Para nuevas integraciones, usa find_content_for_topic con include.products=true directamente. Este wrapper existe solo por compat hacia atras.`,
|
|
5957
7345
|
{
|
|
5958
|
-
brandId:
|
|
5959
|
-
contexto:
|
|
5960
|
-
fecha:
|
|
5961
|
-
limit:
|
|
5962
|
-
diversidad:
|
|
7346
|
+
brandId: import_zod36.z.string().optional().describe("ID de la brand"),
|
|
7347
|
+
contexto: import_zod36.z.string().describe("Parrafo, keyword o intencion del contenido"),
|
|
7348
|
+
fecha: import_zod36.z.string().describe("Fecha del contenido en ISO YYYY-MM-DD"),
|
|
7349
|
+
limit: import_zod36.z.number().int().min(1).max(10).default(5),
|
|
7350
|
+
diversidad: import_zod36.z.boolean().default(true)
|
|
5963
7351
|
},
|
|
5964
7352
|
async ({ brandId: inputBrandId, contexto, fecha, limit, diversidad }) => {
|
|
5965
7353
|
console.warn(
|
|
@@ -6010,12 +7398,12 @@ Retorna:
|
|
|
6010
7398
|
|
|
6011
7399
|
RETORNA { plantillaId, titulo, thumbnailUrl } o { plantillaId: null, motivo: 'no_canva' } si tenant no tiene Canva conectado.
|
|
6012
7400
|
|
|
6013
|
-
USAR: solo si tenant tiene Canva conectado (marketing_config
|
|
7401
|
+
USAR: solo si tenant tiene Canva conectado (tenants/{tenantId}/marketing_config/{brandId}.canva.connected=true).`,
|
|
6014
7402
|
{
|
|
6015
|
-
brandId:
|
|
6016
|
-
plataforma:
|
|
6017
|
-
tipoContenido:
|
|
6018
|
-
keyword:
|
|
7403
|
+
brandId: import_zod36.z.string().optional().describe("ID de la brand"),
|
|
7404
|
+
plataforma: import_zod36.z.string().describe("gbp | instagram | shopify_blog"),
|
|
7405
|
+
tipoContenido: import_zod36.z.string().describe("post | carousel | story | blog"),
|
|
7406
|
+
keyword: import_zod36.z.string().describe("Keyword del slot")
|
|
6019
7407
|
},
|
|
6020
7408
|
async ({ brandId: inputBrandId, plataforma, tipoContenido, keyword }) => {
|
|
6021
7409
|
const tenantId = session.requireTenant();
|
|
@@ -6045,15 +7433,15 @@ USAR: cuando get_photos_for_slot retorna pocas fotos y el slot aun no esta cubie
|
|
|
6045
7433
|
|
|
6046
7434
|
IMPORTANTE \u2014 campo 'razon': Este texto lo ve el tenant en la app. Escribe en lenguaje amigable y claro, NO tecnico. Ejemplo MALO: "get_photos_for_slot retorno 0 fotos para shopify_blog". Ejemplo BUENO: "No hay fotos de alcatraz para el blog del 8 de abril". Siempre en espanol si el tenant habla espanol.`,
|
|
6047
7435
|
{
|
|
6048
|
-
brandId:
|
|
6049
|
-
semana:
|
|
6050
|
-
necesidades:
|
|
6051
|
-
|
|
6052
|
-
tema:
|
|
6053
|
-
keyword:
|
|
6054
|
-
cantidadSugerida:
|
|
6055
|
-
razon:
|
|
6056
|
-
slotsAfectados:
|
|
7436
|
+
brandId: import_zod36.z.string().optional().describe("ID de la brand"),
|
|
7437
|
+
semana: import_zod36.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "semana debe ser ISO YYYY-MM-DD").describe("Semana en ISO del lunes"),
|
|
7438
|
+
necesidades: import_zod36.z.array(
|
|
7439
|
+
import_zod36.z.object({
|
|
7440
|
+
tema: import_zod36.z.string(),
|
|
7441
|
+
keyword: import_zod36.z.string(),
|
|
7442
|
+
cantidadSugerida: import_zod36.z.number().int().positive(),
|
|
7443
|
+
razon: import_zod36.z.string(),
|
|
7444
|
+
slotsAfectados: import_zod36.z.array(import_zod36.z.string()).optional().describe('Refs de slots afectados (formato "semana:N:slot:M")')
|
|
6057
7445
|
})
|
|
6058
7446
|
).min(1)
|
|
6059
7447
|
},
|
|
@@ -6192,16 +7580,6 @@ async function linkContenidoAPasoPipeline(params) {
|
|
|
6192
7580
|
}
|
|
6193
7581
|
|
|
6194
7582
|
// src/tools/marketing.ts
|
|
6195
|
-
async function resolveLastImportId3(tenantId, brandId) {
|
|
6196
|
-
const config = await readDoc("marketing_config", tenantId);
|
|
6197
|
-
const brands = config?.brands ?? {};
|
|
6198
|
-
const brand = brands[brandId];
|
|
6199
|
-
const canalId = brand?.canalId;
|
|
6200
|
-
if (!canalId) return null;
|
|
6201
|
-
const canal = await readDoc("canales_venta", canalId);
|
|
6202
|
-
const app = canal?.app ?? {};
|
|
6203
|
-
return app.lastImportId ?? null;
|
|
6204
|
-
}
|
|
6205
7583
|
function registerMarketingTools(server, session) {
|
|
6206
7584
|
registerPhotoTools(server, session);
|
|
6207
7585
|
registerContentTools(server, session);
|
|
@@ -6209,48 +7587,46 @@ function registerMarketingTools(server, session) {
|
|
|
6209
7587
|
"get_calendar",
|
|
6210
7588
|
"Lee el calendario editorial del mes. Muestra semanas con items planificados por plataforma.",
|
|
6211
7589
|
{
|
|
6212
|
-
brandId:
|
|
6213
|
-
mes:
|
|
7590
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand"),
|
|
7591
|
+
mes: import_zod37.z.string().optional().describe("Mes en formato YYYY-MM (default: mes actual)")
|
|
6214
7592
|
},
|
|
6215
7593
|
async ({ brandId: inputBrandId, mes }) => {
|
|
6216
|
-
session.requireTenant();
|
|
7594
|
+
const tenantId = session.requireTenant();
|
|
6217
7595
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
6218
7596
|
const targetMes = mes ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
|
|
6219
|
-
const
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
7597
|
+
const ctx = buildMartinContext(session, brandId);
|
|
7598
|
+
const result = await dispatchWithContract({
|
|
7599
|
+
contract: getCalendarContract,
|
|
7600
|
+
helper: getCalendar,
|
|
7601
|
+
callable: callGetCalendar,
|
|
7602
|
+
input: { tenantId, brandId, mes: targetMes },
|
|
7603
|
+
ctx
|
|
7604
|
+
});
|
|
7605
|
+
let payload;
|
|
7606
|
+
if (result.state === "success" && result.structuredOutput) {
|
|
7607
|
+
const out = result.structuredOutput;
|
|
7608
|
+
payload = out.calendario ?? {
|
|
7609
|
+
mes: out.mes,
|
|
7610
|
+
brandId: out.brandId,
|
|
7611
|
+
semanas: [],
|
|
7612
|
+
mensaje: out.mensaje
|
|
6234
7613
|
};
|
|
7614
|
+
} else {
|
|
7615
|
+
payload = { ok: false, state: result.state, mensaje: result.text };
|
|
6235
7616
|
}
|
|
6236
|
-
return { content: [{ type: "text", text: JSON.stringify(
|
|
7617
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
6237
7618
|
}
|
|
6238
7619
|
);
|
|
6239
7620
|
server.tool(
|
|
6240
7621
|
"get_seo_snapshot",
|
|
6241
7622
|
"Lee el snapshot SEO de la brand: rank, keywords, oportunidades, competidores.",
|
|
6242
7623
|
{
|
|
6243
|
-
brandId:
|
|
7624
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand")
|
|
6244
7625
|
},
|
|
6245
7626
|
async ({ brandId: inputBrandId }) => {
|
|
6246
7627
|
const tenantId = session.requireTenant();
|
|
6247
7628
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
6248
|
-
const
|
|
6249
|
-
if (!config) {
|
|
6250
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: "No hay marketing_config" }) }] };
|
|
6251
|
-
}
|
|
6252
|
-
const brands = config.brands ?? {};
|
|
6253
|
-
const brand = brands[brandId];
|
|
7629
|
+
const brand = await readDoc(`tenants/${tenantId}/marketing_config`, brandId);
|
|
6254
7630
|
if (!brand) {
|
|
6255
7631
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Brand "${brandId}" no encontrada` }) }] };
|
|
6256
7632
|
}
|
|
@@ -6264,8 +7640,8 @@ function registerMarketingTools(server, session) {
|
|
|
6264
7640
|
"get_photo_gallery",
|
|
6265
7641
|
"Fotos disponibles filtradas por estado. Muestra fotos con metadata y conteo.",
|
|
6266
7642
|
{
|
|
6267
|
-
brandId:
|
|
6268
|
-
estado:
|
|
7643
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand"),
|
|
7644
|
+
estado: import_zod37.z.string().optional().describe("Filtrar por estado (nueva, procesando, editada, usada, descartada, error)")
|
|
6269
7645
|
},
|
|
6270
7646
|
async ({ brandId: inputBrandId, estado }) => {
|
|
6271
7647
|
session.requireTenant();
|
|
@@ -6305,7 +7681,7 @@ function registerMarketingTools(server, session) {
|
|
|
6305
7681
|
"generate_marketing_plan",
|
|
6306
7682
|
"Prepara los datos necesarios para generar un plan de marketing estrategico. Retorna snapshot SEO + productos para que Claude genere el plan con el system prompt.",
|
|
6307
7683
|
{
|
|
6308
|
-
brandId:
|
|
7684
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand")
|
|
6309
7685
|
},
|
|
6310
7686
|
async ({ brandId: inputBrandId }) => {
|
|
6311
7687
|
const tenantId = session.requireTenant();
|
|
@@ -6317,25 +7693,33 @@ function registerMarketingTools(server, session) {
|
|
|
6317
7693
|
);
|
|
6318
7694
|
server.tool(
|
|
6319
7695
|
"save_marketing_plan",
|
|
6320
|
-
"Guarda el plan de marketing generado por Claude en la configuracion de la brand. Escribe en marketing_config
|
|
7696
|
+
"Guarda el plan de marketing generado por Claude en la configuracion de la brand. Escribe en tenants/{tenantId}/marketing_config/{brandId}.plan.",
|
|
6321
7697
|
{
|
|
6322
|
-
brandId:
|
|
6323
|
-
plan:
|
|
7698
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand"),
|
|
7699
|
+
plan: import_zod37.z.record(import_zod37.z.string(), import_zod37.z.unknown()).describe("Plan de marketing generado (keywordsPrioritarios, gapsCompetencia, temporadas, tonoMarca, quickWins, etc.)")
|
|
6324
7700
|
},
|
|
6325
7701
|
async ({ brandId: inputBrandId, plan }) => {
|
|
6326
7702
|
const tenantId = session.requireTenant();
|
|
6327
7703
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
6328
|
-
const
|
|
6329
|
-
|
|
7704
|
+
const ctx = buildMartinContext(session, brandId);
|
|
7705
|
+
const result = await dispatchWithContract({
|
|
7706
|
+
contract: planWriterSaveContract,
|
|
7707
|
+
helper: planWriter.save,
|
|
7708
|
+
callable: callPlanWriterSave,
|
|
7709
|
+
input: { tenantId, brandId, plan },
|
|
7710
|
+
ctx
|
|
7711
|
+
});
|
|
7712
|
+
const payload = result.state === "success" ? result.structuredOutput : { ok: false, state: result.state, mensaje: result.text };
|
|
7713
|
+
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
6330
7714
|
}
|
|
6331
7715
|
);
|
|
6332
7716
|
server.tool(
|
|
6333
7717
|
"update_marketing_plan_field",
|
|
6334
7718
|
"Actualiza UN campo del plan de marketing sin sobreescribir el resto. Merge parcial. Ideal para agregar coleccionesPriorizadas, actualizar quickWins, etc.",
|
|
6335
7719
|
{
|
|
6336
|
-
brandId:
|
|
6337
|
-
field:
|
|
6338
|
-
value:
|
|
7720
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand"),
|
|
7721
|
+
field: import_zod37.z.string().describe('Nombre del campo a actualizar (ej: "coleccionesPriorizadas", "quickWins", "temporadas")'),
|
|
7722
|
+
value: import_zod37.z.unknown().describe("Valor del campo")
|
|
6339
7723
|
},
|
|
6340
7724
|
async ({ brandId: inputBrandId, field, value }) => {
|
|
6341
7725
|
const tenantId = session.requireTenant();
|
|
@@ -6348,22 +7732,36 @@ function registerMarketingTools(server, session) {
|
|
|
6348
7732
|
"generate_brand_brief",
|
|
6349
7733
|
"Prepara todos los datos del negocio para que Claude genere un Brand Brief pre-llenado. Retorna Shopify + SEO + GBP + tenant data. Claude genera el brief, luego usa save_brand_brief para guardar.",
|
|
6350
7734
|
{
|
|
6351
|
-
brandId:
|
|
7735
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand")
|
|
6352
7736
|
},
|
|
6353
7737
|
async ({ brandId: inputBrandId }) => {
|
|
6354
7738
|
const tenantId = session.requireTenant();
|
|
6355
7739
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
6356
|
-
const
|
|
6357
|
-
const
|
|
6358
|
-
|
|
7740
|
+
const ctx = buildMartinContext(session, brandId);
|
|
7741
|
+
const result = await dispatchWithContract({
|
|
7742
|
+
contract: brandBriefBuilderContract,
|
|
7743
|
+
helper: brandBriefBuilder,
|
|
7744
|
+
callable: callBrandBriefBuilder,
|
|
7745
|
+
input: { tenantId, brandId },
|
|
7746
|
+
ctx
|
|
7747
|
+
});
|
|
7748
|
+
let returnPayload;
|
|
7749
|
+
if (result.state === "success" && result.structuredOutput?.ok === true) {
|
|
7750
|
+
returnPayload = result.structuredOutput.payload;
|
|
7751
|
+
} else if (result.state === "success") {
|
|
7752
|
+
returnPayload = result.structuredOutput;
|
|
7753
|
+
} else {
|
|
7754
|
+
returnPayload = { ok: false, state: result.state, mensaje: result.text };
|
|
7755
|
+
}
|
|
7756
|
+
return { content: [{ type: "text", text: JSON.stringify(returnPayload, null, 2) }] };
|
|
6359
7757
|
}
|
|
6360
7758
|
);
|
|
6361
7759
|
server.tool(
|
|
6362
7760
|
"save_brand_brief",
|
|
6363
|
-
"Guarda el Brand Brief generado por Claude en marketing_config
|
|
7761
|
+
"Guarda el Brand Brief generado por Claude en tenants/{tenantId}/marketing_config/{brandId}.brandBrief. Escribe solo ese campo (merge parcial, no rebuild del doc).",
|
|
6364
7762
|
{
|
|
6365
|
-
brandId:
|
|
6366
|
-
brandBrief:
|
|
7763
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand"),
|
|
7764
|
+
brandBrief: import_zod37.z.record(import_zod37.z.string(), import_zod37.z.unknown()).describe("Brand Brief completo generado por Claude")
|
|
6367
7765
|
},
|
|
6368
7766
|
async ({ brandId: inputBrandId, brandBrief }) => {
|
|
6369
7767
|
const tenantId = session.requireTenant();
|
|
@@ -6376,9 +7774,9 @@ function registerMarketingTools(server, session) {
|
|
|
6376
7774
|
"generate_weekly_content",
|
|
6377
7775
|
"Prepara datos del calendario + fotos + plan para generar el contenido de la semana. Claude genera con el system prompt, luego usa save_generated_content para guardar.",
|
|
6378
7776
|
{
|
|
6379
|
-
brandId:
|
|
6380
|
-
semana:
|
|
6381
|
-
modo:
|
|
7777
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand"),
|
|
7778
|
+
semana: import_zod37.z.number().optional().describe("Numero de semana (1-5). Default: semana actual del mes."),
|
|
7779
|
+
modo: import_zod37.z.enum(["planificar", "generar"]).optional().describe("'planificar' = proponer distribucion (plataforma+keyword por dia). 'generar' = generar contenido real para slots ya planificados. Default: 'planificar'.")
|
|
6382
7780
|
},
|
|
6383
7781
|
async ({ brandId: inputBrandId, semana, modo }) => {
|
|
6384
7782
|
const tenantId = session.requireTenant();
|
|
@@ -6394,14 +7792,14 @@ function registerMarketingTools(server, session) {
|
|
|
6394
7792
|
|
|
6395
7793
|
IMPORTANTE: Si seleccionaste una foto con get_photos_for_slot, SIEMPRE pasa fotoId aqu\xED. Sin fotoId el post se publica sin imagen.`,
|
|
6396
7794
|
{
|
|
6397
|
-
brandId:
|
|
6398
|
-
plataforma:
|
|
6399
|
-
tipo:
|
|
6400
|
-
keyword:
|
|
6401
|
-
languageCode:
|
|
6402
|
-
fotoId:
|
|
6403
|
-
datos:
|
|
6404
|
-
calendarioItemRef:
|
|
7795
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand"),
|
|
7796
|
+
plataforma: import_zod37.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino"),
|
|
7797
|
+
tipo: import_zod37.z.string().optional().describe("Tipo de contenido (post, blog, carousel, etc.)"),
|
|
7798
|
+
keyword: import_zod37.z.string().optional().describe("Keyword target"),
|
|
7799
|
+
languageCode: import_zod37.z.string().optional().describe("Idioma (es/en)"),
|
|
7800
|
+
fotoId: import_zod37.z.string().optional().describe("ID de la foto a asociar"),
|
|
7801
|
+
datos: import_zod37.z.record(import_zod37.z.string(), import_zod37.z.unknown()).describe("Datos especificos de la plataforma (output de buildDatos*)"),
|
|
7802
|
+
calendarioItemRef: import_zod37.z.string().optional().describe("Referencia al item del calendario")
|
|
6405
7803
|
},
|
|
6406
7804
|
async ({ brandId: inputBrandId, plataforma, tipo, keyword, languageCode, fotoId, datos, calendarioItemRef }) => {
|
|
6407
7805
|
const tenantId = session.requireTenant();
|
|
@@ -6446,13 +7844,13 @@ Usa para: corregir body, metaTitle, tags, fotoId, o cualquier campo sin tener qu
|
|
|
6446
7844
|
NO puede cambiar: tenantId, brandId, id (inmutables).
|
|
6447
7845
|
Si pasas campos dentro de "datos", se hace merge con los datos existentes (no los reemplaza entero).`,
|
|
6448
7846
|
{
|
|
6449
|
-
contenidoId:
|
|
6450
|
-
datos:
|
|
6451
|
-
fotoId:
|
|
6452
|
-
keyword:
|
|
6453
|
-
languageCode:
|
|
6454
|
-
estado:
|
|
6455
|
-
calendarioItemRef:
|
|
7847
|
+
contenidoId: import_zod37.z.string().describe("ID del doc en marketing_contenido"),
|
|
7848
|
+
datos: import_zod37.z.record(import_zod37.z.string(), import_zod37.z.unknown()).optional().describe('Campos de datos a actualizar (merge parcial sobre datos existentes). Ej: { body: "...", metaTitle: "..." }'),
|
|
7849
|
+
fotoId: import_zod37.z.string().nullable().optional().describe("Actualizar foto asociada"),
|
|
7850
|
+
keyword: import_zod37.z.string().nullable().optional().describe("Actualizar keyword"),
|
|
7851
|
+
languageCode: import_zod37.z.string().optional().describe("Actualizar idioma"),
|
|
7852
|
+
estado: import_zod37.z.enum(["borrador", "generado", "pendiente_aprobacion", "aprobado", "rechazado"]).optional().describe("Cambiar estado manualmente"),
|
|
7853
|
+
calendarioItemRef: import_zod37.z.string().nullable().optional().describe("Vincular a un slot del calendario")
|
|
6456
7854
|
},
|
|
6457
7855
|
async ({ contenidoId, datos: newDatos, fotoId, keyword, languageCode, estado, calendarioItemRef }) => {
|
|
6458
7856
|
const tenantId = session.requireTenant();
|
|
@@ -6483,27 +7881,27 @@ Si pasas campos dentro de "datos", se hace merge con los datos existentes (no lo
|
|
|
6483
7881
|
"update_calendar_slot",
|
|
6484
7882
|
"Modifica o agrega un slot del calendario editorial. Si slotIndex >= items.length, agrega un slot nuevo.",
|
|
6485
7883
|
{
|
|
6486
|
-
brandId:
|
|
6487
|
-
mes:
|
|
6488
|
-
semana:
|
|
6489
|
-
slotIndex:
|
|
6490
|
-
cambios:
|
|
6491
|
-
dia:
|
|
6492
|
-
plataforma:
|
|
6493
|
-
tipo:
|
|
6494
|
-
keyword:
|
|
6495
|
-
tema:
|
|
6496
|
-
productoId:
|
|
6497
|
-
estado:
|
|
6498
|
-
contenidoRef:
|
|
6499
|
-
fotoIdAsignada:
|
|
6500
|
-
notas:
|
|
6501
|
-
locationId:
|
|
6502
|
-
locationNombre:
|
|
7884
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand"),
|
|
7885
|
+
mes: import_zod37.z.string().describe("Mes del calendario (YYYY-MM)"),
|
|
7886
|
+
semana: import_zod37.z.number().describe("Numero de semana (1-5)"),
|
|
7887
|
+
slotIndex: import_zod37.z.number().describe("Indice del slot (0-based). Si >= items.length, agrega nuevo slot al final."),
|
|
7888
|
+
cambios: import_zod37.z.object({
|
|
7889
|
+
dia: import_zod37.z.string().nullable().optional(),
|
|
7890
|
+
plataforma: import_zod37.z.enum(["gbp", "shopify_blog", "instagram", "review"]).nullable().optional(),
|
|
7891
|
+
tipo: import_zod37.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]).nullable().optional(),
|
|
7892
|
+
keyword: import_zod37.z.string().nullable().optional(),
|
|
7893
|
+
tema: import_zod37.z.string().nullable().optional(),
|
|
7894
|
+
productoId: import_zod37.z.string().nullable().optional(),
|
|
7895
|
+
estado: import_zod37.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional(),
|
|
7896
|
+
contenidoRef: import_zod37.z.string().nullable().optional(),
|
|
7897
|
+
fotoIdAsignada: import_zod37.z.string().nullable().optional(),
|
|
7898
|
+
notas: import_zod37.z.array(NotaCalendarioSchema).optional(),
|
|
7899
|
+
locationId: import_zod37.z.string().nullable().optional().describe("GBP location ID \u2014 a qu\xE9 sucursal publicar el post"),
|
|
7900
|
+
locationNombre: import_zod37.z.string().nullable().optional().describe("Nombre de la sucursal GBP (para UI)")
|
|
6503
7901
|
}).strict().describe("Campos del slot. Solo acepta campos validos del schema. Campos no reconocidos seran rechazados."),
|
|
6504
|
-
accionContenidoExistente:
|
|
6505
|
-
|
|
6506
|
-
|
|
7902
|
+
accionContenidoExistente: import_zod37.z.union([
|
|
7903
|
+
import_zod37.z.enum(["descartar", "nuevo_slot", "mantener"]),
|
|
7904
|
+
import_zod37.z.string().regex(/^mover:semana:\d+:slot:\d+$/, "Formato: mover:semana:N:slot:M")
|
|
6507
7905
|
]).optional().describe(
|
|
6508
7906
|
"Decision del tenant cuando el slot ya tiene un contenidoRef existente Y los cambios tocan keyword/tema/plataforma/tipo. Requerido en ese escenario (si no se pasa, el helper retorna ACCION_CONTENIDO_EXISTENTE_REQUIRED con las 4 opciones para que preguntes al tenant). Valores: 'descartar' (marca el contenido existente como descartado y aplica cambios al slot) | 'mover:semana:N:slot:M' (mueve el contenido existente al slot destino vacio y aplica cambios al slot origen) | 'nuevo_slot' (NO toca este slot ni su contenido; crea un slot nuevo el mismo dia con los cambios \u2014 util para multiples publicaciones/dia) | 'mantener' (conserva el contenido existente aqui y aplica los cambios igualmente \u2014 caso typo fix)."
|
|
6509
7907
|
)
|
|
@@ -6511,9 +7909,16 @@ Si pasas campos dentro de "datos", se hace merge con los datos existentes (no lo
|
|
|
6511
7909
|
async ({ brandId: inputBrandId, mes, semana, slotIndex, cambios, accionContenidoExistente }) => {
|
|
6512
7910
|
const tenantId = session.requireTenant();
|
|
6513
7911
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
6514
|
-
const
|
|
6515
|
-
const result =
|
|
6516
|
-
|
|
7912
|
+
const ctx = buildMartinContext(session, brandId);
|
|
7913
|
+
const result = await dispatchWithContract({
|
|
7914
|
+
contract: calendarSlotUpdaterContract,
|
|
7915
|
+
helper: calendarSlotUpdater,
|
|
7916
|
+
callable: callCalendarSlotUpdater,
|
|
7917
|
+
input: { tenantId, brandId, mes, semana, slotIndex, cambios, accionContenidoExistente },
|
|
7918
|
+
ctx
|
|
7919
|
+
});
|
|
7920
|
+
const payload = result.state === "success" ? result.structuredOutput : { ok: false, state: result.state, mensaje: result.text };
|
|
7921
|
+
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
6517
7922
|
}
|
|
6518
7923
|
);
|
|
6519
7924
|
server.tool(
|
|
@@ -6526,9 +7931,9 @@ ESCRIBE EN DOS LUGARES:
|
|
|
6526
7931
|
|
|
6527
7932
|
NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agenda, no en el post.`,
|
|
6528
7933
|
{
|
|
6529
|
-
contenidoRef:
|
|
6530
|
-
fotoId:
|
|
6531
|
-
calendarioItemRef:
|
|
7934
|
+
contenidoRef: import_zod37.z.string().describe("ID del doc en marketing_contenido"),
|
|
7935
|
+
fotoId: import_zod37.z.string().describe("ID de la foto en marketing_fotos (estado editada)"),
|
|
7936
|
+
calendarioItemRef: import_zod37.z.string().optional().describe('Ref del slot (formato "semana:N:slot:M") para actualizar fotoIdAsignada')
|
|
6532
7937
|
},
|
|
6533
7938
|
async ({ contenidoRef, fotoId, calendarioItemRef }) => {
|
|
6534
7939
|
const tenantId = session.requireTenant();
|
|
@@ -6541,7 +7946,7 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
|
|
|
6541
7946
|
"approve_content",
|
|
6542
7947
|
"Aprueba contenido para publicacion. Valida transicion de estado.",
|
|
6543
7948
|
{
|
|
6544
|
-
contenidoId:
|
|
7949
|
+
contenidoId: import_zod37.z.string().describe("ID del contenido a aprobar")
|
|
6545
7950
|
},
|
|
6546
7951
|
async ({ contenidoId }) => {
|
|
6547
7952
|
session.requireTenant();
|
|
@@ -6565,8 +7970,8 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
|
|
|
6565
7970
|
"reject_content",
|
|
6566
7971
|
"Rechaza contenido con motivo. Valida transicion de estado.",
|
|
6567
7972
|
{
|
|
6568
|
-
contenidoId:
|
|
6569
|
-
motivo:
|
|
7973
|
+
contenidoId: import_zod37.z.string().describe("ID del contenido a rechazar"),
|
|
7974
|
+
motivo: import_zod37.z.string().describe("Motivo del rechazo")
|
|
6570
7975
|
},
|
|
6571
7976
|
async ({ contenidoId, motivo }) => {
|
|
6572
7977
|
session.requireTenant();
|
|
@@ -6586,44 +7991,44 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
|
|
|
6586
7991
|
}
|
|
6587
7992
|
);
|
|
6588
7993
|
server.tool(
|
|
6589
|
-
"
|
|
6590
|
-
"Lee todas las colecciones
|
|
7994
|
+
"get_collections",
|
|
7995
|
+
"Lee todas las colecciones canonicas (Shopify/WordPress/webonly) con sus 6 campos SEO actuales. Incluye best practices 2026.",
|
|
6591
7996
|
{
|
|
6592
|
-
brandId:
|
|
7997
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand")
|
|
6593
7998
|
},
|
|
6594
7999
|
async ({ brandId: inputBrandId }) => {
|
|
6595
8000
|
const tenantId = session.requireTenant();
|
|
6596
8001
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
6597
|
-
const
|
|
6598
|
-
|
|
8002
|
+
const db = getAdminDb();
|
|
8003
|
+
const collSnap = await db.collection(`tenants/${tenantId}/marketing_snapshots/${brandId}/collections`).get();
|
|
8004
|
+
const items = collSnap.docs.map((d) => d.data());
|
|
8005
|
+
if (items.length === 0) {
|
|
6599
8006
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
6600
8007
|
ok: false,
|
|
6601
|
-
error: "No hay
|
|
8008
|
+
error: "No hay colecciones nested. El tenant debe sincronizar desde Marketing > Configuraci\xF3n > Shopify > Sincronizar."
|
|
6602
8009
|
}) }] };
|
|
6603
8010
|
}
|
|
6604
|
-
const
|
|
6605
|
-
`tenants/${tenantId}/shopify_imports/${lastImportId}/data`,
|
|
6606
|
-
"collections"
|
|
6607
|
-
);
|
|
6608
|
-
const items = collectionsDoc?.items ?? [];
|
|
6609
|
-
const config = await readDoc("marketing_config", tenantId);
|
|
6610
|
-
const brands = config?.brands ?? {};
|
|
6611
|
-
const brand = brands[brandId];
|
|
8011
|
+
const brand = await readDoc(`tenants/${tenantId}/marketing_config`, brandId);
|
|
6612
8012
|
const plan = brand?.plan ?? {};
|
|
6613
8013
|
const keywords = plan.keywordsPrioritarios ?? [];
|
|
6614
8014
|
const existingSuggestions = brand?.collectionSuggestions ?? {};
|
|
6615
|
-
const collections = items.map((c) =>
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
8015
|
+
const collections = items.map((c) => {
|
|
8016
|
+
const seo = c.seo || {};
|
|
8017
|
+
const featured = c.featuredImage || null;
|
|
8018
|
+
return {
|
|
8019
|
+
id: c.platformId,
|
|
8020
|
+
title: c.title,
|
|
8021
|
+
handle: c.handle,
|
|
8022
|
+
body_html: typeof c.descriptionHtml === "string" ? c.descriptionHtml.slice(0, 500) : null,
|
|
8023
|
+
metaTitle: seo.metaTitle ?? null,
|
|
8024
|
+
metaDescription: seo.metaDescription ?? null,
|
|
8025
|
+
products_count: null,
|
|
8026
|
+
// adapter v2 aun no calcula products_count por collection
|
|
8027
|
+
collectionType: "canonical",
|
|
8028
|
+
image: featured ? { src: featured.url, alt: featured.altText } : null,
|
|
8029
|
+
existingSuggestion: existingSuggestions[String(c.platformId)] ?? null
|
|
8030
|
+
};
|
|
8031
|
+
});
|
|
6627
8032
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
6628
8033
|
ok: true,
|
|
6629
8034
|
totalCollections: collections.length,
|
|
@@ -6676,7 +8081,7 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
|
|
|
6676
8081
|
"save_collection_suggestions",
|
|
6677
8082
|
"Guarda sugerencias SEO para colecciones de Shopify. El tenant las aprueba en la UI.",
|
|
6678
8083
|
{
|
|
6679
|
-
brandId:
|
|
8084
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand"),
|
|
6680
8085
|
suggestions: CollectionSuggestionsInputArraySchema.describe("Array de sugerencias SEO. Cada una con max 60 chars en metaTitle, max 158 en metaDescription, max 125 en imageAlt")
|
|
6681
8086
|
},
|
|
6682
8087
|
async ({ brandId: inputBrandId, suggestions }) => {
|