@interware/content-generator 0.1.4 → 0.2.0

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