feat: conversation long-term memory + fix source ENUM bug

- New: conversationSummarizer.js (LLM summary every 3 turns, loadBestSummary, persistFinalSummary)
- db/index.js: conversation_summaries table, upsertConversationSummary, getSessionSummary
- redisClient.js: setSummary/getSummary (TTL 2h)
- nativeVoiceGateway.js: _turnCount tracking, trigger summarize, persist on close
- realtimeDialogRouting.js: inject summary context, reduce history 5->3 rounds
- Fix: messages source ENUM missing 'search_knowledge' causing chat DB writes to fail
This commit is contained in:
User
2026-04-03 10:19:16 +08:00
parent 5b824cd16a
commit fe25229de7
6 changed files with 797 additions and 69 deletions

View File

@@ -26,8 +26,8 @@ async function migrateSchema() {
if (!(await columnMatchesType('messages', 'role', "'system'"))) {
await pool.execute("ALTER TABLE `messages` MODIFY COLUMN `role` ENUM('user', 'assistant', 'tool', 'system') NOT NULL");
}
if (!(await columnMatchesType('messages', 'source', "'chat_bot'"))) {
await pool.execute("ALTER TABLE `messages` MODIFY COLUMN `source` ENUM('voice_asr', 'voice_bot', 'voice_tool', 'chat_user', 'chat_bot') NOT NULL");
if (!(await columnMatchesType('messages', 'source', "'search_knowledge'"))) {
await pool.execute("ALTER TABLE `messages` MODIFY COLUMN `source` ENUM('voice_asr', 'voice_bot', 'voice_tool', 'chat_user', 'chat_bot', 'search_knowledge') NOT NULL");
}
await ensureColumnExists('messages', 'tool_name', '`tool_name` VARCHAR(64) NULL AFTER `source`');
await ensureColumnExists('messages', 'meta_json', '`meta_json` JSON NULL AFTER `tool_name`');
@@ -81,7 +81,7 @@ async function initialize() {
session_id VARCHAR(128) NOT NULL,
role ENUM('user', 'assistant', 'tool', 'system') NOT NULL,
content TEXT NOT NULL,
source ENUM('voice_asr', 'voice_bot', 'voice_tool', 'chat_user', 'chat_bot') NOT NULL,
source ENUM('voice_asr', 'voice_bot', 'voice_tool', 'chat_user', 'chat_bot', 'search_knowledge') NOT NULL,
tool_name VARCHAR(64),
meta_json JSON,
created_at BIGINT,
@@ -90,6 +90,21 @@ async function initialize() {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
`);
await pool.execute(`
CREATE TABLE IF NOT EXISTS conversation_summaries (
id INT AUTO_INCREMENT PRIMARY KEY,
session_id VARCHAR(128) NOT NULL,
user_id VARCHAR(128),
summary TEXT NOT NULL,
turn_count INT DEFAULT 0,
topics JSON,
created_at BIGINT,
updated_at BIGINT,
UNIQUE INDEX idx_session (session_id),
INDEX idx_user_time (user_id, updated_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
`);
await migrateSchema();
console.log(`[DB] MySQL connected: ${dbName}, tables ready`);
@@ -202,9 +217,31 @@ async function getSessionList(userId, limit = 50) {
*/
async function deleteSession(sessionId) {
await pool.execute('DELETE FROM messages WHERE session_id = ?', [sessionId]);
await pool.execute('DELETE FROM conversation_summaries WHERE session_id = ?', [sessionId]);
await pool.execute('DELETE FROM sessions WHERE id = ?', [sessionId]);
}
// ==================== Conversation Summaries ====================
async function upsertConversationSummary(sessionId, userId, summary, { turnCount = 0, topics = null } = {}) {
const now = Date.now();
const topicsJson = topics ? JSON.stringify(topics) : null;
await pool.execute(
`INSERT INTO conversation_summaries (session_id, user_id, summary, turn_count, topics, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE summary=VALUES(summary), turn_count=VALUES(turn_count), topics=VALUES(topics), updated_at=VALUES(updated_at)`,
[sessionId, userId || null, summary, turnCount, topicsJson, now, now]
);
}
async function getSessionSummary(sessionId) {
const [rows] = await pool.execute(
'SELECT summary, turn_count, topics, updated_at FROM conversation_summaries WHERE session_id = ? LIMIT 1',
[sessionId]
);
return rows[0] || null;
}
module.exports = {
initialize,
getPool,
@@ -217,4 +254,6 @@ module.exports = {
getHistoryForLLM,
getSessionList,
deleteSession,
upsertConversationSummary,
getSessionSummary,
};