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分钟 // ============ 连接管理 ============ 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; } } // ============ 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, setKbCache, getKbCache, disconnect, };