"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 {requireLogin} from "../middleware/auth.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: {
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 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("/personalities", (_req, res) => res.json({personalities: listPersonalities(), defaultPersonalityId: DEFAULT_PERSONALITY_ID}));
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"});
});
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 chat = repositories.getChat(chatId, req.session.user);
if (!chat) return res.status(404).json({error: "Chat not found"});
const personalityId = String(req.body?.personalityId || "");
if (!getPersonality(personalityId)) return res.status(400).json({error: "Unknown personality"});
repositories.updateChatPersonality(chatId, req.session.user, personalityId);
return res.json({chat: repositories.getChat(chatId, req.session.user), personality: listPersonalities().find(item => item.id === personalityId)});
const personalityId = req.body?.personalityId;
if (typeof personalityId !== "string" || !personalityId.trim()) {
return res.status(400).json({error: "personalityId must be a non-empty string"});
}
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) => {
+66 -9
View File
@@ -1,3 +1,5 @@
export const DEFAULT_PERSONALITY_ID = "general-assistant";
const entries = [
{
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)]));
export const DEFAULT_PERSONALITY_ID = "general-assistant";
const requiredMetadataFields = [
"id",
"name",
"specialization",
"description",
"longDescription",
"historicalInspiration",
"icon",
"accentColor",
"systemPrompt"
];
export function getPersonality(id) {
return registry.get(id) || null;
function createRegistry(definitions) {
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}`);
}
export function listPersonalities() {
return entries.map(entry => ({
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})]));
}
const registry = createRegistry(entries);
function toPublicMetadata(entry) {
if (!entry) return null;
return {
id: entry.id,
name: entry.name,
specialization: entry.specialization,
@@ -96,10 +137,26 @@ export function listPersonalities() {
historicalInspiration: entry.historicalInspiration,
icon: entry.icon,
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) {
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.
*/
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_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),
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),
is_pinned: Boolean(chat.is_pinned || chat.isPinned),
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"}});
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:["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: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, "/personas", {method:"POST", jar:owner, body:{name:"Custom"}})).status, 404);
});