"Improve personality registry validation and API
CI / check (22) (push) Failing after 4m50s
CI / check (24) (push) Successful in 9m43s

This commit is contained in:
2026-10-13 22:14:51 +02:00
parent 5765922765
commit 42e71fb502
4 changed files with 91 additions and 22 deletions
+19 -9
View File
@@ -7,14 +7,20 @@ import {asyncHandler} from "../middleware/asyncHandler.js";
import {generationCancellation} from "../middleware/generationCancellation.js"; import {generationCancellation} from "../middleware/generationCancellation.js";
import {requireLogin} from "../middleware/auth.js"; import {requireLogin} from "../middleware/auth.js";
import {validateBody} from "../middleware/validate.js"; import {validateBody} from "../middleware/validate.js";
import {getPersonality, listPersonalities, DEFAULT_PERSONALITY_ID} from "../services/personalityRegistry.js"; import {getPersonality, getPersonalityMetadata, listPersonalities, DEFAULT_PERSONALITY_ID} from "../services/personalityRegistry.js";
const createChatValidator = (body) => ({ const createChatValidator = (body) => {
const rawPersonalityId = body?.personalityId;
if (rawPersonalityId != null && (typeof rawPersonalityId !== "string" || !rawPersonalityId.trim())) {
return {error: "personalityId must be a non-empty string"};
}
return {
value: { value: {
title: String(body?.title || "New chat").trim() || "New chat", title: String(body?.title || "New chat").trim() || "New chat",
personalityId: body?.personalityId == null ? DEFAULT_PERSONALITY_ID : String(body.personalityId) personalityId: rawPersonalityId == null ? DEFAULT_PERSONALITY_ID : rawPersonalityId.trim()
} }
}); };
};
const messageValidator = (body) => { const messageValidator = (body) => {
const content = String(body?.content || "").trim(); const content = String(body?.content || "").trim();
@@ -147,7 +153,7 @@ export function createChatsRouter({repositories, chatService, modelService, conf
router.get("/chats", (req, res) => res.json({chats: repositories.listChats(req.session.user)})); router.get("/chats", (req, res) => res.json({chats: repositories.listChats(req.session.user)}));
router.get("/personalities", (_req, res) => res.json({personalities: listPersonalities(), defaultPersonalityId: DEFAULT_PERSONALITY_ID})); router.get("/personalities", (_req, res) => res.json({personalities: listPersonalities(), defaultPersonalityId: DEFAULT_PERSONALITY_ID}));
router.get("/personalities/:id", (req, res) => { router.get("/personalities/:id", (req, res) => {
const personality = listPersonalities().find(item => item.id === req.params.id); const personality = getPersonalityMetadata(req.params.id);
return personality ? res.json({personality}) : res.status(404).json({error: "Personality not found"}); return personality ? res.json({personality}) : res.status(404).json({error: "Personality not found"});
}); });
router.get("/chats/search", (req, res) => res.json({chats: repositories.searchChats(req.session.user, req.query.q || "")})); router.get("/chats/search", (req, res) => res.json({chats: repositories.searchChats(req.session.user, req.query.q || "")}));
@@ -170,10 +176,14 @@ export function createChatsRouter({repositories, chatService, modelService, conf
const chatId = Number(req.params.id); const chatId = Number(req.params.id);
const chat = repositories.getChat(chatId, req.session.user); const chat = repositories.getChat(chatId, req.session.user);
if (!chat) return res.status(404).json({error: "Chat not found"}); if (!chat) return res.status(404).json({error: "Chat not found"});
const personalityId = String(req.body?.personalityId || ""); const personalityId = req.body?.personalityId;
if (!getPersonality(personalityId)) return res.status(400).json({error: "Unknown personality"}); if (typeof personalityId !== "string" || !personalityId.trim()) {
repositories.updateChatPersonality(chatId, req.session.user, personalityId); return res.status(400).json({error: "personalityId must be a non-empty string"});
return res.json({chat: repositories.getChat(chatId, req.session.user), personality: listPersonalities().find(item => item.id === personalityId)}); }
const normalizedPersonalityId = personalityId.trim();
if (!getPersonality(normalizedPersonalityId)) return res.status(400).json({error: "Unknown personality"});
repositories.updateChatPersonality(chatId, req.session.user, normalizedPersonalityId);
return res.json({chat: repositories.getChat(chatId, req.session.user), personality: getPersonalityMetadata(normalizedPersonalityId)});
}); });
router.put("/chats/:id", validateBody((body) => { router.put("/chats/:id", validateBody((body) => {
+66 -9
View File
@@ -1,3 +1,5 @@
export const DEFAULT_PERSONALITY_ID = "general-assistant";
const entries = [ const entries = [
{ {
id: "general-assistant", name: "Leonardo", specialization: "General Assistance", id: "general-assistant", name: "Leonardo", specialization: "General Assistance",
@@ -79,15 +81,54 @@ Present the reasoning in a readable order: frame the problem, identify decisive
} }
]; ];
const registry = new Map(entries.map(entry => [entry.id, Object.freeze(entry)])); const requiredMetadataFields = [
export const DEFAULT_PERSONALITY_ID = "general-assistant"; "id",
"name",
"specialization",
"description",
"longDescription",
"historicalInspiration",
"icon",
"accentColor",
"systemPrompt"
];
export function getPersonality(id) { function createRegistry(definitions) {
return registry.get(id) || null; const ids = new Set();
const defaultEntries = definitions.filter(entry => entry.id === DEFAULT_PERSONALITY_ID);
if (defaultEntries.length !== 1) {
throw new Error(`Personality registry must contain exactly one default personality: ${DEFAULT_PERSONALITY_ID}`);
}
for (const entry of definitions) {
if (!entry || typeof entry !== "object") throw new Error("Personality registry entries must be objects");
for (const field of requiredMetadataFields) {
if (typeof entry[field] !== "string" || !entry[field].trim()) {
throw new Error(`Personality ${entry.id || "<unknown>"} is missing ${field}`);
}
}
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(entry.id)) {
throw new Error(`Personality ID is not kebab-case: ${entry.id}`);
}
if (ids.has(entry.id)) throw new Error(`Duplicate personality ID: ${entry.id}`);
if (!/^#[0-9a-f]{6}$/i.test(entry.accentColor)) {
throw new Error(`Invalid accent color for personality: ${entry.id}`);
}
if (entry.systemPrompt.length < 1200) {
throw new Error(`Personality ${entry.id} needs a substantial system prompt`);
}
ids.add(entry.id);
}
return new Map(definitions.map(entry => [entry.id, Object.freeze({...entry})]));
} }
export function listPersonalities() { const registry = createRegistry(entries);
return entries.map(entry => ({
function toPublicMetadata(entry) {
if (!entry) return null;
return {
id: entry.id, id: entry.id,
name: entry.name, name: entry.name,
specialization: entry.specialization, specialization: entry.specialization,
@@ -96,10 +137,26 @@ export function listPersonalities() {
historicalInspiration: entry.historicalInspiration, historicalInspiration: entry.historicalInspiration,
icon: entry.icon, icon: entry.icon,
accentColor: entry.accentColor, accentColor: entry.accentColor,
...(entry.isDefault ? {isDefault: true} : {}) isDefault: entry.id === DEFAULT_PERSONALITY_ID
})); };
}
export function getPersonality(id) {
return registry.get(id) || null;
}
export function resolvePersonalityId(id) {
return registry.has(id) ? id : DEFAULT_PERSONALITY_ID;
}
export function listPersonalities() {
return entries.map(toPublicMetadata);
}
export function getPersonalityMetadata(id) {
return toPublicMetadata(getPersonality(id));
} }
export function getPersonalitySystemPrompt(id) { export function getPersonalitySystemPrompt(id) {
return getPersonality(id)?.systemPrompt || registry.get(DEFAULT_PERSONALITY_ID).systemPrompt; return registry.get(resolvePersonalityId(id)).systemPrompt;
} }
+2 -2
View File
@@ -2,7 +2,7 @@
* @file Prüft und normalisiert importierte Arbeitsbereiche mit Personas, Chats und Nachrichtenbeziehungen. * @file Prüft und normalisiert importierte Arbeitsbereiche mit Personas, Chats und Nachrichtenbeziehungen.
*/ */
import {parseSources} from "./chatMessageHelpers.js"; import {parseSources} from "./chatMessageHelpers.js";
import {getPersonality, DEFAULT_PERSONALITY_ID} from "./personalityRegistry.js"; import {resolvePersonalityId} from "./personalityRegistry.js";
const VALID_PERSONA_TYPES = new Set(["assistant", "user"]); const VALID_PERSONA_TYPES = new Set(["assistant", "user"]);
const VALID_MESSAGE_ROLES = new Set(["user", "bot", "assistant", "system"]); const VALID_MESSAGE_ROLES = new Set(["user", "bot", "assistant", "system"]);
@@ -197,7 +197,7 @@ function validateChat(chat, index, errors) {
branched_from_message_id: sourceId(chat.branched_from_message_id ?? chat.branchedFromMessageId), branched_from_message_id: sourceId(chat.branched_from_message_id ?? chat.branchedFromMessageId),
title, title,
stored_title: title, stored_title: title,
personality_id: getPersonality(chat.personality_id ?? chat.personalityId)?.id || DEFAULT_PERSONALITY_ID, personality_id: resolvePersonalityId(chat.personality_id ?? chat.personalityId),
folder_name: optionalText(chat.folder_name || chat.folderName), folder_name: optionalText(chat.folder_name || chat.folderName),
is_pinned: Boolean(chat.is_pinned || chat.isPinned), is_pinned: Boolean(chat.is_pinned || chat.isPinned),
archived_at: optionalText(chat.archived_at || chat.archivedAt), archived_at: optionalText(chat.archived_at || chat.archivedAt),
+2
View File
@@ -158,7 +158,9 @@ test("personality API keeps prompts private, validates IDs, and enforces chat ow
const defaultChat = await request(server.baseUrl, "/chats", {method:"POST", jar:owner, body:{systemPrompt:"Injected"}}); const defaultChat = await request(server.baseUrl, "/chats", {method:"POST", jar:owner, body:{systemPrompt:"Injected"}});
assert.equal(defaultChat.json.chat.personality_id, "general-assistant"); assert.equal(defaultChat.json.chat.personality_id, "general-assistant");
assert.equal((await request(server.baseUrl, "/chats", {method:"POST", jar:owner, body:{personalityId:"invented"}})).status, 400); assert.equal((await request(server.baseUrl, "/chats", {method:"POST", jar:owner, body:{personalityId:"invented"}})).status, 400);
assert.equal((await request(server.baseUrl, "/chats", {method:"POST", jar:owner, body:{personalityId:["programmer"]}})).status, 400);
assert.equal((await request(server.baseUrl, `/chats/${created.json.chat.id}/personality`, {method:"PUT", jar:other, body:{personalityId:"tutor"}})).status, 404); assert.equal((await request(server.baseUrl, `/chats/${created.json.chat.id}/personality`, {method:"PUT", jar:other, body:{personalityId:"tutor"}})).status, 404);
assert.equal((await request(server.baseUrl, `/chats/${created.json.chat.id}/personality`, {method:"PUT", jar:owner, body:{personalityId:["tutor"]}})).status, 400);
assert.equal((await request(server.baseUrl, `/chats/${created.json.chat.id}/personality`, {method:"PUT", jar:owner, body:{personalityId:"tutor"}})).json.chat.personality_id, "tutor"); assert.equal((await request(server.baseUrl, `/chats/${created.json.chat.id}/personality`, {method:"PUT", jar:owner, body:{personalityId:"tutor"}})).json.chat.personality_id, "tutor");
assert.equal((await request(server.baseUrl, "/personas", {method:"POST", jar:owner, body:{name:"Custom"}})).status, 404); assert.equal((await request(server.baseUrl, "/personas", {method:"POST", jar:owner, body:{name:"Custom"}})).status, 404);
}); });