mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-23 06:58:12 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5550fb05d6 | ||
|
|
75e1d88c26 | ||
|
|
8155b30c63 | ||
|
|
de7fc7d03c | ||
|
|
517c786be6 | ||
|
|
09b4e597ae | ||
|
|
b00d88f6b0 | ||
|
|
c008b41eb7 |
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -32,7 +32,7 @@ export default function AiChat() {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
const [selectedModel, setSelectedModel] = useState("anthropic/claude-3.5-sonnet");
|
const [selectedModel, setSelectedModel] = useState("anthropic/claude-sonnet-4.5");
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
// Fetch available models from OpenRouter
|
// Fetch available models from OpenRouter
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default function SettingsMobile() {
|
|||||||
const [endpointUrl, setEndpointUrl] = useState("");
|
const [endpointUrl, setEndpointUrl] = useState("");
|
||||||
const [publicUrl, setPublicUrl] = useState("");
|
const [publicUrl, setPublicUrl] = useState("");
|
||||||
const [openrouterApiKey, setOpenrouterApiKey] = useState("");
|
const [openrouterApiKey, setOpenrouterApiKey] = useState("");
|
||||||
const [openrouterModel, setOpenrouterModel] = useState("anthropic/claude-3.5-sonnet");
|
const [openrouterModel, setOpenrouterModel] = useState("anthropic/claude-sonnet-4.5");
|
||||||
const [openrouterSystemPrompt, setOpenrouterSystemPrompt] = useState("Tu es un expert en marketing des réseaux sociaux. Génère 3 variations de textes engageants pour des publications Facebook et Instagram à partir des informations produit fournies. Chaque variation doit être unique, captivante et optimisée pour l'engagement.");
|
const [openrouterSystemPrompt, setOpenrouterSystemPrompt] = useState("Tu es un expert en marketing des réseaux sociaux. Génère 3 variations de textes engageants pour des publications Facebook et Instagram à partir des informations produit fournies. Chaque variation doit être unique, captivante et optimisée pour l'engagement.");
|
||||||
const [logoFile, setLogoFile] = useState<File | null>(null);
|
const [logoFile, setLogoFile] = useState<File | null>(null);
|
||||||
const [logoPreview, setLogoPreview] = useState<string | null>(null);
|
const [logoPreview, setLogoPreview] = useState<string | null>(null);
|
||||||
@@ -64,7 +64,7 @@ export default function SettingsMobile() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (openrouterConfig) {
|
if (openrouterConfig) {
|
||||||
setOpenrouterModel((openrouterConfig as any).model || "anthropic/claude-3.5-sonnet");
|
setOpenrouterModel((openrouterConfig as any).model || "anthropic/claude-sonnet-4.5");
|
||||||
setOpenrouterSystemPrompt((openrouterConfig as any).systemPrompt || "Tu es un expert en marketing des réseaux sociaux. Génère 3 variations de textes engageants pour des publications Facebook et Instagram à partir des informations produit fournies. Chaque variation doit être unique, captivante et optimisée pour l'engagement.");
|
setOpenrouterSystemPrompt((openrouterConfig as any).systemPrompt || "Tu es un expert en marketing des réseaux sociaux. Génère 3 variations de textes engageants pour des publications Facebook et Instagram à partir des informations produit fournies. Chaque variation doit être unique, captivante et optimisée pour l'engagement.");
|
||||||
}
|
}
|
||||||
}, [openrouterConfig]);
|
}, [openrouterConfig]);
|
||||||
@@ -157,10 +157,17 @@ export default function SettingsMobile() {
|
|||||||
});
|
});
|
||||||
setOpenrouterApiKey("");
|
setOpenrouterApiKey("");
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (error: Error) => {
|
||||||
|
let description = "Impossible de sauvegarder la configuration OpenRouter";
|
||||||
|
const body = error.message.match(/^\d+:\s*(.+)$/)?.[1];
|
||||||
|
if (body) {
|
||||||
|
try {
|
||||||
|
description = JSON.parse(body).error || description;
|
||||||
|
} catch { /* garder le message générique */ }
|
||||||
|
}
|
||||||
toast({
|
toast({
|
||||||
title: "Erreur",
|
title: "Erreur",
|
||||||
description: "Impossible de sauvegarder la configuration OpenRouter",
|
description,
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default function Settings() {
|
|||||||
const [endpointUrl, setEndpointUrl] = useState("");
|
const [endpointUrl, setEndpointUrl] = useState("");
|
||||||
const [publicUrl, setPublicUrl] = useState("");
|
const [publicUrl, setPublicUrl] = useState("");
|
||||||
const [openrouterApiKey, setOpenrouterApiKey] = useState("");
|
const [openrouterApiKey, setOpenrouterApiKey] = useState("");
|
||||||
const [openrouterModel, setOpenrouterModel] = useState("anthropic/claude-3.5-sonnet");
|
const [openrouterModel, setOpenrouterModel] = useState("anthropic/claude-sonnet-4.5");
|
||||||
const [openrouterSystemPrompt, setOpenrouterSystemPrompt] = useState("Tu es un expert en marketing des réseaux sociaux. Génère 3 variations de textes engageants pour des publications Facebook et Instagram à partir des informations produit fournies. Chaque variation doit être unique, captivante et optimisée pour l'engagement.");
|
const [openrouterSystemPrompt, setOpenrouterSystemPrompt] = useState("Tu es un expert en marketing des réseaux sociaux. Génère 3 variations de textes engageants pour des publications Facebook et Instagram à partir des informations produit fournies. Chaque variation doit être unique, captivante et optimisée pour l'engagement.");
|
||||||
const [logoFile, setLogoFile] = useState<File | null>(null);
|
const [logoFile, setLogoFile] = useState<File | null>(null);
|
||||||
const [logoPreview, setLogoPreview] = useState<string | null>(null);
|
const [logoPreview, setLogoPreview] = useState<string | null>(null);
|
||||||
@@ -81,7 +81,7 @@ export default function Settings() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (openrouterConfig) {
|
if (openrouterConfig) {
|
||||||
setOpenrouterModel((openrouterConfig as any).model || "anthropic/claude-3.5-sonnet");
|
setOpenrouterModel((openrouterConfig as any).model || "anthropic/claude-sonnet-4.5");
|
||||||
setOpenrouterSystemPrompt((openrouterConfig as any).systemPrompt || "Tu es un expert en marketing des réseaux sociaux. Génère 3 variations de textes engageants pour des publications Facebook et Instagram à partir des informations produit fournies. Chaque variation doit être unique, captivante et optimisée pour l'engagement.");
|
setOpenrouterSystemPrompt((openrouterConfig as any).systemPrompt || "Tu es un expert en marketing des réseaux sociaux. Génère 3 variations de textes engageants pour des publications Facebook et Instagram à partir des informations produit fournies. Chaque variation doit être unique, captivante et optimisée pour l'engagement.");
|
||||||
}
|
}
|
||||||
}, [openrouterConfig]);
|
}, [openrouterConfig]);
|
||||||
@@ -161,10 +161,17 @@ export default function Settings() {
|
|||||||
});
|
});
|
||||||
setOpenrouterApiKey(""); // Clear the API key after saving
|
setOpenrouterApiKey(""); // Clear the API key after saving
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (error: Error) => {
|
||||||
|
let description = "Impossible de sauvegarder la configuration OpenRouter";
|
||||||
|
const body = error.message.match(/^\d+:\s*(.+)$/)?.[1];
|
||||||
|
if (body) {
|
||||||
|
try {
|
||||||
|
description = JSON.parse(body).error || description;
|
||||||
|
} catch { /* garder le message générique */ }
|
||||||
|
}
|
||||||
toast({
|
toast({
|
||||||
title: "Erreur",
|
title: "Erreur",
|
||||||
description: "Impossible de sauvegarder la configuration OpenRouter",
|
description,
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
+43
-20
@@ -159,7 +159,7 @@ class ReelRequest(BaseModel):
|
|||||||
music_volume: float = 0.25
|
music_volume: float = 0.25
|
||||||
tts_enabled: bool = False
|
tts_enabled: bool = False
|
||||||
tts_voice: str = "fr-FR-VivienneMultilingualNeural"
|
tts_voice: str = "fr-FR-VivienneMultilingualNeural"
|
||||||
tts_engine: str = "edge" # "edge" or "gemini"
|
tts_engine: str = "gemini" # "gemini" or "edge"
|
||||||
gemini_api_key: Optional[str] = None # Google Cloud API key for Gemini TTS
|
gemini_api_key: Optional[str] = None # Google Cloud API key for Gemini TTS
|
||||||
draw_text: bool = True
|
draw_text: bool = True
|
||||||
stabilize: bool = False # Stabilisation vidéo via vidstab
|
stabilize: bool = False # Stabilisation vidéo via vidstab
|
||||||
@@ -924,18 +924,30 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
|
|
||||||
if tts_clean_text:
|
if tts_clean_text:
|
||||||
print(f"🔊 Generating TTS audio to: {tts_audio_path}")
|
print(f"🔊 Generating TTS audio to: {tts_audio_path}")
|
||||||
engine = request.tts_engine or "edge"
|
engine = request.tts_engine or "gemini"
|
||||||
|
|
||||||
|
# Primary: Gemini TTS. Fallback: Edge TTS
|
||||||
if engine == "gemini" and request.gemini_api_key:
|
if engine == "gemini" and request.gemini_api_key:
|
||||||
await generate_tts_gemini(
|
try:
|
||||||
tts_clean_text,
|
await generate_tts_gemini(
|
||||||
voice,
|
tts_clean_text,
|
||||||
request.gemini_api_key,
|
voice,
|
||||||
tts_audio_path,
|
request.gemini_api_key,
|
||||||
tts_ass_path,
|
tts_audio_path,
|
||||||
display_text=clean_text_for_display(request.text),
|
tts_ass_path,
|
||||||
delay=2.0,
|
display_text=clean_text_for_display(request.text),
|
||||||
)
|
delay=2.0,
|
||||||
|
)
|
||||||
|
except Exception as gemini_err:
|
||||||
|
print(f"⚠️ Gemini TTS failed ({gemini_err}), falling back to Edge TTS")
|
||||||
|
await generate_tts_with_subs(
|
||||||
|
tts_clean_text,
|
||||||
|
voice,
|
||||||
|
tts_audio_path,
|
||||||
|
tts_ass_path,
|
||||||
|
display_text=clean_text_for_display(request.text),
|
||||||
|
delay=2.0,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await generate_tts_with_subs(
|
await generate_tts_with_subs(
|
||||||
tts_clean_text,
|
tts_clean_text,
|
||||||
@@ -1323,17 +1335,28 @@ async def preview_tts(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
# Note: Gemini voices (fr-FR-Standard-A etc.) are valid Edge voices too,
|
# Note: Gemini voices (fr-FR-Standard-A etc.) are valid Edge voices too,
|
||||||
# so we don't check for "Neural" - let edge_tts handle voice resolution
|
# so we don't check for "Neural" - let edge_tts handle voice resolution
|
||||||
|
|
||||||
engine = request.tts_engine or "edge"
|
engine = request.tts_engine or "gemini"
|
||||||
|
|
||||||
|
# Primary: Gemini TTS. Fallback: Edge TTS
|
||||||
if engine == "gemini" and request.gemini_api_key:
|
if engine == "gemini" and request.gemini_api_key:
|
||||||
await generate_tts_gemini(
|
try:
|
||||||
clean_text,
|
await generate_tts_gemini(
|
||||||
voice,
|
clean_text,
|
||||||
request.gemini_api_key,
|
voice,
|
||||||
tts_audio_path,
|
request.gemini_api_key,
|
||||||
tts_srt_path,
|
tts_audio_path,
|
||||||
display_text=clean_text_for_display(request.text),
|
tts_srt_path,
|
||||||
)
|
display_text=clean_text_for_display(request.text),
|
||||||
|
)
|
||||||
|
except Exception as gemini_err:
|
||||||
|
print(f"⚠️ Gemini TTS failed ({gemini_err}), falling back to Edge TTS")
|
||||||
|
await generate_tts_with_subs(
|
||||||
|
clean_text,
|
||||||
|
voice,
|
||||||
|
tts_audio_path,
|
||||||
|
tts_srt_path,
|
||||||
|
display_text=clean_text_for_display(request.text),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await generate_tts_with_subs(
|
await generate_tts_with_subs(
|
||||||
clean_text,
|
clean_text,
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ CREATE TABLE IF NOT EXISTS openrouter_config (
|
|||||||
id VARCHAR PRIMARY KEY DEFAULT gen_random_uuid(),
|
id VARCHAR PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
user_id VARCHAR NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
|
user_id VARCHAR NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
|
||||||
api_key TEXT NOT NULL,
|
api_key TEXT NOT NULL,
|
||||||
model TEXT NOT NULL DEFAULT 'anthropic/claude-3.5-sonnet',
|
model TEXT NOT NULL DEFAULT 'anthropic/claude-sonnet-4.5',
|
||||||
system_prompt TEXT NOT NULL DEFAULT 'Tu es un expert en marketing des réseaux sociaux. Génère 3 variations de textes engageants pour des publications Facebook et Instagram à partir des informations produit fournies. Chaque variation doit être unique, captivante et optimisée pour l''engagement.',
|
system_prompt TEXT NOT NULL DEFAULT 'Tu es un expert en marketing des réseaux sociaux. Génère 3 variations de textes engageants pour des publications Facebook et Instagram à partir des informations produit fournies. Chaque variation doit être unique, captivante et optimisée pour l''engagement.',
|
||||||
created_at TIMESTAMP DEFAULT NOW(),
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
updated_at TIMESTAMP DEFAULT NOW()
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
|||||||
+13
-1
@@ -1844,6 +1844,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
return res.status(400).json({ error: "Clé API OpenRouter manquante. Veuillez entrer une clé API valide." });
|
return res.status(400).json({ error: "Clé API OpenRouter manquante. Veuillez entrer une clé API valide." });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (incomingKey && !(await openRouterService.verifyApiKey(incomingKey))) {
|
||||||
|
return res.status(400).json({ error: "Clé API refusée par OpenRouter. Vérifiez que vous avez collé la clé complète (sk-or-v1-...)." });
|
||||||
|
}
|
||||||
|
|
||||||
config = await storage.updateOpenrouterConfig(userId, finalData);
|
config = await storage.updateOpenrouterConfig(userId, finalData);
|
||||||
} else {
|
} else {
|
||||||
// Pour les créations, exiger tous les champs
|
// Pour les créations, exiger tous les champs
|
||||||
@@ -1851,14 +1855,22 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
...req.body,
|
...req.body,
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!(await openRouterService.verifyApiKey(configData.apiKey))) {
|
||||||
|
return res.status(400).json({ error: "Clé API refusée par OpenRouter. Vérifiez que vous avez collé la clé complète (sk-or-v1-...)." });
|
||||||
|
}
|
||||||
|
|
||||||
config = await storage.createOpenrouterConfig(configData);
|
config = await storage.createOpenrouterConfig(configData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't send the API key back
|
// Don't send the API key back
|
||||||
const { apiKey, ...safeConfig } = config;
|
const { apiKey, ...safeConfig } = config;
|
||||||
res.json(safeConfig);
|
res.json(safeConfig);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error("Error saving OpenRouter config:", error);
|
console.error("Error saving OpenRouter config:", error);
|
||||||
|
if (error.name === 'ZodError') {
|
||||||
|
return res.status(400).json({ error: error.errors[0]?.message || "Données invalides" });
|
||||||
|
}
|
||||||
res.status(500).json({ error: "Failed to save OpenRouter config" });
|
res.status(500).json({ error: "Failed to save OpenRouter config" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,29 +12,38 @@ interface GeneratedText {
|
|||||||
characterCount: number;
|
characterCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import crypto from 'crypto';
|
||||||
import { storage } from '../storage';
|
import { storage } from '../storage';
|
||||||
|
|
||||||
export class OpenRouterService {
|
export class OpenRouterService {
|
||||||
private baseUrl = "https://openrouter.ai/api/v1/chat/completions";
|
private baseUrl = "https://openrouter.ai/api/v1/chat/completions";
|
||||||
|
|
||||||
async generatePostText(productInfo: ProductInfo, userId: string, modelOverride?: string): Promise<GeneratedText[]> {
|
async generatePostText(productInfo: ProductInfo, userId: string, modelOverride?: string): Promise<GeneratedText[]> {
|
||||||
// Get any available OpenRouter configuration (shared across all users)
|
// Prefer the requesting user's own configuration; fall back to the most
|
||||||
const config = await storage.getAnyOpenrouterConfig();
|
// recently saved shared configuration if this user hasn't set one up.
|
||||||
|
const config = (await storage.getOpenrouterConfig(userId)) || (await storage.getAnyOpenrouterConfig());
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
throw new Error('Configuration OpenRouter non trouvée. Veuillez demander à un administrateur de configurer OpenRouter dans les Paramètres.');
|
throw new Error('Configuration OpenRouter non trouvée. Veuillez demander à un administrateur de configurer OpenRouter dans les Paramètres.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!config.apiKey || !config.apiKey.trim()) {
|
||||||
|
throw new Error('Clé API OpenRouter manquante ou vide en base de données. Veuillez la ressaisir dans les Paramètres.');
|
||||||
|
}
|
||||||
|
|
||||||
const prompt = this.buildPrompt(productInfo, config.systemPrompt);
|
const prompt = this.buildPrompt(productInfo, config.systemPrompt);
|
||||||
|
|
||||||
// Use provided model or fall back to config model
|
// Use provided model or fall back to config model
|
||||||
const modelToUse = modelOverride || config.model;
|
const modelToUse = modelOverride || config.model;
|
||||||
|
|
||||||
|
const keyFingerprint = crypto.createHash('sha256').update(config.apiKey).digest('hex').slice(0, 8);
|
||||||
|
console.log(`[OpenRouter] Generating with model="${modelToUse}" keyFingerprint=${keyFingerprint} keyLength=${config.apiKey.length} configUserId=${config.userId}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(this.baseUrl, {
|
const response = await fetch(this.baseUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": `Bearer ${config.apiKey}`,
|
"Authorization": `Bearer ${config.apiKey.trim()}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"HTTP-Referer": process.env.APP_URL || "http://localhost:5555",
|
"HTTP-Referer": process.env.APP_URL || "http://localhost:5555",
|
||||||
"X-Title": "Social Flow"
|
"X-Title": "Social Flow"
|
||||||
@@ -206,6 +215,22 @@ VERSION 3 - ÉMOTIONNELLE:
|
|||||||
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
|
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie qu'une clé API est acceptée par OpenRouter.
|
||||||
|
* Retourne false uniquement sur un rejet d'authentification explicite (401/403) ;
|
||||||
|
* en cas d'erreur réseau, on laisse passer pour ne pas bloquer la sauvegarde.
|
||||||
|
*/
|
||||||
|
async verifyApiKey(apiKey: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await fetch("https://openrouter.ai/api/v1/key", {
|
||||||
|
headers: { "Authorization": `Bearer ${apiKey}` }
|
||||||
|
});
|
||||||
|
return response.status !== 401 && response.status !== 403;
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getAvailableModels(): Promise<any[]> {
|
async getAvailableModels(): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("https://openrouter.ai/api/v1/models", {
|
const response = await fetch("https://openrouter.ai/api/v1/models", {
|
||||||
|
|||||||
+1
-1
@@ -461,7 +461,7 @@ export class DatabaseStorage implements IStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAnyOpenrouterConfig(): Promise<OpenrouterConfig | undefined> {
|
async getAnyOpenrouterConfig(): Promise<OpenrouterConfig | undefined> {
|
||||||
const [config] = await db.select().from(openrouterConfig).limit(1);
|
const [config] = await db.select().from(openrouterConfig).orderBy(desc(openrouterConfig.updatedAt)).limit(1);
|
||||||
return config || undefined;
|
return config || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -127,7 +127,7 @@ export const openrouterConfig = pgTable("openrouter_config", {
|
|||||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||||
userId: varchar("user_id").notNull().unique().references(() => users.id, { onDelete: "cascade" }),
|
userId: varchar("user_id").notNull().unique().references(() => users.id, { onDelete: "cascade" }),
|
||||||
apiKey: text("api_key").notNull(),
|
apiKey: text("api_key").notNull(),
|
||||||
model: text("model").notNull().default("anthropic/claude-3.5-sonnet"),
|
model: text("model").notNull().default("anthropic/claude-sonnet-4.5"),
|
||||||
systemPrompt: text("system_prompt").notNull().default("Tu es un expert en marketing des réseaux sociaux. Génère 3 variations de textes engageants pour des publications Facebook et Instagram à partir des informations produit fournies. Chaque variation doit être unique, captivante et optimisée pour l'engagement."),
|
systemPrompt: text("system_prompt").notNull().default("Tu es un expert en marketing des réseaux sociaux. Génère 3 variations de textes engageants pour des publications Facebook et Instagram à partir des informations produit fournies. Chaque variation doit être unique, captivante et optimisée pour l'engagement."),
|
||||||
createdAt: timestamp("created_at").defaultNow(),
|
createdAt: timestamp("created_at").defaultNow(),
|
||||||
updatedAt: timestamp("updated_at").defaultNow(),
|
updatedAt: timestamp("updated_at").defaultNow(),
|
||||||
@@ -394,7 +394,7 @@ export const insertOpenrouterConfigSchema = createInsertSchema(openrouterConfig)
|
|||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
}).extend({
|
}).extend({
|
||||||
apiKey: z.string().min(1, "La clé API ne peut pas être vide"),
|
apiKey: z.string().trim().min(20, "La clé API OpenRouter semble incomplète. Collez la clé entière (elle commence par sk-or-v1- et fait environ 73 caractères)."),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateOpenrouterConfigSchema = insertOpenrouterConfigSchema.partial({
|
export const updateOpenrouterConfigSchema = insertOpenrouterConfigSchema.partial({
|
||||||
|
|||||||
Reference in new issue
Block a user