This commit is contained in:
seb
2026-10-12 18:09:57 +02:00
parent 713c31a21e
commit c35126bb11
2 changed files with 32 additions and 9 deletions

View File

@@ -1119,7 +1119,7 @@
* description: Id of the conversation that started this one as a subagent, or null for a top-level conversation. Deleting a conversation also deletes every conversation below it in this chain. * description: Id of the conversation that started this one as a subagent, or null for a top-level conversation. Deleting a conversation also deletes every conversation below it in this chain.
* first_message: * first_message:
* type: [string, 'null'] * type: [string, 'null']
* description: Only returned by the list endpoint. Plain text of the conversation's first user message (max 200 chars, ellipsis if cut), or null if there is none yet. * description: Plain text of the conversation's first user message (XML stripped, max 100 chars, ellipsis if cut), or null if there is none yet.
* ModelConversationCreateInput: * ModelConversationCreateInput:
* type: object * type: object
* required: [provider, model] * required: [provider, model]

View File

@@ -4,7 +4,7 @@ import { RETIRED_MODELS, resolveDefaultModel } from '../../tools/llm-key.js';
import { emit } from '../sse.js'; import { emit } from '../sse.js';
import { getOrCreateManager, removeManager, cleanUserMessageContent } from '../../services/conversationManager.js'; import { getOrCreateManager, removeManager, cleanUserMessageContent } from '../../services/conversationManager.js';
const FIRST_MESSAGE_MAX_CHARS = 200; const FIRST_MESSAGE_MAX_CHARS = 100;
// #region Helpers // #region Helpers
@@ -24,9 +24,26 @@ function parsePositiveInt(value, field) {
return { id }; return { id };
} }
/**
* Removes XML elements together with their content (e.g. `<USERENV><ROUTE>…</ROUTE></USERENV>`)
* and any leftover self-closing tags.
* @param {string} text
* @returns {string}
*/
function stripXmlElements(text) {
const elementPattern = /<([A-Za-z][\w:.-]*)(?:\s[^>]*)?>[\s\S]*?<\/\1\s*>/g;
let previous;
let stripped = text;
do {
previous = stripped;
stripped = stripped.replace(elementPattern, '');
} while (stripped !== previous);
return stripped.replace(/<[A-Za-z][\w:.-]*(?:\s[^>]*)?\/>/g, '');
}
/** /**
* Turns the stored first user message payload (`{"content": ...}`) into short plain text * Turns the stored first user message payload (`{"content": ...}`) into short plain text
* for conversation lists. * for conversation lists: JSON content only, XML elements stripped, outer whitespace trimmed.
* @param {string|null} storedMessage * @param {string|null} storedMessage
* @returns {string|null} * @returns {string|null}
*/ */
@@ -38,10 +55,19 @@ function buildFirstMessagePreview(storedMessage) {
} catch { } catch {
rawContent = storedMessage; rawContent = storedMessage;
} }
const text = cleanUserMessageContent(rawContent).trim(); const text = stripXmlElements(cleanUserMessageContent(rawContent)).trim();
return text.length > FIRST_MESSAGE_MAX_CHARS ? `${text.slice(0, FIRST_MESSAGE_MAX_CHARS)}…` : text; return text.length > FIRST_MESSAGE_MAX_CHARS ? `${text.slice(0, FIRST_MESSAGE_MAX_CHARS)}…` : text;
} }
/**
* Replaces the store's raw `first_user_message` with the plain-text `first_message` preview.
* @param {{ first_user_message: string|null }} row
* @returns {object}
*/
function buildConversationResponse({ first_user_message: storedFirstMessage, ...conversation }) {
return { ...conversation, first_message: buildFirstMessagePreview(storedFirstMessage) };
}
function modelConversationErrorStatus(error) { function modelConversationErrorStatus(error) {
if (error.message === 'No company selected') return 400; if (error.message === 'No company selected') return 400;
if (error.message === 'Not authorized') return 403; if (error.message === 'Not authorized') return 403;
@@ -113,10 +139,7 @@ export function register(app, basePath = '') {
try { try {
const rows = await db.list(req.user.id); const rows = await db.list(req.user.id);
const conversations = rows.map(({ first_user_message: storedFirstMessage, ...conversation }) => ({ const conversations = rows.map(buildConversationResponse);
...conversation,
first_message: buildFirstMessagePreview(storedFirstMessage),
}));
res.status(200).json({ conversations }); res.status(200).json({ conversations });
} catch (error) { } catch (error) {
const status = modelConversationErrorStatus(error); const status = modelConversationErrorStatus(error);
@@ -269,7 +292,7 @@ export function register(app, basePath = '') {
} }
try { try {
const conversation = await db.get(req.user.id, parsedId.id); const conversation = buildConversationResponse(await db.get(req.user.id, parsedId.id));
res.status(200).json({ conversation }); res.status(200).json({ conversation });
} catch (error) { } catch (error) {
const status = modelConversationErrorStatus(error); const status = modelConversationErrorStatus(error);