refactor(server): optimize KB retrieval and voice context
This commit is contained in:
@@ -21,6 +21,7 @@ const {
|
||||
splitTextForSpeech,
|
||||
estimateSpeechDurationMs,
|
||||
shouldForceKnowledgeRoute,
|
||||
isPureChitchat,
|
||||
resolveReply,
|
||||
} = require('./realtimeDialogRouting');
|
||||
const ToolExecutor = require('./toolExecutor');
|
||||
@@ -36,6 +37,9 @@ const redisClient = require('./redisClient');
|
||||
const sessions = new Map();
|
||||
|
||||
const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
|
||||
const AUDIO_KEEPALIVE_INTERVAL_MS = 20 * 1000;
|
||||
// 3200 bytes ≈ 66ms of silence at 24kHz s16le mono (larger frame to ensure S2S acceptance)
|
||||
const SILENT_AUDIO_FRAME = Buffer.alloc(3200, 0);
|
||||
|
||||
const DEFAULT_VOICE_BOT_NAME = DEFAULT_VOICE_ASSISTANT_PROFILE.nickname;
|
||||
|
||||
@@ -60,6 +64,25 @@ function resetIdleTimer(session) {
|
||||
}, IDLE_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
function startAudioKeepalive(session) {
|
||||
clearInterval(session._audioKeepaliveTimer);
|
||||
session._audioKeepaliveTimer = setInterval(() => {
|
||||
if (session.upstream && session.upstream.readyState === WebSocket.OPEN && session.upstreamReady) {
|
||||
session.upstream.send(createAudioMessage(session.sessionId, SILENT_AUDIO_FRAME));
|
||||
console.log(`[NativeVoice] audio keepalive sent session=${session.sessionId}`);
|
||||
} else {
|
||||
console.log(`[NativeVoice] audio keepalive skipped session=${session.sessionId} ready=${session.upstreamReady} wsState=${session.upstream ? session.upstream.readyState : 'null'}`);
|
||||
}
|
||||
}, AUDIO_KEEPALIVE_INTERVAL_MS);
|
||||
}
|
||||
|
||||
function resetAudioKeepalive(session) {
|
||||
if (session._audioKeepaliveTimer) {
|
||||
clearInterval(session._audioKeepaliveTimer);
|
||||
startAudioKeepalive(session);
|
||||
}
|
||||
}
|
||||
|
||||
function sendJson(ws, payload) {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify(payload));
|
||||
@@ -73,7 +96,8 @@ function buildStartSessionPayload(options) {
|
||||
return {
|
||||
asr: {
|
||||
extra: {
|
||||
context: '一成,一成系统,大沃,PM,PM-FitLine,FitLine,细胞营养素,Ai众享,AI众享,盛咖学愿,数字化工作室,Activize,Basics,Restorate,NTC,基础三合一,招商,阿育吠陀,小红产品,小红,小白,大白,肽美,艾特维,德丽,德维,宝丽,美固健,Activize Oxyplus,Basic Power,CitrusCare,NutriSunny,Q10,Omega,葡萄籽,白藜芦醇,益生菌,胶原蛋白肽,Germany,FitLine细胞营养,FitLine营养素,德国PM营养素,德国PM FitLine,德国PM细胞营养,德国PM产品,德国PM健康,德国PM事业,德国PM招商,一成,一成团队,一成商学院,数字化,数字化运营,数字化经营,数字化营销,数字化创业,数字化工作室,数字化事业,招商加盟,合作加盟,事业合作',
|
||||
context: '一成,一成系统,大沃,PM,PM-FitLine,FitLine,细胞营养素,Ai众享,AI众享,盛咖学愿,数字化工作室,Activize,Basics,Restorate,NTC,基础三合一,基础套装,招商,阿育吠陀,小红产品,小红,小白,大白,肽美,艾特维,德丽,德维,宝丽,美固健,Activize Oxyplus,Basic Power,CitrusCare,NutriSunny,Q10,Omega,葡萄籽,白藜芦醇,益生菌,胶原蛋白肽,Germany,FitLine细胞营养,FitLine营养素,德国PM营养素,德国PM FitLine,德国PM细胞营养,德国PM产品,德国PM健康,德国PM事业,德国PM招商,一成,一成团队,一成商学院,数字化,数字化运营,数字化经营,数字化营销,数字化创业,数字化工作室,数字化事业,招商加盟,合作加盟,事业合作,活力健,倍力健,氨基酸,乐活,排毒饮,小绿,纤萃,草本茶,发宝,乳酪煲,关节套装,细胞抗氧素,辅酵素,氧修护,CC套装,CC-Cell,Generation 50+,ProShape,D-Drink,IB5,MEN+,儿童倍适,小红精华液,PowerCocktail,PowerCocktail Junior,TopShape,Fitness-Drink,Herbal Tea,Hair+,Med Dental+,Young Care,Zellschutz,Apple Antioxy,Antioxy,BCAA,Women+,小黑,发健,口腔免疫喷雾,乳清蛋白,男士护肤,去角质,面膜,叶黄素,维适多,护理牙膏,火炉原理,暖炉原理,运动饮料,健康饮品,好转反应,整健反应,骨骼健,顾心,舒采健,衡醇饮,小粉C,异黄酮,倍适,眼霜,洁面,爽肤水',
|
||||
boosting_table_id: 'ab4fde15-79b5-47e9-82b6-5125cca39f63',
|
||||
nbest: 1,
|
||||
},
|
||||
},
|
||||
@@ -417,17 +441,8 @@ async function processReply(session, text, turnSeq = session.latestUserTurnSeq |
|
||||
const activeTurnSeq = turnSeq || session.latestUserTurnSeq || 0;
|
||||
session.processingReply = true;
|
||||
sendJson(session.client, { type: 'assistant_pending', active: true });
|
||||
let isKnowledgeCandidate = shouldForceKnowledgeRoute(cleanText);
|
||||
// KB话题保护窗口:最近60秒内有KB hit,当前轮不是纯闲聊/告别,也视为KB候选
|
||||
// 防止用户质疑/纠正产品信息时S2S自由编造(如"粉末来的呀你搞错了吧")
|
||||
const KB_PROTECTION_WINDOW_MS = 60000;
|
||||
if (!isKnowledgeCandidate && session._lastKbHitAt && (Date.now() - session._lastKbHitAt < KB_PROTECTION_WINDOW_MS)) {
|
||||
const isPureChitchat = /^(喂|你好|嗨|hi|hello|谢谢|谢谢你|谢谢啦|多谢|感谢|再见|拜拜|拜|好的|嗯|哦|行|没事了|不用了|可以了|好的谢谢|没问题|知道了|明白了|了解了|好嘞|好吧|行吧|ok|okay)[,,。!?~\s]*$/i.test(cleanText);
|
||||
if (!isPureChitchat) {
|
||||
isKnowledgeCandidate = true;
|
||||
console.log(`[NativeVoice] KB protection window active, promoting to kbCandidate session=${session.sessionId} lastKbHit=${Math.round((Date.now() - session._lastKbHitAt) / 1000)}s ago`);
|
||||
}
|
||||
}
|
||||
// KB-First: 所有非闲聊查询都视为KB候选,阻断S2S音频等待KB结果
|
||||
let isKnowledgeCandidate = !isPureChitchat(cleanText);
|
||||
if (isKnowledgeCandidate) {
|
||||
session.blockUpstreamAudio = true;
|
||||
suppressUpstreamReply(session, 30000);
|
||||
@@ -623,6 +638,7 @@ function handleUpstreamMessage(session, data) {
|
||||
session.upstreamReady = true;
|
||||
console.log(`[NativeVoice] upstream ready session=${session.sessionId}`);
|
||||
resetIdleTimer(session);
|
||||
startAudioKeepalive(session);
|
||||
sendGreeting(session);
|
||||
return;
|
||||
}
|
||||
@@ -879,13 +895,12 @@ function handleUpstreamMessage(session, data) {
|
||||
const normalizedPartial = normalizeKnowledgeAlias(text);
|
||||
session.latestUserText = normalizedPartial;
|
||||
session._lastPartialAt = now;
|
||||
// 提前阻断:部分识别文字含知识库关键词时,立即阻断S2S音频,防止有害内容播出
|
||||
if (normalizedPartial.length >= 6 && !session.blockUpstreamAudio && shouldForceKnowledgeRoute(normalizedPartial)) {
|
||||
// KB-First: 非闲聊文本一律提前阻断S2S音频,防止有害内容播出
|
||||
if (normalizedPartial.length >= 6 && !session.blockUpstreamAudio && !isPureChitchat(normalizedPartial)) {
|
||||
session.blockUpstreamAudio = true;
|
||||
session.currentTtsType = 'default';
|
||||
// 立即清除客户端已收到的S2S音频,防止用户听到抢答片段
|
||||
sendJson(session.client, { type: 'tts_reset', reason: 'early_block' });
|
||||
console.log(`[NativeVoice] early block: partial text matched KB keywords session=${session.sessionId} text=${JSON.stringify(text.slice(0, 80))}`);
|
||||
console.log(`[NativeVoice] early block: non-chitchat partial session=${session.sessionId} text=${JSON.stringify(text.slice(0, 80))}`);
|
||||
// KB预查询:提前启动知识库查询,减少final ASR后的等待时间
|
||||
const kbPrequeryDebounce = 600;
|
||||
if (normalizedPartial.length >= 8 && (!session._kbPrequeryStartedAt || now - session._kbPrequeryStartedAt > kbPrequeryDebounce)) {
|
||||
@@ -1005,6 +1020,7 @@ function attachClientHandlers(session) {
|
||||
if (isBinary) {
|
||||
if (session.upstream && session.upstream.readyState === WebSocket.OPEN && session.upstreamReady) {
|
||||
session.upstream.send(createAudioMessage(session.sessionId, raw));
|
||||
resetAudioKeepalive(session);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1027,11 +1043,11 @@ function attachClientHandlers(session) {
|
||||
});
|
||||
session.assistantProfile = assistantProfile;
|
||||
session.botName = parsed.botName || assistantProfile.nickname || DEFAULT_VOICE_BOT_NAME;
|
||||
session.systemRole = parsed.systemRole || buildVoiceSystemRole(assistantProfile);
|
||||
session.systemRole = buildVoiceSystemRole(assistantProfile);
|
||||
session.speakingStyle = parsed.speakingStyle || session.speakingStyle || DEFAULT_VOICE_SPEAKING_STYLE;
|
||||
session.speaker = parsed.speaker || process.env.VOLC_S2S_SPEAKER_ID || 'zh_female_vv_jupiter_bigtts';
|
||||
session.modelVersion = parsed.modelVersion || 'O';
|
||||
session.greetingText = parsed.greetingText || buildVoiceGreeting(assistantProfile);
|
||||
session.greetingText = buildVoiceGreeting(assistantProfile);
|
||||
// 立即发送 ready,不等 upstream event 150,大幅缩短前端等待时间
|
||||
sendReady(session);
|
||||
session.upstream = createUpstreamConnection(session);
|
||||
@@ -1069,6 +1085,7 @@ function attachClientHandlers(session) {
|
||||
clearTimeout(session.readyTimer);
|
||||
clearTimeout(session.suppressReplyTimer);
|
||||
clearTimeout(session.idleTimer);
|
||||
clearInterval(session._audioKeepaliveTimer);
|
||||
if (session.upstream && session.upstream.readyState === WebSocket.OPEN) {
|
||||
session.upstream.close();
|
||||
}
|
||||
@@ -1108,6 +1125,8 @@ function createUpstreamConnection(session) {
|
||||
upstream.on('close', (code) => {
|
||||
console.log(`[NativeVoice] upstream closed session=${session.sessionId} code=${code}`);
|
||||
session.upstreamReady = false;
|
||||
clearInterval(session._audioKeepaliveTimer);
|
||||
session._audioKeepaliveTimer = null;
|
||||
sendJson(session.client, { type: 'upstream_closed', code });
|
||||
setTimeout(() => {
|
||||
if (session.client && session.client.readyState === WebSocket.OPEN) {
|
||||
@@ -1181,6 +1200,7 @@ function createSession(client, sessionId) {
|
||||
_audioBlockLogOnce: false,
|
||||
_lastFinalNormalized: '',
|
||||
_lastFinalAt: 0,
|
||||
_audioKeepaliveTimer: null,
|
||||
};
|
||||
sessions.set(sessionId, session);
|
||||
attachClientHandlers(session);
|
||||
|
||||
Reference in New Issue
Block a user