@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.
- package/dist/_chunks/{App-KtJGFVFQ.mjs → App-Cbkccs2K.mjs} +552 -55
- package/dist/_chunks/{App-Bt1yydY9.js → App-DCuBRwX8.js} +545 -48
- package/dist/_chunks/{Settings-CFag78zw.mjs → Settings-B60QGKK4.mjs} +48 -4
- package/dist/_chunks/{Settings-BbIpY74m.js → Settings-DuGbtyWC.js} +48 -4
- package/dist/_chunks/{getTranslation-BcuPOeW7.mjs → getTranslation-B97xbmEl.mjs} +1 -1
- package/dist/_chunks/{getTranslation-DA3AxQPb.js → getTranslation-CacX7ZX5.js} +1 -1
- package/dist/_chunks/{index-B6aEv34B.mjs → index-B2RuvYul.mjs} +2 -2
- package/dist/_chunks/{index-JZA3FSFH.js → index-BVGbP3E-.js} +2 -2
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/src/components/Navigation.d.ts +2 -0
- package/dist/admin/src/components/generate-content/Form.d.ts +1 -1
- package/dist/admin/src/components/generate-images/Image.d.ts +5 -0
- package/dist/admin/src/components/generate-images/Images.d.ts +2 -0
- package/dist/admin/src/components/generate-images/Modal.d.ts +9 -0
- package/dist/admin/src/components/home/table.d.ts +2 -1
- package/dist/admin/src/containers/generate-images/index.d.ts +2 -0
- package/dist/admin/src/containers/settings/index.d.ts +2 -0
- package/dist/admin/src/hooks/generate-images/useImages.d.ts +11 -0
- package/dist/admin/src/lib/use-cases/facebook/index.d.ts +5 -0
- package/dist/admin/src/lib/use-cases/images/index.d.ts +6 -0
- package/dist/admin/src/pages/GenerateImages.d.ts +2 -0
- package/dist/server/index.js +309 -147
- package/dist/server/index.mjs +309 -127
- package/dist/server/src/controllers/controller.d.ts +2 -0
- package/dist/server/src/controllers/index.d.ts +2 -0
- package/dist/server/src/index.d.ts +56 -1
- package/dist/server/src/services/index.d.ts +54 -1
- package/dist/server/src/services/service.d.ts +54 -1
- package/package.json +1 -2
package/dist/server/index.mjs
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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,
|
|
2804
|
+
const { titulo, categoria, autor, temperatura = 0.2, instrucciones } = ctx.request.body;
|
|
2762
2805
|
const promptArticulo = `
|
|
2763
|
-
|
|
2806
|
+
Actúa como un Redactor Técnico Senior experto en tecnología corporativa para la empresa "InterWare"
|
|
2764
2807
|
|
|
2765
|
-
|
|
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
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
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
|
-
|
|
2782
|
-
|
|
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
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2908
|
-
|
|
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
|
-
|
|
2913
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2923
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2933
|
-
|
|
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
|
-
|
|
3109
|
-
|
|
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>;
|