fix: 品牌保护+知识库全量覆盖 - 6层防御解决传销问题 + 30+产品关键词补全
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user