@interware/content-generator 0.5.0 → 0.6.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-DVTFrZvf.js → App-CJxSC8yt.js} +5 -4
- package/dist/_chunks/{App-DPVBu7QB.mjs → App-qbgUXlOZ.mjs} +4 -3
- package/dist/_chunks/{ImageStudioTabs-mlNL6aQ2.mjs → ImageStudioTabs-BLAH3KYv.mjs} +8 -36
- package/dist/_chunks/{ImageStudioTabs-B2UbKYq7.js → ImageStudioTabs-DtusqpQO.js} +7 -35
- package/dist/_chunks/PluginProviders-D79TeGSE.js +34 -0
- package/dist/_chunks/PluginProviders-DWMpcAIh.mjs +35 -0
- package/dist/_chunks/{Settings-DziRY70a.js → Settings-BQXxV3LO.js} +93 -6
- package/dist/_chunks/{Settings-RrBQdijj.mjs → Settings-o2GngLaW.mjs} +94 -7
- package/dist/_chunks/{StudioApp-Cswkzh7F.js → StudioApp-BaySMwrk.js} +4 -3
- package/dist/_chunks/{StudioApp-AUyPNhh2.mjs → StudioApp-C_znlQRo.mjs} +3 -2
- package/dist/_chunks/{getTranslation-DfxaP_TC.mjs → getTranslation-B6Bsu-uq.mjs} +1 -1
- package/dist/_chunks/{getTranslation-D88uLXGd.js → getTranslation-BRVMWSf8.js} +1 -1
- package/dist/_chunks/{index-Ddp0Qvqo.js → index-BKhe-Jyx.js} +3 -3
- package/dist/_chunks/{index-CqDdi211.mjs → index-BKigRXm3.mjs} +3 -3
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/src/components/PluginProviders.d.ts +6 -2
- package/dist/admin/src/components/settings/VertexSettings.d.ts +18 -0
- package/dist/admin/src/hooks/useAdminTheme.d.ts +1 -1
- package/dist/admin/src/lib/use-cases/settings/index.d.ts +1 -0
- package/dist/server/index.js +95 -27
- package/dist/server/index.mjs +96 -28
- package/dist/server/src/controllers/config.d.ts +10 -2
- package/dist/server/src/controllers/index.d.ts +1 -0
- package/dist/server/src/image-studio/models.d.ts +2 -1
- package/dist/server/src/index.d.ts +4 -0
- package/dist/server/src/services/image-studio.d.ts +9 -5
- package/dist/server/src/services/index.d.ts +3 -0
- package/dist/server/src/services/service.d.ts +10 -0
- package/dist/server/src/utils/geminiClient.d.ts +24 -0
- package/dist/server/src/utils/serviceAccount.d.ts +29 -0
- package/package.json +2 -1
|
@@ -79,7 +79,7 @@ const index = {
|
|
|
79
79
|
defaultMessage: "Generador de contenido"
|
|
80
80
|
},
|
|
81
81
|
Component: async () => {
|
|
82
|
-
const { App } = await Promise.resolve().then(() => require("./App-
|
|
82
|
+
const { App } = await Promise.resolve().then(() => require("./App-CJxSC8yt.js"));
|
|
83
83
|
return App;
|
|
84
84
|
},
|
|
85
85
|
permissions: [
|
|
@@ -94,7 +94,7 @@ const index = {
|
|
|
94
94
|
defaultMessage: "Estudio de imágenes"
|
|
95
95
|
},
|
|
96
96
|
Component: async () => {
|
|
97
|
-
const { StudioApp } = await Promise.resolve().then(() => require("./StudioApp-
|
|
97
|
+
const { StudioApp } = await Promise.resolve().then(() => require("./StudioApp-BaySMwrk.js"));
|
|
98
98
|
return StudioApp;
|
|
99
99
|
},
|
|
100
100
|
permissions: [
|
|
@@ -115,7 +115,7 @@ const index = {
|
|
|
115
115
|
defaultMessage: PLUGIN_ID
|
|
116
116
|
},
|
|
117
117
|
Component: async () => {
|
|
118
|
-
const component = await Promise.resolve().then(() => require("./Settings-
|
|
118
|
+
const component = await Promise.resolve().then(() => require("./Settings-BQXxV3LO.js"));
|
|
119
119
|
return component.default;
|
|
120
120
|
},
|
|
121
121
|
to: `plugins/${PLUGIN_ID}/settings`,
|
|
@@ -78,7 +78,7 @@ const index = {
|
|
|
78
78
|
defaultMessage: "Generador de contenido"
|
|
79
79
|
},
|
|
80
80
|
Component: async () => {
|
|
81
|
-
const { App } = await import("./App-
|
|
81
|
+
const { App } = await import("./App-qbgUXlOZ.mjs");
|
|
82
82
|
return App;
|
|
83
83
|
},
|
|
84
84
|
permissions: [
|
|
@@ -93,7 +93,7 @@ const index = {
|
|
|
93
93
|
defaultMessage: "Estudio de imágenes"
|
|
94
94
|
},
|
|
95
95
|
Component: async () => {
|
|
96
|
-
const { StudioApp } = await import("./StudioApp-
|
|
96
|
+
const { StudioApp } = await import("./StudioApp-C_znlQRo.mjs");
|
|
97
97
|
return StudioApp;
|
|
98
98
|
},
|
|
99
99
|
permissions: [
|
|
@@ -114,7 +114,7 @@ const index = {
|
|
|
114
114
|
defaultMessage: PLUGIN_ID
|
|
115
115
|
},
|
|
116
116
|
Component: async () => {
|
|
117
|
-
const component = await import("./Settings-
|
|
117
|
+
const component = await import("./Settings-o2GngLaW.mjs");
|
|
118
118
|
return component.default;
|
|
119
119
|
},
|
|
120
120
|
to: `plugins/${PLUGIN_ID}/settings`,
|
package/dist/admin/index.js
CHANGED
package/dist/admin/index.mjs
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
/**
|
|
3
|
-
* Proveedores del plugin. El plugin
|
|
4
|
-
*
|
|
3
|
+
* Proveedores del plugin. El plugin trae su propia copia del Design System (2.2), así que sus componentes
|
|
4
|
+
* necesitan su tema (claro u oscuro según el panel, S4.10) y el proveedor de tooltips.
|
|
5
|
+
*
|
|
6
|
+
* NO se usa `DesignSystemProvider`: monta un `GlobalStyle` con el mismo id que el del panel y, al desmontarse
|
|
7
|
+
* (al salir del plugin), styled-components borraba también los estilos globales del panel: todo Strapi quedaba
|
|
8
|
+
* en Times hasta recargar. Los estilos globales y las regiones `aria-live` ya los pone el panel.
|
|
5
9
|
*/
|
|
6
10
|
export declare const PluginProviders: ({ children }: {
|
|
7
11
|
children: ReactNode;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Estado público de la cuenta de servicio que devuelve `GET /config`: nunca la llave (S5.1). */
|
|
2
|
+
export interface VertexStatus {
|
|
3
|
+
configured: boolean;
|
|
4
|
+
projectId: string | null;
|
|
5
|
+
clientEmail: string | null;
|
|
6
|
+
}
|
|
7
|
+
interface VertexSettingsProps {
|
|
8
|
+
status: VertexStatus;
|
|
9
|
+
/** Guarda el JSON pegado. Rechaza con el mensaje del servidor si no es válido. */
|
|
10
|
+
onSave: (json: string) => Promise<void>;
|
|
11
|
+
onRemove: () => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Cuenta de servicio de Vertex AI en Ajustes (S5.1, decisión de Israel). Se pega el JSON completo; al guardarse,
|
|
15
|
+
* el campo se limpia y la llave no vuelve a mostrarse: solo el proyecto y el correo de la cuenta.
|
|
16
|
+
*/
|
|
17
|
+
export declare const VertexSettings: ({ status, onSave, onRemove }: VertexSettingsProps) => import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
type Scheme = 'light' | 'dark';
|
|
2
2
|
/**
|
|
3
|
-
* Tema del panel de Strapi para el `
|
|
3
|
+
* Tema del panel de Strapi para el `ThemeProvider` del plugin (S4.10; `PluginProviders`). Sin esto el
|
|
4
4
|
* plugin se pintaba siempre claro aunque el panel estuviera en oscuro. Con «system» sigue los
|
|
5
5
|
* cambios del sistema operativo.
|
|
6
6
|
*/
|
|
@@ -13,5 +13,6 @@ declare const useSettings: () => {
|
|
|
13
13
|
<TData = unknown>(url: string, config?: FetchOptions | undefined): Promise<import("@strapi/strapi/admin").FetchResponse<TData>>;
|
|
14
14
|
}>>;
|
|
15
15
|
setSettings: (data: any, config?: FetchOptions) => Promise<import("@strapi/strapi/admin").FetchResponse<(<TData_1 = unknown, TSend = unknown>(url: string, data?: TSend | undefined, config?: FetchOptions | undefined) => Promise<import("@strapi/strapi/admin").FetchResponse<TData_1>>)>>;
|
|
16
|
+
removeVertex: () => Promise<import("@strapi/strapi/admin").FetchResponse<unknown>>;
|
|
16
17
|
};
|
|
17
18
|
export { useSettings };
|
package/dist/server/index.js
CHANGED
|
@@ -180,6 +180,34 @@ const maskSecret = (value) => {
|
|
|
180
180
|
}
|
|
181
181
|
return { configured: true, last4: value.slice(-4) };
|
|
182
182
|
};
|
|
183
|
+
const VERTEX_KEY = "VERTEX_SERVICE_ACCOUNT";
|
|
184
|
+
const REQUIRED = ["project_id", "client_email", "private_key"];
|
|
185
|
+
const parseServiceAccount = (value) => {
|
|
186
|
+
if (typeof value !== "string") return { ok: false, message: "El texto no es un JSON válido." };
|
|
187
|
+
let data;
|
|
188
|
+
try {
|
|
189
|
+
const parsed = JSON.parse(value);
|
|
190
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error();
|
|
191
|
+
data = parsed;
|
|
192
|
+
} catch {
|
|
193
|
+
return { ok: false, message: "El texto no es un JSON válido." };
|
|
194
|
+
}
|
|
195
|
+
if (data.type !== "service_account") {
|
|
196
|
+
return { ok: false, message: "El JSON no es de una cuenta de servicio (type debe ser service_account)." };
|
|
197
|
+
}
|
|
198
|
+
const missing = REQUIRED.filter((k) => typeof data[k] !== "string" || data[k].trim() === "");
|
|
199
|
+
if (missing.length) return { ok: false, message: `El JSON no es de una cuenta de servicio: falta ${missing.join(", ")}.` };
|
|
200
|
+
return {
|
|
201
|
+
ok: true,
|
|
202
|
+
account: { projectId: data.project_id, clientEmail: data.client_email, privateKey: data.private_key }
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
const storedServiceAccount = (account) => JSON.stringify({ type: "service_account", project_id: account.projectId, client_email: account.clientEmail, private_key: account.privateKey });
|
|
206
|
+
const publicServiceAccount = (stored) => {
|
|
207
|
+
const parsed = parseServiceAccount(stored);
|
|
208
|
+
if (!("account" in parsed)) return { configured: false, projectId: null, clientEmail: null };
|
|
209
|
+
return { configured: true, projectId: parsed.account.projectId, clientEmail: parsed.account.clientEmail };
|
|
210
|
+
};
|
|
183
211
|
const CREDENTIALS = {
|
|
184
212
|
googleGenAI: "GOOGLE_GEN_AI",
|
|
185
213
|
linkedInAccessToken: "LINKEDIN_ACCESS_TOKEN",
|
|
@@ -194,18 +222,31 @@ const config$1 = ({ strapi }) => {
|
|
|
194
222
|
name: "strapi-content-generator-gemini"
|
|
195
223
|
});
|
|
196
224
|
return {
|
|
197
|
-
/**
|
|
225
|
+
/**
|
|
226
|
+
* Devuelve si cada credencial está guardada y sus últimos 4 caracteres, nunca el valor. Para Vertex,
|
|
227
|
+
* el proyecto y el correo de la cuenta de servicio, nunca la llave privada.
|
|
228
|
+
*/
|
|
198
229
|
async get(ctx) {
|
|
199
230
|
const store = pluginStore();
|
|
200
231
|
const entries = await Promise.all(
|
|
201
232
|
FIELDS$1.map(async (field) => [field, maskSecret(await store.get({ key: CREDENTIALS[field] }))])
|
|
202
233
|
);
|
|
203
|
-
ctx.body = Object.fromEntries(entries);
|
|
234
|
+
ctx.body = { ...Object.fromEntries(entries), vertex: publicServiceAccount(await store.get({ key: VERTEX_KEY })) };
|
|
204
235
|
},
|
|
205
|
-
/**
|
|
236
|
+
/**
|
|
237
|
+
* Escribe solo las credenciales conocidas que llegan como texto no vacío. Una cuenta de servicio
|
|
238
|
+
* inválida rechaza la petición completa (400) antes de escribir nada.
|
|
239
|
+
*/
|
|
206
240
|
async set(ctx) {
|
|
207
241
|
const body = ctx.request.body ?? {};
|
|
208
242
|
const store = pluginStore();
|
|
243
|
+
const vertex = typeof body.vertexServiceAccount === "string" && body.vertexServiceAccount.trim() !== "" ? parseServiceAccount(body.vertexServiceAccount) : null;
|
|
244
|
+
if (vertex && "message" in vertex) {
|
|
245
|
+
ctx.status = 400;
|
|
246
|
+
ctx.body = { data: null, error: { status: 400, name: "ValidationError", message: vertex.message } };
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (vertex && "account" in vertex) await store.set({ key: VERTEX_KEY, value: storedServiceAccount(vertex.account) });
|
|
209
250
|
for (const field of FIELDS$1) {
|
|
210
251
|
const value = body[field];
|
|
211
252
|
if (typeof value === "string" && value.length > 0) {
|
|
@@ -213,6 +254,11 @@ const config$1 = ({ strapi }) => {
|
|
|
213
254
|
}
|
|
214
255
|
}
|
|
215
256
|
ctx.body = { ok: true };
|
|
257
|
+
},
|
|
258
|
+
/** Quita la cuenta de servicio: el plugin vuelve a la llave de la Gemini API (S5.1). */
|
|
259
|
+
async removeVertex(ctx) {
|
|
260
|
+
await pluginStore().delete({ key: VERTEX_KEY });
|
|
261
|
+
ctx.body = { ok: true };
|
|
216
262
|
}
|
|
217
263
|
};
|
|
218
264
|
};
|
|
@@ -256,7 +302,7 @@ const inlinePart = (bytes, mimeType) => ({ inlineData: { mimeType, data: bytes.t
|
|
|
256
302
|
const RATIOS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"];
|
|
257
303
|
const FLASH_RATIOS = [...RATIOS, "1:4", "4:1", "1:8", "8:1"];
|
|
258
304
|
const FLASH = { maxReferences: 6, sizes: ["512", "1K", "2K", "4K"], aspectRatios: FLASH_RATIOS, canEdit: true, grounding: true };
|
|
259
|
-
const FLASH_LITE = { maxReferences: 6, sizes: ["
|
|
305
|
+
const FLASH_LITE = { maxReferences: 6, sizes: ["1K"], aspectRatios: RATIOS, canEdit: true, grounding: false };
|
|
260
306
|
const PRO = { maxReferences: 6, sizes: ["1K", "2K", "4K"], aspectRatios: RATIOS, canEdit: true, grounding: false };
|
|
261
307
|
const IMAGE_MODELS = [
|
|
262
308
|
{ id: "gemini-3.1-flash-image", label: "Nano Banana 2", ...FLASH },
|
|
@@ -270,7 +316,7 @@ const IMAGE_MODELS = [
|
|
|
270
316
|
const findModel = (id) => IMAGE_MODELS.find((m) => m.id === id);
|
|
271
317
|
const availableModels = (listed) => {
|
|
272
318
|
if (!listed) return { models: IMAGE_MODELS, verified: false };
|
|
273
|
-
const ids = new Set(listed.map((n) => n.replace(/^models\//, "")));
|
|
319
|
+
const ids = new Set(listed.map((n) => n.replace(/^(publishers\/google\/)?models\//, "")));
|
|
274
320
|
const models = IMAGE_MODELS.filter((m) => ids.has(m.id) && !(m.id.endsWith("-preview") && ids.has(m.id.replace(/-preview$/, ""))));
|
|
275
321
|
return models.length ? { models, verified: true } : { models: IMAGE_MODELS, verified: false };
|
|
276
322
|
};
|
|
@@ -330,10 +376,10 @@ const imageFrom = (res, model) => {
|
|
|
330
376
|
};
|
|
331
377
|
const inline = async (file, read) => inlinePart(await read(file.filepath), file.mimetype ?? "image/png");
|
|
332
378
|
const userTurn = (parts) => [{ role: "user", parts }];
|
|
333
|
-
const createImageStudio = (
|
|
379
|
+
const createImageStudio = (getClient, read = (p) => fs.promises.readFile(p)) => ({
|
|
334
380
|
async models() {
|
|
335
381
|
try {
|
|
336
|
-
const api = await
|
|
382
|
+
const { models: api } = await getClient();
|
|
337
383
|
const names = [];
|
|
338
384
|
for await (const m of await api.list({ config: { pageSize: 100 } })) if (m.name) names.push(m.name);
|
|
339
385
|
return availableModels(names);
|
|
@@ -361,7 +407,7 @@ const createImageStudio = (getModels, read = (p) => fs.promises.readFile(p), bac
|
|
|
361
407
|
return image;
|
|
362
408
|
},
|
|
363
409
|
async generate(input) {
|
|
364
|
-
const api = await
|
|
410
|
+
const { models: api, backend } = await getClient();
|
|
365
411
|
const text2 = cleanPrompt(input.prompt);
|
|
366
412
|
const { model } = input;
|
|
367
413
|
const refs = await Promise.all(input.references.map((f) => inline(f, read)));
|
|
@@ -376,7 +422,7 @@ const createImageStudio = (getModels, read = (p) => fs.promises.readFile(p), bac
|
|
|
376
422
|
return grounding ? { ...image, grounding } : image;
|
|
377
423
|
},
|
|
378
424
|
async edit(input) {
|
|
379
|
-
const api = await
|
|
425
|
+
const { models: api, backend } = await getClient();
|
|
380
426
|
const parts = [
|
|
381
427
|
await inline(input.image, read),
|
|
382
428
|
...await Promise.all(input.references.map((f) => inline(f, read))),
|
|
@@ -387,14 +433,17 @@ const createImageStudio = (getModels, read = (p) => fs.promises.readFile(p), bac
|
|
|
387
433
|
return imageFrom(res, input.model.id);
|
|
388
434
|
},
|
|
389
435
|
async expand(prompt) {
|
|
390
|
-
const api = await
|
|
436
|
+
const { models: api } = await getClient();
|
|
391
437
|
const res = await api.generateContent({ model: EXPAND_MODEL, contents: EXPAND_INSTRUCTION(cleanPrompt(prompt)) });
|
|
392
438
|
const text2 = (res.text ?? "").trim().replace(/^["'«“]+|["'»”]+$/g, "").trim();
|
|
393
439
|
if (!text2) throw new BlockedError("No se pudo expandir la idea. Intenta de nuevo.");
|
|
394
440
|
return text2;
|
|
395
441
|
}
|
|
396
442
|
});
|
|
397
|
-
const imageStudio$1 = ({ strapi }) => createImageStudio(async () =>
|
|
443
|
+
const imageStudio$1 = ({ strapi }) => createImageStudio(async () => {
|
|
444
|
+
const { ai, backend } = await strapi.plugin("strapi-content-generator-gemini").service("service").geminiClient();
|
|
445
|
+
return { models: ai.models, backend };
|
|
446
|
+
});
|
|
398
447
|
class InputError extends Error {
|
|
399
448
|
}
|
|
400
449
|
const ALLOWED = ["image/png", "image/jpeg", "image/webp"];
|
|
@@ -881,7 +930,8 @@ const admin = [
|
|
|
881
930
|
{ method: "POST", path: "/template-folders/rename", handler: "templates.renameFolder", config },
|
|
882
931
|
{ method: "POST", path: "/template-folders/delete", handler: "templates.deleteFolder", config },
|
|
883
932
|
{ method: "GET", path: "/config", handler: "config.get", config },
|
|
884
|
-
{ method: "POST", path: "/config", handler: "config.set", config }
|
|
933
|
+
{ method: "POST", path: "/config", handler: "config.set", config },
|
|
934
|
+
{ method: "DELETE", path: "/config/vertex", handler: "config.removeVertex", config }
|
|
885
935
|
];
|
|
886
936
|
const contentApi = [];
|
|
887
937
|
const routes = {
|
|
@@ -4120,25 +4170,43 @@ const {
|
|
|
4120
4170
|
mergeConfig,
|
|
4121
4171
|
create
|
|
4122
4172
|
} = axios;
|
|
4173
|
+
const VERTEX_LOCATION = "global";
|
|
4174
|
+
const SCOPES = ["https://www.googleapis.com/auth/cloud-platform"];
|
|
4175
|
+
const createGeminiClient = ({ serviceAccount, apiKey, allowVertex = true }, Ctor = genai.GoogleGenAI) => {
|
|
4176
|
+
const parsed = allowVertex && serviceAccount ? parseServiceAccount(serviceAccount) : null;
|
|
4177
|
+
if (parsed && "account" in parsed) {
|
|
4178
|
+
const { projectId, clientEmail, privateKey } = parsed.account;
|
|
4179
|
+
const ai = new Ctor({
|
|
4180
|
+
vertexai: true,
|
|
4181
|
+
project: projectId,
|
|
4182
|
+
location: VERTEX_LOCATION,
|
|
4183
|
+
googleAuthOptions: { credentials: { client_email: clientEmail, private_key: privateKey }, scopes: SCOPES }
|
|
4184
|
+
});
|
|
4185
|
+
return { ai, backend: "vertex" };
|
|
4186
|
+
}
|
|
4187
|
+
if (typeof apiKey === "string" && apiKey) return { ai: new Ctor({ apiKey }), backend: "gemini-api" };
|
|
4188
|
+
throw new Error(
|
|
4189
|
+
allowVertex ? "Configura la cuenta de servicio de Vertex AI o la llave de Gemini en Ajustes del plugin." : "El video necesita la llave de Gemini en Ajustes del plugin."
|
|
4190
|
+
);
|
|
4191
|
+
};
|
|
4123
4192
|
const service = ({ strapi }) => {
|
|
4124
4193
|
return {
|
|
4125
4194
|
// ───────────────────────────────
|
|
4126
4195
|
// 🤖 AI Configuration
|
|
4127
4196
|
// ───────────────────────────────
|
|
4197
|
+
/**
|
|
4198
|
+
* Cliente de Google y su backend (S5.1): con la cuenta de servicio de Ajustes, Vertex AI en `global`; si
|
|
4199
|
+
* no, la llave (`GOOGLE_API_KEY` del entorno gana a la del store, como antes; D10). El video pide
|
|
4200
|
+
* `allowVertex: false` (D7).
|
|
4201
|
+
*/
|
|
4202
|
+
async geminiClient({ allowVertex = true } = {}) {
|
|
4203
|
+
const pluginStore = strapi.store({ type: "plugin", name: "strapi-content-generator-gemini" });
|
|
4204
|
+
const [serviceAccount, storedKey] = await Promise.all([pluginStore.get({ key: VERTEX_KEY }), pluginStore.get({ key: "GOOGLE_GEN_AI" })]);
|
|
4205
|
+
return createGeminiClient({ serviceAccount, apiKey: process.env.GOOGLE_API_KEY || storedKey, allowVertex });
|
|
4206
|
+
},
|
|
4207
|
+
/** Atajo para los usos que solo necesitan el cliente (artículos). */
|
|
4128
4208
|
async geminiConfig() {
|
|
4129
|
-
|
|
4130
|
-
type: "plugin",
|
|
4131
|
-
name: "strapi-content-generator-gemini"
|
|
4132
|
-
});
|
|
4133
|
-
const googleGenAI = await pluginStore.get({ key: "GOOGLE_GEN_AI" });
|
|
4134
|
-
const geminiApiKey = process.env.GOOGLE_API_KEY || googleGenAI;
|
|
4135
|
-
if (!geminiApiKey) {
|
|
4136
|
-
throw new Error(
|
|
4137
|
-
"La variable de entorno GOOGLE_API_KEY o GOOGLE_GEN_AI no está configurada."
|
|
4138
|
-
);
|
|
4139
|
-
}
|
|
4140
|
-
const ai = new genai.GoogleGenAI({ apiKey: geminiApiKey });
|
|
4141
|
-
return ai;
|
|
4209
|
+
return (await this.geminiClient()).ai;
|
|
4142
4210
|
},
|
|
4143
4211
|
async getArticulos(sanitizedQueryParams) {
|
|
4144
4212
|
return await strapi.entityService.findMany("api::articulo.articulo", sanitizedQueryParams);
|
|
@@ -4446,7 +4514,7 @@ const service = ({ strapi }) => {
|
|
|
4446
4514
|
},
|
|
4447
4515
|
// 🎞️ Internal Veo Video Generation / Extension Helper
|
|
4448
4516
|
async _callVeoVideoGeneration(promptText, aspectRatio, durationSeconds, sceneRefs, previousVideo) {
|
|
4449
|
-
const ai = await this.
|
|
4517
|
+
const { ai } = await this.geminiClient({ allowVertex: false });
|
|
4450
4518
|
const cleanPrompt2 = promptText.replace(/\s+/g, " ").trim();
|
|
4451
4519
|
const referenceImages = sceneRefs.map((img) => ({
|
|
4452
4520
|
image: {
|
|
@@ -4516,7 +4584,7 @@ const service = ({ strapi }) => {
|
|
|
4516
4584
|
throw new Error("No se generó ningún video.");
|
|
4517
4585
|
}
|
|
4518
4586
|
const finalVideoPath = `/tmp/video-${Date.now()}.mp4`;
|
|
4519
|
-
const ai = await this.
|
|
4587
|
+
const { ai } = await this.geminiClient({ allowVertex: false });
|
|
4520
4588
|
await ai.files.download({ file: previousVideo, downloadPath: finalVideoPath });
|
|
4521
4589
|
const uploadedFile = await this._uploadGeneratedFile(
|
|
4522
4590
|
finalVideoPath,
|
package/dist/server/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import { promises } from "fs";
|
|
3
|
-
import { HarmCategory, PersonGeneration, Modality, HarmBlockThreshold,
|
|
3
|
+
import { HarmCategory, PersonGeneration, Modality, HarmBlockThreshold, GoogleGenAI, VideoGenerationReferenceType } from "@google/genai";
|
|
4
4
|
const PLUGIN_ID = "strapi-content-generator-gemini";
|
|
5
5
|
const FOLDER_UID = "plugin::upload.folder";
|
|
6
6
|
const hasValidPath = (path) => /^(\/\d+)+$/.test(path);
|
|
@@ -162,6 +162,34 @@ const maskSecret = (value) => {
|
|
|
162
162
|
}
|
|
163
163
|
return { configured: true, last4: value.slice(-4) };
|
|
164
164
|
};
|
|
165
|
+
const VERTEX_KEY = "VERTEX_SERVICE_ACCOUNT";
|
|
166
|
+
const REQUIRED = ["project_id", "client_email", "private_key"];
|
|
167
|
+
const parseServiceAccount = (value) => {
|
|
168
|
+
if (typeof value !== "string") return { ok: false, message: "El texto no es un JSON válido." };
|
|
169
|
+
let data;
|
|
170
|
+
try {
|
|
171
|
+
const parsed = JSON.parse(value);
|
|
172
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error();
|
|
173
|
+
data = parsed;
|
|
174
|
+
} catch {
|
|
175
|
+
return { ok: false, message: "El texto no es un JSON válido." };
|
|
176
|
+
}
|
|
177
|
+
if (data.type !== "service_account") {
|
|
178
|
+
return { ok: false, message: "El JSON no es de una cuenta de servicio (type debe ser service_account)." };
|
|
179
|
+
}
|
|
180
|
+
const missing = REQUIRED.filter((k) => typeof data[k] !== "string" || data[k].trim() === "");
|
|
181
|
+
if (missing.length) return { ok: false, message: `El JSON no es de una cuenta de servicio: falta ${missing.join(", ")}.` };
|
|
182
|
+
return {
|
|
183
|
+
ok: true,
|
|
184
|
+
account: { projectId: data.project_id, clientEmail: data.client_email, privateKey: data.private_key }
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
const storedServiceAccount = (account) => JSON.stringify({ type: "service_account", project_id: account.projectId, client_email: account.clientEmail, private_key: account.privateKey });
|
|
188
|
+
const publicServiceAccount = (stored) => {
|
|
189
|
+
const parsed = parseServiceAccount(stored);
|
|
190
|
+
if (!("account" in parsed)) return { configured: false, projectId: null, clientEmail: null };
|
|
191
|
+
return { configured: true, projectId: parsed.account.projectId, clientEmail: parsed.account.clientEmail };
|
|
192
|
+
};
|
|
165
193
|
const CREDENTIALS = {
|
|
166
194
|
googleGenAI: "GOOGLE_GEN_AI",
|
|
167
195
|
linkedInAccessToken: "LINKEDIN_ACCESS_TOKEN",
|
|
@@ -176,18 +204,31 @@ const config$1 = ({ strapi }) => {
|
|
|
176
204
|
name: "strapi-content-generator-gemini"
|
|
177
205
|
});
|
|
178
206
|
return {
|
|
179
|
-
/**
|
|
207
|
+
/**
|
|
208
|
+
* Devuelve si cada credencial está guardada y sus últimos 4 caracteres, nunca el valor. Para Vertex,
|
|
209
|
+
* el proyecto y el correo de la cuenta de servicio, nunca la llave privada.
|
|
210
|
+
*/
|
|
180
211
|
async get(ctx) {
|
|
181
212
|
const store = pluginStore();
|
|
182
213
|
const entries = await Promise.all(
|
|
183
214
|
FIELDS$1.map(async (field) => [field, maskSecret(await store.get({ key: CREDENTIALS[field] }))])
|
|
184
215
|
);
|
|
185
|
-
ctx.body = Object.fromEntries(entries);
|
|
216
|
+
ctx.body = { ...Object.fromEntries(entries), vertex: publicServiceAccount(await store.get({ key: VERTEX_KEY })) };
|
|
186
217
|
},
|
|
187
|
-
/**
|
|
218
|
+
/**
|
|
219
|
+
* Escribe solo las credenciales conocidas que llegan como texto no vacío. Una cuenta de servicio
|
|
220
|
+
* inválida rechaza la petición completa (400) antes de escribir nada.
|
|
221
|
+
*/
|
|
188
222
|
async set(ctx) {
|
|
189
223
|
const body = ctx.request.body ?? {};
|
|
190
224
|
const store = pluginStore();
|
|
225
|
+
const vertex = typeof body.vertexServiceAccount === "string" && body.vertexServiceAccount.trim() !== "" ? parseServiceAccount(body.vertexServiceAccount) : null;
|
|
226
|
+
if (vertex && "message" in vertex) {
|
|
227
|
+
ctx.status = 400;
|
|
228
|
+
ctx.body = { data: null, error: { status: 400, name: "ValidationError", message: vertex.message } };
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (vertex && "account" in vertex) await store.set({ key: VERTEX_KEY, value: storedServiceAccount(vertex.account) });
|
|
191
232
|
for (const field of FIELDS$1) {
|
|
192
233
|
const value = body[field];
|
|
193
234
|
if (typeof value === "string" && value.length > 0) {
|
|
@@ -195,6 +236,11 @@ const config$1 = ({ strapi }) => {
|
|
|
195
236
|
}
|
|
196
237
|
}
|
|
197
238
|
ctx.body = { ok: true };
|
|
239
|
+
},
|
|
240
|
+
/** Quita la cuenta de servicio: el plugin vuelve a la llave de la Gemini API (S5.1). */
|
|
241
|
+
async removeVertex(ctx) {
|
|
242
|
+
await pluginStore().delete({ key: VERTEX_KEY });
|
|
243
|
+
ctx.body = { ok: true };
|
|
198
244
|
}
|
|
199
245
|
};
|
|
200
246
|
};
|
|
@@ -238,7 +284,7 @@ const inlinePart = (bytes, mimeType) => ({ inlineData: { mimeType, data: bytes.t
|
|
|
238
284
|
const RATIOS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"];
|
|
239
285
|
const FLASH_RATIOS = [...RATIOS, "1:4", "4:1", "1:8", "8:1"];
|
|
240
286
|
const FLASH = { maxReferences: 6, sizes: ["512", "1K", "2K", "4K"], aspectRatios: FLASH_RATIOS, canEdit: true, grounding: true };
|
|
241
|
-
const FLASH_LITE = { maxReferences: 6, sizes: ["
|
|
287
|
+
const FLASH_LITE = { maxReferences: 6, sizes: ["1K"], aspectRatios: RATIOS, canEdit: true, grounding: false };
|
|
242
288
|
const PRO = { maxReferences: 6, sizes: ["1K", "2K", "4K"], aspectRatios: RATIOS, canEdit: true, grounding: false };
|
|
243
289
|
const IMAGE_MODELS = [
|
|
244
290
|
{ id: "gemini-3.1-flash-image", label: "Nano Banana 2", ...FLASH },
|
|
@@ -252,7 +298,7 @@ const IMAGE_MODELS = [
|
|
|
252
298
|
const findModel = (id) => IMAGE_MODELS.find((m) => m.id === id);
|
|
253
299
|
const availableModels = (listed) => {
|
|
254
300
|
if (!listed) return { models: IMAGE_MODELS, verified: false };
|
|
255
|
-
const ids = new Set(listed.map((n) => n.replace(/^models\//, "")));
|
|
301
|
+
const ids = new Set(listed.map((n) => n.replace(/^(publishers\/google\/)?models\//, "")));
|
|
256
302
|
const models = IMAGE_MODELS.filter((m) => ids.has(m.id) && !(m.id.endsWith("-preview") && ids.has(m.id.replace(/-preview$/, ""))));
|
|
257
303
|
return models.length ? { models, verified: true } : { models: IMAGE_MODELS, verified: false };
|
|
258
304
|
};
|
|
@@ -312,10 +358,10 @@ const imageFrom = (res, model) => {
|
|
|
312
358
|
};
|
|
313
359
|
const inline = async (file, read) => inlinePart(await read(file.filepath), file.mimetype ?? "image/png");
|
|
314
360
|
const userTurn = (parts) => [{ role: "user", parts }];
|
|
315
|
-
const createImageStudio = (
|
|
361
|
+
const createImageStudio = (getClient, read = (p) => promises.readFile(p)) => ({
|
|
316
362
|
async models() {
|
|
317
363
|
try {
|
|
318
|
-
const api = await
|
|
364
|
+
const { models: api } = await getClient();
|
|
319
365
|
const names = [];
|
|
320
366
|
for await (const m of await api.list({ config: { pageSize: 100 } })) if (m.name) names.push(m.name);
|
|
321
367
|
return availableModels(names);
|
|
@@ -343,7 +389,7 @@ const createImageStudio = (getModels, read = (p) => promises.readFile(p), backen
|
|
|
343
389
|
return image;
|
|
344
390
|
},
|
|
345
391
|
async generate(input) {
|
|
346
|
-
const api = await
|
|
392
|
+
const { models: api, backend } = await getClient();
|
|
347
393
|
const text2 = cleanPrompt(input.prompt);
|
|
348
394
|
const { model } = input;
|
|
349
395
|
const refs = await Promise.all(input.references.map((f) => inline(f, read)));
|
|
@@ -358,7 +404,7 @@ const createImageStudio = (getModels, read = (p) => promises.readFile(p), backen
|
|
|
358
404
|
return grounding ? { ...image, grounding } : image;
|
|
359
405
|
},
|
|
360
406
|
async edit(input) {
|
|
361
|
-
const api = await
|
|
407
|
+
const { models: api, backend } = await getClient();
|
|
362
408
|
const parts = [
|
|
363
409
|
await inline(input.image, read),
|
|
364
410
|
...await Promise.all(input.references.map((f) => inline(f, read))),
|
|
@@ -369,14 +415,17 @@ const createImageStudio = (getModels, read = (p) => promises.readFile(p), backen
|
|
|
369
415
|
return imageFrom(res, input.model.id);
|
|
370
416
|
},
|
|
371
417
|
async expand(prompt) {
|
|
372
|
-
const api = await
|
|
418
|
+
const { models: api } = await getClient();
|
|
373
419
|
const res = await api.generateContent({ model: EXPAND_MODEL, contents: EXPAND_INSTRUCTION(cleanPrompt(prompt)) });
|
|
374
420
|
const text2 = (res.text ?? "").trim().replace(/^["'«“]+|["'»”]+$/g, "").trim();
|
|
375
421
|
if (!text2) throw new BlockedError("No se pudo expandir la idea. Intenta de nuevo.");
|
|
376
422
|
return text2;
|
|
377
423
|
}
|
|
378
424
|
});
|
|
379
|
-
const imageStudio$1 = ({ strapi }) => createImageStudio(async () =>
|
|
425
|
+
const imageStudio$1 = ({ strapi }) => createImageStudio(async () => {
|
|
426
|
+
const { ai, backend } = await strapi.plugin("strapi-content-generator-gemini").service("service").geminiClient();
|
|
427
|
+
return { models: ai.models, backend };
|
|
428
|
+
});
|
|
380
429
|
class InputError extends Error {
|
|
381
430
|
}
|
|
382
431
|
const ALLOWED = ["image/png", "image/jpeg", "image/webp"];
|
|
@@ -863,7 +912,8 @@ const admin = [
|
|
|
863
912
|
{ method: "POST", path: "/template-folders/rename", handler: "templates.renameFolder", config },
|
|
864
913
|
{ method: "POST", path: "/template-folders/delete", handler: "templates.deleteFolder", config },
|
|
865
914
|
{ method: "GET", path: "/config", handler: "config.get", config },
|
|
866
|
-
{ method: "POST", path: "/config", handler: "config.set", config }
|
|
915
|
+
{ method: "POST", path: "/config", handler: "config.set", config },
|
|
916
|
+
{ method: "DELETE", path: "/config/vertex", handler: "config.removeVertex", config }
|
|
867
917
|
];
|
|
868
918
|
const contentApi = [];
|
|
869
919
|
const routes = {
|
|
@@ -4102,25 +4152,43 @@ const {
|
|
|
4102
4152
|
mergeConfig,
|
|
4103
4153
|
create
|
|
4104
4154
|
} = axios;
|
|
4155
|
+
const VERTEX_LOCATION = "global";
|
|
4156
|
+
const SCOPES = ["https://www.googleapis.com/auth/cloud-platform"];
|
|
4157
|
+
const createGeminiClient = ({ serviceAccount, apiKey, allowVertex = true }, Ctor = GoogleGenAI) => {
|
|
4158
|
+
const parsed = allowVertex && serviceAccount ? parseServiceAccount(serviceAccount) : null;
|
|
4159
|
+
if (parsed && "account" in parsed) {
|
|
4160
|
+
const { projectId, clientEmail, privateKey } = parsed.account;
|
|
4161
|
+
const ai = new Ctor({
|
|
4162
|
+
vertexai: true,
|
|
4163
|
+
project: projectId,
|
|
4164
|
+
location: VERTEX_LOCATION,
|
|
4165
|
+
googleAuthOptions: { credentials: { client_email: clientEmail, private_key: privateKey }, scopes: SCOPES }
|
|
4166
|
+
});
|
|
4167
|
+
return { ai, backend: "vertex" };
|
|
4168
|
+
}
|
|
4169
|
+
if (typeof apiKey === "string" && apiKey) return { ai: new Ctor({ apiKey }), backend: "gemini-api" };
|
|
4170
|
+
throw new Error(
|
|
4171
|
+
allowVertex ? "Configura la cuenta de servicio de Vertex AI o la llave de Gemini en Ajustes del plugin." : "El video necesita la llave de Gemini en Ajustes del plugin."
|
|
4172
|
+
);
|
|
4173
|
+
};
|
|
4105
4174
|
const service = ({ strapi }) => {
|
|
4106
4175
|
return {
|
|
4107
4176
|
// ───────────────────────────────
|
|
4108
4177
|
// 🤖 AI Configuration
|
|
4109
4178
|
// ───────────────────────────────
|
|
4179
|
+
/**
|
|
4180
|
+
* Cliente de Google y su backend (S5.1): con la cuenta de servicio de Ajustes, Vertex AI en `global`; si
|
|
4181
|
+
* no, la llave (`GOOGLE_API_KEY` del entorno gana a la del store, como antes; D10). El video pide
|
|
4182
|
+
* `allowVertex: false` (D7).
|
|
4183
|
+
*/
|
|
4184
|
+
async geminiClient({ allowVertex = true } = {}) {
|
|
4185
|
+
const pluginStore = strapi.store({ type: "plugin", name: "strapi-content-generator-gemini" });
|
|
4186
|
+
const [serviceAccount, storedKey] = await Promise.all([pluginStore.get({ key: VERTEX_KEY }), pluginStore.get({ key: "GOOGLE_GEN_AI" })]);
|
|
4187
|
+
return createGeminiClient({ serviceAccount, apiKey: process.env.GOOGLE_API_KEY || storedKey, allowVertex });
|
|
4188
|
+
},
|
|
4189
|
+
/** Atajo para los usos que solo necesitan el cliente (artículos). */
|
|
4110
4190
|
async geminiConfig() {
|
|
4111
|
-
|
|
4112
|
-
type: "plugin",
|
|
4113
|
-
name: "strapi-content-generator-gemini"
|
|
4114
|
-
});
|
|
4115
|
-
const googleGenAI = await pluginStore.get({ key: "GOOGLE_GEN_AI" });
|
|
4116
|
-
const geminiApiKey = process.env.GOOGLE_API_KEY || googleGenAI;
|
|
4117
|
-
if (!geminiApiKey) {
|
|
4118
|
-
throw new Error(
|
|
4119
|
-
"La variable de entorno GOOGLE_API_KEY o GOOGLE_GEN_AI no está configurada."
|
|
4120
|
-
);
|
|
4121
|
-
}
|
|
4122
|
-
const ai = new GoogleGenAI({ apiKey: geminiApiKey });
|
|
4123
|
-
return ai;
|
|
4191
|
+
return (await this.geminiClient()).ai;
|
|
4124
4192
|
},
|
|
4125
4193
|
async getArticulos(sanitizedQueryParams) {
|
|
4126
4194
|
return await strapi.entityService.findMany("api::articulo.articulo", sanitizedQueryParams);
|
|
@@ -4428,7 +4496,7 @@ const service = ({ strapi }) => {
|
|
|
4428
4496
|
},
|
|
4429
4497
|
// 🎞️ Internal Veo Video Generation / Extension Helper
|
|
4430
4498
|
async _callVeoVideoGeneration(promptText, aspectRatio, durationSeconds, sceneRefs, previousVideo) {
|
|
4431
|
-
const ai = await this.
|
|
4499
|
+
const { ai } = await this.geminiClient({ allowVertex: false });
|
|
4432
4500
|
const cleanPrompt2 = promptText.replace(/\s+/g, " ").trim();
|
|
4433
4501
|
const referenceImages = sceneRefs.map((img) => ({
|
|
4434
4502
|
image: {
|
|
@@ -4498,7 +4566,7 @@ const service = ({ strapi }) => {
|
|
|
4498
4566
|
throw new Error("No se generó ningún video.");
|
|
4499
4567
|
}
|
|
4500
4568
|
const finalVideoPath = `/tmp/video-${Date.now()}.mp4`;
|
|
4501
|
-
const ai = await this.
|
|
4569
|
+
const { ai } = await this.geminiClient({ allowVertex: false });
|
|
4502
4570
|
await ai.files.download({ file: previousVideo, downloadPath: finalVideoPath });
|
|
4503
4571
|
const uploadedFile = await this._uploadGeneratedFile(
|
|
4504
4572
|
finalVideoPath,
|
|
@@ -2,9 +2,17 @@ import type { Core } from '@strapi/strapi';
|
|
|
2
2
|
declare const config: ({ strapi }: {
|
|
3
3
|
strapi: Core.Strapi;
|
|
4
4
|
}) => {
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Devuelve si cada credencial está guardada y sus últimos 4 caracteres, nunca el valor. Para Vertex,
|
|
7
|
+
* el proyecto y el correo de la cuenta de servicio, nunca la llave privada.
|
|
8
|
+
*/
|
|
6
9
|
get(ctx: any): Promise<void>;
|
|
7
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Escribe solo las credenciales conocidas que llegan como texto no vacío. Una cuenta de servicio
|
|
12
|
+
* inválida rechaza la petición completa (400) antes de escribir nada.
|
|
13
|
+
*/
|
|
8
14
|
set(ctx: any): Promise<void>;
|
|
15
|
+
/** Quita la cuenta de servicio: el plugin vuelve a la llave de la Gemini API (S5.1). */
|
|
16
|
+
removeVertex(ctx: any): Promise<void>;
|
|
9
17
|
};
|
|
10
18
|
export default config;
|
|
@@ -26,7 +26,8 @@ export interface ImageModelInfo {
|
|
|
26
26
|
export declare const IMAGE_MODELS: ImageModelInfo[];
|
|
27
27
|
export declare const findModel: (id: string | undefined) => ImageModelInfo;
|
|
28
28
|
/**
|
|
29
|
-
* Catálogo filtrado por los nombres que devuelve `ai.models.list()
|
|
29
|
+
* Catálogo filtrado por los nombres que devuelve `ai.models.list()`: «models/<id>» en la Gemini API y
|
|
30
|
+
* «publishers/google/models/<id>» en Vertex AI (S5.1). Sin lista, o si
|
|
30
31
|
* ninguno coincide (la API cambió de formato), se devuelve el catálogo completo sin verificar para
|
|
31
32
|
* no dejar el Generador vacío.
|
|
32
33
|
*/
|
|
@@ -33,6 +33,7 @@ declare const _default: {
|
|
|
33
33
|
}) => {
|
|
34
34
|
get(ctx: any): Promise<void>;
|
|
35
35
|
set(ctx: any): Promise<void>;
|
|
36
|
+
removeVertex(ctx: any): Promise<void>;
|
|
36
37
|
};
|
|
37
38
|
'image-studio': ({ strapi }: {
|
|
38
39
|
strapi: import("@strapi/types/dist/core").Strapi;
|
|
@@ -189,6 +190,9 @@ declare const _default: {
|
|
|
189
190
|
service: ({ strapi }: {
|
|
190
191
|
strapi: import("@strapi/types/dist/core").Strapi;
|
|
191
192
|
}) => {
|
|
193
|
+
geminiClient({ allowVertex }?: {
|
|
194
|
+
allowVertex?: boolean;
|
|
195
|
+
}): Promise<import("./utils/geminiClient").GeminiClient>;
|
|
192
196
|
geminiConfig(): Promise<import("@google/genai").GoogleGenAI>;
|
|
193
197
|
getArticulos(sanitizedQueryParams: any): Promise<{
|
|
194
198
|
id: import("@strapi/types/dist/data").ID;
|