Files
bigwo/test2/server/services/redisClient.js
User fe25229de7 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
2026-04-03 10:19:16 +08:00

214 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const Redis = require('ioredis');
// ============ 配置 ============
const REDIS_URL = process.env.REDIS_URL || 'redis://127.0.0.1:6379';
const REDIS_PASSWORD = process.env.REDIS_PASSWORD || '';
const REDIS_DB = parseInt(process.env.REDIS_DB) || 0;
const KEY_PREFIX = process.env.REDIS_KEY_PREFIX || 'bigwo:';
const HISTORY_MAX_LEN = 10; // 5轮 × 2条/轮
const HISTORY_TTL_S = 1800; // 30分钟
const KB_CACHE_HIT_TTL_S = 300; // 5分钟
const KB_CACHE_NOHIT_TTL_S = 120; // 2分钟
const SUMMARY_TTL_S = parseInt(process.env.SUMMARY_REDIS_TTL_SECONDS) || 7200; // 2小时
// ============ 连接管理 ============
let client = null;
let isReady = false;
function createClient() {
if (client) return client;
const opts = {
db: REDIS_DB,
keyPrefix: KEY_PREFIX,
retryStrategy(times) {
if (times > 10) {
console.error('[Redis] max retries reached, giving up');
return null;
}
return Math.min(times * 200, 3000);
},
reconnectOnError(err) {
const targetErrors = ['READONLY', 'ECONNRESET'];
return targetErrors.some(e => err.message.includes(e));
},
lazyConnect: true,
maxRetriesPerRequest: 2,
connectTimeout: 5000,
commandTimeout: 3000,
};
if (REDIS_PASSWORD) {
opts.password = REDIS_PASSWORD;
}
client = new Redis(REDIS_URL, opts);
client.on('ready', () => {
isReady = true;
console.log('[Redis] connected and ready');
});
client.on('error', (err) => {
console.warn('[Redis] error:', err.message);
});
client.on('close', () => {
isReady = false;
console.warn('[Redis] connection closed');
});
client.on('reconnecting', () => {
console.log('[Redis] reconnecting...');
});
client.connect().catch((err) => {
console.warn('[Redis] initial connect failed:', err.message);
});
return client;
}
function getClient() {
if (!client) createClient();
return client;
}
function isAvailable() {
return isReady && client && client.status === 'ready';
}
// ============ 对话历史 ============
const historyKey = (sessionId) => `voice:history:${sessionId}`;
async function pushMessage(sessionId, msg) {
if (!isAvailable()) return false;
try {
const key = historyKey(sessionId);
const value = JSON.stringify({
role: msg.role,
content: msg.content,
source: msg.source || '',
ts: Date.now(),
});
await client.lpush(key, value);
await client.ltrim(key, 0, HISTORY_MAX_LEN - 1);
await client.expire(key, HISTORY_TTL_S);
return true;
} catch (err) {
console.warn('[Redis] pushMessage failed:', err.message);
return false;
}
}
async function getRecentHistory(sessionId, maxRounds = 5) {
if (!isAvailable()) return null;
try {
const key = historyKey(sessionId);
const items = await client.lrange(key, 0, maxRounds * 2 - 1);
if (!items || items.length === 0) return [];
// lpush 是倒序插入lrange 取出的也是最新在前,需要 reverse 恢复时间顺序
return items
.map((item) => {
try { return JSON.parse(item); } catch { return null; }
})
.filter(Boolean)
.reverse();
} catch (err) {
console.warn('[Redis] getRecentHistory failed:', err.message);
return null;
}
}
async function clearSession(sessionId) {
if (!isAvailable()) return false;
try {
await client.del(historyKey(sessionId));
return true;
} catch (err) {
console.warn('[Redis] clearSession failed:', err.message);
return false;
}
}
// ============ 对话摘要 ============
const summaryKey = (sessionId) => `voice:summary:${sessionId}`;
async function setSummary(sessionId, summary) {
if (!isAvailable() || !summary) return false;
try {
const key = summaryKey(sessionId);
await client.set(key, summary, 'EX', SUMMARY_TTL_S);
return true;
} catch (err) {
console.warn('[Redis] setSummary failed:', err.message);
return false;
}
}
async function getSummary(sessionId) {
if (!isAvailable()) return null;
try {
const key = summaryKey(sessionId);
return await client.get(key);
} catch (err) {
console.warn('[Redis] getSummary failed:', err.message);
return null;
}
}
// ============ KB 缓存 ============
const kbCacheKey = (cacheKey) => `kb_cache:${cacheKey}`;
async function setKbCache(cacheKey, result) {
// 只缓存 hit 结果no-hit 不写入 Redis避免阻止后续 retry
if (!isAvailable() || !result.hit) return false;
try {
const key = kbCacheKey(cacheKey);
await client.set(key, JSON.stringify(result), 'EX', KB_CACHE_HIT_TTL_S);
return true;
} catch (err) {
console.warn('[Redis] setKbCache failed:', err.message);
return false;
}
}
async function getKbCache(cacheKey) {
if (!isAvailable()) return null;
try {
const key = kbCacheKey(cacheKey);
const data = await client.get(key);
if (!data) return null;
return JSON.parse(data);
} catch (err) {
console.warn('[Redis] getKbCache failed:', err.message);
return null;
}
}
// ============ 优雅关闭 ============
async function disconnect() {
if (client) {
try {
await client.quit();
} catch {
client.disconnect();
}
client = null;
isReady = false;
console.log('[Redis] disconnected');
}
}
module.exports = {
createClient,
getClient,
isAvailable,
pushMessage,
getRecentHistory,
clearSession,
setSummary,
getSummary,
setKbCache,
getKbCache,
disconnect,
};