185 lines
4.6 KiB
JavaScript
185 lines
4.6 KiB
JavaScript
|
|
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,
|
|||
|
|
};
|