This commit is contained in:
rainv123
2026-01-28 15:24:02 +08:00
18 changed files with 348 additions and 369 deletions
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
file: Dockerfile-server-base file: Dockerfile-server-base
push: true push: true
tags: ghcr.io/${{ github.repository }}:server-base tags: ghcr.io/${{ github.repository }}:server-base
platforms: linux/amd64 platforms: linux/amd64,linux/arm64
cache-from: type=gha,scope=server-base cache-from: type=gha,scope=server-base
cache-to: type=gha,mode=max,scope=server-base cache-to: type=gha,mode=max,scope=server-base
build-args: | build-args: |
+2 -2
View File
@@ -66,7 +66,7 @@ jobs:
push: true push: true
tags: | tags: |
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:server_{1},ghcr.io/{0}:server_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:server_latest', github.repository) }} ${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:server_{1},ghcr.io/{0}:server_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:server_latest', github.repository) }}
platforms: linux/amd64 platforms: linux/amd64,linux/arm64
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
build-args: | build-args: |
@@ -81,7 +81,7 @@ jobs:
push: true push: true
tags: | tags: |
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:web_{1},ghcr.io/{0}:web_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:web_latest', github.repository) }} ${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:web_{1},ghcr.io/{0}:web_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:web_latest', github.repository) }}
platforms: linux/amd64 platforms: linux/amd64,linux/arm64
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
build-args: | build-args: |
+7 -7
View File
@@ -1,5 +1,5 @@
# 第一阶段:构建Vue前端 # 第一阶段:构建Vue前端
FROM node:18 as web-builder FROM node:18 AS web-builder
WORKDIR /app WORKDIR /app
COPY main/manager-web/package*.json ./ COPY main/manager-web/package*.json ./
RUN npm install RUN npm install
@@ -7,7 +7,7 @@ COPY main/manager-web .
RUN npm run build RUN npm run build
# 第二阶段:构建Java后端 # 第二阶段:构建Java后端
FROM maven:3.9.4-eclipse-temurin-21 as api-builder FROM maven:3.9.4-eclipse-temurin-21 AS api-builder
WORKDIR /app WORKDIR /app
COPY main/manager-api/pom.xml . COPY main/manager-api/pom.xml .
COPY main/manager-api/src ./src COPY main/manager-api/src ./src
@@ -18,18 +18,18 @@ FROM bellsoft/liberica-runtime-container:jre-21-glibc
# 安装Nginx和字体库 # 安装Nginx和字体库
RUN apk update && \ RUN apk update && \
apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \ apk add --no-cache --no-scripts \
nginx \ nginx \
bash \ bash \
fontconfig \ fontconfig \
ttf-dejavu \ ttf-dejavu \
msttcorefonts-installer \
&& ACCEPT_EULA=Y apk add --no-cache msttcorefonts-installer \
&& fc-cache -f -v \
&& rm -rf /var/cache/apk/* && rm -rf /var/cache/apk/*
# 复制项目自带的中文字体
COPY main/manager-web/public/generator/static/fonts/*.ttf /usr/share/fonts/
# 更新字体缓存 # 更新字体缓存
RUN (printf 'YES\n' | update-ms-fonts || true) && fc-cache -f -v RUN fc-cache -f -v
# 配置Nginx # 配置Nginx
COPY docs/docker/nginx.conf /etc/nginx/nginx.conf COPY docs/docker/nginx.conf /etc/nginx/nginx.conf
@@ -304,7 +304,7 @@ public interface Constant {
/** /**
* 版本号 * 版本号
*/ */
public static final String VERSION = "0.8.11"; public static final String VERSION = "0.9.1";
/** /**
* 无效固件URL * 无效固件URL
@@ -235,7 +235,7 @@ function showAbout() {
title: t('settings.aboutApp', { appName: import.meta.env.VITE_APP_TITLE }), title: t('settings.aboutApp', { appName: import.meta.env.VITE_APP_TITLE }),
content: t('settings.aboutContent', { content: t('settings.aboutContent', {
appName: import.meta.env.VITE_APP_TITLE, appName: import.meta.env.VITE_APP_TITLE,
version: '0.8.11' version: '0.9.1'
}), }),
showCancel: false, showCancel: false,
confirmText: t('common.confirm'), confirmText: t('common.confirm'),
+1 -1
View File
@@ -5,7 +5,7 @@ from config.config_loader import load_config
from config.settings import check_config_file from config.settings import check_config_file
from datetime import datetime from datetime import datetime
SERVER_VERSION = "0.8.11" SERVER_VERSION = "0.9.1"
_logger_initialized = False _logger_initialized = False
@@ -141,7 +141,7 @@ class TTSProviderBase(ABC):
logger.bind(tag=TAG).error( logger.bind(tag=TAG).error(
f"语音生成失败: {text},请检查网络或服务是否正常" f"语音生成失败: {text},请检查网络或服务是否正常"
) )
self.tts_audio_queue.put((SentenceType.FIRST, None, text)) self.tts_audio_queue.put((SentenceType.FIRST, None, text))
self._process_audio_file_stream(tmp_file, callback=opus_handler) self._process_audio_file_stream(tmp_file, callback=opus_handler)
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"Failed to generate TTS file: {e}") logger.bind(tag=TAG).error(f"Failed to generate TTS file: {e}")
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+35 -21
View File
@@ -1,9 +1,10 @@
// 主应用入口 // 主应用入口
import { log } from './utils/logger.js'; import { checkOpusLoaded, initOpusEncoder } from './core/audio/opus-codec.js?v=0127';
import { checkOpusLoaded, initOpusEncoder } from './core/audio/opus-codec.js'; import { getAudioPlayer } from './core/audio/player.js?v=0127';
import { uiController } from './ui/controller.js'; import { checkMicrophoneAvailability, isHttpNonLocalhost } from './core/audio/recorder.js?v=0127';
import { getAudioPlayer } from './core/audio/player.js'; import { initMcpTools } from './core/mcp/tools.js?v=0127';
import { initMcpTools } from './core/mcp/tools.js'; import { uiController } from './ui/controller.js?v=0127';
import { log } from './utils/logger.js?v=0127';
// 应用类 // 应用类
class App { class App {
@@ -16,30 +17,24 @@ class App {
// 初始化应用 // 初始化应用
async init() { async init() {
log('正在初始化应用...', 'info'); log('正在初始化应用...', 'info');
// 初始化UI控制器 // 初始化UI控制器
this.uiController = uiController; this.uiController = uiController;
this.uiController.init(); this.uiController.init();
// 检查Opus库 // 检查Opus库
checkOpusLoaded(); checkOpusLoaded();
// 初始化Opus编码器 // 初始化Opus编码器
initOpusEncoder(); initOpusEncoder();
// 初始化音频播放器 // 初始化音频播放器
this.audioPlayer = getAudioPlayer(); this.audioPlayer = getAudioPlayer();
await this.audioPlayer.start(); await this.audioPlayer.start();
// 初始化MCP工具 // 初始化MCP工具
initMcpTools(); initMcpTools();
// 检查麦克风可用性
await this.checkMicrophoneAvailability();
// 初始化Live2D // 初始化Live2D
await this.initLive2D(); await this.initLive2D();
// 关闭加载loading // 关闭加载loading
this.setModelLoadingStatus(false); this.setModelLoadingStatus(false);
log('应用初始化完成', 'success'); log('应用初始化完成', 'success');
} }
@@ -50,21 +45,17 @@ class App {
if (typeof window.Live2DManager === 'undefined') { if (typeof window.Live2DManager === 'undefined') {
throw new Error('Live2DManager未加载,请检查脚本引入顺序'); throw new Error('Live2DManager未加载,请检查脚本引入顺序');
} }
this.live2dManager = new window.Live2DManager(); this.live2dManager = new window.Live2DManager();
await this.live2dManager.initializeLive2D(); await this.live2dManager.initializeLive2D();
// 更新UI状态 // 更新UI状态
const live2dStatus = document.getElementById('live2dStatus'); const live2dStatus = document.getElementById('live2dStatus');
if (live2dStatus) { if (live2dStatus) {
live2dStatus.textContent = '● 已加载'; live2dStatus.textContent = '● 已加载';
live2dStatus.className = 'status loaded'; live2dStatus.className = 'status loaded';
} }
log('Live2D初始化完成', 'success'); log('Live2D初始化完成', 'success');
} catch (error) { } catch (error) {
log(`Live2D初始化失败: ${error.message}`, 'error'); log(`Live2D初始化失败: ${error.message}`, 'error');
// 更新UI状态 // 更新UI状态
const live2dStatus = document.getElementById('live2dStatus'); const live2dStatus = document.getElementById('live2dStatus');
if (live2dStatus) { if (live2dStatus) {
@@ -81,18 +72,41 @@ class App {
modelLoading.style.display = isLoading ? 'flex' : 'none'; modelLoading.style.display = isLoading ? 'flex' : 'none';
} }
} }
/**
* 检查麦克风可用性
* 在应用初始化时调用,检查麦克风是否可用并更新UI状态
*/
async checkMicrophoneAvailability() {
try {
const isAvailable = await checkMicrophoneAvailability();
const isHttp = isHttpNonLocalhost();
// 保存可用性状态到全局变量
window.microphoneAvailable = isAvailable;
window.isHttpNonLocalhost = isHttp;
// 更新UI
if (this.uiController) {
this.uiController.updateMicrophoneAvailability(isAvailable, isHttp);
}
log(`麦克风可用性检查完成: ${isAvailable ? '可用' : '不可用'}`, isAvailable ? 'success' : 'warning');
} catch (error) {
log(`检查麦克风可用性失败: ${error.message}`, 'error');
// 默认设置为不可用
window.microphoneAvailable = false;
window.isHttpNonLocalhost = isHttpNonLocalhost();
if (this.uiController) {
this.uiController.updateMicrophoneAvailability(false, window.isHttpNonLocalhost);
}
}
}
} }
// 创建并启动应用 // 创建并启动应用
const app = new App(); const app = new App();
// 将应用实例暴露到全局,供其他模块访问 // 将应用实例暴露到全局,供其他模块访问
window.chatApp = app; window.chatApp = app;
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// 初始化应用 // 初始化应用
app.init(); app.init();
}); });
export default app; export default app;
@@ -1,4 +1,4 @@
import { log } from '../../utils/logger.js'; import { log } from '../../utils/logger.js?v=0127';
// 检查Opus库是否已加载 // 检查Opus库是否已加载
@@ -1,7 +1,7 @@
// 音频播放模块 // 音频播放模块
import { log } from '../../utils/logger.js'; import BlockingQueue from '../../utils/blocking-queue.js?v=0127';
import BlockingQueue from '../../utils/blocking-queue.js'; import { log } from '../../utils/logger.js?v=0127';
import { createStreamingContext } from './stream-context.js'; import { createStreamingContext } from './stream-context.js?v=0127';
// 音频播放器类 // 音频播放器类
export class AudioPlayer { export class AudioPlayer {
@@ -1,9 +1,9 @@
// 音频录制模块 // Audio recording module
import { log } from '../../utils/logger.js'; import { log } from '../../utils/logger.js?v=0127';
import { initOpusEncoder } from './opus-codec.js'; import { initOpusEncoder } from './opus-codec.js?v=0127';
import { getAudioPlayer } from './player.js'; import { getAudioPlayer } from './player.js?v=0127';
// 音频录制器类 // Audio recorder class
export class AudioRecorder { export class AudioRecorder {
constructor() { constructor() {
this.isRecording = false; this.isRecording = false;
@@ -19,25 +19,23 @@ export class AudioRecorder {
this.visualizationRequest = null; this.visualizationRequest = null;
this.recordingTimer = null; this.recordingTimer = null;
this.websocket = null; this.websocket = null;
// Callback functions
// 回调函数
this.onRecordingStart = null; this.onRecordingStart = null;
this.onRecordingStop = null; this.onRecordingStop = null;
this.onVisualizerUpdate = null; this.onVisualizerUpdate = null;
} }
// 设置WebSocket实例 // Set WebSocket instance
setWebSocket(ws) { setWebSocket(ws) {
this.websocket = ws; this.websocket = ws;
} }
// 获取AudioContext实例 // Get AudioContext instance
getAudioContext() { getAudioContext() {
const audioPlayer = getAudioPlayer(); return getAudioPlayer().getAudioContext();
return audioPlayer.getAudioContext();
} }
// 初始化编码器 // Initialize encoder
initEncoder() { initEncoder() {
if (!this.opusEncoder) { if (!this.opusEncoder) {
this.opusEncoder = initOpusEncoder(); this.opusEncoder = initOpusEncoder();
@@ -45,7 +43,7 @@ export class AudioRecorder {
return this.opusEncoder; return this.opusEncoder;
} }
// PCM处理器代码 // PCM processor code
getAudioProcessorCode() { getAudioProcessorCode() {
return ` return `
class AudioRecorderProcessor extends AudioWorkletProcessor { class AudioRecorderProcessor extends AudioWorkletProcessor {
@@ -56,166 +54,132 @@ export class AudioRecorder {
this.buffer = new Int16Array(this.frameSize); this.buffer = new Int16Array(this.frameSize);
this.bufferIndex = 0; this.bufferIndex = 0;
this.isRecording = false; this.isRecording = false;
this.port.onmessage = (event) => { this.port.onmessage = (event) => {
if (event.data.command === 'start') { if (event.data.command === 'start') {
this.isRecording = true; this.isRecording = true;
this.port.postMessage({ type: 'status', status: 'started' }); this.port.postMessage({ type: 'status', status: 'started' });
} else if (event.data.command === 'stop') { } else if (event.data.command === 'stop') {
this.isRecording = false; this.isRecording = false;
if (this.bufferIndex > 0) { if (this.bufferIndex > 0) {
const finalBuffer = this.buffer.slice(0, this.bufferIndex); const finalBuffer = this.buffer.slice(0, this.bufferIndex);
this.port.postMessage({ this.port.postMessage({ type: 'buffer', buffer: finalBuffer });
type: 'buffer',
buffer: finalBuffer
});
this.bufferIndex = 0; this.bufferIndex = 0;
} }
this.port.postMessage({ type: 'status', status: 'stopped' }); this.port.postMessage({ type: 'status', status: 'stopped' });
} }
}; };
} }
process(inputs, outputs, parameters) { process(inputs, outputs, parameters) {
if (!this.isRecording) return true; if (!this.isRecording) return true;
const input = inputs[0][0]; const input = inputs[0][0];
if (!input) return true; if (!input) return true;
for (let i = 0; i < input.length; i++) { for (let i = 0; i < input.length; i++) {
if (this.bufferIndex >= this.frameSize) { if (this.bufferIndex >= this.frameSize) {
this.port.postMessage({ this.port.postMessage({ type: 'buffer', buffer: this.buffer.slice(0) });
type: 'buffer',
buffer: this.buffer.slice(0)
});
this.bufferIndex = 0; this.bufferIndex = 0;
} }
this.buffer[this.bufferIndex++] = Math.max(-32768, Math.min(32767, Math.floor(input[i] * 32767))); this.buffer[this.bufferIndex++] = Math.max(-32768, Math.min(32767, Math.floor(input[i] * 32767)));
} }
return true; return true;
} }
} }
registerProcessor('audio-recorder-processor', AudioRecorderProcessor); registerProcessor('audio-recorder-processor', AudioRecorderProcessor);
`; `;
} }
// 创建音频处理器 // Create audio processor
async createAudioProcessor() { async createAudioProcessor() {
this.audioContext = this.getAudioContext(); this.audioContext = this.getAudioContext();
try { try {
if (this.audioContext.audioWorklet) { if (this.audioContext.audioWorklet) {
const blob = new Blob([this.getAudioProcessorCode()], { type: 'application/javascript' }); const blob = new Blob([this.getAudioProcessorCode()], { type: 'application/javascript' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
await this.audioContext.audioWorklet.addModule(url); await this.audioContext.audioWorklet.addModule(url);
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
const audioProcessor = new AudioWorkletNode(this.audioContext, 'audio-recorder-processor'); const audioProcessor = new AudioWorkletNode(this.audioContext, 'audio-recorder-processor');
audioProcessor.port.onmessage = (event) => { audioProcessor.port.onmessage = (event) => {
if (event.data.type === 'buffer') { if (event.data.type === 'buffer') {
this.processPCMBuffer(event.data.buffer); this.processPCMBuffer(event.data.buffer);
} }
}; };
log('使用AudioWorklet处理音频', 'success'); log('使用AudioWorklet处理音频', 'success');
const silent = this.audioContext.createGain(); const silent = this.audioContext.createGain();
silent.gain.value = 0; silent.gain.value = 0;
audioProcessor.connect(silent); audioProcessor.connect(silent);
silent.connect(this.audioContext.destination); silent.connect(this.audioContext.destination);
return { node: audioProcessor, type: 'worklet' }; return { node: audioProcessor, type: 'worklet' };
} else { } else {
log('AudioWorklet不可用,使用ScriptProcessorNode作为回退方案', 'warning'); log('AudioWorklet不可用,使用ScriptProcessorNode作为后备方案', 'warning');
return this.createScriptProcessor(); return this.createScriptProcessor();
} }
} catch (error) { } catch (error) {
log(`创建音频处理器失败: ${error.message},尝试回退方案`, 'error'); log(`创建音频处理器失败: ${error.message},尝试后备方案`, 'error');
return this.createScriptProcessor(); return this.createScriptProcessor();
} }
} }
// 创建ScriptProcessor作为回退 // Create ScriptProcessor as fallback
createScriptProcessor() { createScriptProcessor() {
try { try {
const frameSize = 4096; const frameSize = 4096;
const scriptProcessor = this.audioContext.createScriptProcessor(frameSize, 1, 1); const scriptProcessor = this.audioContext.createScriptProcessor(frameSize, 1, 1);
scriptProcessor.onaudioprocess = (event) => { scriptProcessor.onaudioprocess = (event) => {
if (!this.isRecording) return; if (!this.isRecording) return;
const input = event.inputBuffer.getChannelData(0); const input = event.inputBuffer.getChannelData(0);
const buffer = new Int16Array(input.length); const buffer = new Int16Array(input.length);
for (let i = 0; i < input.length; i++) { for (let i = 0; i < input.length; i++) {
buffer[i] = Math.max(-32768, Math.min(32767, Math.floor(input[i] * 32767))); buffer[i] = Math.max(-32768, Math.min(32767, Math.floor(input[i] * 32767)));
} }
this.processPCMBuffer(buffer); this.processPCMBuffer(buffer);
}; };
const silent = this.audioContext.createGain(); const silent = this.audioContext.createGain();
silent.gain.value = 0; silent.gain.value = 0;
scriptProcessor.connect(silent); scriptProcessor.connect(silent);
silent.connect(this.audioContext.destination); silent.connect(this.audioContext.destination);
log('使用ScriptProcessorNode作为后备方案成功', 'warning');
log('使用ScriptProcessorNode作为回退方案成功', 'warning');
return { node: scriptProcessor, type: 'processor' }; return { node: scriptProcessor, type: 'processor' };
} catch (fallbackError) { } catch (fallbackError) {
log(`回退方案也失败: ${fallbackError.message}`, 'error'); log(`后备方案也失败: ${fallbackError.message}`, 'error');
return null; return null;
} }
} }
// 处理PCM缓冲数据 // Process PCM buffer data
processPCMBuffer(buffer) { processPCMBuffer(buffer) {
if (!this.isRecording) return; if (!this.isRecording) return;
const newBuffer = new Int16Array(this.pcmDataBuffer.length + buffer.length); const newBuffer = new Int16Array(this.pcmDataBuffer.length + buffer.length);
newBuffer.set(this.pcmDataBuffer); newBuffer.set(this.pcmDataBuffer);
newBuffer.set(buffer, this.pcmDataBuffer.length); newBuffer.set(buffer, this.pcmDataBuffer.length);
this.pcmDataBuffer = newBuffer; this.pcmDataBuffer = newBuffer;
const samplesPerFrame = 960; const samplesPerFrame = 960;
while (this.pcmDataBuffer.length >= samplesPerFrame) { while (this.pcmDataBuffer.length >= samplesPerFrame) {
const frameData = this.pcmDataBuffer.slice(0, samplesPerFrame); const frameData = this.pcmDataBuffer.slice(0, samplesPerFrame);
this.pcmDataBuffer = this.pcmDataBuffer.slice(samplesPerFrame); this.pcmDataBuffer = this.pcmDataBuffer.slice(samplesPerFrame);
this.encodeAndSendOpus(frameData); this.encodeAndSendOpus(frameData);
} }
} }
// 编码并发送Opus数据 // Encode and send Opus data
encodeAndSendOpus(pcmData = null) { encodeAndSendOpus(pcmData = null) {
if (!this.opusEncoder) { if (!this.opusEncoder) {
log('Opus编码器未初始化', 'error'); log('Opus编码器未初始化', 'error');
return; return;
} }
try { try {
if (pcmData) { if (pcmData) {
const opusData = this.opusEncoder.encode(pcmData); const opusData = this.opusEncoder.encode(pcmData);
if (opusData && opusData.length > 0) { if (opusData && opusData.length > 0) {
this.audioBuffers.push(opusData.buffer); this.audioBuffers.push(opusData.buffer);
this.totalAudioSize += opusData.length; this.totalAudioSize += opusData.length;
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
try { try {
this.websocket.send(opusData.buffer); this.websocket.send(opusData.buffer);
log(`发送Opus帧,大小:${opusData.length}字节`, 'debug');
} catch (error) { } catch (error) {
log(`WebSocket发送错误: ${error.message}`, 'error'); log(`WebSocket发送错误: ${error.message}`, 'error');
} }
} }
} else { } else {
log('Opus编码失败,有效数据返回', 'error'); log('Opus编码失败,未返回有效数据', 'error');
} }
} else { } else {
if (this.pcmDataBuffer.length > 0) { if (this.pcmDataBuffer.length > 0) {
@@ -235,96 +199,67 @@ export class AudioRecorder {
} }
} }
// 开始录音 // Start recording
async start() { async start() {
if (this.isRecording) return false; if (this.isRecording) return false;
try { try {
// 检查是否有WebSocketHandler实例 // Check if WebSocketHandler instance exists
const { getWebSocketHandler } = await import('../network/websocket.js'); const { getWebSocketHandler } = await import('../network/websocket.js?v=0127');
const wsHandler = getWebSocketHandler(); const wsHandler = getWebSocketHandler();
// If machine is speaking, send abort message
// 如果机器正在说话,发送打断消息
if (wsHandler && wsHandler.isRemoteSpeaking && wsHandler.currentSessionId) { if (wsHandler && wsHandler.isRemoteSpeaking && wsHandler.currentSessionId) {
const abortMessage = { const abortMessage = { session_id: wsHandler.currentSessionId, type: 'abort', reason: 'wake_word_detected' };
session_id: wsHandler.currentSessionId,
type: 'abort',
reason: 'wake_word_detected'
};
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
this.websocket.send(JSON.stringify(abortMessage)); this.websocket.send(JSON.stringify(abortMessage));
log('发送打断消息', 'info'); log('发送中止消息', 'info');
} }
} }
if (!this.initEncoder()) { if (!this.initEncoder()) {
log('无法启动录音: Opus编码器初始化失败', 'error'); log('无法开始录音: Opus编码器初始化失败', 'error');
return false; return false;
} }
log('请至少录制1-2秒音频以确保收集足够的数据', 'info');
log('请至少录制1-2秒钟的音频,确保采集到足够数据', 'info'); const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 16000, channelCount: 1 } });
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
sampleRate: 16000,
channelCount: 1
}
});
this.audioContext = this.getAudioContext(); this.audioContext = this.getAudioContext();
if (this.audioContext.state === 'suspended') { if (this.audioContext.state === 'suspended') {
await this.audioContext.resume(); await this.audioContext.resume();
} }
const processorResult = await this.createAudioProcessor(); const processorResult = await this.createAudioProcessor();
if (!processorResult) { if (!processorResult) {
log('无法创建音频处理器', 'error'); log('无法创建音频处理器', 'error');
return false; return false;
} }
this.audioProcessor = processorResult.node; this.audioProcessor = processorResult.node;
this.audioProcessorType = processorResult.type; this.audioProcessorType = processorResult.type;
this.audioSource = this.audioContext.createMediaStreamSource(stream); this.audioSource = this.audioContext.createMediaStreamSource(stream);
this.analyser = this.audioContext.createAnalyser(); this.analyser = this.audioContext.createAnalyser();
this.analyser.fftSize = 2048; this.analyser.fftSize = 2048;
this.audioSource.connect(this.analyser); this.audioSource.connect(this.analyser);
this.audioSource.connect(this.audioProcessor); this.audioSource.connect(this.audioProcessor);
this.pcmDataBuffer = new Int16Array(); this.pcmDataBuffer = new Int16Array();
this.audioBuffers = []; this.audioBuffers = [];
this.totalAudioSize = 0; this.totalAudioSize = 0;
this.isRecording = true; this.isRecording = true;
if (this.audioProcessorType === 'worklet' && this.audioProcessor.port) { if (this.audioProcessorType === 'worklet' && this.audioProcessor.port) {
this.audioProcessor.port.postMessage({ command: 'start' }); this.audioProcessor.port.postMessage({ command: 'start' });
} }
// Send listening start message
// 发送监听开始消息
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
log(`发送录音开始消息`, 'info'); log(`发送录音开始消息`, 'info');
} else { } else {
log('WebSocket未连接,无法发送开始消息', 'error'); log('WebSocket未连接,无法发送开始消息', 'error');
return false; return false;
} }
// Start visualization
// 开始可视化
if (this.onVisualizerUpdate) { if (this.onVisualizerUpdate) {
const dataArray = new Uint8Array(this.analyser.frequencyBinCount); const dataArray = new Uint8Array(this.analyser.frequencyBinCount);
this.startVisualization(dataArray); this.startVisualization(dataArray);
} }
// Immediately notify recording start, update button state
// 立即通知录音开始,更新按钮状态
if (this.onRecordingStart) { if (this.onRecordingStart) {
this.onRecordingStart(0); this.onRecordingStart(0);
} }
// Start recording timer
// 启动录音计时器
let recordingSeconds = 0; let recordingSeconds = 0;
this.recordingTimer = setInterval(() => { this.recordingTimer = setInterval(() => {
recordingSeconds += 0.1; recordingSeconds += 0.1;
@@ -332,8 +267,7 @@ export class AudioRecorder {
this.onRecordingStart(recordingSeconds); this.onRecordingStart(recordingSeconds);
} }
}, 100); }, 100);
log('已开始PCM直接录音', 'success');
log('开始PCM直接录音', 'success');
return true; return true;
} catch (error) { } catch (error) {
log(`直接录音启动错误: ${error.message}`, 'error'); log(`直接录音启动错误: ${error.message}`, 'error');
@@ -342,15 +276,12 @@ export class AudioRecorder {
} }
} }
// 开始可视化 // Start visualization
startVisualization(dataArray) { startVisualization(dataArray) {
const draw = () => { const draw = () => {
this.visualizationRequest = requestAnimationFrame(() => draw()); this.visualizationRequest = requestAnimationFrame(() => draw());
if (!this.isRecording) return; if (!this.isRecording) return;
this.analyser.getByteFrequencyData(dataArray); this.analyser.getByteFrequencyData(dataArray);
if (this.onVisualizerUpdate) { if (this.onVisualizerUpdate) {
this.onVisualizerUpdate(dataArray); this.onVisualizerUpdate(dataArray);
} }
@@ -358,52 +289,42 @@ export class AudioRecorder {
draw(); draw();
} }
// 停止录音 // Stop recording
stop() { stop() {
if (!this.isRecording) return false; if (!this.isRecording) return false;
try { try {
this.isRecording = false; this.isRecording = false;
if (this.audioProcessor) { if (this.audioProcessor) {
if (this.audioProcessorType === 'worklet' && this.audioProcessor.port) { if (this.audioProcessorType === 'worklet' && this.audioProcessor.port) {
this.audioProcessor.port.postMessage({ command: 'stop' }); this.audioProcessor.port.postMessage({ command: 'stop' });
} }
this.audioProcessor.disconnect(); this.audioProcessor.disconnect();
this.audioProcessor = null; this.audioProcessor = null;
} }
if (this.audioSource) { if (this.audioSource) {
this.audioSource.disconnect(); this.audioSource.disconnect();
this.audioSource = null; this.audioSource = null;
} }
if (this.visualizationRequest) { if (this.visualizationRequest) {
cancelAnimationFrame(this.visualizationRequest); cancelAnimationFrame(this.visualizationRequest);
this.visualizationRequest = null; this.visualizationRequest = null;
} }
if (this.recordingTimer) { if (this.recordingTimer) {
clearInterval(this.recordingTimer); clearInterval(this.recordingTimer);
this.recordingTimer = null; this.recordingTimer = null;
} }
// Encode and send remaining data
// 编码并发送剩余的数据
this.encodeAndSendOpus(); this.encodeAndSendOpus();
// Send end signal
// 发送结束信号
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
const emptyOpusFrame = new Uint8Array(0); const emptyOpusFrame = new Uint8Array(0);
this.websocket.send(emptyOpusFrame); this.websocket.send(emptyOpusFrame);
log('已发送录音停止信号', 'info'); log('已发送录音停止信号', 'info');
} }
if (this.onRecordingStop) { if (this.onRecordingStop) {
this.onRecordingStop(); this.onRecordingStop();
} }
log('已停止PCM直接录音', 'success');
log('停止PCM直接录音', 'success');
return true; return true;
} catch (error) { } catch (error) {
log(`直接录音停止错误: ${error.message}`, 'error'); log(`直接录音停止错误: ${error.message}`, 'error');
@@ -411,13 +332,13 @@ export class AudioRecorder {
} }
} }
// 获取分析器 // Get analyser
getAnalyser() { getAnalyser() {
return this.analyser; return this.analyser;
} }
} }
// 创建单例 // Create singleton instance
let audioRecorderInstance = null; let audioRecorderInstance = null;
export function getAudioRecorder() { export function getAudioRecorder() {
@@ -426,3 +347,49 @@ export function getAudioRecorder() {
} }
return audioRecorderInstance; return audioRecorderInstance;
} }
/**
* Check if microphone is available
* @returns {Promise<boolean>} Returns true if available, false if not available
*/
export async function checkMicrophoneAvailability() {
// Check if browser supports getUserMedia API
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
log('浏览器不支持getUserMedia API', 'warning');
return false;
}
try {
// Try to access microphone
const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 16000, channelCount: 1 } });
// Immediately stop all tracks to release microphone
stream.getTracks().forEach(track => track.stop());
log('麦克风可用性检查成功', 'success');
return true;
} catch (error) {
log(`麦克风不可用: ${error.message}`, 'warning');
return false;
}
}
/**
* Check if it is HTTP non-localhost access
* @returns {boolean} Returns true if it is HTTP non-localhost access
*/
export function isHttpNonLocalhost() {
const protocol = window.location.protocol;
const hostname = window.location.hostname;
// Check if it is HTTP protocol
if (protocol !== 'http:') {
return false;
}
// localhost and 127.0.0.1 can use microphone
if (hostname === 'localhost' || hostname === '127.0.0.1') {
return false;
}
// Private IP addresses can also use microphone (browser allows)
if (hostname.startsWith('192.168.') || hostname.startsWith('10.') || hostname.startsWith('172.')) {
return false;
}
// Other HTTP access is considered non-localhost
return true;
}
@@ -1,5 +1,5 @@
import BlockingQueue from '../../utils/blocking-queue.js'; import BlockingQueue from '../../utils/blocking-queue.js?v=0127';
import { log } from '../../utils/logger.js'; import { log } from '../../utils/logger.js?v=0127';
// 音频流播放上下文类 // 音频流播放上下文类
export class StreamingContext { export class StreamingContext {
+23 -76
View File
@@ -1,4 +1,4 @@
import { log } from '../../utils/logger.js'; import { log } from '../../utils/logger.js?v=0127';
// ========================================== // ==========================================
// MCP 工具管理逻辑 // MCP 工具管理逻辑
@@ -24,7 +24,6 @@ export function setWebSocket(ws) {
export async function initMcpTools() { export async function initMcpTools() {
// 加载默认工具数据 // 加载默认工具数据
const defaultMcpTools = await fetch("js/config/default-mcp-tools.json").then(res => res.json()); const defaultMcpTools = await fetch("js/config/default-mcp-tools.json").then(res => res.json());
const savedTools = localStorage.getItem('mcpTools'); const savedTools = localStorage.getItem('mcpTools');
if (savedTools) { if (savedTools) {
try { try {
@@ -36,9 +35,11 @@ export async function initMcpTools() {
} else { } else {
mcpTools = [...defaultMcpTools]; mcpTools = [...defaultMcpTools];
} }
renderMcpTools(); renderMcpTools();
setupMcpEventListeners(); // Only setup event listeners if DOM elements exist
if (document.getElementById('toggleMcpTools')) {
setupMcpEventListeners();
}
} }
/** /**
@@ -47,21 +48,20 @@ export async function initMcpTools() {
function renderMcpTools() { function renderMcpTools() {
const container = document.getElementById('mcpToolsContainer'); const container = document.getElementById('mcpToolsContainer');
const countSpan = document.getElementById('mcpToolsCount'); const countSpan = document.getElementById('mcpToolsCount');
if (!container) {
return; // Container not found, skip rendering
}
if (countSpan) { if (countSpan) {
countSpan.textContent = `${mcpTools.length} 个工具`; countSpan.textContent = `${mcpTools.length} 个工具`;
} }
if (mcpTools.length === 0) { if (mcpTools.length === 0) {
container.innerHTML = '<div style="text-align: center; padding: 30px; color: #999;">暂无工具,点击下方按钮添加新工具</div>'; container.innerHTML = '<div style="text-align: center; padding: 30px; color: #999;">暂无工具,点击下方按钮添加新工具</div>';
return; return;
} }
container.innerHTML = mcpTools.map((tool, index) => { container.innerHTML = mcpTools.map((tool, index) => {
const paramCount = tool.inputSchema.properties ? Object.keys(tool.inputSchema.properties).length : 0; const paramCount = tool.inputSchema.properties ? Object.keys(tool.inputSchema.properties).length : 0;
const requiredCount = tool.inputSchema.required ? tool.inputSchema.required.length : 0; const requiredCount = tool.inputSchema.required ? tool.inputSchema.required.length : 0;
const hasMockResponse = tool.mockResponse && Object.keys(tool.mockResponse).length > 0; const hasMockResponse = tool.mockResponse && Object.keys(tool.mockResponse).length > 0;
return ` return `
<div class="mcp-tool-card"> <div class="mcp-tool-card">
<div class="mcp-tool-header"> <div class="mcp-tool-header">
@@ -96,12 +96,13 @@ function renderMcpTools() {
*/ */
function renderMcpProperties() { function renderMcpProperties() {
const container = document.getElementById('mcpPropertiesContainer'); const container = document.getElementById('mcpPropertiesContainer');
if (!container) {
return; // Container not found, skip rendering
}
if (mcpProperties.length === 0) { if (mcpProperties.length === 0) {
container.innerHTML = '<div style="text-align: center; padding: 20px; color: #999; font-size: 14px;">暂无参数,点击下方按钮添加参数</div>'; container.innerHTML = '<div style="text-align: center; padding: 20px; color: #999; font-size: 14px;">暂无参数,点击下方按钮添加参数</div>';
return; return;
} }
container.innerHTML = mcpProperties.map((prop, index) => ` container.innerHTML = mcpProperties.map((prop, index) => `
<div class="mcp-property-item"> <div class="mcp-property-item">
<div class="mcp-property-header"> <div class="mcp-property-header">
@@ -161,12 +162,7 @@ function renderMcpProperties() {
* 添加参数 * 添加参数
*/ */
function addMcpProperty() { function addMcpProperty() {
mcpProperties.push({ mcpProperties.push({ name: `param_${mcpProperties.length + 1}`, type: 'string', required: false, description: '' });
name: `param_${mcpProperties.length + 1}`,
type: 'string',
required: false,
description: ''
});
renderMcpProperties(); renderMcpProperties();
} }
@@ -182,9 +178,7 @@ function updateMcpProperty(index, field, value) {
return; return;
} }
} }
mcpProperties[index][field] = value; mcpProperties[index][field] = value;
if (field === 'type' && value !== 'integer' && value !== 'number') { if (field === 'type' && value !== 'integer' && value !== 'number') {
delete mcpProperties[index].minimum; delete mcpProperties[index].minimum;
delete mcpProperties[index].maximum; delete mcpProperties[index].maximum;
@@ -212,25 +206,24 @@ function setupMcpEventListeners() {
const cancelBtn = document.getElementById('cancelMcpBtn'); const cancelBtn = document.getElementById('cancelMcpBtn');
const form = document.getElementById('mcpToolForm'); const form = document.getElementById('mcpToolForm');
const addPropertyBtn = document.getElementById('addMcpPropertyBtn'); const addPropertyBtn = document.getElementById('addMcpPropertyBtn');
// Return early if required elements don't exist (e.g., in test environment)
if (!toggleBtn || !panel || !addBtn || !modal || !closeBtn || !cancelBtn || !form || !addPropertyBtn) {
return;
}
toggleBtn.addEventListener('click', () => { toggleBtn.addEventListener('click', () => {
const isExpanded = panel.classList.contains('expanded'); const isExpanded = panel.classList.contains('expanded');
panel.classList.toggle('expanded'); panel.classList.toggle('expanded');
toggleBtn.textContent = isExpanded ? '展开' : '收起'; toggleBtn.textContent = isExpanded ? '收起' : '展开';
}); });
// 确保面板默认展开 // 确保面板默认展开
panel.classList.add('expanded'); panel.classList.add('expanded');
addBtn.addEventListener('click', () => openMcpModal()); addBtn.addEventListener('click', () => openMcpModal());
closeBtn.addEventListener('click', closeMcpModal); closeBtn.addEventListener('click', closeMcpModal);
cancelBtn.addEventListener('click', closeMcpModal); cancelBtn.addEventListener('click', closeMcpModal);
addPropertyBtn.addEventListener('click', addMcpProperty); addPropertyBtn.addEventListener('click', addMcpProperty);
modal.addEventListener('click', (e) => { modal.addEventListener('click', (e) => {
if (e.target === modal) closeMcpModal(); if (e.target === modal) closeMcpModal();
}); });
form.addEventListener('submit', handleMcpSubmit); form.addEventListener('submit', handleMcpSubmit);
} }
@@ -243,18 +236,15 @@ function openMcpModal(index = null) {
alert('WebSocket 已连接,无法编辑工具'); alert('WebSocket 已连接,无法编辑工具');
return; return;
} }
mcpEditingIndex = index; mcpEditingIndex = index;
const errorContainer = document.getElementById('mcpErrorContainer'); const errorContainer = document.getElementById('mcpErrorContainer');
errorContainer.innerHTML = ''; errorContainer.innerHTML = '';
if (index !== null) { if (index !== null) {
document.getElementById('mcpModalTitle').textContent = '编辑工具'; document.getElementById('mcpModalTitle').textContent = '编辑工具';
const tool = mcpTools[index]; const tool = mcpTools[index];
document.getElementById('mcpToolName').value = tool.name; document.getElementById('mcpToolName').value = tool.name;
document.getElementById('mcpToolDescription').value = tool.description; document.getElementById('mcpToolDescription').value = tool.description;
document.getElementById('mcpMockResponse').value = tool.mockResponse ? JSON.stringify(tool.mockResponse, null, 2) : ''; document.getElementById('mcpMockResponse').value = tool.mockResponse ? JSON.stringify(tool.mockResponse, null, 2) : '';
mcpProperties = []; mcpProperties = [];
const schema = tool.inputSchema; const schema = tool.inputSchema;
if (schema.properties) { if (schema.properties) {
@@ -275,7 +265,6 @@ function openMcpModal(index = null) {
document.getElementById('mcpToolForm').reset(); document.getElementById('mcpToolForm').reset();
mcpProperties = []; mcpProperties = [];
} }
renderMcpProperties(); renderMcpProperties();
document.getElementById('mcpToolModal').style.display = 'block'; document.getElementById('mcpToolModal').style.display = 'block';
} }
@@ -298,21 +287,15 @@ function handleMcpSubmit(e) {
e.preventDefault(); e.preventDefault();
const errorContainer = document.getElementById('mcpErrorContainer'); const errorContainer = document.getElementById('mcpErrorContainer');
errorContainer.innerHTML = ''; errorContainer.innerHTML = '';
const name = document.getElementById('mcpToolName').value.trim(); const name = document.getElementById('mcpToolName').value.trim();
const description = document.getElementById('mcpToolDescription').value.trim(); const description = document.getElementById('mcpToolDescription').value.trim();
const mockResponseText = document.getElementById('mcpMockResponse').value.trim(); const mockResponseText = document.getElementById('mcpMockResponse').value.trim();
// 检查名称重复 // 检查名称重复
const isDuplicate = mcpTools.some((tool, index) => const isDuplicate = mcpTools.some((tool, index) => tool.name === name && index !== mcpEditingIndex);
tool.name === name && index !== mcpEditingIndex
);
if (isDuplicate) { if (isDuplicate) {
showMcpError('工具名称已存在,请使用不同的名称'); showMcpError('工具名称已存在,请使用不同的名称');
return; return;
} }
// 解析模拟返回结果 // 解析模拟返回结果
let mockResponse = null; let mockResponse = null;
if (mockResponseText) { if (mockResponseText) {
@@ -323,21 +306,13 @@ function handleMcpSubmit(e) {
return; return;
} }
} }
// 构建 inputSchema // 构建 inputSchema
const inputSchema = { const inputSchema = { type: "object", properties: {}, required: [] };
type: "object",
properties: {},
required: []
};
mcpProperties.forEach(prop => { mcpProperties.forEach(prop => {
const propSchema = { type: prop.type }; const propSchema = { type: prop.type };
if (prop.description) { if (prop.description) {
propSchema.description = prop.description; propSchema.description = prop.description;
} }
if ((prop.type === 'integer' || prop.type === 'number')) { if ((prop.type === 'integer' || prop.type === 'number')) {
if (prop.minimum !== undefined && prop.minimum !== '') { if (prop.minimum !== undefined && prop.minimum !== '') {
propSchema.minimum = prop.minimum; propSchema.minimum = prop.minimum;
@@ -346,20 +321,15 @@ function handleMcpSubmit(e) {
propSchema.maximum = prop.maximum; propSchema.maximum = prop.maximum;
} }
} }
inputSchema.properties[prop.name] = propSchema; inputSchema.properties[prop.name] = propSchema;
if (prop.required) { if (prop.required) {
inputSchema.required.push(prop.name); inputSchema.required.push(prop.name);
} }
}); });
if (inputSchema.required.length === 0) { if (inputSchema.required.length === 0) {
delete inputSchema.required; delete inputSchema.required;
} }
const tool = { name, description, inputSchema, mockResponse }; const tool = { name, description, inputSchema, mockResponse };
if (mcpEditingIndex !== null) { if (mcpEditingIndex !== null) {
mcpTools[mcpEditingIndex] = tool; mcpTools[mcpEditingIndex] = tool;
log(`已更新工具: ${name}`, 'success'); log(`已更新工具: ${name}`, 'success');
@@ -367,7 +337,6 @@ function handleMcpSubmit(e) {
mcpTools.push(tool); mcpTools.push(tool);
log(`已添加工具: ${name}`, 'success'); log(`已添加工具: ${name}`, 'success');
} }
saveMcpTools(); saveMcpTools();
renderMcpTools(); renderMcpTools();
closeMcpModal(); closeMcpModal();
@@ -417,11 +386,7 @@ function saveMcpTools() {
* 获取工具列表 * 获取工具列表
*/ */
export function getMcpTools() { export function getMcpTools() {
return mcpTools.map(tool => ({ return mcpTools.map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema }));
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema
}));
} }
/** /**
@@ -429,20 +394,14 @@ export function getMcpTools() {
*/ */
export function executeMcpTool(toolName, toolArgs) { export function executeMcpTool(toolName, toolArgs) {
const tool = mcpTools.find(t => t.name === toolName); const tool = mcpTools.find(t => t.name === toolName);
if (!tool) { if (!tool) {
log(`未找到工具: ${toolName}`, 'error'); log(`未找到工具: ${toolName}`, 'error');
return { return { success: false, error: `未知工具: ${toolName}` };
success: false,
error: `未知工具: ${toolName}`
};
} }
// 如果有模拟返回结果,使用它 // 如果有模拟返回结果,使用它
if (tool.mockResponse) { if (tool.mockResponse) {
// 替换模板变量 // 替换模板变量
let responseStr = JSON.stringify(tool.mockResponse); let responseStr = JSON.stringify(tool.mockResponse);
// 替换 ${paramName} 格式的变量 // 替换 ${paramName} 格式的变量
if (toolArgs) { if (toolArgs) {
Object.keys(toolArgs).forEach(key => { Object.keys(toolArgs).forEach(key => {
@@ -450,7 +409,6 @@ export function executeMcpTool(toolName, toolArgs) {
responseStr = responseStr.replace(regex, toolArgs[key]); responseStr = responseStr.replace(regex, toolArgs[key]);
}); });
} }
try { try {
const response = JSON.parse(responseStr); const response = JSON.parse(responseStr);
log(`工具 ${toolName} 执行成功,返回模拟结果: ${responseStr}`, 'success'); log(`工具 ${toolName} 执行成功,返回模拟结果: ${responseStr}`, 'success');
@@ -460,21 +418,10 @@ export function executeMcpTool(toolName, toolArgs) {
return tool.mockResponse; return tool.mockResponse;
} }
} }
// 没有模拟返回结果,返回默认成功消息 // 没有模拟返回结果,返回默认成功消息
log(`工具 ${toolName} 执行成功,返回默认结果`, 'success'); log(`工具 ${toolName} 执行成功,返回默认结果`, 'success');
return { return { success: true, message: `工具 ${toolName} 执行成功`, tool: toolName, arguments: toolArgs };
success: true,
message: `工具 ${toolName} 执行成功`,
tool: toolName,
arguments: toolArgs
};
} }
// 暴露全局方法供 HTML 内联事件调用 // 暴露全局方法供 HTML 内联事件调用
window.mcpModule = { window.mcpModule = { updateMcpProperty, deleteMcpProperty, editMcpTool, deleteMcpTool };
updateMcpProperty,
deleteMcpProperty,
editMcpTool,
deleteMcpTool
};
@@ -1,4 +1,4 @@
import { log } from '../../utils/logger.js'; import { log } from '../../utils/logger.js?v=0127';
// WebSocket 连接 // WebSocket 连接
export async function webSocketConnect(otaUrl, config) { export async function webSocketConnect(otaUrl, config) {
@@ -1,11 +1,11 @@
// WebSocket消息处理模块 // WebSocket消息处理模块
import { getConfig, saveConnectionUrls } from '../../config/manager.js'; import { getConfig, saveConnectionUrls } from '../../config/manager.js?v=0127';
import { uiController } from '../../ui/controller.js'; import { uiController } from '../../ui/controller.js?v=0127';
import { log } from '../../utils/logger.js'; import { log } from '../../utils/logger.js?v=0127';
import { getAudioPlayer } from '../audio/player.js'; import { getAudioPlayer } from '../audio/player.js?v=0127';
import { getAudioRecorder } from '../audio/recorder.js'; import { getAudioRecorder } from '../audio/recorder.js?v=0127';
import { executeMcpTool, getMcpTools, setWebSocket as setMcpWebSocket } from '../mcp/tools.js'; import { executeMcpTool, getMcpTools, setWebSocket as setMcpWebSocket } from '../mcp/tools.js?v=0127';
import { webSocketConnect } from './ota-connector.js'; import { webSocketConnect } from './ota-connector.js?v=0127';
// WebSocket处理器类 // WebSocket处理器类
export class WebSocketHandler { export class WebSocketHandler {
@@ -304,7 +304,6 @@ export class WebSocketHandler {
let arrayBuffer; let arrayBuffer;
if (data instanceof ArrayBuffer) { if (data instanceof ArrayBuffer) {
arrayBuffer = data; arrayBuffer = data;
log(`收到ArrayBuffer音频数据,大小: ${data.byteLength}字节`, 'debug');
} else if (data instanceof Blob) { } else if (data instanceof Blob) {
arrayBuffer = await data.arrayBuffer(); arrayBuffer = await data.arrayBuffer();
log(`收到Blob音频数据,大小: ${arrayBuffer.byteLength}字节`, 'debug'); log(`收到Blob音频数据,大小: ${arrayBuffer.byteLength}字节`, 'debug');
@@ -392,7 +391,7 @@ export class WebSocketHandler {
this.websocket.onerror = (error) => { this.websocket.onerror = (error) => {
log(`WebSocket错误: ${error.message || '未知错误'}`, 'error'); log(`WebSocket错误: ${error.message || '未知错误'}`, 'error');
uiController.addChatMessage(`⚠️ WebSocket错误: ${error.message || '未知错误'}`, false);
if (this.onConnectionStateChange) { if (this.onConnectionStateChange) {
this.onConnectionStateChange(false); this.onConnectionStateChange(false);
} }
+161 -110
View File
@@ -1,10 +1,10 @@
// UI控制模块 // UI controller module
import { loadConfig, saveConfig } from '../config/manager.js'; import { loadConfig, saveConfig } from '../config/manager.js?v=0127';
import { getAudioRecorder } from '../core/audio/recorder.js'; import { getAudioPlayer } from '../core/audio/player.js?v=0127';
import { getWebSocketHandler } from '../core/network/websocket.js'; import { getAudioRecorder } from '../core/audio/recorder.js?v=0127';
import { getAudioPlayer } from '../core/audio/player.js'; import { getWebSocketHandler } from '../core/network/websocket.js?v=0127';
// UI控制器类 // UI controller class
class UIController { class UIController {
constructor() { constructor() {
this.isEditing = false; this.isEditing = false;
@@ -14,7 +14,7 @@ class UIController {
this.currentBackgroundIndex = 0; this.currentBackgroundIndex = 0;
this.backgroundImages = ['1.png', '2.png', '3.png']; this.backgroundImages = ['1.png', '2.png', '3.png'];
// 绑定方法 // Bind methods
this.init = this.init.bind(this); this.init = this.init.bind(this);
this.initEventListeners = this.initEventListeners.bind(this); this.initEventListeners = this.initEventListeners.bind(this);
this.updateDialButton = this.updateDialButton.bind(this); this.updateDialButton = this.updateDialButton.bind(this);
@@ -25,7 +25,7 @@ class UIController {
this.switchTab = this.switchTab.bind(this); this.switchTab = this.switchTab.bind(this);
} }
// 初始化 // Initialize
init() { init() {
console.log('UIController init started'); console.log('UIController init started');
@@ -35,7 +35,7 @@ class UIController {
this.initVisualizer(); this.initVisualizer();
} }
// 检查连接按钮在初始化时是否存在 // Check if connect button exists during initialization
const connectBtn = document.getElementById('connectBtn'); const connectBtn = document.getElementById('connectBtn');
console.log('connectBtn during init:', connectBtn); console.log('connectBtn during init:', connectBtn);
@@ -43,20 +43,20 @@ class UIController {
this.startAudioStatsMonitor(); this.startAudioStatsMonitor();
loadConfig(); loadConfig();
// 设置录音器回调 // Register recording callback
const audioRecorder = getAudioRecorder(); const audioRecorder = getAudioRecorder();
audioRecorder.onRecordingStart = (seconds) => { audioRecorder.onRecordingStart = (seconds) => {
this.updateRecordButtonState(true, seconds); this.updateRecordButtonState(true, seconds);
}; };
// 初始化状态显示 // Initialize status display
this.updateConnectionUI(false); this.updateConnectionUI(false);
this.updateDialButton(false); this.updateDialButton(false);
console.log('UIController init completed'); console.log('UIController init completed');
} }
// 初始化可视化器 // Initialize visualizer
initVisualizer() { initVisualizer() {
if (this.visualizerCanvas) { if (this.visualizerCanvas) {
this.visualizerCanvas.width = this.visualizerCanvas.clientWidth; this.visualizerCanvas.width = this.visualizerCanvas.clientWidth;
@@ -66,9 +66,9 @@ class UIController {
} }
} }
// 初始化事件监听器 // Initialize event listeners
initEventListeners() { initEventListeners() {
// 设置按钮 // Settings button
const settingsBtn = document.getElementById('settingsBtn'); const settingsBtn = document.getElementById('settingsBtn');
if (settingsBtn) { if (settingsBtn) {
settingsBtn.addEventListener('click', () => { settingsBtn.addEventListener('click', () => {
@@ -76,13 +76,13 @@ class UIController {
}); });
} }
// 背景切换按钮 // Background switch button
const backgroundBtn = document.getElementById('backgroundBtn'); const backgroundBtn = document.getElementById('backgroundBtn');
if (backgroundBtn) { if (backgroundBtn) {
backgroundBtn.addEventListener('click', this.switchBackground); backgroundBtn.addEventListener('click', this.switchBackground);
} }
// 拨号按钮 // Dial button
const dialBtn = document.getElementById('dialBtn'); const dialBtn = document.getElementById('dialBtn');
if (dialBtn) { if (dialBtn) {
dialBtn.addEventListener('click', () => { dialBtn.addEventListener('click', () => {
@@ -92,40 +92,40 @@ class UIController {
if (isConnected) { if (isConnected) {
wsHandler.disconnect(); wsHandler.disconnect();
this.updateDialButton(false); this.updateDialButton(false);
this.addChatMessage('已断开连接,期待下次再见~😉', false); this.addChatMessage('Disconnected, see you next time~😊', false);
} else { } else {
// 检查OTA地址是否已填写 // Check if OTA URL is filled
const otaUrlInput = document.getElementById('otaUrl'); const otaUrlInput = document.getElementById('otaUrl');
if (!otaUrlInput || !otaUrlInput.value.trim()) { if (!otaUrlInput || !otaUrlInput.value.trim()) {
// 如果OTA地址未填写,显示设置弹窗并切换到设备配置页 // If OTA URL is not filled, show settings modal and switch to device tab
this.showModal('settingsModal'); this.showModal('settingsModal');
this.switchTab('device'); this.switchTab('device');
this.addChatMessage('请先填写OTA服务器地址', false); this.addChatMessage('Please fill in OTA server URL', false);
return; return;
} }
// 执行连接操作 // Start connection process
this.handleConnect(); this.handleConnect();
} }
}); });
} }
// 录音按钮 // Record button
const recordBtn = document.getElementById('recordBtn'); const recordBtn = document.getElementById('recordBtn');
if (recordBtn) { if (recordBtn) {
recordBtn.addEventListener('click', () => { recordBtn.addEventListener('click', () => {
const audioRecorder = getAudioRecorder(); const audioRecorder = getAudioRecorder();
if (audioRecorder.isRecording) { if (audioRecorder.isRecording) {
audioRecorder.stop(); audioRecorder.stop();
// 停止录音时移除录音样式 // Restore record button to normal state
recordBtn.classList.remove('recording'); recordBtn.classList.remove('recording');
recordBtn.querySelector('.btn-text').textContent = '录音'; recordBtn.querySelector('.btn-text').textContent = '录音';
} else { } else {
// 先更新按钮状态为录音中 // Update button state to recording
recordBtn.classList.add('recording'); recordBtn.classList.add('recording');
recordBtn.querySelector('.btn-text').textContent = '录音中'; recordBtn.querySelector('.btn-text').textContent = '录音中';
// 延迟开始录音,确保按钮状态已更新 // Start recording, update button state after delay
setTimeout(() => { setTimeout(() => {
audioRecorder.start(); audioRecorder.start();
}, 100); }, 100);
@@ -133,7 +133,7 @@ class UIController {
}); });
} }
// 消息输入框事件 // Chat input event listener
const chatIpt = document.getElementById('chatIpt'); const chatIpt = document.getElementById('chatIpt');
if (chatIpt) { if (chatIpt) {
const wsHandler = getWebSocketHandler(); const wsHandler = getWebSocketHandler();
@@ -148,7 +148,7 @@ class UIController {
}); });
} }
// 关闭按钮 // Close button
const closeButtons = document.querySelectorAll('.close-btn'); const closeButtons = document.querySelectorAll('.close-btn');
closeButtons.forEach(btn => { closeButtons.forEach(btn => {
btn.addEventListener('click', (e) => { btn.addEventListener('click', (e) => {
@@ -163,7 +163,7 @@ class UIController {
}); });
}); });
// 设置标签页切换 // Settings tab switch
const tabBtns = document.querySelectorAll('.tab-btn'); const tabBtns = document.querySelectorAll('.tab-btn');
tabBtns.forEach(btn => { tabBtns.forEach(btn => {
btn.addEventListener('click', (e) => { btn.addEventListener('click', (e) => {
@@ -171,7 +171,7 @@ class UIController {
}); });
}); });
// 点击模态框外部关闭 // Click modal background to close
const modals = document.querySelectorAll('.modal'); const modals = document.querySelectorAll('.modal');
modals.forEach(modal => { modals.forEach(modal => {
modal.addEventListener('click', (e) => { modal.addEventListener('click', (e) => {
@@ -184,7 +184,7 @@ class UIController {
}); });
}); });
// 添加MCP工具按钮 // Add MCP tool button
const addMCPToolBtn = document.getElementById('addMCPToolBtn'); const addMCPToolBtn = document.getElementById('addMCPToolBtn');
if (addMCPToolBtn) { if (addMCPToolBtn) {
addMCPToolBtn.addEventListener('click', (e) => { addMCPToolBtn.addEventListener('click', (e) => {
@@ -193,10 +193,10 @@ class UIController {
}); });
} }
// 连接按钮和取消按钮已被移除,功能已集成到拨号按钮中 // Connect button and send button are not removed, can be added to dial button later
} }
// 更新连接状态UI // Update connection status UI
updateConnectionUI(isConnected) { updateConnectionUI(isConnected) {
const connectionStatus = document.getElementById('connectionStatus'); const connectionStatus = document.getElementById('connectionStatus');
const statusDot = document.querySelector('.status-dot'); const statusDot = document.querySelector('.status-dot');
@@ -216,7 +216,7 @@ class UIController {
} }
} }
// 更新拨号按钮状态 // Update dial button state
updateDialButton(isConnected) { updateDialButton(isConnected) {
const dialBtn = document.getElementById('dialBtn'); const dialBtn = document.getElementById('dialBtn');
const recordBtn = document.getElementById('recordBtn'); const recordBtn = document.getElementById('recordBtn');
@@ -225,39 +225,44 @@ class UIController {
if (isConnected) { if (isConnected) {
dialBtn.classList.add('dial-active'); dialBtn.classList.add('dial-active');
dialBtn.querySelector('.btn-text').textContent = '挂断'; dialBtn.querySelector('.btn-text').textContent = '挂断';
// 更新拨号按钮图标为挂断图标 // Update dial button icon to hang up icon
dialBtn.querySelector('svg').innerHTML = ` dialBtn.querySelector('svg').innerHTML = `
<path d="M12,9C10.4,9 9,10.4 9,12C9,13.6 10.4,15 12,15C13.6,15 15,13.6 15,12C15,10.4 13.6,9 12,9M12,17C9.2,17 7,14.8 7,12C7,9.2 9.2,7 12,7C14.8,7 17,9.2 17,12C17,14.8 14.8,17 12,17M12,4.5C7,4.5 2.7,7.6 1,12C2.7,16.4 7,19.5 12,19.5C17,19.5 21.3,16.4 23,12C21.3,7.6 17,4.5 12,4.5Z"/> <path d="M12,9C10.4,9 9,10.4 9,12C9,13.6 10.4,15 12,15C13.6,15 15,13.6 15,12C15,10.4 13.6,9 12,9M12,17C9.2,17 7,14.8 7,12C7,9.2 9.2,7 12,7C14.8,7 17,9.2 17,12C17,14.8 14.8,17 12,17M12,4.5C7,4.5 2.7,7.6 1,12C2.7,16.4 7,19.5 12,19.5C17,19.5 21.3,16.4 23,12C21.3,7.6 17,4.5 12,4.5Z"/>
`; `;
} else { } else {
dialBtn.classList.remove('dial-active'); dialBtn.classList.remove('dial-active');
dialBtn.querySelector('.btn-text').textContent = '拨号'; dialBtn.querySelector('.btn-text').textContent = '拨号';
// 恢复拨号按钮图标 // Restore dial button icon
dialBtn.querySelector('svg').innerHTML = ` dialBtn.querySelector('svg').innerHTML = `
<path d="M6.62,10.79C8.06,13.62 10.38,15.94 13.21,17.38L15.41,15.18C15.69,14.9 16.08,14.82 16.43,14.93C17.55,15.3 18.75,15.5 20,15.5A1,1 0 0,1 21,16.5V20A1,1 0 0,1 20,21A17,17 0 0,1 3,4A1,1 0 0,1 4,3H7.5A1,1 0 0,1 8.5,4C8.5,5.25 8.7,6.45 9.07,7.57C9.18,7.92 9.1,8.31 8.82,8.59L6.62,10.79Z"/> <path d="M6.62,10.79C8.06,13.62 10.38,15.94 13.21,17.38L15.41,15.18C15.69,14.9 16.08,14.82 16.43,14.93C17.55,15.3 18.75,15.5 20,15.5A1,1 0 0,1 21,16.5V20A1,1 0 0,1 20,21A17,17 0 0,1 3,4A1,1 0 0,1 4,3H7.5A1,1 0 0,1 8.5,4C8.5,5.25 8.7,6.45 9.07,7.57C9.18,7.92 9.1,8.31 8.82,8.59L6.62,10.79Z"/>
`; `;
} }
} }
// 更新录音按钮状态 // Update record button state
if (recordBtn) { if (recordBtn) {
if (isConnected) { const microphoneAvailable = window.microphoneAvailable !== false;
if (isConnected && microphoneAvailable) {
recordBtn.disabled = false; recordBtn.disabled = false;
recordBtn.title = '开始录音'; recordBtn.title = '开始录音';
// 确保录音按钮恢复到正常状态 // Restore record button to normal state
recordBtn.querySelector('.btn-text').textContent = '录音'; recordBtn.querySelector('.btn-text').textContent = '录音';
recordBtn.classList.remove('recording'); recordBtn.classList.remove('recording');
} else { } else {
recordBtn.disabled = true; recordBtn.disabled = true;
recordBtn.title = '请先连接服务器'; if (!microphoneAvailable) {
// 确保录音按钮恢复到正常状态 recordBtn.title = window.isHttpNonLocalhost ? '当前由于是http访问,无法录音,只能用文字交互' : '麦克风不可用';
} else {
recordBtn.title = '请先连接服务器';
}
// Restore record button to normal state
recordBtn.querySelector('.btn-text').textContent = '录音'; recordBtn.querySelector('.btn-text').textContent = '录音';
recordBtn.classList.remove('recording'); recordBtn.classList.remove('recording');
} }
} }
} }
// 更新录音按钮状态 // Update record button state
updateRecordButtonState(isRecording, seconds = 0) { updateRecordButtonState(isRecording, seconds = 0) {
const recordBtn = document.getElementById('recordBtn'); const recordBtn = document.getElementById('recordBtn');
if (recordBtn) { if (recordBtn) {
@@ -268,11 +273,37 @@ class UIController {
recordBtn.querySelector('.btn-text').textContent = '录音'; recordBtn.querySelector('.btn-text').textContent = '录音';
recordBtn.classList.remove('recording'); recordBtn.classList.remove('recording');
} }
recordBtn.disabled = false; // Only enable button when microphone is available
recordBtn.disabled = window.microphoneAvailable === false;
} }
} }
// 添加聊天消息 /**
* Update microphone availability state
* @param {boolean} isAvailable - Whether microphone is available
* @param {boolean} isHttpNonLocalhost - Whether it is HTTP non-localhost access
*/
updateMicrophoneAvailability(isAvailable, isHttpNonLocalhost) {
const recordBtn = document.getElementById('recordBtn');
if (!recordBtn) return;
if (!isAvailable) {
// Disable record button
recordBtn.disabled = true;
// Update button text and title
recordBtn.querySelector('.btn-text').textContent = '录音';
recordBtn.title = isHttpNonLocalhost ? '当前由于是http访问,无法录音,只能用文字交互' : '麦克风不可用';
} else {
// If connected, enable record button
const wsHandler = getWebSocketHandler();
if (wsHandler && wsHandler.isConnected()) {
recordBtn.disabled = false;
recordBtn.title = '开始录音';
}
}
}
// Add chat message
addChatMessage(content, isUser = false) { addChatMessage(content, isUser = false) {
const chatStream = document.getElementById('chatStream'); const chatStream = document.getElementById('chatStream');
if (!chatStream) return; if (!chatStream) return;
@@ -282,11 +313,11 @@ class UIController {
messageDiv.innerHTML = `<div class="message-bubble">${content}</div>`; messageDiv.innerHTML = `<div class="message-bubble">${content}</div>`;
chatStream.appendChild(messageDiv); chatStream.appendChild(messageDiv);
// 自动滚动到底部 // Scroll to bottom
chatStream.scrollTop = chatStream.scrollHeight; chatStream.scrollTop = chatStream.scrollHeight;
} }
// 切换背景 // Switch background
switchBackground() { switchBackground() {
this.currentBackgroundIndex = (this.currentBackgroundIndex + 1) % this.backgroundImages.length; this.currentBackgroundIndex = (this.currentBackgroundIndex + 1) % this.backgroundImages.length;
const backgroundContainer = document.querySelector('.background-container'); const backgroundContainer = document.querySelector('.background-container');
@@ -295,7 +326,7 @@ class UIController {
} }
} }
// 显示模态框 // Show modal
showModal(modalId) { showModal(modalId) {
const modal = document.getElementById(modalId); const modal = document.getElementById(modalId);
if (modal) { if (modal) {
@@ -303,7 +334,7 @@ class UIController {
} }
} }
// 隐藏模态框 // Hide modal
hideModal(modalId) { hideModal(modalId) {
const modal = document.getElementById(modalId); const modal = document.getElementById(modalId);
if (modal) { if (modal) {
@@ -311,16 +342,16 @@ class UIController {
} }
} }
// 切换标签页 // Switch tab
switchTab(tabName) { switchTab(tabName) {
// 移除所有标签页的active类 // Remove active class from all tabs
const tabBtns = document.querySelectorAll('.tab-btn'); const tabBtns = document.querySelectorAll('.tab-btn');
const tabContents = document.querySelectorAll('.tab-content'); const tabContents = document.querySelectorAll('.tab-content');
tabBtns.forEach(btn => btn.classList.remove('active')); tabBtns.forEach(btn => btn.classList.remove('active'));
tabContents.forEach(content => content.classList.remove('active')); tabContents.forEach(content => content.classList.remove('active'));
// 激活选中的标签页 // Activate selected tab
const activeTabBtn = document.querySelector(`[data-tab="${tabName}"]`); const activeTabBtn = document.querySelector(`[data-tab="${tabName}"]`);
const activeTabContent = document.getElementById(`${tabName}Tab`); const activeTabContent = document.getElementById(`${tabName}Tab`);
@@ -330,24 +361,34 @@ class UIController {
} }
} }
// 连接成功后开始对话 // Start AI chat session after connection
startAIChatSession() { startAIChatSession() {
this.addChatMessage('连接成功,开始聊天吧~🙂', false); this.addChatMessage('连接成功,开始聊天吧~😊', false);
// 开启录音 // Check microphone availability and show error messages if needed
const recordBtn = document.getElementById('recordBtn'); if (!window.microphoneAvailable) {
if (recordBtn) { if (window.isHttpNonLocalhost) {
recordBtn.click(); this.addChatMessage('⚠️ 当前由于是http访问,无法录音,只能用文字交互', false);
} else {
this.addChatMessage('⚠️ 麦克风不可用,请检查权限设置,只能用文字交互', false);
}
}
// Start recording only if microphone is available
if (window.microphoneAvailable) {
const recordBtn = document.getElementById('recordBtn');
if (recordBtn) {
recordBtn.click();
}
} }
} }
// 处理连接按钮点击 // Handle connect button click
async handleConnect() { async handleConnect() {
console.log('handleConnect called'); console.log('handleConnect called');
// 确保切换到设备配置标签页 // Switch to device settings tab
this.switchTab('device'); this.switchTab('device');
// 等待DOM更新 // Wait for DOM update
await new Promise(resolve => setTimeout(resolve, 50)); await new Promise(resolve => setTimeout(resolve, 50));
const otaUrlInput = document.getElementById('otaUrl'); const otaUrlInput = document.getElementById('otaUrl');
@@ -362,7 +403,7 @@ class UIController {
const otaUrl = otaUrlInput.value; const otaUrl = otaUrlInput.value;
console.log('otaUrl value:', otaUrl); console.log('otaUrl value:', otaUrl);
// 更新拨号按钮状态为连接中 // Update dial button state to connecting
const dialBtn = document.getElementById('dialBtn'); const dialBtn = document.getElementById('dialBtn');
if (dialBtn) { if (dialBtn) {
dialBtn.classList.add('dial-active'); dialBtn.classList.add('dial-active');
@@ -370,7 +411,7 @@ class UIController {
dialBtn.disabled = true; dialBtn.disabled = true;
} }
// 显示连接中消息 // Show connecting message
this.addChatMessage('正在连接服务器...', false); this.addChatMessage('正在连接服务器...', false);
const chatIpt = document.getElementById('chatIpt'); const chatIpt = document.getElementById('chatIpt');
@@ -380,41 +421,51 @@ class UIController {
try { try {
// 获取WebSocket处理器 // Get WebSocket handler instance
const wsHandler = getWebSocketHandler(); const wsHandler = getWebSocketHandler();
// Register connection state callback BEFORE connecting
wsHandler.onConnectionStateChange = (isConnected) => {
this.updateConnectionUI(isConnected);
this.updateDialButton(isConnected);
};
// Register chat message callback BEFORE connecting
wsHandler.onChatMessage = (text, isUser) => {
this.addChatMessage(text, isUser);
};
// Register record button state callback BEFORE connecting
wsHandler.onRecordButtonStateChange = (isRecording) => {
const recordBtn = document.getElementById('recordBtn');
if (recordBtn) {
if (isRecording) {
recordBtn.classList.add('recording');
recordBtn.querySelector('.btn-text').textContent = '录音中';
} else {
recordBtn.classList.remove('recording');
recordBtn.querySelector('.btn-text').textContent = '录音';
}
}
};
const isConnected = await wsHandler.connect(); const isConnected = await wsHandler.connect();
if (isConnected) { if (isConnected) {
// Check microphone availability (check again after connection)
const { checkMicrophoneAvailability } = await import('../core/audio/recorder.js?v=0127');
const micAvailable = await checkMicrophoneAvailability();
// 设置连接状态回调 if (!micAvailable) {
wsHandler.onConnectionStateChange = (isConnected) => { const isHttp = window.isHttpNonLocalhost;
this.updateConnectionUI(isConnected); if (isHttp) {
this.updateDialButton(isConnected); this.addChatMessage('⚠️ 当前由于是http访问,无法录音,只能用文字交互', false);
};
// 设置聊天消息回调
wsHandler.onChatMessage = (text, isUser) => {
this.addChatMessage(text, isUser);
};
// 设置录音按钮状态回调
wsHandler.onRecordButtonStateChange = (isRecording) => {
const recordBtn = document.getElementById('recordBtn');
if (recordBtn) {
if (isRecording) {
recordBtn.classList.add('recording');
recordBtn.querySelector('.btn-text').textContent = '录音中';
} else {
recordBtn.classList.remove('recording');
recordBtn.querySelector('.btn-text').textContent = '录音';
}
} }
}; // Update global state
window.microphoneAvailable = false;
}
// 连接成功 // Update dial button state
this.addChatMessage('OTA连接成功,正在建立WebSocket连接...', false);
// 更新拨号按钮状态
const dialBtn = document.getElementById('dialBtn'); const dialBtn = document.getElementById('dialBtn');
if (dialBtn) { if (dialBtn) {
dialBtn.disabled = false; dialBtn.disabled = false;
@@ -434,14 +485,14 @@ class UIController {
name: error.name name: error.name
}); });
// 显示错误消息 // Show error message
const errorMessage = error.message.includes('Cannot set properties of null') const errorMessage = error.message.includes('Cannot set properties of null')
? '连接失败:请刷新页面重试' ? '连接失败:请检查设备连接'
: `连接失败: ${error.message}`; : `连接失败: ${error.message}`;
this.addChatMessage(errorMessage, false); this.addChatMessage(errorMessage, false);
// 恢复拨号按钮状态 // Restore dial button state
const dialBtn = document.getElementById('dialBtn'); const dialBtn = document.getElementById('dialBtn');
if (dialBtn) { if (dialBtn) {
dialBtn.disabled = false; dialBtn.disabled = false;
@@ -452,7 +503,7 @@ class UIController {
} }
} }
// 添加MCP工具 // Add MCP tool
addMCPTool() { addMCPTool() {
const mcpToolsList = document.getElementById('mcpToolsList'); const mcpToolsList = document.getElementById('mcpToolsList');
if (!mcpToolsList) return; if (!mcpToolsList) return;
@@ -471,7 +522,7 @@ class UIController {
mcpToolsList.appendChild(toolDiv); mcpToolsList.appendChild(toolDiv);
} }
// 移除MCP工具 // Remove MCP tool
removeMCPTool(toolId) { removeMCPTool(toolId) {
const toolElement = document.getElementById(toolId); const toolElement = document.getElementById(toolId);
if (toolElement) { if (toolElement) {
@@ -479,24 +530,24 @@ class UIController {
} }
} }
// 更新音频统计信息 // Update audio statistics display
updateAudioStats() { updateAudioStats() {
const audioPlayer = getAudioPlayer(); const audioPlayer = getAudioPlayer();
if (!audioPlayer) return; if (!audioPlayer) return;
const stats = audioPlayer.getAudioStats(); const stats = audioPlayer.getAudioStats();
// 这里可以添加音频统计的UI更新逻辑 // Here can add audio statistics UI update logic
} }
// 启动音频统计监控 // Start audio statistics monitor
startAudioStatsMonitor() { startAudioStatsMonitor() {
// 每100ms更新一次音频统计 // Update audio statistics every 100ms
this.audioStatsTimer = setInterval(() => { this.audioStatsTimer = setInterval(() => {
this.updateAudioStats(); this.updateAudioStats();
}, 100); }, 100);
} }
// 停止音频统计监控 // Stop audio statistics monitor
stopAudioStatsMonitor() { stopAudioStatsMonitor() {
if (this.audioStatsTimer) { if (this.audioStatsTimer) {
clearInterval(this.audioStatsTimer); clearInterval(this.audioStatsTimer);
@@ -504,7 +555,7 @@ class UIController {
} }
} }
// 绘制音频可视化效果 // Draw audio visualizer waveform
drawVisualizer(dataArray) { drawVisualizer(dataArray) {
if (!this.visualizerContext || !this.visualizerCanvas) return; if (!this.visualizerContext || !this.visualizerCanvas) return;
@@ -518,7 +569,7 @@ class UIController {
for (let i = 0; i < dataArray.length; i++) { for (let i = 0; i < dataArray.length; i++) {
barHeight = dataArray[i] / 2; barHeight = dataArray[i] / 2;
// 创建渐变色:从紫色到蓝色到青色 // Create gradient color: from purple to blue to green
const gradient = this.visualizerContext.createLinearGradient(0, 0, 0, this.visualizerCanvas.height); const gradient = this.visualizerContext.createLinearGradient(0, 0, 0, this.visualizerCanvas.height);
gradient.addColorStop(0, '#8e44ad'); gradient.addColorStop(0, '#8e44ad');
gradient.addColorStop(0.5, '#3498db'); gradient.addColorStop(0.5, '#3498db');
@@ -530,21 +581,21 @@ class UIController {
} }
} }
// 更新会话状态UI // Update session status UI
updateSessionStatus(isSpeaking) { updateSessionStatus(isSpeaking) {
// 这里可以添加会话状态的UI更新逻辑 // Here can add session status UI update logic
// 例如:更新Live2D角色的表情或状态指示器 // For example: update Live2D model's mouth movement status
} }
// 更新会话表情 // Update session emotion
updateSessionEmotion(emoji) { updateSessionEmotion(emoji) {
// 这里可以添加表情更新的逻辑 // Here can add emotion update logic
// 例如:在状态指示器中显示表情 // For example: display emoji in status indicator
} }
} }
// 创建全局实例 // Create singleton instance
export const uiController = new UIController(); export const uiController = new UIController();
// 导出类供其他模块使用 // Export class for module usage
export { UIController }; export { UIController };
+10 -9
View File
@@ -5,7 +5,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>小智服务器测试页面</title> <title>小智服务器测试页面</title>
<link rel="stylesheet" href="css/test_page.css"> <link rel="stylesheet" href="css/test_page.css?v=0127">
<script> <script>
// 检测是否使用file://协议打开 // 检测是否使用file://协议打开
if (window.location.protocol === 'file:') { if (window.location.protocol === 'file:') {
@@ -143,7 +143,8 @@
</div> </div>
<div class="config-item"> <div class="config-item">
<label for="deviceName">设备名称:</label> <label for="deviceName">设备名称:</label>
<input type="text" id="deviceName" value="Web测试设备" maxlength="50" placeholder="deviceName"> <input type="text" id="deviceName" value="Web测试设备" maxlength="50"
placeholder="deviceName">
</div> </div>
</div> </div>
</div> </div>
@@ -237,23 +238,23 @@
</div> </div>
<!-- 背景加载 --> <!-- 背景加载 -->
<script src="js/ui/background-load.js"></script> <script src="js/ui/background-load.js?v=0127"></script>
<!-- PIXI.js 2D渲染引擎 --> <!-- PIXI.js 2D渲染引擎 -->
<script src="js/live2d/pixi.js"></script> <script src="js/live2d/pixi.js?v=0127"></script>
<!-- Live2D Cubism 4.0 SDK --> <!-- Live2D Cubism 4.0 SDK -->
<script src="js/live2d/live2dcubismcore.min.js"></script> <script src="js/live2d/live2dcubismcore.min.js?v=0127"></script>
<script src="js/live2d/cubism4.min.js"></script> <script src="js/live2d/cubism4.min.js?v=0127"></script>
<!-- Live2D 管理器 --> <!-- Live2D 管理器 -->
<script src="js/live2d/live2d.js"></script> <script src="js/live2d/live2d.js?v=0127"></script>
<!-- Opus解码库 --> <!-- Opus解码库 -->
<script src="js/utils/libopus.js"></script> <script src="js/utils/libopus.js?v=0127"></script>
<!-- 主应用入口 --> <!-- 主应用入口 -->
<script type="module" src="js/app.js"></script> <script type="module" src="js/app.js?v=0127"></script>
<!-- 全局错误处理 --> <!-- 全局错误处理 -->
<script> <script>