@interware/content-generator 0.1.4 → 0.2.1

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.
Files changed (30) hide show
  1. package/dist/_chunks/{App-KtJGFVFQ.mjs → App-Cbkccs2K.mjs} +552 -55
  2. package/dist/_chunks/{App-Bt1yydY9.js → App-DCuBRwX8.js} +545 -48
  3. package/dist/_chunks/{Settings-CFag78zw.mjs → Settings-B60QGKK4.mjs} +48 -4
  4. package/dist/_chunks/{Settings-BbIpY74m.js → Settings-DuGbtyWC.js} +48 -4
  5. package/dist/_chunks/{getTranslation-BcuPOeW7.mjs → getTranslation-B97xbmEl.mjs} +1 -1
  6. package/dist/_chunks/{getTranslation-DA3AxQPb.js → getTranslation-CacX7ZX5.js} +1 -1
  7. package/dist/_chunks/{index-B6aEv34B.mjs → index-B2RuvYul.mjs} +2 -2
  8. package/dist/_chunks/{index-JZA3FSFH.js → index-BVGbP3E-.js} +2 -2
  9. package/dist/admin/index.js +1 -1
  10. package/dist/admin/index.mjs +1 -1
  11. package/dist/admin/src/components/Navigation.d.ts +2 -0
  12. package/dist/admin/src/components/generate-content/Form.d.ts +1 -1
  13. package/dist/admin/src/components/generate-images/Image.d.ts +5 -0
  14. package/dist/admin/src/components/generate-images/Images.d.ts +2 -0
  15. package/dist/admin/src/components/generate-images/Modal.d.ts +9 -0
  16. package/dist/admin/src/components/home/table.d.ts +2 -1
  17. package/dist/admin/src/containers/generate-images/index.d.ts +2 -0
  18. package/dist/admin/src/containers/settings/index.d.ts +2 -0
  19. package/dist/admin/src/hooks/generate-images/useImages.d.ts +11 -0
  20. package/dist/admin/src/lib/use-cases/facebook/index.d.ts +5 -0
  21. package/dist/admin/src/lib/use-cases/images/index.d.ts +6 -0
  22. package/dist/admin/src/pages/GenerateImages.d.ts +2 -0
  23. package/dist/server/index.js +309 -147
  24. package/dist/server/index.mjs +309 -127
  25. package/dist/server/src/controllers/controller.d.ts +2 -0
  26. package/dist/server/src/controllers/index.d.ts +2 -0
  27. package/dist/server/src/index.d.ts +56 -1
  28. package/dist/server/src/services/index.d.ts +54 -1
  29. package/dist/server/src/services/service.d.ts +54 -1
  30. package/package.json +1 -2
@@ -1,6 +1,4 @@
1
1
  import { GoogleGenAI } from "@google/genai";
2
- import slugify from "slugify";
3
- import * as https from "https";
4
2
  const PLUGIN_ID = "strapi-content-generator-gemini";
5
3
  const bootstrap = ({ strapi }) => {
6
4
  strapi.admin.services.permission.actionProvider.registerMany([
@@ -76,6 +74,30 @@ const controller = ({ strapi }) => {
76
74
  auth: ctx.state.auth
77
75
  });
78
76
  },
77
+ async getImages(ctx) {
78
+ const contentType = strapi.contentType("plugin::upload.file");
79
+ const files = await pluginService().getImagenes();
80
+ return await strapi.contentAPI.sanitize.output(files, contentType, {
81
+ auth: ctx.state.auth
82
+ });
83
+ },
84
+ async deleteImage(ctx) {
85
+ const contentType = strapi.contentType("plugin::upload.file");
86
+ await strapi.contentAPI.validate.query(ctx.query, contentType, {
87
+ auth: ctx.state.auth
88
+ });
89
+ const sanitizedQueryParams = await strapi.contentAPI.sanitize.query(
90
+ ctx.query,
91
+ contentType,
92
+ {
93
+ auth: ctx.state.auth
94
+ }
95
+ );
96
+ const file = await pluginService().eliminarImagen(sanitizedQueryParams);
97
+ return await strapi.contentAPI.sanitize.output(file, contentType, {
98
+ auth: ctx.state.auth
99
+ });
100
+ },
79
101
  async generateArticle(ctx) {
80
102
  ctx.body = await pluginService().generateArticle(ctx);
81
103
  },
@@ -121,10 +143,12 @@ const config = ({ strapi }) => {
121
143
  const googleGenAI = await pluginStore.get({ key: "GOOGLE_GEN_AI" });
122
144
  const linkedInAccessToken = await pluginStore.get({ key: "LINKEDIN_ACCESS_TOKEN" });
123
145
  const linkedInOrganizationId = await pluginStore.get({ key: "LINKEDIN_ORGANIZATION_ID" });
124
- ctx.body = { googleGenAI, linkedInAccessToken, linkedInOrganizationId };
146
+ const facebookClientToken = await pluginStore.get({ key: "FACEBOOK_CLIENT_TOKEN" });
147
+ const facebookPageId = await pluginStore.get({ key: "FACEBOOK_PAGE_ID" });
148
+ ctx.body = { googleGenAI, linkedInAccessToken, linkedInOrganizationId, facebookClientToken, facebookPageId };
125
149
  },
126
150
  async set(ctx) {
127
- const { googleGenAI, linkedInAccessToken, linkedInOrganizationId } = ctx.request.body;
151
+ const { googleGenAI, linkedInAccessToken, linkedInOrganizationId, facebookClientToken, facebookPageId } = ctx.request.body;
128
152
  const pluginStore = strapi.store({
129
153
  type: "plugin",
130
154
  name: "strapi-content-generator-gemini"
@@ -132,6 +156,8 @@ const config = ({ strapi }) => {
132
156
  await pluginStore.set({ key: "GOOGLE_GEN_AI", value: googleGenAI });
133
157
  await pluginStore.set({ key: "LINKEDIN_ACCESS_TOKEN", value: linkedInAccessToken });
134
158
  await pluginStore.set({ key: "LINKEDIN_ORGANIZATION_ID", value: linkedInOrganizationId });
159
+ await pluginStore.set({ key: "FACEBOOK_CLIENT_TOKEN", value: facebookClientToken });
160
+ await pluginStore.set({ key: "FACEBOOK_PAGE_ID", value: facebookPageId });
135
161
  ctx.body = { ok: true };
136
162
  }
137
163
  };
@@ -170,6 +196,24 @@ const admin = [
170
196
  auth: false
171
197
  }
172
198
  },
199
+ {
200
+ method: "GET",
201
+ path: "/images",
202
+ handler: "controller.getImages",
203
+ config: {
204
+ policies: [],
205
+ auth: false
206
+ }
207
+ },
208
+ {
209
+ method: "DELETE",
210
+ path: "/delete-image",
211
+ handler: "controller.deleteImage",
212
+ config: {
213
+ policies: [],
214
+ auth: false
215
+ }
216
+ },
173
217
  {
174
218
  method: "POST",
175
219
  path: "/generate-article",
@@ -2700,29 +2744,6 @@ const {
2700
2744
  getAdapter,
2701
2745
  mergeConfig
2702
2746
  } = axios;
2703
- function imageUrlToBase64(url) {
2704
- return new Promise((resolve, reject) => {
2705
- https.get(url, (response) => {
2706
- if (response.statusCode < 200 || response.statusCode >= 300) {
2707
- return reject(
2708
- new Error(
2709
- `Failed to download image, status code: ${response.statusCode}`
2710
- )
2711
- );
2712
- }
2713
- const chunks = [];
2714
- response.on("data", (chunk) => {
2715
- chunks.push(chunk);
2716
- });
2717
- response.on("end", () => {
2718
- const buffer = Buffer.concat(chunks);
2719
- resolve(buffer.toString("base64"));
2720
- });
2721
- }).on("error", (error) => {
2722
- reject(error);
2723
- });
2724
- });
2725
- }
2726
2747
  const service = ({ strapi }) => {
2727
2748
  return {
2728
2749
  // ───────────────────────────────
@@ -2752,39 +2773,69 @@ const service = ({ strapi }) => {
2752
2773
  return await strapi.entityService.findMany("api::categoria.categoria", sanitizedQueryParams);
2753
2774
  },
2754
2775
  async getUsuarios(sanitizedQueryParams) {
2755
- return await strapi.entityService.findMany("plugin::users-permissions.user", sanitizedQueryParams);
2776
+ return await strapi.documents("plugin::users-permissions.user").findMany(sanitizedQueryParams);
2777
+ },
2778
+ async getImagenes() {
2779
+ const existingFolders = await strapi.documents("plugin::upload.folder").findMany({
2780
+ filters: {
2781
+ name: "Generated"
2782
+ }
2783
+ });
2784
+ if (existingFolders) {
2785
+ return await strapi.documents("plugin::upload.file").findMany({
2786
+ filters: {
2787
+ folder: {
2788
+ id: {
2789
+ $eq: existingFolders[0].id
2790
+ }
2791
+ }
2792
+ }
2793
+ });
2794
+ }
2795
+ return [];
2796
+ },
2797
+ async eliminarImagen(sanitizedQueryParams) {
2798
+ return await strapi.documents("plugin::upload.file").delete(sanitizedQueryParams);
2756
2799
  },
2757
2800
  // ───────────────────────────────
2758
2801
  // 🧠 Generate Article with AI
2759
2802
  // ───────────────────────────────
2760
2803
  async generateArticle(ctx) {
2761
- const { titulo, categoria, autor, keywords, instrucciones } = ctx.request.body;
2804
+ const { titulo, categoria, autor, temperatura = 0.2, instrucciones } = ctx.request.body;
2762
2805
  const promptArticulo = `
2763
- Genera un artículo en español neutro, orientado a SEO, con estructura clara, sin emojis, sin texto fuera del JSON, respetando estrictamente el schema proporcionado.
2806
+ Actúa como un Redactor Técnico Senior experto en tecnología corporativa para la empresa "InterWare"
2764
2807
 
2765
- Requisitos para "content":
2766
- - HTML compatible con @_sh/strapi-plugin-ckeditor (DefaultHTML editor)
2767
- - Debe incluir:
2768
- - Título en H1
2769
- - Introducción
2770
- - Subtítulos H2 (puedes usar hasta H6)
2771
- - Listas
2772
- - Tablas (sólo si el contenido lo amerita)
2773
- - Ejemplo práctico (sólo si el contenido lo amerita)
2774
- - Conclusión o comentarios finales (no es necesario etiquetarlo con un encabezado)
2808
+ Tu tarea es generar un artículo de blog en formato JSON estricto, siguiendo el schema proporcionado.
2775
2809
 
2776
- SEO:
2777
- - Incluir metadatos necesarios para Facebook y LinkedIn
2778
- - Omitir author, publisher, image y robots
2779
- - og:url es: https:://interware.com.mx/blog/{SLUG DEL TÍTULO}
2810
+ IDENTIDAD Y TONO DE VOZ (Brand Book):
2811
+ - Tono: Profesional, innovador, confiable y empático.
2812
+ - Estilo: Español neutro corporativo. Evita coloquialismos excesivos pero no seas robótico.
2813
+ - Enfoque: Tecnología, Innovación y Soluciones Empresariales.
2780
2814
 
2781
- Restricciones:
2782
- - No usar frases como: deja tu opinión, comenta, suscríbete, etc.
2815
+ ESTRUCTURA DEL CONTENIDO (HTML para CKEditor):
2816
+ - El campo "content" debe contener HTML válido y limpio.
2817
+ - Estructura obligatoria dentro del HTML:
2818
+ 1. <h1>Título del Artículo</h1> (Debe ser impactante y contener la palabra clave principal).
2819
+ 2. <p>Introducción</p> (Planteamiento del problema o contexto tecnológico).
2820
+ 3. Desarrollo usando <h2> y hasta <h6> para jerarquía.
2821
+ 4. Uso de <ul> o <ol> para listar beneficios o características.
2822
+ 5. (Opcional) <table> si hay datos comparativos.
2823
+ 6. Conclusión o Cierre (Sin subtítulo "Conclusión", intégralo naturalmente).
2783
2824
 
2784
- ${titulo ? `Básate en el título: "${titulo}".` : ""}
2785
- ${categoria ? `Categoría(s): "${categoria}".` : ""}
2786
- ${keywords ? `Palabras clave: "${keywords}".` : ""}
2787
- ${instrucciones ? `Instrucciones adicionales: "${instrucciones}".` : ""}
2825
+ SEO:
2826
+ - Incluir metadatos necesarios para Facebook y LinkedIn
2827
+ - og:url: Debe ser "https://interware.com.mx/blog/{slug-generado}"
2828
+ - Omitir author, publisher, image y robots
2829
+
2830
+ RESTRICCIONES TÉCNICAS Y DE ESTILO:
2831
+ - PROHIBIDO: Emojis, Markdown (no usar **negritas** estilo MD, usar <strong>), bloques de código sin etiqueta <pre>.
2832
+ - PROHIBIDO: Frases de "Youtuber" (ej: "Suscríbete", "Deja tu like", "Comenta abajo").
2833
+ - PROHIBIDO: Texto introductorio fuera del JSON (ej: "Aquí tienes tu JSON..."). Solo devuelve el objeto JSON.
2834
+
2835
+ ENTRADA DE CONTEXTO:
2836
+ ${titulo ? `- Título sugerido: "${titulo}"` : ""}
2837
+ ${categoria ? `- Categoría/Tema: "${categoria}"` : ""}
2838
+ ${instrucciones ? `- Instrucciones extra: "${instrucciones}"` : ""}
2788
2839
  `;
2789
2840
  try {
2790
2841
  const ai = await this.geminiConfig();
@@ -2793,27 +2844,11 @@ const service = ({ strapi }) => {
2793
2844
  contents: promptArticulo,
2794
2845
  config: {
2795
2846
  responseMimeType: "application/json",
2796
- temperature: 0.2,
2847
+ temperature: temperatura,
2797
2848
  responseSchema: {
2798
2849
  type: "object",
2799
2850
  required: ["titulo", "descripcion", "content", "seo", "minute_read"],
2800
2851
  properties: {
2801
- titulo: {
2802
- type: "string",
2803
- minLength: 40,
2804
- maxLength: 70,
2805
- description: "Título del artículo"
2806
- },
2807
- descripcion: {
2808
- type: "string",
2809
- minLength: 80,
2810
- maxLength: 160,
2811
- description: "Descripción breve del artículo"
2812
- },
2813
- content: {
2814
- type: "string",
2815
- description: "Contenido HTML compatible con CKEditor"
2816
- },
2817
2852
  seo: {
2818
2853
  type: "object",
2819
2854
  required: ["metaTitle", "metaDescription", "metadata"],
@@ -2837,11 +2872,46 @@ const service = ({ strapi }) => {
2837
2872
  }
2838
2873
  }
2839
2874
  },
2875
+ titulo: {
2876
+ type: "string",
2877
+ minLength: 40,
2878
+ maxLength: 70,
2879
+ description: "Título del artículo"
2880
+ },
2881
+ descripcion: {
2882
+ type: "string",
2883
+ minLength: 80,
2884
+ maxLength: 160,
2885
+ description: "Descripción breve del artículo"
2886
+ },
2887
+ slug: {
2888
+ type: "string",
2889
+ minLength: 80,
2890
+ maxLength: 160,
2891
+ description: "URL-friendly basado en el título (ej: estrategias-migracion-nube)"
2892
+ },
2893
+ content: {
2894
+ type: "string",
2895
+ description: "Contenido HTML compatible con CKEditor"
2896
+ },
2840
2897
  minute_read: {
2841
2898
  type: "number",
2842
2899
  minimum: 1,
2843
2900
  maximum: 30,
2844
2901
  description: "Tiempo de lectura calculado usando 200 palabras por minuto"
2902
+ },
2903
+ keywords: {
2904
+ type: "array",
2905
+ minItems: 3,
2906
+ maxItems: 5,
2907
+ items: {
2908
+ type: "object",
2909
+ required: ["name"],
2910
+ properties: {
2911
+ name: { type: "string" }
2912
+ }
2913
+ },
2914
+ description: "Hash tags y palabras clave para la publicación en LinkedIn y Facebook"
2845
2915
  }
2846
2916
  }
2847
2917
  }
@@ -2854,6 +2924,7 @@ const service = ({ strapi }) => {
2854
2924
  );
2855
2925
  }
2856
2926
  const parsed = JSON.parse(rawText);
2927
+ console.log("🚀 ~ service ~ parsed:", parsed);
2857
2928
  const article = await strapi.documents("api::articulo.articulo").create({
2858
2929
  data: {
2859
2930
  titulo: parsed.titulo,
@@ -2861,10 +2932,11 @@ const service = ({ strapi }) => {
2861
2932
  content: parsed.content,
2862
2933
  seo: parsed.seo,
2863
2934
  minute_read: parsed.minute_read,
2864
- slug: slugify(parsed.titulo, { lower: true, strict: true }),
2935
+ slug: parsed.slug,
2865
2936
  categoria,
2866
2937
  autor,
2867
- image: null
2938
+ image: null,
2939
+ keywords: parsed.keywords
2868
2940
  },
2869
2941
  status: "draft"
2870
2942
  });
@@ -2880,7 +2952,7 @@ const service = ({ strapi }) => {
2880
2952
  // 🖼️ Generate Image for Article
2881
2953
  // ───────────────────────────────
2882
2954
  async generateImageForArticle(ctx) {
2883
- const { articleId, imageInstructions } = ctx.request.body;
2955
+ const { articleId } = ctx.request.body;
2884
2956
  if (!articleId) {
2885
2957
  throw new Error(
2886
2958
  "Se requiere 'articleId' en el cuerpo de la petición."
@@ -2890,66 +2962,59 @@ const service = ({ strapi }) => {
2890
2962
  const article = await strapi.documents("api::articulo.articulo").findOne({
2891
2963
  documentId: articleId
2892
2964
  });
2893
- const reference_1 = await imageUrlToBase64(
2894
- "https://api.interware.com.mx/static/images/event.png"
2895
- );
2896
- const reference_2 = await imageUrlToBase64(
2897
- "https://api.interware.com.mx/static/images/info_data.png"
2898
- );
2899
- const reference_3 = await imageUrlToBase64(
2900
- "https://api.interware.com.mx/static/images/Oportunidad_IWRobot.png"
2901
- );
2902
2965
  const prompt = [
2903
2966
  {
2904
2967
  text: `
2905
- Ilustración digital editorial para blog 1920x1080.
2968
+ Act as a Corporate Art Director for "InterWare". Generate a high-quality blog header image based on the following article context:
2906
2969
 
2907
- REGLA CRÍTICA:
2908
- La imagen NO debe contener texto de ningún tipo.
2909
- No incluir palabras, letras, números, tipografía, símbolos tipográficos, rótulos, títulos,
2910
- ni elementos gráficos que simulen texto.
2970
+ Article Title: ${article.titulo}
2971
+ Article Summary: ${article.descripcion}
2911
2972
 
2912
- Estilo:
2913
- Estilo contemporáneo, corporativo y atemporal.
2914
- Estética limpia, profesional y coherente.
2915
- Lenguaje visual sobrio, tecnológico y confiable.
2973
+ Creative Direction (Auto-interpret):
2974
+ Analyze the summary above to determine the visual category:
2916
2975
 
2917
- Paleta de colores:
2918
- Extraer exclusivamente la paleta cromática principal de las imagenes de referencia.
2919
- Si vas a usar la ilustración del robot, no cambies ni estilo ni forma.
2920
- No introducir colores nuevos.
2976
+ 1. **Technical Topic (Software, Cloud, Data, Security):**
2921
2977
 
2922
- Iluminación uniforme y suave.
2923
- Diseño plano con ligera profundidad.
2924
- Estilo vectorial semi-realista, no fotográfico, no 3D.
2925
- Fondo minimalista.
2978
+ **CRITICAL:** Do NOT generate a central "atom", "sphere", or "floating ball" structure. Avoid the generic "connected dots" cliché.
2979
+ If the topic is **TECHNICAL (Software, Data, Cloud)**, you must break the repetitive visual pattern.
2926
2980
 
2927
- Concepto visual a representar:
2928
- Interpretar visualmente el siguiente tema de forma simbólica y abstracta,
2929
- usando metáforas gráficas y elementos visuales, SIN TEXTO:
2930
- ${article.titulo} — ${article.descripcion}
2981
+ **STRICT PROHIBITION:** Do NOT generate the standard "isometric cluster of glass cubes" or "floating square blocks".
2931
2982
 
2932
- Instrucciones adicionales:
2933
- ${imageInstructions || "Ninguna"}
2983
+ Instead, strictly choose ONE of the following distinct visual approaches based on the nuance of the text:
2984
+
2985
+ * **OPTION A: "The Stream" (Speed & Flow)**
2986
+ * *Visuals:* Long-exposure light trails, fiber optic cables, or liquid data streams moving horizontally.
2987
+ * *Camera:* **Side profile or Worm's-eye view**.
2988
+ * *Vibe:* Fast, motion-blurred, linear. No cubes.
2989
+
2990
+ * **OPTION B: "Macro Precision" (Code & Detail)**
2991
+ * *Visuals:* Extreme close-up of a single abstract circuit line, a glass edge refraction, or a micro-texture.
2992
+ * *Camera:* **Macro lens (f/1.8)** with heavy bokeh.
2993
+ * *Vibe:* Minimalist, sharp focus on one detail, blurry background. Abstract.
2994
+
2995
+ * **OPTION C: "The Network" (Connectivity)**
2996
+ * *Visuals:* A vast, flat constellation of small glowing nodes connected by thin lines (plexus effect).
2997
+ * *Camera:* **Top-down satellite view** (90 degrees).
2998
+ * *Vibe:* Flat, cartographic, expansive.
2999
+
3000
+ * **OPTION D: "Abstract Architecture" (Structure)**
3001
+ * *Visuals:* Large, towering monoliths (not small cubes) or massive server rack silhouettes fading into fog.
3002
+ * *Camera:* **Low angle looking up** (implying scale).
3003
+ * *Vibe:* Monumental, heavy, stable.
3004
+
3005
+ 2. **Human/Business Topic (Culture, Leadership, Agile, Management):
3006
+ Create a cinematic photography scene with a randomized composition and moment in time.
3007
+ Show modern professionals in a clean, contemporary office environment, captured from varying perspectives (back view, side angles, or softly blurred faces to maintain anonymity).
3008
+ Vary the number of people, their posture and interaction, the lighting conditions, depth of field, and color grading in each generation.
3009
+ Use natural, candid collaboration moments rather than posed scenes. The result should fe.
3010
+
3011
+ Brand Identity Guidelines (STRICT):
3012
+ - **Primary Background:** Deep Corporate Navy Blue (Hex: #1F2A44). The image must feel predominantly dark blue.
3013
+ - **Accent Color:** Vibrant Red (Hex: #E4002B). Use sparingly for a single focal point (e.g., a dot, a notebook, a graphical node).
3014
+ - **Tech Glow:** Mint Turquoise (Hex: #00C7B1). Use for subtle lighting edges or digital gradients.
3015
+ - **Composition:** Asymmetrical balance. Leave **empty negative space on the RIGHT side** specifically for text overlay.
3016
+ - **Prohibition:** Do NOT generate text inside the image. Do NOT use pure black shadows; use the Navy Blue.
2934
3017
  `
2935
- },
2936
- {
2937
- inlineData: {
2938
- mimeType: "image/png",
2939
- data: reference_1
2940
- }
2941
- },
2942
- {
2943
- inlineData: {
2944
- mimeType: "image/png",
2945
- data: reference_2
2946
- }
2947
- },
2948
- {
2949
- inlineData: {
2950
- mimeType: "image/png",
2951
- data: reference_3
2952
- }
2953
3018
  }
2954
3019
  ];
2955
3020
  const ai = await this.geminiConfig();
@@ -2974,12 +3039,43 @@ const service = ({ strapi }) => {
2974
3039
  throw new Error("No se encontró ninguna imagen en la respuesta del modelo");
2975
3040
  }
2976
3041
  const buffer = Buffer.from(imagePart.inlineData.data, "base64");
3042
+ const existingFolders = await strapi.documents("plugin::upload.folder").findMany({
3043
+ filters: {
3044
+ name: "Generated"
3045
+ }
3046
+ });
3047
+ let folder;
3048
+ let folderPath;
3049
+ if (existingFolders.length === 0) {
3050
+ const folders = await strapi.documents("plugin::upload.folder").findMany(
3051
+ {
3052
+ sort: {
3053
+ pathId: "desc"
3054
+ },
3055
+ limit: 1
3056
+ }
3057
+ );
3058
+ const pathId = Array.isArray(folders) && folders.length > 0 ? folders[0].pathId + 1 : 1;
3059
+ const createdFolder = await strapi.documents("plugin::upload.folder").create({
3060
+ data: {
3061
+ name: "Generated",
3062
+ path: "/generated",
3063
+ pathId
3064
+ }
3065
+ });
3066
+ folder = createdFolder.id;
3067
+ folderPath = createdFolder.path;
3068
+ } else {
3069
+ folder = existingFolders[0].id;
3070
+ folderPath = existingFolders[0].path;
3071
+ }
2977
3072
  const uploadedFiles = await strapi.plugins.upload.services.upload.upload({
2978
3073
  data: {
2979
3074
  fileInfo: {
2980
3075
  name: `article-${articleId}.png`,
2981
3076
  alternativeText: `Imagen para el artículo ${article.titulo}`,
2982
- caption: `Imagen generada por IA para el artículo ${article.titulo}`
3077
+ caption: `Imagen generada por IA para el artículo ${article.titulo}`,
3078
+ folder
2983
3079
  }
2984
3080
  },
2985
3081
  files: {
@@ -3007,6 +3103,7 @@ const service = ({ strapi }) => {
3007
3103
  image: uploadedFiles[0]
3008
3104
  };
3009
3105
  } catch (err) {
3106
+ console.log("🚀 ~ service ~ err:", err);
3010
3107
  throw new Error("Error al generar la imagen para el artículo.");
3011
3108
  }
3012
3109
  },
@@ -3037,13 +3134,15 @@ const service = ({ strapi }) => {
3037
3134
  try {
3038
3135
  const article = await strapi.documents("api::articulo.articulo").findOne({
3039
3136
  documentId: articleId,
3040
- populate: {
3041
- image: true
3042
- }
3137
+ populate: ["image", "keywords"]
3043
3138
  });
3044
3139
  if (!article) {
3045
3140
  throw new Error(`Artículo con ID ${articleId} no encontrado.`);
3046
3141
  }
3142
+ const hashtags = article.keywords.map(
3143
+ (keyword) => `#${keyword.name.trim().replace(/\s+/g, "").replace(/[^\w]/g, "")}`
3144
+ // limpia caracteres raros
3145
+ ).join(" ");
3047
3146
  const postBody = {
3048
3147
  author: `urn:li:organization:${linkedInOrganizationId}`,
3049
3148
  lifecycleState: "PUBLISHED",
@@ -3052,10 +3151,26 @@ const service = ({ strapi }) => {
3052
3151
  shareCommentary: {
3053
3152
  text: `${article.titulo}
3054
3153
 
3055
- ${article.descripcion}`
3154
+ ${article.descripcion}
3155
+
3156
+ ${hashtags}`
3056
3157
  },
3057
3158
  shareMediaCategory: "ARTICLE",
3058
3159
  media: [
3160
+ // Imagen del artículo
3161
+ ...article.image ? [
3162
+ {
3163
+ status: "READY",
3164
+ description: {
3165
+ text: article.descripcion
3166
+ },
3167
+ media: `https://api.interware.com.mx${article.image.formats.small.url}`,
3168
+ title: {
3169
+ text: article.titulo
3170
+ }
3171
+ }
3172
+ ] : [],
3173
+ // URL del artículo
3059
3174
  {
3060
3175
  status: "READY",
3061
3176
  originalUrl: `https://interware.com.mx/blog/${article.slug}`,
@@ -3095,6 +3210,7 @@ ${article.descripcion}`
3095
3210
  message: "Publicado en LinkedIn correctamente.",
3096
3211
  linkedinResponse: response.data
3097
3212
  };
3213
+ return true;
3098
3214
  } catch (error) {
3099
3215
  console.error(
3100
3216
  "Error al publicar en LinkedIn:",
@@ -3105,8 +3221,74 @@ ${article.descripcion}`
3105
3221
  },
3106
3222
  async publishToFacebook(ctx) {
3107
3223
  const { articleId } = ctx.request.body;
3108
- console.log("Publishing to Facebook (not implemented):", articleId);
3109
- return { message: "Publicación en Facebook no implementada aún." };
3224
+ const pluginStore = strapi.store({
3225
+ type: "plugin",
3226
+ name: "strapi-content-generator-gemini"
3227
+ });
3228
+ const facebookAccessToken = await pluginStore.get({ key: "FACEBOOK_CLIENT_TOKEN" });
3229
+ const facebookPageId = await pluginStore.get({ key: "FACEBOOK_PAGE_ID" });
3230
+ if (!articleId) {
3231
+ throw new Error("Se requiere 'articleId' en el cuerpo de la petición.");
3232
+ }
3233
+ if (!facebookAccessToken) {
3234
+ throw new Error(
3235
+ "La variable de entorno FACEBOOK_CLIENT_TOKEN no está configurada."
3236
+ );
3237
+ }
3238
+ if (!facebookPageId) {
3239
+ throw new Error(
3240
+ "La variable de entorno FACEBOOK_PAGE_ID no está configurada."
3241
+ );
3242
+ }
3243
+ try {
3244
+ const article = await strapi.documents("api::articulo.articulo").findOne({
3245
+ documentId: articleId,
3246
+ populate: ["keywords"]
3247
+ });
3248
+ if (!article) {
3249
+ throw new Error(`Artículo con ID ${articleId} no encontrado.`);
3250
+ }
3251
+ const articleUrl = `https://interware.com.mx/blog/${article.slug}`;
3252
+ const hashtags = article.keywords.map(
3253
+ (keyword) => `#${keyword.name.trim().replace(/\s+/g, "").replace(/[^\w]/g, "")}`
3254
+ // quita caracteres raros
3255
+ ).join(" ");
3256
+ const postBody = {
3257
+ message: `${article.titulo}
3258
+
3259
+ ${article.descripcion}
3260
+
3261
+ ${hashtags}`,
3262
+ link: articleUrl,
3263
+ access_token: facebookAccessToken
3264
+ };
3265
+ const response = await axios.post(
3266
+ `https://graph.facebook.com/v24.0/${facebookPageId}/feed`,
3267
+ postBody,
3268
+ {
3269
+ headers: {
3270
+ "Content-Type": "application/json"
3271
+ }
3272
+ }
3273
+ );
3274
+ strapi.log.info("Successfully published to Facebook:", JSON.stringify(response.data));
3275
+ await strapi.documents("api::articulo.articulo").update({
3276
+ documentId: articleId,
3277
+ data: {
3278
+ facebookPostId: response.data.id
3279
+ }
3280
+ });
3281
+ return {
3282
+ message: "Publicado en Facebook correctamente.",
3283
+ facebookResponse: response.data
3284
+ };
3285
+ } catch (error) {
3286
+ console.error(
3287
+ "Error al publicar en Facebook:",
3288
+ error.response ? error.response.data : error.message
3289
+ );
3290
+ throw new Error("Error al publicar en Facebook.");
3291
+ }
3110
3292
  }
3111
3293
  };
3112
3294
  };
@@ -5,6 +5,8 @@ declare const controller: ({ strapi }: {
5
5
  getArticles(ctx: any): Promise<unknown>;
6
6
  getCategories(ctx: any): Promise<unknown>;
7
7
  getUsers(ctx: any): Promise<unknown>;
8
+ getImages(ctx: any): Promise<unknown>;
9
+ deleteImage(ctx: any): Promise<unknown>;
8
10
  generateArticle(ctx: any): Promise<void>;
9
11
  generateImageForArticle(ctx: any): Promise<void>;
10
12
  publishToLinkedIn(ctx: any): Promise<void>;
@@ -5,6 +5,8 @@ declare const _default: {
5
5
  getArticles(ctx: any): Promise<unknown>;
6
6
  getCategories(ctx: any): Promise<unknown>;
7
7
  getUsers(ctx: any): Promise<unknown>;
8
+ getImages(ctx: any): Promise<unknown>;
9
+ deleteImage(ctx: any): Promise<unknown>;
8
10
  generateArticle(ctx: any): Promise<void>;
9
11
  generateImageForArticle(ctx: any): Promise<void>;
10
12
  publishToLinkedIn(ctx: any): Promise<void>;