fix(test2): 稳定语音知识库回复并补齐热门问法覆盖
This commit is contained in:
@@ -107,11 +107,15 @@ function extractUserText(jsonPayload, sessionId = null) {
|
||||
|
||||
const BRAND_HARMFUL_PATTERN = /传销|骗局|骗子公司|骗子|非法集资|非法经营|非法营销|不正规|不合法|庞氏骗局|老鼠会|拉人头|割韭菜|资金盘|涉嫌违法|涉嫌传销|疑似传销|层级分销|PM.*(?:是|属于|涉嫌|疑似).*(?:传销|骗局|非法|不合法|不正规)|(?:传销|骗局|非法|不合法|不正规).*(?:组织|公司|企业|模式)/;
|
||||
const BRAND_SAFE_REPLY = '德国PM是一家1993年成立于德国的合法直销公司,获得邓白氏AAA+认证,业务覆盖100多个国家和地区。如果你想了解更多,可以问我关于PM公司的详细介绍哦。';
|
||||
const BRAND_POSITIVE_LEGALITY_PATTERN = /(德国PM|PM-International|PM公司|PM-FitLine|FitLine).*(不是传销|合法正规的?直销企业|合法直销公司|正规直销企业|正规持牌直销公司|邓白氏AAA\+)|((不是传销|合法正规的?直销企业|合法直销公司|正规直销企业|正规持牌直销公司|邓白氏AAA\+).*(德国PM|PM-International|PM公司|PM-FitLine|FitLine))/i;
|
||||
|
||||
const THINKING_PATTERN = /^(首轮对话|用户想|用户问|应该回复|需要列举|可列举|突出特色|引导进一步|引导用户|让用户|回复后询问|语气要|用温和|需热情|需简洁|需专业)/;
|
||||
|
||||
function sanitizeAssistantText(text) {
|
||||
if (!text) return text;
|
||||
if (BRAND_POSITIVE_LEGALITY_PATTERN.test(String(text || '').replace(/\s+/g, ' '))) {
|
||||
return text;
|
||||
}
|
||||
if (BRAND_HARMFUL_PATTERN.test(text)) {
|
||||
console.warn(`[NativeVoice][SafeGuard] blocked harmful content: ${JSON.stringify(text.slice(0, 200))}`);
|
||||
return BRAND_SAFE_REPLY;
|
||||
@@ -143,6 +147,7 @@ function persistUserSpeech(session, text) {
|
||||
session.lastPersistedUserText = cleanText;
|
||||
session.lastPersistedUserAt = now;
|
||||
session.latestUserText = cleanText;
|
||||
session.latestUserTurnSeq = (session.latestUserTurnSeq || 0) + 1;
|
||||
resetIdleTimer(session);
|
||||
db.addMessage(session.sessionId, 'user', cleanText, 'voice_asr').catch((e) => console.warn('[NativeVoice][DB] add user failed:', e.message));
|
||||
sendJson(session.client, {
|
||||
@@ -379,20 +384,23 @@ function suppressUpstreamReply(session, durationMs) {
|
||||
}, Math.max(300, session.suppressUpstreamUntil - Date.now()));
|
||||
}
|
||||
|
||||
async function processReply(session, text) {
|
||||
async function processReply(session, text, turnSeq = session.latestUserTurnSeq || 0) {
|
||||
const cleanText = (text || '').trim();
|
||||
if (!cleanText) return;
|
||||
if (session.processingReply) {
|
||||
session.queuedUserText = cleanText;
|
||||
session.queuedUserTurnSeq = turnSeq;
|
||||
console.log(`[NativeVoice] processReply queued(busy) session=${session.sessionId} text=${JSON.stringify(cleanText.slice(0, 80))}`);
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
if (session.directSpeakUntil && now < session.directSpeakUntil) {
|
||||
session.queuedUserText = cleanText;
|
||||
session.queuedUserTurnSeq = turnSeq;
|
||||
console.log(`[NativeVoice] processReply queued(speaking) session=${session.sessionId} waitMs=${session.directSpeakUntil - now} text=${JSON.stringify(cleanText.slice(0, 80))}`);
|
||||
return;
|
||||
}
|
||||
const activeTurnSeq = turnSeq || session.latestUserTurnSeq || 0;
|
||||
session.processingReply = true;
|
||||
sendJson(session.client, { type: 'assistant_pending', active: true });
|
||||
const isKnowledgeCandidate = shouldForceKnowledgeRoute(cleanText);
|
||||
@@ -404,6 +412,11 @@ async function processReply(session, text) {
|
||||
console.log(`[NativeVoice] processReply start session=${session.sessionId} text=${JSON.stringify(cleanText.slice(0, 120))} blocked=${session.blockUpstreamAudio} kbCandidate=${isKnowledgeCandidate}`);
|
||||
try {
|
||||
const { delivery, speechText, ragItems, source, toolName, routeDecision, responseMeta } = await resolveReply(session.sessionId, session, cleanText);
|
||||
if (activeTurnSeq !== (session.latestUserTurnSeq || 0)) {
|
||||
console.log(`[NativeVoice] stale reply ignored session=${session.sessionId} activeTurn=${activeTurnSeq} latestTurn=${session.latestUserTurnSeq || 0}`);
|
||||
clearUpstreamSuppression(session);
|
||||
return;
|
||||
}
|
||||
if (delivery === 'upstream_chat') {
|
||||
if (isKnowledgeCandidate) {
|
||||
console.log(`[NativeVoice] processReply kb-nohit retrigger session=${session.sessionId}`);
|
||||
@@ -462,23 +475,27 @@ async function processReply(session, text) {
|
||||
sendJson(session.client, { type: 'assistant_pending', active: false });
|
||||
}
|
||||
const pending = session.queuedUserText;
|
||||
const pendingTurnSeq = session.queuedUserTurnSeq || 0;
|
||||
session.queuedUserText = '';
|
||||
if (pending && pending !== cleanText && (!session.directSpeakUntil || Date.now() >= session.directSpeakUntil)) {
|
||||
session.queuedUserTurnSeq = 0;
|
||||
if (pending && pendingTurnSeq && pendingTurnSeq !== activeTurnSeq && (!session.directSpeakUntil || Date.now() >= session.directSpeakUntil)) {
|
||||
setTimeout(() => {
|
||||
session.blockUpstreamAudio = true;
|
||||
processReply(session, pending).catch((err) => {
|
||||
processReply(session, pending, pendingTurnSeq).catch((err) => {
|
||||
console.error('[NativeVoice] queued processReply failed:', err.message);
|
||||
});
|
||||
}, 200);
|
||||
} else if (pending && pending !== cleanText) {
|
||||
} else if (pending && pendingTurnSeq && pendingTurnSeq !== activeTurnSeq) {
|
||||
const waitMs = Math.max(200, session.directSpeakUntil - Date.now() + 200);
|
||||
clearTimeout(session.queuedReplyTimer);
|
||||
session.queuedReplyTimer = setTimeout(() => {
|
||||
session.queuedReplyTimer = null;
|
||||
const queuedText = session.queuedUserText || pending;
|
||||
const queuedTurnSeq = session.queuedUserTurnSeq || pendingTurnSeq;
|
||||
session.queuedUserText = '';
|
||||
session.queuedUserTurnSeq = 0;
|
||||
session.blockUpstreamAudio = true;
|
||||
processReply(session, queuedText).catch((err) => {
|
||||
processReply(session, queuedText, queuedTurnSeq).catch((err) => {
|
||||
console.error('[NativeVoice] delayed queued processReply failed:', err.message);
|
||||
});
|
||||
}, waitMs);
|
||||
@@ -496,7 +513,6 @@ function handleUpstreamMessage(session, data) {
|
||||
}
|
||||
|
||||
if (message.type === MsgType.AUDIO_ONLY_SERVER) {
|
||||
// blockUpstreamAudio 只阻断 S2S default 音频,不阻断我们注入的 chat_tts_text 音频
|
||||
const isDefaultTts = !session.currentTtsType || session.currentTtsType === 'default';
|
||||
const isSuppressingUpstreamAudio = (session.suppressUpstreamUntil || 0) > Date.now() && isDefaultTts;
|
||||
if ((session.blockUpstreamAudio && isDefaultTts) || isSuppressingUpstreamAudio) {
|
||||
@@ -544,10 +560,15 @@ function handleUpstreamMessage(session, data) {
|
||||
clearTimeout(session.greetingAckTimer);
|
||||
session.greetingAckTimer = null;
|
||||
}
|
||||
// 不再在此处清除 blockUpstreamAudio — 音频处理器已通过 ttsType 区分,
|
||||
// 允许 chat_tts_text 音频通过,同时保持对 S2S default 响应的阻断
|
||||
if (session.blockUpstreamAudio && payload?.tts_type && payload.tts_type !== 'default') {
|
||||
console.log(`[NativeVoice] non-default tts=${payload.tts_type} started, audio passthrough via ttsType check session=${session.sessionId}`);
|
||||
if (session.blockUpstreamAudio && payload?.tts_type === 'external_rag') {
|
||||
session.blockUpstreamAudio = false;
|
||||
session.suppressUpstreamUntil = 0;
|
||||
clearTimeout(session.suppressReplyTimer);
|
||||
session.suppressReplyTimer = null;
|
||||
session.discardNextAssistantResponse = false;
|
||||
console.log(`[NativeVoice] unblock for external_rag tts session=${session.sessionId}`);
|
||||
} else if (session.blockUpstreamAudio && payload?.tts_type === 'chat_tts_text') {
|
||||
console.log(`[NativeVoice] chat_tts_text started, keeping block for S2S default response session=${session.sessionId}`);
|
||||
}
|
||||
console.log(`[NativeVoice] upstream tts_event session=${session.sessionId} ttsType=${payload?.tts_type || ''}`);
|
||||
sendJson(session.client, { type: 'tts_event', payload });
|
||||
@@ -783,10 +804,11 @@ function attachClientHandlers(session) {
|
||||
}
|
||||
|
||||
if (parsed.type === 'text' && parsed.text) {
|
||||
persistUserSpeech(session, parsed.text);
|
||||
processReply(session, parsed.text).catch((error) => {
|
||||
console.error('[NativeVoice] text processReply failed:', error.message);
|
||||
});
|
||||
if (persistUserSpeech(session, parsed.text)) {
|
||||
processReply(session, parsed.text, session.latestUserTurnSeq).catch((error) => {
|
||||
console.error('[NativeVoice] text processReply failed:', error.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -855,7 +877,9 @@ function createSession(client, sessionId) {
|
||||
upstreamReady: false,
|
||||
isSendingChatTTSText: false,
|
||||
latestUserText: '',
|
||||
latestUserTurnSeq: 0,
|
||||
queuedUserText: '',
|
||||
queuedUserTurnSeq: 0,
|
||||
processingReply: false,
|
||||
blockUpstreamAudio: false,
|
||||
directSpeakUntil: 0,
|
||||
|
||||
Reference in New Issue
Block a user