@interware/content-generator 0.1.3 → 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.
- package/dist/_chunks/{App-BO5dp398.mjs → App-Cbkccs2K.mjs} +585 -58
- package/dist/_chunks/{App-Z_jLmgEa.js → App-DCuBRwX8.js} +578 -51
- package/dist/_chunks/{Settings-C6a6AgQy.mjs → Settings-B60QGKK4.mjs} +48 -4
- package/dist/_chunks/{Settings-CT0AQ5Px.js → Settings-DuGbtyWC.js} +48 -4
- package/dist/_chunks/{getTranslation-CIMKEgGr.mjs → getTranslation-B97xbmEl.mjs} +1 -1
- package/dist/_chunks/{getTranslation-BdWuhMoN.js → getTranslation-CacX7ZX5.js} +1 -1
- package/dist/_chunks/{index-CMQYbwsn.mjs → index-B2RuvYul.mjs} +2 -2
- package/dist/_chunks/{index-CSMac09y.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.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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,
|
|
2805
|
+
const { titulo, categoria, autor, temperatura = 0.2, instrucciones } = ctx.request.body;
|
|
2783
2806
|
const promptArticulo = `
|
|
2784
|
-
|
|
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
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
-
|
|
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
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
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
|
-
|
|
2803
|
-
|
|
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
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
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
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
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
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
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
|
-
|
|
2954
|
-
|
|
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
|
-
|
|
3130
|
-
|
|
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
|
};
|