feat: NPC system improvements - custom prompt, NSFW, full body generation

This commit is contained in:
Alexej Wolff
2026-05-05 00:11:43 +02:00
parent efd2332875
commit dad5aa47cb
11 changed files with 1860 additions and 43 deletions
+229
View File
@@ -602,6 +602,111 @@ app.delete("/api/characters/:id", requireAuth, async (req, res) => {
}
});
// ============ NPC CHARACTERS ROUTES ============
// Получить всех NPC пользователя
app.get("/api/npc", requireAuth, async (req, res) => {
try {
const npcCharacters = db.collection("npc_characters");
const userNPCs = await npcCharacters
.find({ userId: req.session.userId })
.sort({ updatedAt: -1 })
.toArray();
res.json(userNPCs);
} catch (error) {
console.error("Get NPCs error:", error);
res.status(500).json({ error: "Failed to get NPCs" });
}
});
// Получить одного NPC
app.get("/api/npc/:id", requireAuth, async (req, res) => {
try {
const npcCharacters = db.collection("npc_characters");
const npc = await npcCharacters.findOne({
_id: new ObjectId(req.params.id),
userId: req.session.userId,
});
if (!npc) {
return res.status(404).json({ error: "NPC not found" });
}
res.json(npc);
} catch (error) {
console.error("Get NPC error:", error);
res.status(500).json({ error: "Failed to get NPC" });
}
});
// Создать NPC
app.post("/api/npc", requireAuth, async (req, res) => {
try {
const npcCharacters = db.collection("npc_characters");
const newNPC = {
...req.body,
userId: req.session.userId,
createdAt: new Date(),
updatedAt: new Date(),
};
const result = await npcCharacters.insertOne(newNPC);
res.json({ ...newNPC, _id: result.insertedId });
} catch (error) {
console.error("Create NPC error:", error);
res.status(500).json({ error: "Failed to create NPC" });
}
});
// Обновить NPC
app.put("/api/npc/:id", requireAuth, async (req, res) => {
try {
const npcCharacters = db.collection("npc_characters");
const result = await npcCharacters.updateOne(
{
_id: new ObjectId(req.params.id),
userId: req.session.userId,
},
{
$set: {
...req.body,
updatedAt: new Date(),
},
},
);
if (result.matchedCount === 0) {
return res.status(404).json({ error: "NPC not found" });
}
res.json({ success: true });
} catch (error) {
console.error("Update NPC error:", error);
res.status(500).json({ error: "Failed to update NPC" });
}
});
// Удалить NPC
app.delete("/api/npc/:id", requireAuth, async (req, res) => {
try {
const npcCharacters = db.collection("npc_characters");
const result = await npcCharacters.deleteOne({
_id: new ObjectId(req.params.id),
userId: req.session.userId,
});
if (result.deletedCount === 0) {
return res.status(404).json({ error: "NPC not found" });
}
res.json({ success: true });
} catch (error) {
console.error("Delete NPC error:", error);
res.status(500).json({ error: "Failed to delete NPC" });
}
});
// ============ ADMIN STATS ============
// Получить статистику по всем историям и токенам
@@ -701,6 +806,130 @@ app.get("/api/admin/stats", requireAuth, async (req, res) => {
}
});
// ============ IMAGE GENERATION ============
// Прокси для генерации изображений через Grok (обход CORS)
app.post("/api/generate-image", requireAuth, async (req, res) => {
try {
const { prompt } = req.body;
const apiKey = process.env.GEMINIGEN_API_KEY;
if (!apiKey) {
return res
.status(500)
.json({ error: "GeminiGen API key not configured" });
}
console.log("Generating image with Grok, prompt:", prompt);
// Используем FormData для multipart/form-data
const formData = new FormData();
formData.append("prompt", prompt);
formData.append("orientation", "portrait"); // 9:16
formData.append("num_result", "1");
const response = await fetch(
"https://api.geminigen.ai/uapi/v1/imagen/grok",
{
method: "POST",
headers: {
"x-api-key": apiKey,
},
body: formData,
},
);
if (!response.ok) {
const error = await response.text();
console.error("Grok API error:", error);
return res
.status(response.status)
.json({ error: "Image generation failed", details: error });
}
const data = await response.json();
console.log("Grok response:", data);
// Проверяем статус генерации
if (data.status === 2 && data.generate_result) {
// Готово - возвращаем URL
res.json({ url: data.generate_result });
} else if (data.status === 1) {
// В процессе - возвращаем uuid для polling
res.json({
pending: true,
uuid: data.uuid,
status_percentage: data.status_percentage,
});
} else {
res
.status(500)
.json({ error: data.error_message || "Generation failed" });
}
} catch (error) {
console.error("Image generation error:", error);
res.status(500).json({ error: "Failed to generate image" });
}
});
// Проверка статуса генерации
app.get("/api/generate-image/status/:uuid", requireAuth, async (req, res) => {
try {
const apiKey = process.env.GEMINIGEN_API_KEY;
const { uuid } = req.params;
const response = await fetch(
`https://api.geminigen.ai/uapi/v1/history/${uuid}`,
{
headers: {
"x-api-key": apiKey,
},
},
);
if (!response.ok) {
const errorText = await response.text();
console.error("History API error:", response.status, errorText);
return res
.status(response.status)
.json({ error: "Failed to check status" });
}
const data = await response.json();
console.log(
"History API status:",
data.status,
"images:",
data.generated_image?.length,
);
if (data.status === 2) {
// Completed - get image URL from generated_image array
const imageUrl =
data.generated_image?.[0]?.image_url ||
data.generated_image?.[0]?.file_download_url ||
data.generate_result;
if (imageUrl) {
res.json({ url: imageUrl, done: true });
} else {
res.status(500).json({ error: "No image URL in response" });
}
} else if (data.status === 1) {
res.json({ pending: true, status_percentage: data.status_percentage });
} else if (data.status === 3) {
res
.status(500)
.json({ error: data.error_message || "Generation failed" });
} else {
// Unknown status - keep polling
res.json({ pending: true, status_percentage: data.status_percentage });
}
} catch (error) {
console.error("Status check error:", error);
res.status(500).json({ error: "Failed to check status" });
}
});
// Запуск сервера
connectDB().then(() => {
app.listen(PORT, () => {