ponch-mcp-server 1.0.71 → 1.0.73
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 +1844 -471
- 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` };
|
|
@@ -2713,7 +3465,7 @@ async function calendarSlotUpdater(input) {
|
|
|
2713
3465
|
if (slotIndex < 0) {
|
|
2714
3466
|
return { ok: false, error: "slotIndex no puede ser negativo" };
|
|
2715
3467
|
}
|
|
2716
|
-
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();
|
|
2717
3469
|
if (calQuery.empty) {
|
|
2718
3470
|
return { ok: false, error: "Calendario no encontrado" };
|
|
2719
3471
|
}
|
|
@@ -2750,7 +3502,7 @@ async function calendarSlotUpdater(input) {
|
|
|
2750
3502
|
}
|
|
2751
3503
|
let contenidosADescartar = [];
|
|
2752
3504
|
if (oldContenidoRef && (accionContenidoExistente === "descartar" || !tocaSemantica)) {
|
|
2753
|
-
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();
|
|
2754
3506
|
contenidosADescartar = contenidoQuery.docs.filter((c) => {
|
|
2755
3507
|
const data = c.data();
|
|
2756
3508
|
return data.estado !== "descartado" && data.estado !== ESTADO_CONTENIDO.PUBLICADO;
|
|
@@ -2810,7 +3562,7 @@ async function calendarSlotUpdater(input) {
|
|
|
2810
3562
|
let movedSlotEstado = null;
|
|
2811
3563
|
if (moveTarget && oldContenidoRef) {
|
|
2812
3564
|
try {
|
|
2813
|
-
const contenidoSnap = await db.
|
|
3565
|
+
const contenidoSnap = await db.doc(`tenants/${tenantId}/marketing_contenido/${oldContenidoRef}`).get();
|
|
2814
3566
|
if (contenidoSnap.exists) {
|
|
2815
3567
|
const estadoContenido = contenidoSnap.data().estado;
|
|
2816
3568
|
movedSlotEstado = mapContenidoEstadoToSlotEstado(estadoContenido);
|
|
@@ -2883,7 +3635,7 @@ async function calendarSlotUpdater(input) {
|
|
|
2883
3635
|
});
|
|
2884
3636
|
if (moveTarget && oldContenidoRef) {
|
|
2885
3637
|
try {
|
|
2886
|
-
await db.
|
|
3638
|
+
await db.doc(`tenants/${tenantId}/marketing_contenido/${oldContenidoRef}`).update({
|
|
2887
3639
|
calendarioItemRef: moveTarget.targetRef
|
|
2888
3640
|
});
|
|
2889
3641
|
} catch (err) {
|
|
@@ -2982,6 +3734,134 @@ async function handleNuevoSlot(args) {
|
|
|
2982
3734
|
descartados: 0
|
|
2983
3735
|
};
|
|
2984
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
|
+
);
|
|
2985
3865
|
var PLATAFORMA_A_FORMATO = {
|
|
2986
3866
|
gbp: "gbp_4_3",
|
|
2987
3867
|
shopify_blog: "blog_3_2",
|
|
@@ -2990,7 +3870,7 @@ var PLATAFORMA_A_FORMATO = {
|
|
|
2990
3870
|
};
|
|
2991
3871
|
async function photoAssigner(input) {
|
|
2992
3872
|
const { db, tenantId, brandId, contenidoRef, fotoId, calendarioItemRef } = input;
|
|
2993
|
-
const contenidoRefDoc = db.
|
|
3873
|
+
const contenidoRefDoc = db.doc(`tenants/${tenantId}/marketing_contenido/${contenidoRef}`);
|
|
2994
3874
|
const contenidoSnap = await contenidoRefDoc.get();
|
|
2995
3875
|
if (!contenidoSnap.exists) {
|
|
2996
3876
|
return { ok: false, error: `Contenido ${contenidoRef} no encontrado` };
|
|
@@ -2999,7 +3879,7 @@ async function photoAssigner(input) {
|
|
|
2999
3879
|
if (contenido.tenantId !== tenantId) {
|
|
3000
3880
|
return { ok: false, error: "Contenido no pertenece a este tenant" };
|
|
3001
3881
|
}
|
|
3002
|
-
const fotoQuery = await db.collection("
|
|
3882
|
+
const fotoQuery = await db.collection("tenants").doc(tenantId).collection("marketing_fotos").where("id", "==", fotoId).limit(1).get();
|
|
3003
3883
|
if (fotoQuery.empty) {
|
|
3004
3884
|
return { ok: false, error: `Foto ${fotoId} no encontrada` };
|
|
3005
3885
|
}
|
|
@@ -3031,7 +3911,7 @@ async function photoAssigner(input) {
|
|
|
3031
3911
|
if (match) {
|
|
3032
3912
|
const semanaNum = parseInt(match[1], 10);
|
|
3033
3913
|
const slotIdx = parseInt(match[2], 10);
|
|
3034
|
-
const calQuery = await db.collection("
|
|
3914
|
+
const calQuery = await db.collection("tenants").doc(tenantId).collection("marketing_calendario").where("brandId", "==", brandId).limit(12).get();
|
|
3035
3915
|
for (const calDoc of calQuery.docs) {
|
|
3036
3916
|
const cal = calDoc.data();
|
|
3037
3917
|
const semanas = cal.semanas ?? [];
|
|
@@ -3088,101 +3968,87 @@ async function photoAssigner(input) {
|
|
|
3088
3968
|
formato
|
|
3089
3969
|
};
|
|
3090
3970
|
}
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
const config = configSnap.data();
|
|
3094
|
-
const brands = config?.brands ?? {};
|
|
3095
|
-
const brand = brands[brandId];
|
|
3096
|
-
const canalId = brand?.canalId;
|
|
3097
|
-
if (!canalId) return null;
|
|
3098
|
-
const canalSnap = await db.collection("canales_venta").doc(canalId).get();
|
|
3099
|
-
const canal = canalSnap.data();
|
|
3100
|
-
const app = canal?.app ?? {};
|
|
3101
|
-
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;
|
|
3102
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;
|
|
3103
3976
|
async function brandBriefBuilder(input) {
|
|
3104
3977
|
const { db, tenantId, brandId } = input;
|
|
3105
|
-
const configSnap = await db.collection("
|
|
3978
|
+
const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
|
|
3106
3979
|
if (!configSnap.exists) {
|
|
3107
|
-
return { ok: false, error: "No hay marketing_config" };
|
|
3108
|
-
}
|
|
3109
|
-
const config = configSnap.data();
|
|
3110
|
-
const brands = config.brands ?? {};
|
|
3111
|
-
const brand = brands[brandId];
|
|
3112
|
-
if (!brand) {
|
|
3113
3980
|
return { ok: false, error: `Brand "${brandId}" no encontrada` };
|
|
3114
3981
|
}
|
|
3115
|
-
const
|
|
3116
|
-
|
|
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),
|
|
3117
3989
|
db.collection("tenants").doc(tenantId).get()
|
|
3118
3990
|
]);
|
|
3119
3991
|
const tenantDoc = tenantSnap.data();
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
const
|
|
3134
|
-
const
|
|
3135
|
-
const
|
|
3136
|
-
|
|
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 {
|
|
3137
4009
|
title: p.title,
|
|
3138
|
-
product_type: p.
|
|
3139
|
-
tags: p.tags,
|
|
3140
|
-
price_min: p.
|
|
3141
|
-
price_max:
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
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,
|
|
3146
4022
|
title: c.title,
|
|
3147
4023
|
handle: c.handle,
|
|
3148
|
-
products_count:
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
const faqEn = sitePages["/pages/faq"] || {};
|
|
3177
|
-
const faqEs = sitePages["/pages/preguntas-frecuentes"] || {};
|
|
3178
|
-
const sitePayload = siteContentDoc ? {
|
|
3179
|
-
homeTitle: home.title || null,
|
|
3180
|
-
homeMeta: home.metaDescription || null,
|
|
3181
|
-
homeH1: home.h1 || null,
|
|
3182
|
-
homeOgDescription: home.ogDescription || null,
|
|
3183
|
-
aboutFirstParagraph: aboutEn.firstParagraph || aboutEs.firstParagraph || null,
|
|
3184
|
-
aboutH1: aboutEn.h1 || aboutEs.h1 || null,
|
|
3185
|
-
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
|
|
3186
4052
|
} : null;
|
|
3187
4053
|
const seo = brand.seoSnapshot;
|
|
3188
4054
|
const gbpPerfiles = brand.gbpPerfiles ?? [];
|
|
@@ -3215,14 +4081,20 @@ async function brandBriefBuilder(input) {
|
|
|
3215
4081
|
}
|
|
3216
4082
|
const topTags = Object.entries(tagCount).sort(([, a], [, b]) => b - a).slice(0, 20).map(([tag, count]) => ({ tag, count }));
|
|
3217
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.`;
|
|
3218
|
-
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
|
+
}));
|
|
3219
4090
|
const payload = {
|
|
3220
4091
|
instruccion: instruccionBase + instruccionSiteContent,
|
|
3221
|
-
|
|
4092
|
+
_lastImportId: lastImportId,
|
|
3222
4093
|
_schema: BRAND_BRIEF_SCHEMA_HINT,
|
|
3223
4094
|
datosDisponibles: {
|
|
3224
4095
|
shopInfo,
|
|
3225
4096
|
sitePayload,
|
|
4097
|
+
availablePages,
|
|
3226
4098
|
precioStats,
|
|
3227
4099
|
productTypes: typeCount,
|
|
3228
4100
|
topTags,
|
|
@@ -3283,11 +4155,51 @@ var BRAND_BRIEF_SCHEMA_HINT = {
|
|
|
3283
4155
|
escenas: [{ id: string, nombre: string, promptHint: string }]
|
|
3284
4156
|
}`
|
|
3285
4157
|
};
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
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();
|
|
3291
4203
|
const canalId = brand?.canalId;
|
|
3292
4204
|
if (!canalId) return null;
|
|
3293
4205
|
const canalSnap = await db.collection("canales_venta").doc(canalId).get();
|
|
@@ -3297,16 +4209,11 @@ async function resolveLastImportId2(db, tenantId, brandId) {
|
|
|
3297
4209
|
}
|
|
3298
4210
|
async function marketingPlanBuilder(input) {
|
|
3299
4211
|
const { db, tenantId, brandId } = input;
|
|
3300
|
-
const configSnap = await db.collection("
|
|
4212
|
+
const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
|
|
3301
4213
|
if (!configSnap.exists) {
|
|
3302
|
-
return { ok: false, error: "No hay marketing_config" };
|
|
3303
|
-
}
|
|
3304
|
-
const config = configSnap.data();
|
|
3305
|
-
const brands = config.brands ?? {};
|
|
3306
|
-
const brand = brands[brandId];
|
|
3307
|
-
if (!brand) {
|
|
3308
4214
|
return { ok: false, error: `Brand "${brandId}" no encontrada` };
|
|
3309
4215
|
}
|
|
4216
|
+
const brand = configSnap.data();
|
|
3310
4217
|
if (!brand.seoSnapshot) {
|
|
3311
4218
|
return {
|
|
3312
4219
|
ok: false,
|
|
@@ -3315,9 +4222,9 @@ async function marketingPlanBuilder(input) {
|
|
|
3315
4222
|
}
|
|
3316
4223
|
const productosQ = await db.collection("productos").where("tenantId", "==", tenantId).limit(100).get();
|
|
3317
4224
|
const productos = productosQ.docs.map((d) => d.data());
|
|
3318
|
-
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();
|
|
3319
4226
|
const historial = histQ.docs.map((d) => d.data());
|
|
3320
|
-
const lastImportId = await
|
|
4227
|
+
const lastImportId = await resolveLastImportId(db, tenantId, brandId);
|
|
3321
4228
|
let colecciones = [];
|
|
3322
4229
|
let shopifyBlogs = [];
|
|
3323
4230
|
if (lastImportId) {
|
|
@@ -3492,13 +4399,11 @@ async function contenidoWriter(input) {
|
|
|
3492
4399
|
calendarioItemRef,
|
|
3493
4400
|
linkPipeline
|
|
3494
4401
|
} = input;
|
|
3495
|
-
const configSnap = await db.collection("
|
|
3496
|
-
const
|
|
3497
|
-
const brands = config?.brands ?? {};
|
|
3498
|
-
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;
|
|
3499
4404
|
const frecuencia = brand?.frecuencia ?? {};
|
|
3500
4405
|
const estadosQueOcupanSlot = ["aprobado", "publicado"];
|
|
3501
|
-
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();
|
|
3502
4407
|
const contenidoEstaSemana = contenidoQ.docs.map((d2) => ({ id: d2.id, ...d2.data() }));
|
|
3503
4408
|
const now = /* @__PURE__ */ new Date();
|
|
3504
4409
|
const startOfWeek = new Date(now);
|
|
@@ -3531,7 +4436,7 @@ async function contenidoWriter(input) {
|
|
|
3531
4436
|
(c) => c.calendarioItemRef === calendarioItemRef && c.estado !== "descartado"
|
|
3532
4437
|
);
|
|
3533
4438
|
for (const existente of existentes) {
|
|
3534
|
-
await db.
|
|
4439
|
+
await db.doc(`tenants/${tenantId}/marketing_contenido/${existente.id}`).update({
|
|
3535
4440
|
estado: "descartado",
|
|
3536
4441
|
rechazadoMotivo: "Reemplazado por contenido nuevo para el mismo slot",
|
|
3537
4442
|
rechazadoAt: import_firebase_admin8.firestore.FieldValue.serverTimestamp()
|
|
@@ -3575,6 +4480,11 @@ async function contenidoWriter(input) {
|
|
|
3575
4480
|
}
|
|
3576
4481
|
}
|
|
3577
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
|
+
}
|
|
3578
4488
|
const contenido = buildContenido({
|
|
3579
4489
|
id,
|
|
3580
4490
|
tenantId,
|
|
@@ -3582,7 +4492,7 @@ async function contenidoWriter(input) {
|
|
|
3582
4492
|
plataforma,
|
|
3583
4493
|
tipo: tipo ?? "post",
|
|
3584
4494
|
keyword: keyword ?? null,
|
|
3585
|
-
languageCode
|
|
4495
|
+
languageCode,
|
|
3586
4496
|
estado: ESTADO_CONTENIDO.PENDIENTE_APROBACION,
|
|
3587
4497
|
fotoId: fotoId ?? null,
|
|
3588
4498
|
mediaUrl: null,
|
|
@@ -3592,14 +4502,14 @@ async function contenidoWriter(input) {
|
|
|
3592
4502
|
creadoPorId: "mcp-cowork",
|
|
3593
4503
|
origen: "ai_assisted"
|
|
3594
4504
|
});
|
|
3595
|
-
const contenidoRef = db.
|
|
4505
|
+
const contenidoRef = db.doc(`tenants/${tenantId}/marketing_contenido/${id}`);
|
|
3596
4506
|
let slotResolved = null;
|
|
3597
4507
|
if (calendarioItemRef) {
|
|
3598
4508
|
const match = calendarioItemRef.match(/^semana:(\d+):slot:(\d+)$/);
|
|
3599
4509
|
if (match) {
|
|
3600
4510
|
const semanaNum = parseInt(match[1], 10);
|
|
3601
4511
|
const slotIdx = parseInt(match[2], 10);
|
|
3602
|
-
const calQ = await db.collection("
|
|
4512
|
+
const calQ = await db.collection("tenants").doc(tenantId).collection("marketing_calendario").where("brandId", "==", brandId).limit(12).get();
|
|
3603
4513
|
for (const calDoc of calQ.docs) {
|
|
3604
4514
|
const cal = calDoc.data();
|
|
3605
4515
|
const semanas = cal.semanas ?? [];
|
|
@@ -3748,14 +4658,12 @@ async function weeklyContentBuilder(input) {
|
|
|
3748
4658
|
const now = /* @__PURE__ */ new Date();
|
|
3749
4659
|
const targetMes = now.toISOString().slice(0, 7);
|
|
3750
4660
|
const targetSemana = semana ?? Math.ceil(now.getDate() / 7);
|
|
3751
|
-
const configSnap = await db.collection("
|
|
4661
|
+
const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
|
|
3752
4662
|
if (!configSnap.exists) {
|
|
3753
|
-
return { ok: false, error: "
|
|
4663
|
+
return { ok: false, error: `Brand "${brandId}" no encontrada` };
|
|
3754
4664
|
}
|
|
3755
|
-
const
|
|
3756
|
-
const
|
|
3757
|
-
const brand = brands[brandId];
|
|
3758
|
-
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();
|
|
3759
4667
|
let calendario = calQuery.empty ? null : { id: calQuery.docs[0].id, ...calQuery.docs[0].data() };
|
|
3760
4668
|
if (!calendario) {
|
|
3761
4669
|
const calId = `${tenantId}_${brandId}_${targetMes}`;
|
|
@@ -3770,14 +4678,14 @@ async function weeklyContentBuilder(input) {
|
|
|
3770
4678
|
creadoPorId: "mcp-cowork",
|
|
3771
4679
|
updatedAt: null
|
|
3772
4680
|
};
|
|
3773
|
-
await db.
|
|
4681
|
+
await db.doc(`tenants/${tenantId}/marketing_calendario/${calId}`).set(calDoc);
|
|
3774
4682
|
calendario = { ...calDoc, semanas };
|
|
3775
4683
|
}
|
|
3776
4684
|
const semanasArr = calendario.semanas ?? [];
|
|
3777
4685
|
const semanaData = semanasArr[targetSemana - 1] ?? null;
|
|
3778
4686
|
const slotsExistentes = semanaData?.items ?? [];
|
|
3779
4687
|
if (targetModo === "planificar") {
|
|
3780
|
-
const histQ2 = await db.collection("
|
|
4688
|
+
const histQ2 = await db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).limit(20).get();
|
|
3781
4689
|
const historial2 = histQ2.docs.map((d) => d.data());
|
|
3782
4690
|
return {
|
|
3783
4691
|
ok: true,
|
|
@@ -3835,9 +4743,9 @@ async function weeklyContentBuilder(input) {
|
|
|
3835
4743
|
}
|
|
3836
4744
|
};
|
|
3837
4745
|
}
|
|
3838
|
-
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();
|
|
3839
4747
|
const fotosDisponibles = fotosQ.docs.map((d) => d.data());
|
|
3840
|
-
const histQ = await db.collection("
|
|
4748
|
+
const histQ = await db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).limit(20).get();
|
|
3841
4749
|
const historial = histQ.docs.map((d) => d.data());
|
|
3842
4750
|
const hasBlogSlots = slotsParaGenerar.some((s) => s.plataforma === "shopify_blog");
|
|
3843
4751
|
let blogJIT = null;
|
|
@@ -3845,7 +4753,7 @@ async function weeklyContentBuilder(input) {
|
|
|
3845
4753
|
const blogStrategy = brand.blogStrategy;
|
|
3846
4754
|
const blogs = blogStrategy?.blogs ?? [];
|
|
3847
4755
|
const idiomas = brand.idiomas;
|
|
3848
|
-
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();
|
|
3849
4757
|
const articulosExistentes = articulosQ.docs.map(
|
|
3850
4758
|
(d) => d.data()
|
|
3851
4759
|
);
|
|
@@ -3881,7 +4789,15 @@ async function weeklyContentBuilder(input) {
|
|
|
3881
4789
|
userId: blogStrategy?.defaultAuthorId ?? null,
|
|
3882
4790
|
name: blogStrategy?.defaultAuthorName ?? null
|
|
3883
4791
|
},
|
|
3884
|
-
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
|
+
})()
|
|
3885
4801
|
});
|
|
3886
4802
|
}
|
|
3887
4803
|
blogJIT = {
|
|
@@ -4014,10 +4930,8 @@ async function contentFinder(input) {
|
|
|
4014
4930
|
const textThreshold = deps.thresholds?.text ?? DEFAULT_TEXT_THRESHOLD;
|
|
4015
4931
|
const imageThreshold = deps.thresholds?.image ?? DEFAULT_IMAGE_THRESHOLD;
|
|
4016
4932
|
const conflictThreshold = deps.thresholds?.conflictSimilarity ?? DEFAULT_CONFLICT_THRESHOLD;
|
|
4017
|
-
const configSnap = await db.collection("
|
|
4018
|
-
const
|
|
4019
|
-
const brands = config?.brands ?? {};
|
|
4020
|
-
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;
|
|
4021
4935
|
const brandBrief = brand?.brandBrief ?? null;
|
|
4022
4936
|
const visualRules = brandBrief?.visualRules ?? {};
|
|
4023
4937
|
const plan = brand?.plan ?? null;
|
|
@@ -4074,22 +4988,22 @@ async function contentFinder(input) {
|
|
|
4074
4988
|
const productsLimit = include.products ? collFilter ? limit.products * 4 : limit.products * 2 : 0;
|
|
4075
4989
|
const [productsRaw, collectionsRaw, articlesRaw, pagesRaw] = await Promise.all([
|
|
4076
4990
|
searchCollection(
|
|
4077
|
-
"
|
|
4991
|
+
"products",
|
|
4078
4992
|
include.products && limit.products > 0,
|
|
4079
4993
|
productsLimit
|
|
4080
4994
|
),
|
|
4081
4995
|
searchCollection(
|
|
4082
|
-
"
|
|
4996
|
+
"collections",
|
|
4083
4997
|
include.collections && limit.collections > 0,
|
|
4084
4998
|
limit.collections * 2
|
|
4085
4999
|
),
|
|
4086
5000
|
searchCollection(
|
|
4087
|
-
"
|
|
5001
|
+
"articles",
|
|
4088
5002
|
include.articles && limit.articles > 0,
|
|
4089
5003
|
limit.articles * 2
|
|
4090
5004
|
),
|
|
4091
5005
|
searchCollection(
|
|
4092
|
-
"
|
|
5006
|
+
"pages",
|
|
4093
5007
|
include.pages && limit.pages > 0,
|
|
4094
5008
|
limit.pages * 2
|
|
4095
5009
|
)
|
|
@@ -4194,16 +5108,11 @@ async function slotAssetFinder(input) {
|
|
|
4194
5108
|
const limit = input.limit ?? 5;
|
|
4195
5109
|
const photoThreshold = deps.thresholds?.photos ?? DEFAULT_PHOTO_THRESHOLD;
|
|
4196
5110
|
const shopifyThreshold = deps.thresholds?.shopify ?? DEFAULT_SHOPIFY_THRESHOLD;
|
|
4197
|
-
const configSnap = await db.collection("
|
|
5111
|
+
const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
|
|
4198
5112
|
if (!configSnap.exists) {
|
|
4199
|
-
return { ok: false, error: `Brand "${brandId}" no encontrada (no marketing_config)` };
|
|
4200
|
-
}
|
|
4201
|
-
const config = configSnap.data();
|
|
4202
|
-
const brands = config.brands ?? {};
|
|
4203
|
-
const brand = brands[brandId] ?? null;
|
|
4204
|
-
if (!brand) {
|
|
4205
5113
|
return { ok: false, error: `Brand "${brandId}" no encontrada` };
|
|
4206
5114
|
}
|
|
5115
|
+
const brand = configSnap.data();
|
|
4207
5116
|
const brandBrief = brand.brandBrief ?? null;
|
|
4208
5117
|
const visualRules = brandBrief?.visualRules ?? {};
|
|
4209
5118
|
const plan = brand.plan ?? null;
|
|
@@ -4229,7 +5138,7 @@ async function slotAssetFinder(input) {
|
|
|
4229
5138
|
try {
|
|
4230
5139
|
const fetchLimit = bloqueoProducto && temporada?.coleccion?.handle ? limit * 4 : limit;
|
|
4231
5140
|
const nearest = await deps.findNearestInCollection({
|
|
4232
|
-
collection: "
|
|
5141
|
+
collection: "products",
|
|
4233
5142
|
tenantId,
|
|
4234
5143
|
brandId,
|
|
4235
5144
|
queryEmbedding: queryEmbedding ?? void 0,
|
|
@@ -4331,16 +5240,11 @@ function cosineSimilarity(a, b) {
|
|
|
4331
5240
|
async function canvaTemplateSelector(input) {
|
|
4332
5241
|
const { db, tenantId, brandId, plataforma, tipoContenido, keyword, deps } = input;
|
|
4333
5242
|
const threshold = deps.threshold ?? DEFAULT_SIMILARITY_THRESHOLD;
|
|
4334
|
-
const configSnap = await db.collection("
|
|
5243
|
+
const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
|
|
4335
5244
|
if (!configSnap.exists) {
|
|
4336
5245
|
return { plantillaId: null, motivo: "brand_no_encontrada" };
|
|
4337
5246
|
}
|
|
4338
|
-
const
|
|
4339
|
-
const brands = config.brands ?? {};
|
|
4340
|
-
const brand = brands[brandId] ?? null;
|
|
4341
|
-
if (!brand) {
|
|
4342
|
-
return { plantillaId: null, motivo: "brand_no_encontrada" };
|
|
4343
|
-
}
|
|
5247
|
+
const brand = configSnap.data();
|
|
4344
5248
|
const canva = brand.canva ?? {};
|
|
4345
5249
|
if (!canva.connected || !Array.isArray(canva.templates) || canva.templates.length === 0) {
|
|
4346
5250
|
return {
|
|
@@ -4498,7 +5402,7 @@ var ERROR_MESSAGES = {
|
|
|
4498
5402
|
en: () => `External infrastructure failure. You can retry ONCE. If it persists, report the error and try another photo.`
|
|
4499
5403
|
}
|
|
4500
5404
|
};
|
|
4501
|
-
function instruccionesParaError(code, details, lang
|
|
5405
|
+
function instruccionesParaError(code, details, lang) {
|
|
4502
5406
|
const messages = ERROR_MESSAGES[code];
|
|
4503
5407
|
if (!messages) {
|
|
4504
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.`;
|
|
@@ -4507,16 +5411,14 @@ function instruccionesParaError(code, details, lang = "es") {
|
|
|
4507
5411
|
}
|
|
4508
5412
|
async function photoDirectorPlan(input) {
|
|
4509
5413
|
const { db, tenantId, fotoId, deps } = input;
|
|
4510
|
-
const fotoQ = await db.collection("
|
|
5414
|
+
const fotoQ = await db.collection("tenants").doc(tenantId).collection("marketing_fotos").where("id", "==", fotoId).limit(1).get();
|
|
4511
5415
|
if (fotoQ.empty) {
|
|
4512
5416
|
return { ok: false, code: "FOTO_NOT_FOUND", error: `Foto ${fotoId} no encontrada` };
|
|
4513
5417
|
}
|
|
4514
5418
|
const foto = fotoQ.docs[0].data();
|
|
4515
5419
|
const brandId = foto.brandId;
|
|
4516
|
-
const configSnap = await db.collection("
|
|
4517
|
-
const
|
|
4518
|
-
const brands = config?.brands ?? {};
|
|
4519
|
-
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;
|
|
4520
5422
|
const brandBrief = (brand?.brandBrief ?? null) || {};
|
|
4521
5423
|
const visualRules = brandBrief.visualRules ?? {};
|
|
4522
5424
|
const catalogoVisual = brandBrief.catalogoVisual;
|
|
@@ -4943,15 +5845,12 @@ Campos comunes de save_generated_content:
|
|
|
4943
5845
|
tipo: string (del slot)
|
|
4944
5846
|
`;
|
|
4945
5847
|
async function buildTenantContext(db, tenantId, brandId) {
|
|
4946
|
-
const configSnap = await db.collection("
|
|
5848
|
+
const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
|
|
4947
5849
|
if (!configSnap.exists) return "";
|
|
4948
|
-
const
|
|
4949
|
-
const brands = config.brands ?? {};
|
|
4950
|
-
const brand = brands[brandId];
|
|
4951
|
-
if (!brand) return "";
|
|
5850
|
+
const brand = configSnap.data();
|
|
4952
5851
|
const [fotosQ, contenidoQ, productosQ] = await Promise.all([
|
|
4953
|
-
db.collection("
|
|
4954
|
-
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(),
|
|
4955
5854
|
db.collection("productos").where("tenantId", "==", tenantId).limit(50).get()
|
|
4956
5855
|
]);
|
|
4957
5856
|
const fotosEditadas = fotosQ.docs.map((d) => d.data());
|
|
@@ -4976,7 +5875,12 @@ Dominio: ${brand.dominio ?? "sin dominio"}
|
|
|
4976
5875
|
ctx += `Tono: ${plan?.tonoMarca ?? "profesional y cercano"}
|
|
4977
5876
|
`;
|
|
4978
5877
|
if (idiomas) {
|
|
4979
|
-
|
|
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}
|
|
4980
5884
|
`;
|
|
4981
5885
|
if (idiomas.distribucion) {
|
|
4982
5886
|
ctx += `Distribucion de idiomas: ${JSON.stringify(idiomas.distribucion)}
|
|
@@ -5155,10 +6059,8 @@ async function buildSystemPrompt(input) {
|
|
|
5155
6059
|
const p2 = PARTE_2_REGLAS.replace("{{SHAPES_BLOCK}}", shapesBlock);
|
|
5156
6060
|
return p1 + p2;
|
|
5157
6061
|
}
|
|
5158
|
-
const configSnap = await db.collection("
|
|
5159
|
-
const
|
|
5160
|
-
const brands = config?.brands ?? {};
|
|
5161
|
-
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;
|
|
5162
6064
|
let prompt = PARTE_1_IDENTIDAD.replace(
|
|
5163
6065
|
"{{brand_nombre}}",
|
|
5164
6066
|
brand?.nombre ?? brandId
|
|
@@ -5170,6 +6072,452 @@ async function buildSystemPrompt(input) {
|
|
|
5170
6072
|
}
|
|
5171
6073
|
return prompt;
|
|
5172
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 (userRol === "super_admin") return true;
|
|
6264
|
+
if (required.includes("*")) return true;
|
|
6265
|
+
return required.includes(userRol);
|
|
6266
|
+
}
|
|
6267
|
+
function wrapWithContract(contract, helper) {
|
|
6268
|
+
return async function wrappedTool(input, ctx) {
|
|
6269
|
+
const startMs = Date.now();
|
|
6270
|
+
const locale = ctx.user.idiomaPreferido;
|
|
6271
|
+
if (!hasRequiredRole(ctx.user.rol, contract.requiredRoles)) {
|
|
6272
|
+
await writeAuditLog({
|
|
6273
|
+
tenantId: ctx.tenantId,
|
|
6274
|
+
brandId: ctx.brandId ?? null,
|
|
6275
|
+
actor: { type: "martin", uid: ctx.user.uid, nombre: ctx.user.nombre },
|
|
6276
|
+
action: contract.auditAction,
|
|
6277
|
+
motivo: `Acceso denegado \u2014 rol '${ctx.user.rol}' no autorizado`,
|
|
6278
|
+
conversacionId: ctx.conversacionId ?? null,
|
|
6279
|
+
status: "error",
|
|
6280
|
+
errorMessage: `Required: ${contract.requiredRoles.join(",")}, got: ${ctx.user.rol}`,
|
|
6281
|
+
durationMs: Date.now() - startMs
|
|
6282
|
+
});
|
|
6283
|
+
return {
|
|
6284
|
+
text: getWrapperMessage("denied_role", locale),
|
|
6285
|
+
structuredOutput: null,
|
|
6286
|
+
state: "denied_role"
|
|
6287
|
+
};
|
|
6288
|
+
}
|
|
6289
|
+
const parseResult = contract.paramsSchema.safeParse(input);
|
|
6290
|
+
if (!parseResult.success) {
|
|
6291
|
+
await writeAuditLog({
|
|
6292
|
+
tenantId: ctx.tenantId,
|
|
6293
|
+
brandId: ctx.brandId ?? null,
|
|
6294
|
+
actor: { type: "martin", uid: ctx.user.uid, nombre: ctx.user.nombre },
|
|
6295
|
+
action: contract.auditAction,
|
|
6296
|
+
motivo: "Input inv\xE1lido \u2014 Zod parse failed",
|
|
6297
|
+
conversacionId: ctx.conversacionId ?? null,
|
|
6298
|
+
status: "error",
|
|
6299
|
+
errorMessage: `Input shape inv\xE1lido: ${parseResult.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`,
|
|
6300
|
+
durationMs: Date.now() - startMs
|
|
6301
|
+
});
|
|
6302
|
+
return {
|
|
6303
|
+
text: getWrapperMessage("input_invalido", locale),
|
|
6304
|
+
structuredOutput: null,
|
|
6305
|
+
state: "error"
|
|
6306
|
+
};
|
|
6307
|
+
}
|
|
6308
|
+
const parsedInput = parseResult.data;
|
|
6309
|
+
if (contract.requiresConfirmation && !ctx.confirmationGranted) {
|
|
6310
|
+
const confirmMsg = contract.martinConfirmationTemplate ? contract.martinConfirmationTemplate(parsedInput, locale) : getWrapperMessage("confirm_default", locale);
|
|
6311
|
+
return {
|
|
6312
|
+
text: confirmMsg,
|
|
6313
|
+
structuredOutput: null,
|
|
6314
|
+
state: "pending_confirmation"
|
|
6315
|
+
};
|
|
6316
|
+
}
|
|
6317
|
+
if (contract.requiresDoubleConfirmation && !ctx.doubleConfirmationGranted) {
|
|
6318
|
+
const msg = contract.martinConfirmationTemplate ? contract.martinConfirmationTemplate(parsedInput, locale) : getWrapperMessage("confirm_default", locale);
|
|
6319
|
+
return {
|
|
6320
|
+
text: msg,
|
|
6321
|
+
structuredOutput: null,
|
|
6322
|
+
state: "pending_double_confirmation"
|
|
6323
|
+
};
|
|
6324
|
+
}
|
|
6325
|
+
for (const quotaName of contract.quotasConsumed) {
|
|
6326
|
+
const check = await checkQuota(
|
|
6327
|
+
ctx.tenantId,
|
|
6328
|
+
quotaName
|
|
6329
|
+
);
|
|
6330
|
+
if (!check.ok) {
|
|
6331
|
+
await writeAuditLog({
|
|
6332
|
+
tenantId: ctx.tenantId,
|
|
6333
|
+
brandId: ctx.brandId ?? null,
|
|
6334
|
+
actor: { type: "martin", uid: ctx.user.uid, nombre: ctx.user.nombre },
|
|
6335
|
+
action: contract.auditAction,
|
|
6336
|
+
motivo: `Quota excedida \u2014 ${quotaName}: ${check.current}/${check.limit}`,
|
|
6337
|
+
conversacionId: ctx.conversacionId ?? null,
|
|
6338
|
+
status: "error",
|
|
6339
|
+
errorMessage: check.reason ?? "quota exceeded",
|
|
6340
|
+
durationMs: Date.now() - startMs
|
|
6341
|
+
});
|
|
6342
|
+
return {
|
|
6343
|
+
text: martinSafeError(new Error(check.reason ?? "quota"), locale),
|
|
6344
|
+
structuredOutput: null,
|
|
6345
|
+
state: "quota_exceeded"
|
|
6346
|
+
};
|
|
6347
|
+
}
|
|
6348
|
+
}
|
|
6349
|
+
let output;
|
|
6350
|
+
try {
|
|
6351
|
+
output = await helper(parsedInput);
|
|
6352
|
+
contract.outputSchema.parse(output);
|
|
6353
|
+
} catch (err) {
|
|
6354
|
+
await writeAuditLog({
|
|
6355
|
+
tenantId: ctx.tenantId,
|
|
6356
|
+
brandId: ctx.brandId ?? null,
|
|
6357
|
+
actor: { type: "martin", uid: ctx.user.uid, nombre: ctx.user.nombre },
|
|
6358
|
+
action: contract.auditAction,
|
|
6359
|
+
motivo: "Acci\xF3n solicitada v\xEDa Martin",
|
|
6360
|
+
conversacionId: ctx.conversacionId ?? null,
|
|
6361
|
+
status: "error",
|
|
6362
|
+
errorMessage: err?.message ?? "unknown",
|
|
6363
|
+
durationMs: Date.now() - startMs
|
|
6364
|
+
});
|
|
6365
|
+
return {
|
|
6366
|
+
text: martinSafeError(err, locale),
|
|
6367
|
+
structuredOutput: null,
|
|
6368
|
+
state: "error"
|
|
6369
|
+
};
|
|
6370
|
+
}
|
|
6371
|
+
const maybeDisabled = output;
|
|
6372
|
+
if (maybeDisabled.disabled === true) {
|
|
6373
|
+
const code = maybeDisabled.code;
|
|
6374
|
+
if (!code) {
|
|
6375
|
+
throw new Error(
|
|
6376
|
+
`Wrapper: helper "${contract.name}" retorn\xF3 disabled:true pero sin 'code'. Helper debe retornar { disabled:true, code, detail? }.`
|
|
6377
|
+
);
|
|
6378
|
+
}
|
|
6379
|
+
if (contract.disabledReasonCodes && !contract.disabledReasonCodes.includes(code)) {
|
|
6380
|
+
throw new Error(
|
|
6381
|
+
`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.`
|
|
6382
|
+
);
|
|
6383
|
+
}
|
|
6384
|
+
const detail = maybeDisabled.detail;
|
|
6385
|
+
const disabledText = getDisabledMessage(code, locale, detail);
|
|
6386
|
+
await writeAuditLog({
|
|
6387
|
+
tenantId: ctx.tenantId,
|
|
6388
|
+
brandId: ctx.brandId ?? null,
|
|
6389
|
+
actor: { type: "martin", uid: ctx.user.uid, nombre: ctx.user.nombre },
|
|
6390
|
+
action: contract.auditAction,
|
|
6391
|
+
motivo: `disabled \u2014 ${code}${detail ? `: ${JSON.stringify(detail)}` : ""}`,
|
|
6392
|
+
conversacionId: ctx.conversacionId ?? null,
|
|
6393
|
+
status: "partial",
|
|
6394
|
+
durationMs: Date.now() - startMs
|
|
6395
|
+
});
|
|
6396
|
+
return {
|
|
6397
|
+
text: disabledText,
|
|
6398
|
+
structuredOutput: output,
|
|
6399
|
+
state: "disabled",
|
|
6400
|
+
disabledReason: code
|
|
6401
|
+
};
|
|
6402
|
+
}
|
|
6403
|
+
const targetPath = contract.extractTargetPath ? contract.extractTargetPath(parsedInput, output) : "";
|
|
6404
|
+
const changes = contract.extractChanges ? contract.extractChanges(parsedInput, output) : { before: null, after: null };
|
|
6405
|
+
await writeAuditLog({
|
|
6406
|
+
tenantId: ctx.tenantId,
|
|
6407
|
+
brandId: ctx.brandId ?? null,
|
|
6408
|
+
actor: { type: "martin", uid: ctx.user.uid, nombre: ctx.user.nombre },
|
|
6409
|
+
action: contract.auditAction,
|
|
6410
|
+
targetPath,
|
|
6411
|
+
changes,
|
|
6412
|
+
motivo: "Acci\xF3n solicitada v\xEDa Martin",
|
|
6413
|
+
conversacionId: ctx.conversacionId ?? null,
|
|
6414
|
+
status: "success",
|
|
6415
|
+
durationMs: Date.now() - startMs
|
|
6416
|
+
});
|
|
6417
|
+
const summary = contract.martinSummaryTemplate(parsedInput, output, locale);
|
|
6418
|
+
return {
|
|
6419
|
+
text: summary,
|
|
6420
|
+
structuredOutput: output,
|
|
6421
|
+
state: "success"
|
|
6422
|
+
};
|
|
6423
|
+
};
|
|
6424
|
+
}
|
|
6425
|
+
var RecordarMemoriaParamsSchema = import_zod33.z.object({
|
|
6426
|
+
tipo: TipoMemoriaEnum,
|
|
6427
|
+
categoria: CategoriaMemoriaEnum,
|
|
6428
|
+
contenido: import_zod33.z.string().min(3).max(500)
|
|
6429
|
+
});
|
|
6430
|
+
var RecordarMemoriaOutputSchema = import_zod33.z.object({
|
|
6431
|
+
memoriaId: import_zod33.z.string(),
|
|
6432
|
+
status: import_zod33.z.literal("creada")
|
|
6433
|
+
});
|
|
6434
|
+
var OlvidarMemoriaParamsSchema = import_zod33.z.object({
|
|
6435
|
+
memoriaId: import_zod33.z.string(),
|
|
6436
|
+
motivo: import_zod33.z.string().optional()
|
|
6437
|
+
});
|
|
6438
|
+
var OlvidarMemoriaOutputSchema = import_zod33.z.object({
|
|
6439
|
+
status: import_zod33.z.literal("archivada")
|
|
6440
|
+
});
|
|
6441
|
+
var ConfigInputSchema = import_zod34.z.object({
|
|
6442
|
+
diaSemana: import_zod34.z.number().int().min(0).max(6).nullable(),
|
|
6443
|
+
diaMes: import_zod34.z.number().int().min(1).max(31).nullable(),
|
|
6444
|
+
hora: import_zod34.z.string().regex(/^\d{2}:\d{2}$/),
|
|
6445
|
+
// Timezone NO viene en input — hereda de tenants/{tid}.zonaHoraria (audit fix 2).
|
|
6446
|
+
fechaPuntual: import_zod34.z.string().datetime({ offset: true }).nullable()
|
|
6447
|
+
}).strict();
|
|
6448
|
+
var AccionInputSchema = import_zod34.z.object({
|
|
6449
|
+
tool: import_zod34.z.string().min(1),
|
|
6450
|
+
params: import_zod34.z.record(import_zod34.z.string(), import_zod34.z.unknown())
|
|
6451
|
+
}).strict();
|
|
6452
|
+
var ProgramarRutinaParamsSchema = import_zod34.z.object({
|
|
6453
|
+
tipo: TipoRutinaEnum,
|
|
6454
|
+
frecuencia: FrecuenciaRutinaEnum,
|
|
6455
|
+
config: ConfigInputSchema,
|
|
6456
|
+
accion: AccionInputSchema,
|
|
6457
|
+
uidDestinatario: import_zod34.z.string()
|
|
6458
|
+
});
|
|
6459
|
+
var ProgramarRutinaOutputSchema = import_zod34.z.object({
|
|
6460
|
+
rutinaId: import_zod34.z.string(),
|
|
6461
|
+
proximaEjecucionAt: import_zod34.z.string().datetime()
|
|
6462
|
+
});
|
|
6463
|
+
var PausarRutinaParamsSchema = import_zod34.z.object({
|
|
6464
|
+
rutinaId: import_zod34.z.string(),
|
|
6465
|
+
motivo: import_zod34.z.string().optional()
|
|
6466
|
+
});
|
|
6467
|
+
var PausarRutinaOutputSchema = import_zod34.z.object({
|
|
6468
|
+
status: import_zod34.z.literal("pausada")
|
|
6469
|
+
});
|
|
6470
|
+
var ArchivarRutinaParamsSchema = import_zod34.z.object({
|
|
6471
|
+
rutinaId: import_zod34.z.string(),
|
|
6472
|
+
motivo: import_zod34.z.string().optional()
|
|
6473
|
+
});
|
|
6474
|
+
var ArchivarRutinaOutputSchema = import_zod34.z.object({
|
|
6475
|
+
status: import_zod34.z.literal("archivada")
|
|
6476
|
+
});
|
|
6477
|
+
var ListarRutinasParamsSchema = import_zod34.z.object({
|
|
6478
|
+
uid: import_zod34.z.string().optional()
|
|
6479
|
+
});
|
|
6480
|
+
var ListarRutinasOutputSchema = import_zod34.z.object({
|
|
6481
|
+
rutinas: import_zod34.z.array(MartinRutinaSchema)
|
|
6482
|
+
});
|
|
6483
|
+
|
|
6484
|
+
// src/tools/martinContext.ts
|
|
6485
|
+
function buildMartinContext(session, brandId, opts = {}) {
|
|
6486
|
+
return {
|
|
6487
|
+
tenantId: session.requireTenant(),
|
|
6488
|
+
brandId,
|
|
6489
|
+
user: {
|
|
6490
|
+
uid: session.userId ?? "cowork-admin",
|
|
6491
|
+
nombre: session.userName ?? "Cowork Admin",
|
|
6492
|
+
rol: session.rol ?? "super_admin",
|
|
6493
|
+
idiomaPreferido: "es"
|
|
6494
|
+
// TODO HITO 8: leer usuarios/{uid}.idiomaPreferido
|
|
6495
|
+
},
|
|
6496
|
+
conversacionId: opts.conversacionId ?? null,
|
|
6497
|
+
confirmationGranted: opts.confirmationGranted ?? true,
|
|
6498
|
+
doubleConfirmationGranted: opts.doubleConfirmationGranted ?? false
|
|
6499
|
+
};
|
|
6500
|
+
}
|
|
6501
|
+
async function dispatchWithContract(args) {
|
|
6502
|
+
const { contract, helper, callable, input, ctx } = args;
|
|
6503
|
+
if (getMode() === "admin") {
|
|
6504
|
+
const db = getAdminDb();
|
|
6505
|
+
const wrapped = wrapWithContract(
|
|
6506
|
+
contract,
|
|
6507
|
+
async (i) => helper({ ...i, db })
|
|
6508
|
+
);
|
|
6509
|
+
return wrapped(input, ctx);
|
|
6510
|
+
}
|
|
6511
|
+
return callable({
|
|
6512
|
+
...input,
|
|
6513
|
+
_martinContext: {
|
|
6514
|
+
conversacionId: ctx.conversacionId ?? null,
|
|
6515
|
+
confirmationGranted: ctx.confirmationGranted === true,
|
|
6516
|
+
doubleConfirmationGranted: ctx.doubleConfirmationGranted === true,
|
|
6517
|
+
userIdiomaPreferido: ctx.user.idiomaPreferido
|
|
6518
|
+
}
|
|
6519
|
+
});
|
|
6520
|
+
}
|
|
5173
6521
|
|
|
5174
6522
|
// src/services/marketingHelperCallables.ts
|
|
5175
6523
|
var import_app3 = require("firebase/app");
|
|
@@ -5189,6 +6537,9 @@ async function callCF(name, input) {
|
|
|
5189
6537
|
const result = await fn(input);
|
|
5190
6538
|
return result.data;
|
|
5191
6539
|
}
|
|
6540
|
+
function callGetCalendar(input) {
|
|
6541
|
+
return callCF("marketingGetCalendarCallable", input);
|
|
6542
|
+
}
|
|
5192
6543
|
function callBrandBriefWriter(input) {
|
|
5193
6544
|
return callCF("marketingBrandBriefWriterCallable", input);
|
|
5194
6545
|
}
|
|
@@ -5239,7 +6590,7 @@ function callPhotoDirectorExecute(input) {
|
|
|
5239
6590
|
}
|
|
5240
6591
|
|
|
5241
6592
|
// src/tools/marketing/photos.ts
|
|
5242
|
-
var
|
|
6593
|
+
var import_zod36 = require("zod");
|
|
5243
6594
|
|
|
5244
6595
|
// src/services/marketingEmbeddings.ts
|
|
5245
6596
|
var import_google_auth_library = require("google-auth-library");
|
|
@@ -5325,7 +6676,7 @@ var MARKETING_THRESHOLDS = {
|
|
|
5325
6676
|
gbp: 0.4
|
|
5326
6677
|
},
|
|
5327
6678
|
content: {
|
|
5328
|
-
// find_content_for_topic busca text→text en
|
|
6679
|
+
// find_content_for_topic busca text→text en products / collections /
|
|
5329
6680
|
// articles / pages. Scores reales de top-10 andan 0.45-0.50, umbral 0.35
|
|
5330
6681
|
// deja pasar los relevantes sin ser laxo.
|
|
5331
6682
|
text: 0.35,
|
|
@@ -5412,12 +6763,12 @@ async function findNearestInCollection(params) {
|
|
|
5412
6763
|
);
|
|
5413
6764
|
}
|
|
5414
6765
|
const db = getAdminDb();
|
|
5415
|
-
const
|
|
6766
|
+
const FieldValue3 = import_firebase_admin.default.firestore.FieldValue;
|
|
5416
6767
|
const fetchLimit = extraFilters.length > 0 ? Math.max(limit * 4, 20) : limit;
|
|
5417
6768
|
const baseQ = db.collection(collection).where("tenantId", "==", tenantId).where("brandId", "==", brandId);
|
|
5418
6769
|
const q = baseQ.findNearest({
|
|
5419
6770
|
vectorField,
|
|
5420
|
-
queryVector:
|
|
6771
|
+
queryVector: FieldValue3.vector(queryEmbedding),
|
|
5421
6772
|
limit: fetchLimit,
|
|
5422
6773
|
distanceMeasure: "COSINE",
|
|
5423
6774
|
distanceResultField: "_distance"
|
|
@@ -5507,7 +6858,7 @@ async function findNearestInCollectionWithOverride(params) {
|
|
|
5507
6858
|
}
|
|
5508
6859
|
|
|
5509
6860
|
// src/tools/marketing/content.ts
|
|
5510
|
-
var
|
|
6861
|
+
var import_zod35 = require("zod");
|
|
5511
6862
|
var _logOverride = null;
|
|
5512
6863
|
async function logToMcpLogs(entry) {
|
|
5513
6864
|
if (_logOverride) return _logOverride(entry);
|
|
@@ -5520,22 +6871,22 @@ async function logToMcpLogs(entry) {
|
|
|
5520
6871
|
} catch {
|
|
5521
6872
|
}
|
|
5522
6873
|
}
|
|
5523
|
-
var IncludeSchema =
|
|
5524
|
-
products:
|
|
5525
|
-
collections:
|
|
5526
|
-
articles:
|
|
5527
|
-
pages:
|
|
6874
|
+
var IncludeSchema = import_zod35.z.object({
|
|
6875
|
+
products: import_zod35.z.boolean().default(true),
|
|
6876
|
+
collections: import_zod35.z.boolean().default(true),
|
|
6877
|
+
articles: import_zod35.z.boolean().default(true),
|
|
6878
|
+
pages: import_zod35.z.boolean().default(false)
|
|
5528
6879
|
}).default({
|
|
5529
6880
|
products: true,
|
|
5530
6881
|
collections: true,
|
|
5531
6882
|
articles: true,
|
|
5532
6883
|
pages: false
|
|
5533
6884
|
});
|
|
5534
|
-
var LimitSchema =
|
|
5535
|
-
products:
|
|
5536
|
-
collections:
|
|
5537
|
-
articles:
|
|
5538
|
-
pages:
|
|
6885
|
+
var LimitSchema = import_zod35.z.object({
|
|
6886
|
+
products: import_zod35.z.number().int().min(0).max(20).default(5),
|
|
6887
|
+
collections: import_zod35.z.number().int().min(0).max(10).default(3),
|
|
6888
|
+
articles: import_zod35.z.number().int().min(0).max(20).default(5),
|
|
6889
|
+
pages: import_zod35.z.number().int().min(0).max(10).default(2)
|
|
5539
6890
|
}).default({ products: 5, collections: 3, articles: 5, pages: 2 });
|
|
5540
6891
|
async function findContentForTopicHandler(input, session) {
|
|
5541
6892
|
const tenantId = session.requireTenant();
|
|
@@ -5621,13 +6972,13 @@ MODOS (parametro mode):
|
|
|
5621
6972
|
|
|
5622
6973
|
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.`,
|
|
5623
6974
|
{
|
|
5624
|
-
brandId:
|
|
5625
|
-
contexto:
|
|
5626
|
-
fecha:
|
|
6975
|
+
brandId: import_zod35.z.string().optional().describe("ID de la brand"),
|
|
6976
|
+
contexto: import_zod35.z.string().min(1).describe("Parrafo, keyword o intencion"),
|
|
6977
|
+
fecha: import_zod35.z.string().describe("Fecha del contenido en ISO YYYY-MM-DD"),
|
|
5627
6978
|
include: IncludeSchema.optional(),
|
|
5628
6979
|
limit: LimitSchema.optional(),
|
|
5629
|
-
diversidad:
|
|
5630
|
-
mode:
|
|
6980
|
+
diversidad: import_zod35.z.boolean().default(true),
|
|
6981
|
+
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)")
|
|
5631
6982
|
},
|
|
5632
6983
|
async ({ brandId: inputBrandId, contexto, fecha, include, limit, diversidad, mode: mode2 }) => {
|
|
5633
6984
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
@@ -5731,11 +7082,11 @@ REGLAS:
|
|
|
5731
7082
|
|
|
5732
7083
|
USAR: antes de generar contenido de cualquier slot del calendario.`,
|
|
5733
7084
|
{
|
|
5734
|
-
brandId:
|
|
5735
|
-
keyword:
|
|
5736
|
-
plataforma:
|
|
5737
|
-
fecha:
|
|
5738
|
-
limit:
|
|
7085
|
+
brandId: import_zod36.z.string().optional().describe("ID de la brand"),
|
|
7086
|
+
keyword: import_zod36.z.string().describe("Keyword del slot"),
|
|
7087
|
+
plataforma: import_zod36.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino del slot"),
|
|
7088
|
+
fecha: import_zod36.z.string().describe("Fecha del slot en ISO YYYY-MM-DD"),
|
|
7089
|
+
limit: import_zod36.z.number().int().min(1).max(10).default(5)
|
|
5739
7090
|
},
|
|
5740
7091
|
async ({ brandId: inputBrandId, keyword, plataforma, fecha, limit }) => {
|
|
5741
7092
|
const tenantId = session.requireTenant();
|
|
@@ -5776,15 +7127,17 @@ DESPUES de ver la foto, decide:
|
|
|
5776
7127
|
|
|
5777
7128
|
Luego llama execute_photo_edit con tu analisis y prompt.`,
|
|
5778
7129
|
{
|
|
5779
|
-
fotoId:
|
|
7130
|
+
fotoId: import_zod36.z.string().describe("ID de la foto")
|
|
5780
7131
|
},
|
|
5781
7132
|
async ({ fotoId }) => {
|
|
5782
7133
|
const tenantId = session.requireTenant();
|
|
7134
|
+
const lang = await resolveTenantIdioma(tenantId);
|
|
5783
7135
|
const result = getMode() === "admin" ? await photoDirectorPlan({
|
|
5784
7136
|
db: getAdminDb(),
|
|
5785
7137
|
tenantId,
|
|
5786
7138
|
fotoId,
|
|
5787
|
-
deps: { compressImageForTransport }
|
|
7139
|
+
deps: { compressImageForTransport },
|
|
7140
|
+
lang
|
|
5788
7141
|
}) : await callPhotoDirectorPlan({ tenantId, fotoId });
|
|
5789
7142
|
if (!result.ok) {
|
|
5790
7143
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
@@ -5807,16 +7160,18 @@ Retorna la foto editada para que la revises. Si no te gusta:
|
|
|
5807
7160
|
|
|
5808
7161
|
Si estrategia era 'tal_cual', pasa acciones=['none'] \u2014 no se edita pero si se genera thumbnail y embedding con tus tags.`,
|
|
5809
7162
|
{
|
|
5810
|
-
fotoId:
|
|
5811
|
-
prompt:
|
|
5812
|
-
acciones:
|
|
5813
|
-
descripcion:
|
|
5814
|
-
tipo:
|
|
5815
|
-
tagsPrimarios:
|
|
5816
|
-
tagsSecundarios:
|
|
5817
|
-
tagsContexto:
|
|
7163
|
+
fotoId: import_zod36.z.string(),
|
|
7164
|
+
prompt: import_zod36.z.string().nullable().describe("Prompt en ingles para Gemini Image Edit. null si tal_cual"),
|
|
7165
|
+
acciones: import_zod36.z.array(import_zod36.z.enum(["edit_background", "none"])),
|
|
7166
|
+
descripcion: import_zod36.z.string().describe("Descripcion semantica en espanol"),
|
|
7167
|
+
tipo: import_zod36.z.string().nullable().describe("Tipo del catalogoVisual del tenant"),
|
|
7168
|
+
tagsPrimarios: import_zod36.z.array(import_zod36.z.string()),
|
|
7169
|
+
tagsSecundarios: import_zod36.z.array(import_zod36.z.string()),
|
|
7170
|
+
tagsContexto: import_zod36.z.array(import_zod36.z.string())
|
|
5818
7171
|
},
|
|
5819
7172
|
async ({ fotoId, prompt, acciones, descripcion, tipo, tagsPrimarios, tagsSecundarios, tagsContexto }) => {
|
|
7173
|
+
const tenantId = session.requireTenant();
|
|
7174
|
+
const lang = await resolveTenantIdioma(tenantId);
|
|
5820
7175
|
if (getMode() === "admin") {
|
|
5821
7176
|
const executePhotoEditAdapter = async (payload) => {
|
|
5822
7177
|
const cfUrl = process.env.MARKETING_PHOTO_EDIT_CF_URL || "https://us-central1-atteyo-ops.cloudfunctions.net/marketingExecutePhotoEdit";
|
|
@@ -5840,7 +7195,8 @@ Si estrategia era 'tal_cual', pasa acciones=['none'] \u2014 no se edita pero si
|
|
|
5840
7195
|
tagsPrimarios,
|
|
5841
7196
|
tagsSecundarios,
|
|
5842
7197
|
tagsContexto,
|
|
5843
|
-
deps: { compressImageForTransport, executePhotoEdit: executePhotoEditAdapter }
|
|
7198
|
+
deps: { compressImageForTransport, executePhotoEdit: executePhotoEditAdapter },
|
|
7199
|
+
lang
|
|
5844
7200
|
});
|
|
5845
7201
|
if (!result2.ok) {
|
|
5846
7202
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
@@ -5854,7 +7210,7 @@ Si estrategia era 'tal_cual', pasa acciones=['none'] \u2014 no se edita pero si
|
|
|
5854
7210
|
return { content: contentArray2 };
|
|
5855
7211
|
}
|
|
5856
7212
|
const result = await callPhotoDirectorExecute({
|
|
5857
|
-
tenantId
|
|
7213
|
+
tenantId,
|
|
5858
7214
|
fotoId,
|
|
5859
7215
|
prompt,
|
|
5860
7216
|
acciones,
|
|
@@ -5888,7 +7244,7 @@ Retorna:
|
|
|
5888
7244
|
- creditos: { balance, planId, periodEnd, costoPhotoEdit }
|
|
5889
7245
|
- _instrucciones: que hacer segun el estado`,
|
|
5890
7246
|
{
|
|
5891
|
-
fotoId:
|
|
7247
|
+
fotoId: import_zod36.z.string().describe("ID de la foto")
|
|
5892
7248
|
},
|
|
5893
7249
|
async ({ fotoId }) => {
|
|
5894
7250
|
const tenantId = session.requireTenant();
|
|
@@ -5931,7 +7287,7 @@ Retorna:
|
|
|
5931
7287
|
};
|
|
5932
7288
|
}
|
|
5933
7289
|
}
|
|
5934
|
-
const creditsDoc = await readDoc(
|
|
7290
|
+
const creditsDoc = await readDoc(`tenants/${tenantId}/brand_credits`, brandId);
|
|
5935
7291
|
const balance = creditsDoc?.balance ?? 0;
|
|
5936
7292
|
const planId = creditsDoc?.planId ?? null;
|
|
5937
7293
|
const periodEnd = creditsDoc?.periodEnd ?? null;
|
|
@@ -5988,11 +7344,11 @@ Retorna:
|
|
|
5988
7344
|
"find_products_for_content",
|
|
5989
7345
|
`[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.`,
|
|
5990
7346
|
{
|
|
5991
|
-
brandId:
|
|
5992
|
-
contexto:
|
|
5993
|
-
fecha:
|
|
5994
|
-
limit:
|
|
5995
|
-
diversidad:
|
|
7347
|
+
brandId: import_zod36.z.string().optional().describe("ID de la brand"),
|
|
7348
|
+
contexto: import_zod36.z.string().describe("Parrafo, keyword o intencion del contenido"),
|
|
7349
|
+
fecha: import_zod36.z.string().describe("Fecha del contenido en ISO YYYY-MM-DD"),
|
|
7350
|
+
limit: import_zod36.z.number().int().min(1).max(10).default(5),
|
|
7351
|
+
diversidad: import_zod36.z.boolean().default(true)
|
|
5996
7352
|
},
|
|
5997
7353
|
async ({ brandId: inputBrandId, contexto, fecha, limit, diversidad }) => {
|
|
5998
7354
|
console.warn(
|
|
@@ -6043,12 +7399,12 @@ Retorna:
|
|
|
6043
7399
|
|
|
6044
7400
|
RETORNA { plantillaId, titulo, thumbnailUrl } o { plantillaId: null, motivo: 'no_canva' } si tenant no tiene Canva conectado.
|
|
6045
7401
|
|
|
6046
|
-
USAR: solo si tenant tiene Canva conectado (marketing_config
|
|
7402
|
+
USAR: solo si tenant tiene Canva conectado (tenants/{tenantId}/marketing_config/{brandId}.canva.connected=true).`,
|
|
6047
7403
|
{
|
|
6048
|
-
brandId:
|
|
6049
|
-
plataforma:
|
|
6050
|
-
tipoContenido:
|
|
6051
|
-
keyword:
|
|
7404
|
+
brandId: import_zod36.z.string().optional().describe("ID de la brand"),
|
|
7405
|
+
plataforma: import_zod36.z.string().describe("gbp | instagram | shopify_blog"),
|
|
7406
|
+
tipoContenido: import_zod36.z.string().describe("post | carousel | story | blog"),
|
|
7407
|
+
keyword: import_zod36.z.string().describe("Keyword del slot")
|
|
6052
7408
|
},
|
|
6053
7409
|
async ({ brandId: inputBrandId, plataforma, tipoContenido, keyword }) => {
|
|
6054
7410
|
const tenantId = session.requireTenant();
|
|
@@ -6078,15 +7434,15 @@ USAR: cuando get_photos_for_slot retorna pocas fotos y el slot aun no esta cubie
|
|
|
6078
7434
|
|
|
6079
7435
|
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.`,
|
|
6080
7436
|
{
|
|
6081
|
-
brandId:
|
|
6082
|
-
semana:
|
|
6083
|
-
necesidades:
|
|
6084
|
-
|
|
6085
|
-
tema:
|
|
6086
|
-
keyword:
|
|
6087
|
-
cantidadSugerida:
|
|
6088
|
-
razon:
|
|
6089
|
-
slotsAfectados:
|
|
7437
|
+
brandId: import_zod36.z.string().optional().describe("ID de la brand"),
|
|
7438
|
+
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"),
|
|
7439
|
+
necesidades: import_zod36.z.array(
|
|
7440
|
+
import_zod36.z.object({
|
|
7441
|
+
tema: import_zod36.z.string(),
|
|
7442
|
+
keyword: import_zod36.z.string(),
|
|
7443
|
+
cantidadSugerida: import_zod36.z.number().int().positive(),
|
|
7444
|
+
razon: import_zod36.z.string(),
|
|
7445
|
+
slotsAfectados: import_zod36.z.array(import_zod36.z.string()).optional().describe('Refs de slots afectados (formato "semana:N:slot:M")')
|
|
6090
7446
|
})
|
|
6091
7447
|
).min(1)
|
|
6092
7448
|
},
|
|
@@ -6225,16 +7581,6 @@ async function linkContenidoAPasoPipeline(params) {
|
|
|
6225
7581
|
}
|
|
6226
7582
|
|
|
6227
7583
|
// src/tools/marketing.ts
|
|
6228
|
-
async function resolveLastImportId3(tenantId, brandId) {
|
|
6229
|
-
const config = await readDoc("marketing_config", tenantId);
|
|
6230
|
-
const brands = config?.brands ?? {};
|
|
6231
|
-
const brand = brands[brandId];
|
|
6232
|
-
const canalId = brand?.canalId;
|
|
6233
|
-
if (!canalId) return null;
|
|
6234
|
-
const canal = await readDoc("canales_venta", canalId);
|
|
6235
|
-
const app = canal?.app ?? {};
|
|
6236
|
-
return app.lastImportId ?? null;
|
|
6237
|
-
}
|
|
6238
7584
|
function registerMarketingTools(server, session) {
|
|
6239
7585
|
registerPhotoTools(server, session);
|
|
6240
7586
|
registerContentTools(server, session);
|
|
@@ -6242,48 +7588,46 @@ function registerMarketingTools(server, session) {
|
|
|
6242
7588
|
"get_calendar",
|
|
6243
7589
|
"Lee el calendario editorial del mes. Muestra semanas con items planificados por plataforma.",
|
|
6244
7590
|
{
|
|
6245
|
-
brandId:
|
|
6246
|
-
mes:
|
|
7591
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand"),
|
|
7592
|
+
mes: import_zod37.z.string().optional().describe("Mes en formato YYYY-MM (default: mes actual)")
|
|
6247
7593
|
},
|
|
6248
7594
|
async ({ brandId: inputBrandId, mes }) => {
|
|
6249
|
-
session.requireTenant();
|
|
7595
|
+
const tenantId = session.requireTenant();
|
|
6250
7596
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
6251
7597
|
const targetMes = mes ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
|
|
6252
|
-
const
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
7598
|
+
const ctx = buildMartinContext(session, brandId);
|
|
7599
|
+
const result = await dispatchWithContract({
|
|
7600
|
+
contract: getCalendarContract,
|
|
7601
|
+
helper: getCalendar,
|
|
7602
|
+
callable: callGetCalendar,
|
|
7603
|
+
input: { tenantId, brandId, mes: targetMes },
|
|
7604
|
+
ctx
|
|
7605
|
+
});
|
|
7606
|
+
let payload;
|
|
7607
|
+
if (result.state === "success" && result.structuredOutput) {
|
|
7608
|
+
const out = result.structuredOutput;
|
|
7609
|
+
payload = out.calendario ?? {
|
|
7610
|
+
mes: out.mes,
|
|
7611
|
+
brandId: out.brandId,
|
|
7612
|
+
semanas: [],
|
|
7613
|
+
mensaje: out.mensaje
|
|
6267
7614
|
};
|
|
7615
|
+
} else {
|
|
7616
|
+
payload = { ok: false, state: result.state, mensaje: result.text };
|
|
6268
7617
|
}
|
|
6269
|
-
return { content: [{ type: "text", text: JSON.stringify(
|
|
7618
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
6270
7619
|
}
|
|
6271
7620
|
);
|
|
6272
7621
|
server.tool(
|
|
6273
7622
|
"get_seo_snapshot",
|
|
6274
7623
|
"Lee el snapshot SEO de la brand: rank, keywords, oportunidades, competidores.",
|
|
6275
7624
|
{
|
|
6276
|
-
brandId:
|
|
7625
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand")
|
|
6277
7626
|
},
|
|
6278
7627
|
async ({ brandId: inputBrandId }) => {
|
|
6279
7628
|
const tenantId = session.requireTenant();
|
|
6280
7629
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
6281
|
-
const
|
|
6282
|
-
if (!config) {
|
|
6283
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: "No hay marketing_config" }) }] };
|
|
6284
|
-
}
|
|
6285
|
-
const brands = config.brands ?? {};
|
|
6286
|
-
const brand = brands[brandId];
|
|
7630
|
+
const brand = await readDoc(`tenants/${tenantId}/marketing_config`, brandId);
|
|
6287
7631
|
if (!brand) {
|
|
6288
7632
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Brand "${brandId}" no encontrada` }) }] };
|
|
6289
7633
|
}
|
|
@@ -6297,8 +7641,8 @@ function registerMarketingTools(server, session) {
|
|
|
6297
7641
|
"get_photo_gallery",
|
|
6298
7642
|
"Fotos disponibles filtradas por estado. Muestra fotos con metadata y conteo.",
|
|
6299
7643
|
{
|
|
6300
|
-
brandId:
|
|
6301
|
-
estado:
|
|
7644
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand"),
|
|
7645
|
+
estado: import_zod37.z.string().optional().describe("Filtrar por estado (nueva, procesando, editada, usada, descartada, error)")
|
|
6302
7646
|
},
|
|
6303
7647
|
async ({ brandId: inputBrandId, estado }) => {
|
|
6304
7648
|
session.requireTenant();
|
|
@@ -6338,7 +7682,7 @@ function registerMarketingTools(server, session) {
|
|
|
6338
7682
|
"generate_marketing_plan",
|
|
6339
7683
|
"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.",
|
|
6340
7684
|
{
|
|
6341
|
-
brandId:
|
|
7685
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand")
|
|
6342
7686
|
},
|
|
6343
7687
|
async ({ brandId: inputBrandId }) => {
|
|
6344
7688
|
const tenantId = session.requireTenant();
|
|
@@ -6350,25 +7694,33 @@ function registerMarketingTools(server, session) {
|
|
|
6350
7694
|
);
|
|
6351
7695
|
server.tool(
|
|
6352
7696
|
"save_marketing_plan",
|
|
6353
|
-
"Guarda el plan de marketing generado por Claude en la configuracion de la brand. Escribe en marketing_config
|
|
7697
|
+
"Guarda el plan de marketing generado por Claude en la configuracion de la brand. Escribe en tenants/{tenantId}/marketing_config/{brandId}.plan.",
|
|
6354
7698
|
{
|
|
6355
|
-
brandId:
|
|
6356
|
-
plan:
|
|
7699
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand"),
|
|
7700
|
+
plan: import_zod37.z.record(import_zod37.z.string(), import_zod37.z.unknown()).describe("Plan de marketing generado (keywordsPrioritarios, gapsCompetencia, temporadas, tonoMarca, quickWins, etc.)")
|
|
6357
7701
|
},
|
|
6358
7702
|
async ({ brandId: inputBrandId, plan }) => {
|
|
6359
7703
|
const tenantId = session.requireTenant();
|
|
6360
7704
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
6361
|
-
const
|
|
6362
|
-
|
|
7705
|
+
const ctx = buildMartinContext(session, brandId);
|
|
7706
|
+
const result = await dispatchWithContract({
|
|
7707
|
+
contract: planWriterSaveContract,
|
|
7708
|
+
helper: planWriter.save,
|
|
7709
|
+
callable: callPlanWriterSave,
|
|
7710
|
+
input: { tenantId, brandId, plan },
|
|
7711
|
+
ctx
|
|
7712
|
+
});
|
|
7713
|
+
const payload = result.state === "success" ? result.structuredOutput : { ok: false, state: result.state, mensaje: result.text };
|
|
7714
|
+
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
6363
7715
|
}
|
|
6364
7716
|
);
|
|
6365
7717
|
server.tool(
|
|
6366
7718
|
"update_marketing_plan_field",
|
|
6367
7719
|
"Actualiza UN campo del plan de marketing sin sobreescribir el resto. Merge parcial. Ideal para agregar coleccionesPriorizadas, actualizar quickWins, etc.",
|
|
6368
7720
|
{
|
|
6369
|
-
brandId:
|
|
6370
|
-
field:
|
|
6371
|
-
value:
|
|
7721
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand"),
|
|
7722
|
+
field: import_zod37.z.string().describe('Nombre del campo a actualizar (ej: "coleccionesPriorizadas", "quickWins", "temporadas")'),
|
|
7723
|
+
value: import_zod37.z.unknown().describe("Valor del campo")
|
|
6372
7724
|
},
|
|
6373
7725
|
async ({ brandId: inputBrandId, field, value }) => {
|
|
6374
7726
|
const tenantId = session.requireTenant();
|
|
@@ -6381,22 +7733,36 @@ function registerMarketingTools(server, session) {
|
|
|
6381
7733
|
"generate_brand_brief",
|
|
6382
7734
|
"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.",
|
|
6383
7735
|
{
|
|
6384
|
-
brandId:
|
|
7736
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand")
|
|
6385
7737
|
},
|
|
6386
7738
|
async ({ brandId: inputBrandId }) => {
|
|
6387
7739
|
const tenantId = session.requireTenant();
|
|
6388
7740
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
6389
|
-
const
|
|
6390
|
-
const
|
|
6391
|
-
|
|
7741
|
+
const ctx = buildMartinContext(session, brandId);
|
|
7742
|
+
const result = await dispatchWithContract({
|
|
7743
|
+
contract: brandBriefBuilderContract,
|
|
7744
|
+
helper: brandBriefBuilder,
|
|
7745
|
+
callable: callBrandBriefBuilder,
|
|
7746
|
+
input: { tenantId, brandId },
|
|
7747
|
+
ctx
|
|
7748
|
+
});
|
|
7749
|
+
let returnPayload;
|
|
7750
|
+
if (result.state === "success" && result.structuredOutput?.ok === true) {
|
|
7751
|
+
returnPayload = result.structuredOutput.payload;
|
|
7752
|
+
} else if (result.state === "success") {
|
|
7753
|
+
returnPayload = result.structuredOutput;
|
|
7754
|
+
} else {
|
|
7755
|
+
returnPayload = { ok: false, state: result.state, mensaje: result.text };
|
|
7756
|
+
}
|
|
7757
|
+
return { content: [{ type: "text", text: JSON.stringify(returnPayload, null, 2) }] };
|
|
6392
7758
|
}
|
|
6393
7759
|
);
|
|
6394
7760
|
server.tool(
|
|
6395
7761
|
"save_brand_brief",
|
|
6396
|
-
"Guarda el Brand Brief generado por Claude en marketing_config
|
|
7762
|
+
"Guarda el Brand Brief generado por Claude en tenants/{tenantId}/marketing_config/{brandId}.brandBrief. Escribe solo ese campo (merge parcial, no rebuild del doc).",
|
|
6397
7763
|
{
|
|
6398
|
-
brandId:
|
|
6399
|
-
brandBrief:
|
|
7764
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand"),
|
|
7765
|
+
brandBrief: import_zod37.z.record(import_zod37.z.string(), import_zod37.z.unknown()).describe("Brand Brief completo generado por Claude")
|
|
6400
7766
|
},
|
|
6401
7767
|
async ({ brandId: inputBrandId, brandBrief }) => {
|
|
6402
7768
|
const tenantId = session.requireTenant();
|
|
@@ -6409,9 +7775,9 @@ function registerMarketingTools(server, session) {
|
|
|
6409
7775
|
"generate_weekly_content",
|
|
6410
7776
|
"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.",
|
|
6411
7777
|
{
|
|
6412
|
-
brandId:
|
|
6413
|
-
semana:
|
|
6414
|
-
modo:
|
|
7778
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand"),
|
|
7779
|
+
semana: import_zod37.z.number().optional().describe("Numero de semana (1-5). Default: semana actual del mes."),
|
|
7780
|
+
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'.")
|
|
6415
7781
|
},
|
|
6416
7782
|
async ({ brandId: inputBrandId, semana, modo }) => {
|
|
6417
7783
|
const tenantId = session.requireTenant();
|
|
@@ -6427,14 +7793,14 @@ function registerMarketingTools(server, session) {
|
|
|
6427
7793
|
|
|
6428
7794
|
IMPORTANTE: Si seleccionaste una foto con get_photos_for_slot, SIEMPRE pasa fotoId aqu\xED. Sin fotoId el post se publica sin imagen.`,
|
|
6429
7795
|
{
|
|
6430
|
-
brandId:
|
|
6431
|
-
plataforma:
|
|
6432
|
-
tipo:
|
|
6433
|
-
keyword:
|
|
6434
|
-
languageCode:
|
|
6435
|
-
fotoId:
|
|
6436
|
-
datos:
|
|
6437
|
-
calendarioItemRef:
|
|
7796
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand"),
|
|
7797
|
+
plataforma: import_zod37.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino"),
|
|
7798
|
+
tipo: import_zod37.z.string().optional().describe("Tipo de contenido (post, blog, carousel, etc.)"),
|
|
7799
|
+
keyword: import_zod37.z.string().optional().describe("Keyword target"),
|
|
7800
|
+
languageCode: import_zod37.z.string().optional().describe("Idioma (es/en)"),
|
|
7801
|
+
fotoId: import_zod37.z.string().optional().describe("ID de la foto a asociar"),
|
|
7802
|
+
datos: import_zod37.z.record(import_zod37.z.string(), import_zod37.z.unknown()).describe("Datos especificos de la plataforma (output de buildDatos*)"),
|
|
7803
|
+
calendarioItemRef: import_zod37.z.string().optional().describe("Referencia al item del calendario")
|
|
6438
7804
|
},
|
|
6439
7805
|
async ({ brandId: inputBrandId, plataforma, tipo, keyword, languageCode, fotoId, datos, calendarioItemRef }) => {
|
|
6440
7806
|
const tenantId = session.requireTenant();
|
|
@@ -6479,13 +7845,13 @@ Usa para: corregir body, metaTitle, tags, fotoId, o cualquier campo sin tener qu
|
|
|
6479
7845
|
NO puede cambiar: tenantId, brandId, id (inmutables).
|
|
6480
7846
|
Si pasas campos dentro de "datos", se hace merge con los datos existentes (no los reemplaza entero).`,
|
|
6481
7847
|
{
|
|
6482
|
-
contenidoId:
|
|
6483
|
-
datos:
|
|
6484
|
-
fotoId:
|
|
6485
|
-
keyword:
|
|
6486
|
-
languageCode:
|
|
6487
|
-
estado:
|
|
6488
|
-
calendarioItemRef:
|
|
7848
|
+
contenidoId: import_zod37.z.string().describe("ID del doc en marketing_contenido"),
|
|
7849
|
+
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: "..." }'),
|
|
7850
|
+
fotoId: import_zod37.z.string().nullable().optional().describe("Actualizar foto asociada"),
|
|
7851
|
+
keyword: import_zod37.z.string().nullable().optional().describe("Actualizar keyword"),
|
|
7852
|
+
languageCode: import_zod37.z.string().optional().describe("Actualizar idioma"),
|
|
7853
|
+
estado: import_zod37.z.enum(["borrador", "generado", "pendiente_aprobacion", "aprobado", "rechazado"]).optional().describe("Cambiar estado manualmente"),
|
|
7854
|
+
calendarioItemRef: import_zod37.z.string().nullable().optional().describe("Vincular a un slot del calendario")
|
|
6489
7855
|
},
|
|
6490
7856
|
async ({ contenidoId, datos: newDatos, fotoId, keyword, languageCode, estado, calendarioItemRef }) => {
|
|
6491
7857
|
const tenantId = session.requireTenant();
|
|
@@ -6516,27 +7882,27 @@ Si pasas campos dentro de "datos", se hace merge con los datos existentes (no lo
|
|
|
6516
7882
|
"update_calendar_slot",
|
|
6517
7883
|
"Modifica o agrega un slot del calendario editorial. Si slotIndex >= items.length, agrega un slot nuevo.",
|
|
6518
7884
|
{
|
|
6519
|
-
brandId:
|
|
6520
|
-
mes:
|
|
6521
|
-
semana:
|
|
6522
|
-
slotIndex:
|
|
6523
|
-
cambios:
|
|
6524
|
-
dia:
|
|
6525
|
-
plataforma:
|
|
6526
|
-
tipo:
|
|
6527
|
-
keyword:
|
|
6528
|
-
tema:
|
|
6529
|
-
productoId:
|
|
6530
|
-
estado:
|
|
6531
|
-
contenidoRef:
|
|
6532
|
-
fotoIdAsignada:
|
|
6533
|
-
notas:
|
|
6534
|
-
locationId:
|
|
6535
|
-
locationNombre:
|
|
7885
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand"),
|
|
7886
|
+
mes: import_zod37.z.string().describe("Mes del calendario (YYYY-MM)"),
|
|
7887
|
+
semana: import_zod37.z.number().describe("Numero de semana (1-5)"),
|
|
7888
|
+
slotIndex: import_zod37.z.number().describe("Indice del slot (0-based). Si >= items.length, agrega nuevo slot al final."),
|
|
7889
|
+
cambios: import_zod37.z.object({
|
|
7890
|
+
dia: import_zod37.z.string().nullable().optional(),
|
|
7891
|
+
plataforma: import_zod37.z.enum(["gbp", "shopify_blog", "instagram", "review"]).nullable().optional(),
|
|
7892
|
+
tipo: import_zod37.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]).nullable().optional(),
|
|
7893
|
+
keyword: import_zod37.z.string().nullable().optional(),
|
|
7894
|
+
tema: import_zod37.z.string().nullable().optional(),
|
|
7895
|
+
productoId: import_zod37.z.string().nullable().optional(),
|
|
7896
|
+
estado: import_zod37.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional(),
|
|
7897
|
+
contenidoRef: import_zod37.z.string().nullable().optional(),
|
|
7898
|
+
fotoIdAsignada: import_zod37.z.string().nullable().optional(),
|
|
7899
|
+
notas: import_zod37.z.array(NotaCalendarioSchema).optional(),
|
|
7900
|
+
locationId: import_zod37.z.string().nullable().optional().describe("GBP location ID \u2014 a qu\xE9 sucursal publicar el post"),
|
|
7901
|
+
locationNombre: import_zod37.z.string().nullable().optional().describe("Nombre de la sucursal GBP (para UI)")
|
|
6536
7902
|
}).strict().describe("Campos del slot. Solo acepta campos validos del schema. Campos no reconocidos seran rechazados."),
|
|
6537
|
-
accionContenidoExistente:
|
|
6538
|
-
|
|
6539
|
-
|
|
7903
|
+
accionContenidoExistente: import_zod37.z.union([
|
|
7904
|
+
import_zod37.z.enum(["descartar", "nuevo_slot", "mantener"]),
|
|
7905
|
+
import_zod37.z.string().regex(/^mover:semana:\d+:slot:\d+$/, "Formato: mover:semana:N:slot:M")
|
|
6540
7906
|
]).optional().describe(
|
|
6541
7907
|
"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)."
|
|
6542
7908
|
)
|
|
@@ -6544,9 +7910,16 @@ Si pasas campos dentro de "datos", se hace merge con los datos existentes (no lo
|
|
|
6544
7910
|
async ({ brandId: inputBrandId, mes, semana, slotIndex, cambios, accionContenidoExistente }) => {
|
|
6545
7911
|
const tenantId = session.requireTenant();
|
|
6546
7912
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
6547
|
-
const
|
|
6548
|
-
const result =
|
|
6549
|
-
|
|
7913
|
+
const ctx = buildMartinContext(session, brandId);
|
|
7914
|
+
const result = await dispatchWithContract({
|
|
7915
|
+
contract: calendarSlotUpdaterContract,
|
|
7916
|
+
helper: calendarSlotUpdater,
|
|
7917
|
+
callable: callCalendarSlotUpdater,
|
|
7918
|
+
input: { tenantId, brandId, mes, semana, slotIndex, cambios, accionContenidoExistente },
|
|
7919
|
+
ctx
|
|
7920
|
+
});
|
|
7921
|
+
const payload = result.state === "success" ? result.structuredOutput : { ok: false, state: result.state, mensaje: result.text };
|
|
7922
|
+
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
6550
7923
|
}
|
|
6551
7924
|
);
|
|
6552
7925
|
server.tool(
|
|
@@ -6559,9 +7932,9 @@ ESCRIBE EN DOS LUGARES:
|
|
|
6559
7932
|
|
|
6560
7933
|
NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agenda, no en el post.`,
|
|
6561
7934
|
{
|
|
6562
|
-
contenidoRef:
|
|
6563
|
-
fotoId:
|
|
6564
|
-
calendarioItemRef:
|
|
7935
|
+
contenidoRef: import_zod37.z.string().describe("ID del doc en marketing_contenido"),
|
|
7936
|
+
fotoId: import_zod37.z.string().describe("ID de la foto en marketing_fotos (estado editada)"),
|
|
7937
|
+
calendarioItemRef: import_zod37.z.string().optional().describe('Ref del slot (formato "semana:N:slot:M") para actualizar fotoIdAsignada')
|
|
6565
7938
|
},
|
|
6566
7939
|
async ({ contenidoRef, fotoId, calendarioItemRef }) => {
|
|
6567
7940
|
const tenantId = session.requireTenant();
|
|
@@ -6574,7 +7947,7 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
|
|
|
6574
7947
|
"approve_content",
|
|
6575
7948
|
"Aprueba contenido para publicacion. Valida transicion de estado.",
|
|
6576
7949
|
{
|
|
6577
|
-
contenidoId:
|
|
7950
|
+
contenidoId: import_zod37.z.string().describe("ID del contenido a aprobar")
|
|
6578
7951
|
},
|
|
6579
7952
|
async ({ contenidoId }) => {
|
|
6580
7953
|
session.requireTenant();
|
|
@@ -6598,8 +7971,8 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
|
|
|
6598
7971
|
"reject_content",
|
|
6599
7972
|
"Rechaza contenido con motivo. Valida transicion de estado.",
|
|
6600
7973
|
{
|
|
6601
|
-
contenidoId:
|
|
6602
|
-
motivo:
|
|
7974
|
+
contenidoId: import_zod37.z.string().describe("ID del contenido a rechazar"),
|
|
7975
|
+
motivo: import_zod37.z.string().describe("Motivo del rechazo")
|
|
6603
7976
|
},
|
|
6604
7977
|
async ({ contenidoId, motivo }) => {
|
|
6605
7978
|
session.requireTenant();
|
|
@@ -6619,44 +7992,44 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
|
|
|
6619
7992
|
}
|
|
6620
7993
|
);
|
|
6621
7994
|
server.tool(
|
|
6622
|
-
"
|
|
6623
|
-
"Lee todas las colecciones
|
|
7995
|
+
"get_collections",
|
|
7996
|
+
"Lee todas las colecciones canonicas (Shopify/WordPress/webonly) con sus 6 campos SEO actuales. Incluye best practices 2026.",
|
|
6624
7997
|
{
|
|
6625
|
-
brandId:
|
|
7998
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand")
|
|
6626
7999
|
},
|
|
6627
8000
|
async ({ brandId: inputBrandId }) => {
|
|
6628
8001
|
const tenantId = session.requireTenant();
|
|
6629
8002
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
6630
|
-
const
|
|
6631
|
-
|
|
8003
|
+
const db = getAdminDb();
|
|
8004
|
+
const collSnap = await db.collection(`tenants/${tenantId}/marketing_snapshots/${brandId}/collections`).get();
|
|
8005
|
+
const items = collSnap.docs.map((d) => d.data());
|
|
8006
|
+
if (items.length === 0) {
|
|
6632
8007
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
6633
8008
|
ok: false,
|
|
6634
|
-
error: "No hay
|
|
8009
|
+
error: "No hay colecciones nested. El tenant debe sincronizar desde Marketing > Configuraci\xF3n > Shopify > Sincronizar."
|
|
6635
8010
|
}) }] };
|
|
6636
8011
|
}
|
|
6637
|
-
const
|
|
6638
|
-
`tenants/${tenantId}/shopify_imports/${lastImportId}/data`,
|
|
6639
|
-
"collections"
|
|
6640
|
-
);
|
|
6641
|
-
const items = collectionsDoc?.items ?? [];
|
|
6642
|
-
const config = await readDoc("marketing_config", tenantId);
|
|
6643
|
-
const brands = config?.brands ?? {};
|
|
6644
|
-
const brand = brands[brandId];
|
|
8012
|
+
const brand = await readDoc(`tenants/${tenantId}/marketing_config`, brandId);
|
|
6645
8013
|
const plan = brand?.plan ?? {};
|
|
6646
8014
|
const keywords = plan.keywordsPrioritarios ?? [];
|
|
6647
8015
|
const existingSuggestions = brand?.collectionSuggestions ?? {};
|
|
6648
|
-
const collections = items.map((c) =>
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
8016
|
+
const collections = items.map((c) => {
|
|
8017
|
+
const seo = c.seo || {};
|
|
8018
|
+
const featured = c.featuredImage || null;
|
|
8019
|
+
return {
|
|
8020
|
+
id: c.platformId,
|
|
8021
|
+
title: c.title,
|
|
8022
|
+
handle: c.handle,
|
|
8023
|
+
body_html: typeof c.descriptionHtml === "string" ? c.descriptionHtml.slice(0, 500) : null,
|
|
8024
|
+
metaTitle: seo.metaTitle ?? null,
|
|
8025
|
+
metaDescription: seo.metaDescription ?? null,
|
|
8026
|
+
products_count: null,
|
|
8027
|
+
// adapter v2 aun no calcula products_count por collection
|
|
8028
|
+
collectionType: "canonical",
|
|
8029
|
+
image: featured ? { src: featured.url, alt: featured.altText } : null,
|
|
8030
|
+
existingSuggestion: existingSuggestions[String(c.platformId)] ?? null
|
|
8031
|
+
};
|
|
8032
|
+
});
|
|
6660
8033
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
6661
8034
|
ok: true,
|
|
6662
8035
|
totalCollections: collections.length,
|
|
@@ -6709,7 +8082,7 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
|
|
|
6709
8082
|
"save_collection_suggestions",
|
|
6710
8083
|
"Guarda sugerencias SEO para colecciones de Shopify. El tenant las aprueba en la UI.",
|
|
6711
8084
|
{
|
|
6712
|
-
brandId:
|
|
8085
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand"),
|
|
6713
8086
|
suggestions: CollectionSuggestionsInputArraySchema.describe("Array de sugerencias SEO. Cada una con max 60 chars en metaTitle, max 158 en metaDescription, max 125 en imageAlt")
|
|
6714
8087
|
},
|
|
6715
8088
|
async ({ brandId: inputBrandId, suggestions }) => {
|