- 新增 kbRetriever.js: VikingDB search_knowledge 纯检索替代 Ark chat/completions, doubao-seed-rerank 重排, RAG payload 语气引导缓解音色差异 - 新增 redisClient.js: Redis 连接管理 + 5轮对话历史 + KB缓存双写 - toolExecutor.js: 产品别名扩展25条, 全库检索topK=25, 检索阈值0.01, 精简 buildDeterministicKnowledgeQuery - nativeVoiceGateway.js: isPureChitchat扩展, KB保护窗口60s, prequery参数调优 - realtimeDialogRouting.js: resolveReply感知KB保护窗口, fast-path适配raw模式 - app.js: 健康检查新增 redis/reranker/kbRetrievalMode - 新增测试: alias A/B测试, KB retriever测试, Redis客户端测试, raw模式集成测试
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,
|
||
};
|