feat(F-103): completed feature
This commit is contained in:
@@ -7,6 +7,7 @@ import { requireRole, type Authenticate } from '../../../shared/auth.js';
|
||||
import { AppError } from '../../../shared/errors.js';
|
||||
import { parseJson } from '../../../shared/http-input.js';
|
||||
import { errorSchema } from '../../../shared/swagger.js';
|
||||
import { aiTextToHtml, generateWithModel } from '../../../shared/ai-content.js';
|
||||
import {
|
||||
AttachProductImage,
|
||||
DetachProductImage,
|
||||
@@ -285,13 +286,14 @@ export async function registerCatalogRoutes(
|
||||
(template || fallback).replace(/\{\{(name|description|brand)\}\}/g, (_, key: string) => replacements[key] ?? '');
|
||||
const patch: { description?: string; seoTitle?: string; seoDescription?: string } = {};
|
||||
if (!product.description?.trim()) {
|
||||
patch.description = (await generateWithModel(baseUrl, model, apiKey, promptFor(settings.ai_product_description_prompt, 'Escribe una descripción comercial clara y útil en español para este producto: {{name}}. Devuelve solo la descripción.'))).slice(0, 2_000);
|
||||
const raw = await generateWithModel(baseUrl, model, apiKey, promptFor(settings.ai_product_description_prompt, 'Escribe una descripción comercial clara y útil en español para este producto: {{name}}. Devuelve SOLO HTML válido y seguro (párrafos <p>, y si procede listas <ul><li> y <strong> para resaltar). No uses títulos ni markdown.'));
|
||||
patch.description = aiTextToHtml(raw).slice(0, 4_000);
|
||||
}
|
||||
if (!product.seoTitle?.trim()) {
|
||||
patch.seoTitle = (await generateWithModel(baseUrl, model, apiKey, promptFor(settings.ai_seo_title_prompt, 'Genera un título SEO breve para {{name}}. Devuelve solo el título.'))).slice(0, 200);
|
||||
patch.seoTitle = (await generateWithModel(baseUrl, model, apiKey, promptFor(settings.ai_seo_title_prompt, 'Genera un título SEO breve para {{name}}. Devuelve solo el título.'))).replace(/<[^>]*>/g, '').trim().slice(0, 200);
|
||||
}
|
||||
if (!product.seoDescription?.trim()) {
|
||||
patch.seoDescription = (await generateWithModel(baseUrl, model, apiKey, promptFor(settings.ai_seo_description_prompt, 'Genera una meta descripción SEO en español para {{name}}. Devuelve solo la descripción.'))).slice(0, 500);
|
||||
patch.seoDescription = (await generateWithModel(baseUrl, model, apiKey, promptFor(settings.ai_seo_description_prompt, 'Genera una meta descripción SEO en español para {{name}}. Devuelve solo la descripción.'))).replace(/<[^>]*>/g, '').trim().slice(0, 500);
|
||||
}
|
||||
const updated = await repository.update(id, patch);
|
||||
return reply.send(serializeProduct(updated ?? product));
|
||||
@@ -675,20 +677,6 @@ function mapProductError(error: unknown): Error {
|
||||
return error instanceof Error ? error : new Error('Unknown product error');
|
||||
}
|
||||
|
||||
async function generateWithModel(baseUrl: string, model: string, apiKey: string, prompt: string): Promise<string> {
|
||||
const response = await fetch(`${baseUrl.replace(/\/$/, '')}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ model, messages: [{ role: 'user', content: prompt }], temperature: 0.4 }),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
const payload = await response.json().catch(() => null) as { choices?: Array<{ message?: { content?: unknown } }>; error?: { message?: string } } | null;
|
||||
if (!response.ok) throw new AppError(502, 'AI_PROVIDER_ERROR', payload?.error?.message ?? `El proveedor IA respondió ${response.status}`);
|
||||
const content = payload?.choices?.[0]?.message?.content;
|
||||
if (typeof content !== 'string' || !content.trim()) throw new AppError(502, 'AI_EMPTY_RESPONSE', 'El modelo IA no devolvió contenido');
|
||||
return content.trim();
|
||||
}
|
||||
|
||||
function sanitizeSearchTelemetryQuery(query: string | undefined): string | undefined {
|
||||
if (query === undefined) {
|
||||
return undefined;
|
||||
|
||||
Reference in New Issue
Block a user