feat: release chat and mindspace UI updates
This commit is contained in:
+184
-6
@@ -11,11 +11,78 @@
|
||||
* conversation content, so it can be dropped/truncated safely at any time.
|
||||
*/
|
||||
|
||||
const DERIVED_TITLE_MAX_LEN = 40;
|
||||
|
||||
function isGeneratedSessionName(name) {
|
||||
return /^20\d{6}(?:[_-]\d+)?$/.test(String(name ?? '').trim());
|
||||
}
|
||||
|
||||
function isDefaultSessionName(name) {
|
||||
const trimmed = String(name ?? '').trim();
|
||||
return !trimmed || trimmed === 'New Chat' || trimmed === '新对话' || isGeneratedSessionName(trimmed);
|
||||
}
|
||||
|
||||
/** Extract a plain-text snippet from a stored normalized message. */
|
||||
function messageSnippet(message) {
|
||||
const fromContent = (message?.content ?? [])
|
||||
.filter((item) => item?.type === 'text' && typeof item.text === 'string')
|
||||
.map((item) => item.text)
|
||||
.join('')
|
||||
.trim();
|
||||
if (fromContent) return fromContent;
|
||||
const displayText = message?.metadata?.displayText;
|
||||
return typeof displayText === 'string' ? displayText.trim() : '';
|
||||
}
|
||||
|
||||
/** Build a one-line title from the first user message of a stored conversation. */
|
||||
function deriveTitleFromMessages(messages) {
|
||||
if (!Array.isArray(messages)) return '';
|
||||
const firstUser = messages.find((m) => m?.role === 'user' && messageSnippet(m));
|
||||
const text = (firstUser ? messageSnippet(firstUser) : '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
if (!text) return '';
|
||||
return text.length > DERIVED_TITLE_MAX_LEN
|
||||
? `${text.slice(0, DERIVED_TITLE_MAX_LEN)}…`
|
||||
: text;
|
||||
}
|
||||
|
||||
function resolveDisplayTitle(session, messages) {
|
||||
if (session?.user_set_name) {
|
||||
const explicit = String(session.name ?? '').trim();
|
||||
if (explicit) return explicit;
|
||||
}
|
||||
|
||||
const recipeTitle = String(session?.recipe?.title ?? '').trim();
|
||||
if (recipeTitle) return recipeTitle;
|
||||
|
||||
const sessionName = String(session?.name ?? '').trim();
|
||||
if (sessionName && !isDefaultSessionName(sessionName)) return sessionName;
|
||||
|
||||
return deriveTitleFromMessages(messages);
|
||||
}
|
||||
|
||||
export function createSessionSnapshotService(pool) {
|
||||
function isEnabled() {
|
||||
return process.env.SESSION_SNAPSHOT_CACHE_ENABLED !== '0';
|
||||
}
|
||||
|
||||
async function persistDisplayTitle(sessionId, title) {
|
||||
const normalized = String(title ?? '').trim();
|
||||
if (!normalized || !pool) return;
|
||||
try {
|
||||
await pool.query(
|
||||
`UPDATE h5_session_snapshots
|
||||
SET display_title = ?
|
||||
WHERE agent_session_id = ?
|
||||
AND (display_title IS NULL OR display_title = '')`,
|
||||
[normalized, sessionId],
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn('[snapshot] persistDisplayTitle failed:', err instanceof Error ? err.message : err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a snapshot for a session.
|
||||
* @param {string} sessionId
|
||||
@@ -29,6 +96,7 @@ export function createSessionSnapshotService(pool) {
|
||||
const now = Date.now();
|
||||
const sessionMeta = {
|
||||
name: session.name ?? '',
|
||||
display_title: resolveDisplayTitle(session, messages),
|
||||
working_dir: session.working_dir ?? '',
|
||||
created_at_str: session.created_at ?? '',
|
||||
updated_at_str: session.updated_at ?? '',
|
||||
@@ -37,13 +105,14 @@ export function createSessionSnapshotService(pool) {
|
||||
};
|
||||
await pool.query(
|
||||
`INSERT INTO h5_session_snapshots
|
||||
(agent_session_id, user_id, name, working_dir,
|
||||
(agent_session_id, user_id, name, display_title, working_dir,
|
||||
created_at_str, updated_at_str, user_set_name, recipe_json,
|
||||
synced_msg_count, source_updated_at,
|
||||
messages_json, synced_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
display_title = VALUES(display_title),
|
||||
working_dir = VALUES(working_dir),
|
||||
created_at_str = VALUES(created_at_str),
|
||||
updated_at_str = VALUES(updated_at_str),
|
||||
@@ -57,6 +126,7 @@ export function createSessionSnapshotService(pool) {
|
||||
sessionId,
|
||||
userId,
|
||||
sessionMeta.name,
|
||||
sessionMeta.display_title,
|
||||
sessionMeta.working_dir,
|
||||
sessionMeta.created_at_str,
|
||||
sessionMeta.updated_at_str,
|
||||
@@ -82,7 +152,7 @@ export function createSessionSnapshotService(pool) {
|
||||
if (!isEnabled() || !pool) return null;
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT agent_session_id, user_id, name, working_dir,
|
||||
`SELECT agent_session_id, user_id, name, display_title, working_dir,
|
||||
created_at_str, updated_at_str, user_set_name, recipe_json,
|
||||
synced_msg_count, source_updated_at, messages_json, synced_at
|
||||
FROM h5_session_snapshots
|
||||
@@ -92,10 +162,26 @@ export function createSessionSnapshotService(pool) {
|
||||
);
|
||||
if (!rows.length) return null;
|
||||
const row = rows[0];
|
||||
const parsedMessages = JSON.parse(row.messages_json);
|
||||
const storedDisplayTitle = String(row.display_title ?? '').trim();
|
||||
const rawName = String(row.name ?? '').trim();
|
||||
const fallbackDisplayTitle =
|
||||
storedDisplayTitle ||
|
||||
resolveDisplayTitle(
|
||||
{
|
||||
name: row.name,
|
||||
recipe: row.recipe_json ? JSON.parse(row.recipe_json) : null,
|
||||
user_set_name: row.user_set_name === 1,
|
||||
},
|
||||
parsedMessages,
|
||||
);
|
||||
if (!storedDisplayTitle && fallbackDisplayTitle) {
|
||||
void persistDisplayTitle(sessionId, fallbackDisplayTitle);
|
||||
}
|
||||
return {
|
||||
session: {
|
||||
id: sessionId,
|
||||
name: row.name,
|
||||
name: fallbackDisplayTitle || rawName,
|
||||
working_dir: row.working_dir,
|
||||
message_count: row.synced_msg_count,
|
||||
created_at: row.created_at_str || undefined,
|
||||
@@ -104,7 +190,7 @@ export function createSessionSnapshotService(pool) {
|
||||
recipe: row.recipe_json ? JSON.parse(row.recipe_json) : null,
|
||||
conversation: null,
|
||||
},
|
||||
messages: JSON.parse(row.messages_json),
|
||||
messages: parsedMessages,
|
||||
meta: {
|
||||
synced_msg_count: row.synced_msg_count,
|
||||
source_updated_at: row.source_updated_at,
|
||||
@@ -153,5 +239,97 @@ export function createSessionSnapshotService(pool) {
|
||||
}
|
||||
}
|
||||
|
||||
return { save, get, remove, refresh, isEnabled };
|
||||
/**
|
||||
* Derive display titles from the first user message for the given session ids.
|
||||
* Used to give unnamed/default sessions a meaningful label in the history list.
|
||||
* Returns a Map<sessionId, title>; sessions without a usable snapshot are omitted.
|
||||
*/
|
||||
async function getDerivedTitles(sessionIds) {
|
||||
if (!isEnabled() || !pool) return new Map();
|
||||
const ids = [...new Set((sessionIds ?? []).filter(Boolean))];
|
||||
if (ids.length === 0) return new Map();
|
||||
const titles = new Map();
|
||||
try {
|
||||
const placeholders = ids.map(() => '?').join(', ');
|
||||
const [rows] = await pool.query(
|
||||
`SELECT agent_session_id, display_title, name, recipe_json, user_set_name, messages_json
|
||||
FROM h5_session_snapshots
|
||||
WHERE agent_session_id IN (${placeholders})`,
|
||||
ids,
|
||||
);
|
||||
for (const row of rows) {
|
||||
try {
|
||||
const parsedMessages = JSON.parse(row.messages_json);
|
||||
const title =
|
||||
String(row.display_title ?? '').trim() ||
|
||||
resolveDisplayTitle(
|
||||
{
|
||||
name: row.name,
|
||||
recipe: row.recipe_json ? JSON.parse(row.recipe_json) : null,
|
||||
user_set_name: row.user_set_name === 1,
|
||||
},
|
||||
parsedMessages,
|
||||
);
|
||||
if (title) {
|
||||
titles.set(row.agent_session_id, title);
|
||||
if (!String(row.display_title ?? '').trim()) {
|
||||
void persistDisplayTitle(row.agent_session_id, title);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip rows with unparseable message payloads.
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[snapshot] getDerivedTitles failed:', err instanceof Error ? err.message : err);
|
||||
}
|
||||
return titles;
|
||||
}
|
||||
|
||||
async function getHistorySummaries(sessionIds) {
|
||||
if (!isEnabled() || !pool) return new Map();
|
||||
const ids = [...new Set((sessionIds ?? []).filter(Boolean))];
|
||||
if (ids.length === 0) return new Map();
|
||||
const summaries = new Map();
|
||||
try {
|
||||
const placeholders = ids.map(() => '?').join(', ');
|
||||
const [rows] = await pool.query(
|
||||
`SELECT agent_session_id, display_title, name, recipe_json, user_set_name,
|
||||
synced_msg_count, messages_json
|
||||
FROM h5_session_snapshots
|
||||
WHERE agent_session_id IN (${placeholders})`,
|
||||
ids,
|
||||
);
|
||||
for (const row of rows) {
|
||||
try {
|
||||
const parsedMessages = JSON.parse(row.messages_json);
|
||||
const title =
|
||||
String(row.display_title ?? '').trim() ||
|
||||
resolveDisplayTitle(
|
||||
{
|
||||
name: row.name,
|
||||
recipe: row.recipe_json ? JSON.parse(row.recipe_json) : null,
|
||||
user_set_name: row.user_set_name === 1,
|
||||
},
|
||||
parsedMessages,
|
||||
);
|
||||
const messageCount = Number(row.synced_msg_count ?? 0);
|
||||
summaries.set(row.agent_session_id, {
|
||||
title,
|
||||
messageCount,
|
||||
});
|
||||
if (title && !String(row.display_title ?? '').trim()) {
|
||||
void persistDisplayTitle(row.agent_session_id, title);
|
||||
}
|
||||
} catch {
|
||||
// Skip rows with unparseable payloads.
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[snapshot] getHistorySummaries failed:', err instanceof Error ? err.message : err);
|
||||
}
|
||||
return summaries;
|
||||
}
|
||||
|
||||
return { save, get, remove, refresh, getDerivedTitles, getHistorySummaries, isEnabled };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user