fix: 品牌保护+知识库全量覆盖 - 6层防御解决传销问题 + 30+产品关键词补全

This commit is contained in:
User
2026-03-17 11:00:09 +08:00
parent f97dd7e3d5
commit 0560db1048
46 changed files with 1948 additions and 120 deletions

View File

@@ -11,28 +11,19 @@ export function useNativeVoiceChat() {
const [duration, setDuration] = useState(0);
const sessionRef = useRef(null);
const timerRef = useRef(null);
const greetingUtteranceRef = useRef(null);
const greetingFallbackTimerRef = useRef(null);
const greetingAudioDetectedRef = useRef(false);
const stopGreeting = useCallback(() => {
if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
window.speechSynthesis.cancel();
const clearGreetingFallback = useCallback(() => {
if (greetingFallbackTimerRef.current) {
clearTimeout(greetingFallbackTimerRef.current);
greetingFallbackTimerRef.current = null;
}
greetingUtteranceRef.current = null;
}, []);
const playGreeting = useCallback((text) => {
const greetingText = String(text || '').trim();
if (!greetingText || typeof window === 'undefined' || !('speechSynthesis' in window) || typeof window.SpeechSynthesisUtterance === 'undefined') {
return;
}
stopGreeting();
const utterance = new window.SpeechSynthesisUtterance(greetingText);
utterance.lang = 'zh-CN';
utterance.rate = 1;
utterance.pitch = 1;
greetingUtteranceRef.current = utterance;
window.speechSynthesis.speak(utterance);
}, [stopGreeting]);
const stopGreeting = useCallback(() => {
clearGreetingFallback();
}, [clearGreetingFallback]);
useEffect(() => {
nativeVoiceService.on('onSubtitle', (subtitle) => {
@@ -45,21 +36,60 @@ export function useNativeVoiceChat() {
const finals = prev.filter((s) => s.isFinal);
return [...finals, subtitle];
});
if (subtitle?.role === 'assistant' && subtitle?.isFinal && /^greeting_/.test(String(subtitle.sequence || ''))) {
clearGreetingFallback();
greetingAudioDetectedRef.current = false;
greetingFallbackTimerRef.current = setTimeout(() => {
if (!greetingAudioDetectedRef.current) {
nativeVoiceService.requestGreetingReplay();
}
greetingFallbackTimerRef.current = null;
}, 4000);
}
});
nativeVoiceService.on('onConnectionStateChange', setConnectionState);
nativeVoiceService.on('onError', (err) => setError(err?.message || 'Native voice error'));
nativeVoiceService.on('onDiagnostic', (diag) => {
if (!diag) return;
if (diag.type === 'audio_chunk') {
greetingAudioDetectedRef.current = true;
clearGreetingFallback();
return;
}
if (diag.type === 'ws_message' && diag.payload?.type === 'tts_event' && diag.payload?.payload?.tts_type === 'chat_tts_text') {
greetingAudioDetectedRef.current = true;
clearGreetingFallback();
}
});
nativeVoiceService.on('onIdleTimeout', (timeout) => {
const mins = Math.round((timeout || 300000) / 60000);
console.log(`[useNativeVoiceChat] Idle timeout (${mins}min), auto disconnecting`);
setError(`通话已空闲${mins}分钟,已自动退出`);
nativeVoiceService.disconnect();
setIsActive(false);
setIsMuted(false);
setConnectionState('disconnected');
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
sessionRef.current = null;
});
return () => {
stopGreeting();
nativeVoiceService.disconnect();
if (timerRef.current) clearInterval(timerRef.current);
};
}, [stopGreeting]);
}, [clearGreetingFallback, stopGreeting]);
const start = useCallback(async (options = {}) => {
setError(null);
setIsConnecting(true);
greetingAudioDetectedRef.current = false;
clearGreetingFallback();
stopGreeting();
try {
const userId = `user_${Date.now().toString(36)}`;
@@ -74,12 +104,12 @@ export function useNativeVoiceChat() {
speakingStyle: options.speakingStyle,
modelVersion: options.modelVersion,
speaker: options.speaker,
greetingText: options.greetingText,
});
setIsActive(true);
setSubtitles([]);
setDuration(0);
playGreeting(options.greetingText);
timerRef.current = setInterval(() => {
setDuration((d) => d + 1);
}, 1000);
@@ -93,7 +123,7 @@ export function useNativeVoiceChat() {
} finally {
setIsConnecting(false);
}
}, []);
}, [clearGreetingFallback, stopGreeting]);
const stop = useCallback(async () => {
let result = { sessionId: null, subtitles: [] };

View File

@@ -18,6 +18,7 @@ class NativeVoiceService {
onError: null,
onAssistantPending: null,
onDiagnostic: null,
onIdleTimeout: null,
};
}
@@ -76,7 +77,7 @@ class NativeVoiceService {
}
}
async connect({ sessionId, userId, botName, systemRole, speakingStyle, modelVersion, speaker }) {
async connect({ sessionId, userId, botName, systemRole, speakingStyle, modelVersion, speaker, greetingText }) {
await this.disconnect();
const wsUrl = this.resolveWebSocketUrl(sessionId, userId);
this.emitConnectionState('connecting');
@@ -86,6 +87,22 @@ class NativeVoiceService {
}
this.playbackTime = this.playbackContext.currentTime;
// 并行: 同时预获取麦克风和建立WS连接节省500ms+
const micPromise = navigator.mediaDevices.getUserMedia({
audio: {
channelCount: 1,
noiseSuppression: true,
echoCancellation: true,
autoGainControl: true,
},
video: false,
}).catch((err) => {
console.warn('[NativeVoice] Pre-fetch getUserMedia failed:', err.message);
return null;
});
const CONNECTION_TIMEOUT_MS = 12000;
await new Promise((resolve, reject) => {
this.readyResolver = resolve;
this.readyRejector = reject;
@@ -93,6 +110,18 @@ class NativeVoiceService {
ws.binaryType = 'arraybuffer';
this.ws = ws;
// 超时兜底:避免无限等待
const timeoutId = setTimeout(() => {
if (this.readyResolver) {
console.warn(`[NativeVoice] Connection timeout (${CONNECTION_TIMEOUT_MS}ms), forcing ready`);
this.readyResolver();
this.readyResolver = null;
this.readyRejector = null;
}
}, CONNECTION_TIMEOUT_MS);
const clearTimeoutOnSettle = () => clearTimeout(timeoutId);
ws.onopen = () => {
this.emitConnectionState('connected');
ws.send(JSON.stringify({
@@ -104,10 +133,12 @@ class NativeVoiceService {
speakingStyle,
modelVersion,
speaker,
greetingText,
}));
};
ws.onerror = () => {
clearTimeoutOnSettle();
const error = new Error('WebSocket connection failed');
this.callbacks.onError?.(error);
this.readyRejector?.(error);
@@ -117,6 +148,7 @@ class NativeVoiceService {
};
ws.onclose = () => {
clearTimeoutOnSettle();
this.emitConnectionState('disconnected');
if (this.readyRejector) {
this.readyRejector(new Error('WebSocket closed before ready'));
@@ -127,14 +159,20 @@ class NativeVoiceService {
ws.onmessage = (event) => {
if (typeof event.data === 'string') {
this.handleJsonMessage(event.data);
const peek = event.data;
if (peek.includes('"ready"')) {
clearTimeoutOnSettle();
}
this.handleJsonMessage(peek);
return;
}
this.handleAudioMessage(event.data);
};
});
await this.startCapture();
// 使用预获取的mediaStream已并行获取避免重复申请
const preFetchedStream = await micPromise;
await this.startCapture(preFetchedStream);
}
handleJsonMessage(raw) {
@@ -164,6 +202,14 @@ class NativeVoiceService {
this.callbacks.onAssistantPending?.(!!msg.active);
return;
}
if (msg.type === 'idle_timeout') {
this.callbacks.onIdleTimeout?.(msg.timeout || 300000);
return;
}
if (msg.type === 'upstream_closed') {
this.callbacks.onError?.(new Error('语音服务已断开,请重新开始通话'));
return;
}
if (msg.type === 'error') {
this.callbacks.onError?.(new Error(msg.error || 'native voice error'));
return;
@@ -206,8 +252,8 @@ class NativeVoiceService {
this.emitDiagnostic('audio_chunk', { samples: pcm16.length, duration: audioBuffer.duration });
}
async startCapture() {
this.mediaStream = await navigator.mediaDevices.getUserMedia({
async startCapture(preFetchedStream) {
this.mediaStream = preFetchedStream || await navigator.mediaDevices.getUserMedia({
audio: {
channelCount: 1,
noiseSuppression: true,
@@ -274,6 +320,13 @@ class NativeVoiceService {
});
}
requestGreetingReplay() {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'replay_greeting' }));
this.emitDiagnostic('replay_greeting', { sent: true });
}
}
async disconnect() {
if (this.captureProcessor) {
this.captureProcessor.disconnect();