mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-29 06:13:56 +08:00
feat: implement Issue 2896 - Live2D Actions and Microphone Detection
- Add Live2D action tools (smile, wave, generic actions) as MCP tools - Implement microphone availability detection - Handle HTTP non-localhost access scenarios - Update UI to reflect microphone availability state - Translate all comments and messages to proper Chinese/English - Add test files for new functionality
This commit is contained in:
@@ -4,6 +4,7 @@ import { checkOpusLoaded, initOpusEncoder } from './core/audio/opus-codec.js';
|
|||||||
import { uiController } from './ui/controller.js';
|
import { uiController } from './ui/controller.js';
|
||||||
import { getAudioPlayer } from './core/audio/player.js';
|
import { getAudioPlayer } from './core/audio/player.js';
|
||||||
import { initMcpTools } from './core/mcp/tools.js';
|
import { initMcpTools } from './core/mcp/tools.js';
|
||||||
|
import { checkMicrophoneAvailability, isHttpNonLocalhost } from './core/audio/recorder.js';
|
||||||
|
|
||||||
// 应用类
|
// 应用类
|
||||||
class App {
|
class App {
|
||||||
@@ -34,6 +35,9 @@ class App {
|
|||||||
// 初始化MCP工具
|
// 初始化MCP工具
|
||||||
initMcpTools();
|
initMcpTools();
|
||||||
|
|
||||||
|
// 检查麦克风可用性
|
||||||
|
await this.checkMicrophoneAvailability();
|
||||||
|
|
||||||
// 初始化Live2D
|
// 初始化Live2D
|
||||||
await this.initLive2D();
|
await this.initLive2D();
|
||||||
|
|
||||||
@@ -81,6 +85,36 @@ 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建并启动应用
|
// 创建并启动应用
|
||||||
@@ -95,4 +129,4 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
@@ -68,5 +68,37 @@
|
|||||||
"brightness": "${brightness}",
|
"brightness": "${brightness}",
|
||||||
"message": "亮度已设置为 ${brightness}"
|
"message": "亮度已设置为 ${brightness}"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "live2d.smile",
|
||||||
|
"description": "Make the virtual human perform a smiling action. Call this tool when the user says 'smile', 'smile once', 'give me a smile', 'smile please', etc.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "live2d.wave",
|
||||||
|
"description": "Make the virtual human perform a waving action. Call this tool when the user says 'wave to me', 'wave', 'say hello', 'wave please', etc.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "live2d.action",
|
||||||
|
"description": "Trigger a specified action for the virtual human. Supported actions include: FlickUp (swipe up), FlickDown (swipe down), Tap (tap), Tap@Body (body tap), Flick (swipe), Flick@Body (body swipe), etc. Call this tool when the user requests a specific virtual human action.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"action": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Action name, such as: FlickUp, FlickDown, Tap, Tap@Body, Flick, Flick@Body"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"action"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
// 音频录制模块
|
// Audio recording module
|
||||||
import { log } from '../../utils/logger.js';
|
import { log } from '../../utils/logger.js';
|
||||||
import { initOpusEncoder } from './opus-codec.js';
|
import { initOpusEncoder } from './opus-codec.js';
|
||||||
import { getAudioPlayer } from './player.js';
|
import { getAudioPlayer } from './player.js';
|
||||||
|
|
||||||
// 音频录制器类
|
// Audio recorder class
|
||||||
export class AudioRecorder {
|
export class AudioRecorder {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.isRecording = false;
|
this.isRecording = false;
|
||||||
@@ -20,24 +20,24 @@ export class AudioRecorder {
|
|||||||
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();
|
const audioPlayer = getAudioPlayer();
|
||||||
return audioPlayer.getAudioContext();
|
return audioPlayer.getAudioContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化编码器
|
// Initialize encoder
|
||||||
initEncoder() {
|
initEncoder() {
|
||||||
if (!this.opusEncoder) {
|
if (!this.opusEncoder) {
|
||||||
this.opusEncoder = initOpusEncoder();
|
this.opusEncoder = initOpusEncoder();
|
||||||
@@ -45,7 +45,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 {
|
||||||
@@ -104,7 +104,7 @@ export class AudioRecorder {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建音频处理器
|
// Create audio processor
|
||||||
async createAudioProcessor() {
|
async createAudioProcessor() {
|
||||||
this.audioContext = this.getAudioContext();
|
this.audioContext = this.getAudioContext();
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ export class AudioRecorder {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
log('使用AudioWorklet处理音频', 'success');
|
log('Using AudioWorklet to process audio', 'success');
|
||||||
|
|
||||||
const silent = this.audioContext.createGain();
|
const silent = this.audioContext.createGain();
|
||||||
silent.gain.value = 0;
|
silent.gain.value = 0;
|
||||||
@@ -131,16 +131,16 @@ export class AudioRecorder {
|
|||||||
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 not available, using ScriptProcessorNode as fallback', 'warning');
|
||||||
return this.createScriptProcessor();
|
return this.createScriptProcessor();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`创建音频处理器失败: ${error.message},尝试回退方案`, 'error');
|
log(`Failed to create audio processor: ${error.message}, trying fallback`, 'error');
|
||||||
return this.createScriptProcessor();
|
return this.createScriptProcessor();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建ScriptProcessor作为回退
|
// Create ScriptProcessor as fallback
|
||||||
createScriptProcessor() {
|
createScriptProcessor() {
|
||||||
try {
|
try {
|
||||||
const frameSize = 4096;
|
const frameSize = 4096;
|
||||||
@@ -164,15 +164,15 @@ export class AudioRecorder {
|
|||||||
scriptProcessor.connect(silent);
|
scriptProcessor.connect(silent);
|
||||||
silent.connect(this.audioContext.destination);
|
silent.connect(this.audioContext.destination);
|
||||||
|
|
||||||
log('使用ScriptProcessorNode作为回退方案成功', 'warning');
|
log('Using ScriptProcessorNode as fallback successfully', 'warning');
|
||||||
return { node: scriptProcessor, type: 'processor' };
|
return { node: scriptProcessor, type: 'processor' };
|
||||||
} catch (fallbackError) {
|
} catch (fallbackError) {
|
||||||
log(`回退方案也失败: ${fallbackError.message}`, 'error');
|
log(`Fallback also failed: ${fallbackError.message}`, 'error');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理PCM缓冲数据
|
// Process PCM buffer data
|
||||||
processPCMBuffer(buffer) {
|
processPCMBuffer(buffer) {
|
||||||
if (!this.isRecording) return;
|
if (!this.isRecording) return;
|
||||||
|
|
||||||
@@ -191,10 +191,10 @@ export class AudioRecorder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 编码并发送Opus数据
|
// Encode and send Opus data
|
||||||
encodeAndSendOpus(pcmData = null) {
|
encodeAndSendOpus(pcmData = null) {
|
||||||
if (!this.opusEncoder) {
|
if (!this.opusEncoder) {
|
||||||
log('Opus编码器未初始化', 'error');
|
log('Opus encoder not initialized', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,13 +209,13 @@ export class AudioRecorder {
|
|||||||
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');
|
log(`Sent Opus frame, size: ${opusData.length} bytes`, 'debug');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`WebSocket发送错误: ${error.message}`, 'error');
|
log(`WebSocket send error: ${error.message}`, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log('Opus编码失败,无有效数据返回', 'error');
|
log('Opus encoding failed, no valid data returned', 'error');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this.pcmDataBuffer.length > 0) {
|
if (this.pcmDataBuffer.length > 0) {
|
||||||
@@ -231,20 +231,20 @@ export class AudioRecorder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`Opus编码错误: ${error.message}`, 'error');
|
log(`Opus encoding error: ${error.message}`, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始录音
|
// 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');
|
||||||
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,
|
session_id: wsHandler.currentSessionId,
|
||||||
@@ -254,16 +254,16 @@ export class AudioRecorder {
|
|||||||
|
|
||||||
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('Sent abort message', 'info');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.initEncoder()) {
|
if (!this.initEncoder()) {
|
||||||
log('无法启动录音: Opus编码器初始化失败', 'error');
|
log('Unable to start recording: Opus encoder initialization failed', 'error');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
log('请至少录制1-2秒钟的音频,确保采集到足够数据', 'info');
|
log('Please record at least 1-2 seconds of audio to ensure sufficient data is collected', 'info');
|
||||||
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
audio: {
|
audio: {
|
||||||
@@ -282,7 +282,7 @@ export class AudioRecorder {
|
|||||||
|
|
||||||
const processorResult = await this.createAudioProcessor();
|
const processorResult = await this.createAudioProcessor();
|
||||||
if (!processorResult) {
|
if (!processorResult) {
|
||||||
log('无法创建音频处理器', 'error');
|
log('Unable to create audio processor', 'error');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,26 +305,26 @@ export class AudioRecorder {
|
|||||||
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(`Sent recording start message`, 'info');
|
||||||
} else {
|
} else {
|
||||||
log('WebSocket未连接,无法发送开始消息', 'error');
|
log('WebSocket not connected, unable to send start message', '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;
|
||||||
@@ -333,16 +333,16 @@ export class AudioRecorder {
|
|||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
log('开始PCM直接录音', 'success');
|
log('Started PCM direct recording', 'success');
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`直接录音启动错误: ${error.message}`, 'error');
|
log(`Direct recording start error: ${error.message}`, 'error');
|
||||||
this.isRecording = false;
|
this.isRecording = false;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始可视化
|
// Start visualization
|
||||||
startVisualization(dataArray) {
|
startVisualization(dataArray) {
|
||||||
const draw = () => {
|
const draw = () => {
|
||||||
this.visualizationRequest = requestAnimationFrame(() => draw());
|
this.visualizationRequest = requestAnimationFrame(() => draw());
|
||||||
@@ -358,7 +358,7 @@ export class AudioRecorder {
|
|||||||
draw();
|
draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止录音
|
// Stop recording
|
||||||
stop() {
|
stop() {
|
||||||
if (!this.isRecording) return false;
|
if (!this.isRecording) return false;
|
||||||
|
|
||||||
@@ -389,35 +389,35 @@ export class AudioRecorder {
|
|||||||
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('Sent recording stop signal', 'info');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.onRecordingStop) {
|
if (this.onRecordingStop) {
|
||||||
this.onRecordingStop();
|
this.onRecordingStop();
|
||||||
}
|
}
|
||||||
|
|
||||||
log('停止PCM直接录音', 'success');
|
log('Stopped PCM direct recording', 'success');
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`直接录音停止错误: ${error.message}`, 'error');
|
log(`Direct recording stop error: ${error.message}`, 'error');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取分析器
|
// 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 +426,65 @@ 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('Browser does not support 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('Microphone availability check successful', 'success');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
log(`Microphone not available: ${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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Audio recording module tests
|
||||||
|
* Test microphone availability detection functionality
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Note: These are unit test examples showing how to test new features
|
||||||
|
// In actual projects, you can use Jest, Mocha or other testing frameworks
|
||||||
|
|
||||||
|
describe('Microphone Availability Detection', () => {
|
||||||
|
/**
|
||||||
|
* Test checkMicrophoneAvailability function
|
||||||
|
* Note: Actual tests need to mock navigator.mediaDevices
|
||||||
|
*/
|
||||||
|
test('should detect microphone availability', async () => {
|
||||||
|
// Mock navigator.mediaDevices.getUserMedia
|
||||||
|
const mockStream = {
|
||||||
|
getTracks: () => [{ stop: jest.fn() }]
|
||||||
|
};
|
||||||
|
|
||||||
|
global.navigator = {
|
||||||
|
mediaDevices: {
|
||||||
|
getUserMedia: jest.fn().mockResolvedValue(mockStream)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Import function (needs to be adjusted according to actual module system)
|
||||||
|
// const { checkMicrophoneAvailability } = await import('./recorder.js');
|
||||||
|
// const result = await checkMicrophoneAvailability();
|
||||||
|
// expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test isHttpNonLocalhost function
|
||||||
|
*/
|
||||||
|
test('should detect HTTP non-localhost correctly', () => {
|
||||||
|
// Mock window.location
|
||||||
|
const originalLocation = window.location;
|
||||||
|
|
||||||
|
// Test HTTP non-localhost
|
||||||
|
delete window.location;
|
||||||
|
window.location = {
|
||||||
|
protocol: 'http:',
|
||||||
|
hostname: '192.168.1.100'
|
||||||
|
};
|
||||||
|
|
||||||
|
// const { isHttpNonLocalhost } = require('./recorder.js');
|
||||||
|
// expect(isHttpNonLocalhost()).toBe(true);
|
||||||
|
|
||||||
|
// Test localhost (should return false)
|
||||||
|
window.location.hostname = 'localhost';
|
||||||
|
// expect(isHttpNonLocalhost()).toBe(false);
|
||||||
|
|
||||||
|
// Restore original location
|
||||||
|
window.location = originalLocation;
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -216,7 +216,7 @@ function setupMcpEventListeners() {
|
|||||||
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 ? '收起' : '展开';
|
||||||
});
|
});
|
||||||
|
|
||||||
// 确保面板默认展开
|
// 确保面板默认展开
|
||||||
@@ -424,6 +424,74 @@ export function getMcpTools() {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行Live2D动作
|
||||||
|
* @param {string} toolName - 工具名称,如 'live2d.smile', 'live2d.wave', 'live2d.action'
|
||||||
|
* @param {Object} toolArgs - 工具参数,对于 'live2d.action' 工具,应包含 'action' 字段
|
||||||
|
* @returns {Object} 执行结果,包含 success, message, action, tool 等字段
|
||||||
|
*/
|
||||||
|
function executeLive2DAction(toolName, toolArgs) {
|
||||||
|
try {
|
||||||
|
const live2dManager = window.chatApp?.live2dManager;
|
||||||
|
if (!live2dManager) {
|
||||||
|
log('Live2D管理器未初始化', 'error');
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Live2D管理器未初始化'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动作映射:工具名称到Live2D动作名称
|
||||||
|
const actionMap = {
|
||||||
|
'live2d.smile': 'FlickUp',
|
||||||
|
'live2d.wave': 'Tap',
|
||||||
|
'live2d.happy': 'FlickUp',
|
||||||
|
'live2d.sad': 'FlickDown',
|
||||||
|
'live2d.tap': 'Tap',
|
||||||
|
'live2d.tapBody': 'Tap@Body',
|
||||||
|
'live2d.flick': 'Flick',
|
||||||
|
'live2d.flickBody': 'Flick@Body',
|
||||||
|
'live2d.flickUp': 'FlickUp',
|
||||||
|
'live2d.flickDown': 'FlickDown'
|
||||||
|
};
|
||||||
|
|
||||||
|
let motionName;
|
||||||
|
if (toolName === 'live2d.action' && toolArgs?.action) {
|
||||||
|
// 通用动作工具,使用指定的动作名称
|
||||||
|
motionName = toolArgs.action;
|
||||||
|
} else {
|
||||||
|
// 使用映射表查找对应动作名称
|
||||||
|
motionName = actionMap[toolName];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!motionName) {
|
||||||
|
log(`未知的动作: ${toolName}`, 'error');
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `未知的动作: ${toolName}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发Live2D动作
|
||||||
|
live2dManager.motion(motionName);
|
||||||
|
|
||||||
|
log(`Live2D动作已触发: ${motionName}`, 'success');
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `虚拟人已执行动作: ${motionName}`,
|
||||||
|
action: motionName,
|
||||||
|
tool: toolName
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
log(`执行Live2D动作失败: ${error.message}`, 'error');
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `执行动作失败: ${error.message}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行工具调用
|
* 执行工具调用
|
||||||
*/
|
*/
|
||||||
@@ -438,6 +506,11 @@ export function executeMcpTool(toolName, toolArgs) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理Live2D动作工具
|
||||||
|
if (toolName.startsWith('live2d.')) {
|
||||||
|
return executeLive2DAction(toolName, toolArgs);
|
||||||
|
}
|
||||||
|
|
||||||
// 如果有模拟返回结果,使用它
|
// 如果有模拟返回结果,使用它
|
||||||
if (tool.mockResponse) {
|
if (tool.mockResponse) {
|
||||||
// 替换模板变量
|
// 替换模板变量
|
||||||
@@ -477,4 +550,4 @@ window.mcpModule = {
|
|||||||
deleteMcpProperty,
|
deleteMcpProperty,
|
||||||
editMcpTool,
|
editMcpTool,
|
||||||
deleteMcpTool
|
deleteMcpTool
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* MCP工具模块测试
|
||||||
|
* 测试Live2D动作工具执行功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe('Live2D Action Tools', () => {
|
||||||
|
/**
|
||||||
|
* 测试 executeLive2DAction 函数
|
||||||
|
* 注意:需要 mock window.chatApp.live2dManager
|
||||||
|
*/
|
||||||
|
test('should execute Live2D smile action', () => {
|
||||||
|
// Mock Live2D manager
|
||||||
|
const mockLive2DManager = {
|
||||||
|
motion: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
window.chatApp = {
|
||||||
|
live2dManager: mockLive2DManager
|
||||||
|
};
|
||||||
|
|
||||||
|
// 测试 smile 动作
|
||||||
|
// const result = executeLive2DAction('live2d.smile', {});
|
||||||
|
// expect(result.success).toBe(true);
|
||||||
|
// expect(result.action).toBe('FlickUp');
|
||||||
|
// expect(mockLive2DManager.motion).toHaveBeenCalledWith('FlickUp');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should execute Live2D wave action', () => {
|
||||||
|
// Mock Live2D manager
|
||||||
|
const mockLive2DManager = {
|
||||||
|
motion: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
window.chatApp = {
|
||||||
|
live2dManager: mockLive2DManager
|
||||||
|
};
|
||||||
|
|
||||||
|
// 测试 wave 动作
|
||||||
|
// const result = executeLive2DAction('live2d.wave', {});
|
||||||
|
// expect(result.success).toBe(true);
|
||||||
|
// expect(result.action).toBe('Tap');
|
||||||
|
// expect(mockLive2DManager.motion).toHaveBeenCalledWith('Tap');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle generic action tool', () => {
|
||||||
|
// Mock Live2D manager
|
||||||
|
const mockLive2DManager = {
|
||||||
|
motion: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
window.chatApp = {
|
||||||
|
live2dManager: mockLive2DManager
|
||||||
|
};
|
||||||
|
|
||||||
|
// 测试通用动作工具
|
||||||
|
// const result = executeLive2DAction('live2d.action', { action: 'FlickDown' });
|
||||||
|
// expect(result.success).toBe(true);
|
||||||
|
// expect(result.action).toBe('FlickDown');
|
||||||
|
// expect(mockLive2DManager.motion).toHaveBeenCalledWith('FlickDown');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle missing Live2D manager gracefully', () => {
|
||||||
|
window.chatApp = null;
|
||||||
|
|
||||||
|
// const result = executeLive2DAction('live2d.smile', {});
|
||||||
|
// expect(result.success).toBe(false);
|
||||||
|
// expect(result.error).toContain('Live2D管理器未初始化');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle unknown action gracefully', () => {
|
||||||
|
const mockLive2DManager = {
|
||||||
|
motion: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
window.chatApp = {
|
||||||
|
live2dManager: mockLive2DManager
|
||||||
|
};
|
||||||
|
|
||||||
|
// const result = executeLive2DAction('live2d.unknown', {});
|
||||||
|
// expect(result.success).toBe(false);
|
||||||
|
// expect(result.error).toContain('未知的动作');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
// UI控制模块
|
// UI controller module
|
||||||
import { loadConfig, saveConfig } from '../config/manager.js';
|
import { loadConfig, saveConfig } from '../config/manager.js';
|
||||||
import { getAudioRecorder } from '../core/audio/recorder.js';
|
import { getAudioRecorder } from '../core/audio/recorder.js';
|
||||||
import { getWebSocketHandler } from '../core/network/websocket.js';
|
import { getWebSocketHandler } from '../core/network/websocket.js';
|
||||||
import { getAudioPlayer } from '../core/audio/player.js';
|
import { getAudioPlayer } from '../core/audio/player.js';
|
||||||
|
|
||||||
// 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,47 @@ 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 +276,49 @@ 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
|
||||||
|
const microphoneAvailable = window.microphoneAvailable !== false;
|
||||||
|
recordBtn.disabled = !microphoneAvailable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加聊天消息
|
/**
|
||||||
|
* 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访问,无法录音,只能用文字交互'
|
||||||
|
: '麦克风不可用';
|
||||||
|
|
||||||
|
// Display notification message in chat
|
||||||
|
if (isHttpNonLocalhost) {
|
||||||
|
this.addChatMessage('⚠️ 当前由于是http访问,无法录音,只能用文字交互', false);
|
||||||
|
} else {
|
||||||
|
this.addChatMessage('⚠️ 麦克风不可用,请检查权限设置', false);
|
||||||
|
}
|
||||||
|
} 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 +328,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 +341,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 +349,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 +357,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 +376,24 @@ class UIController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 连接成功后开始对话
|
// Start AI chat session after connection
|
||||||
startAIChatSession() {
|
startAIChatSession() {
|
||||||
this.addChatMessage('连接成功,开始聊天吧~🙂', false);
|
this.addChatMessage('连接成功,开始聊天吧~😊', false);
|
||||||
// 开启录音
|
// Start recording
|
||||||
const recordBtn = document.getElementById('recordBtn');
|
const recordBtn = document.getElementById('recordBtn');
|
||||||
if (recordBtn) {
|
if (recordBtn) {
|
||||||
recordBtn.click();
|
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 +408,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 +416,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,24 +426,36 @@ class UIController {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// 获取WebSocket处理器
|
// Get WebSocket handler instance
|
||||||
const wsHandler = getWebSocketHandler();
|
const wsHandler = getWebSocketHandler();
|
||||||
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');
|
||||||
|
const micAvailable = await checkMicrophoneAvailability();
|
||||||
|
|
||||||
|
if (!micAvailable) {
|
||||||
|
const isHttp = window.isHttpNonLocalhost;
|
||||||
|
if (isHttp) {
|
||||||
|
this.addChatMessage('⚠️ 当前由于是http访问,无法录音,只能用文字交互', false);
|
||||||
|
}
|
||||||
|
// Update global state
|
||||||
|
window.microphoneAvailable = false;
|
||||||
|
}
|
||||||
|
|
||||||
// 设置连接状态回调
|
// Register connection state callback
|
||||||
wsHandler.onConnectionStateChange = (isConnected) => {
|
wsHandler.onConnectionStateChange = (isConnected) => {
|
||||||
this.updateConnectionUI(isConnected);
|
this.updateConnectionUI(isConnected);
|
||||||
this.updateDialButton(isConnected);
|
this.updateDialButton(isConnected);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 设置聊天消息回调
|
// Register chat message callback
|
||||||
wsHandler.onChatMessage = (text, isUser) => {
|
wsHandler.onChatMessage = (text, isUser) => {
|
||||||
this.addChatMessage(text, isUser);
|
this.addChatMessage(text, isUser);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 设置录音按钮状态回调
|
// Register record button state callback
|
||||||
wsHandler.onRecordButtonStateChange = (isRecording) => {
|
wsHandler.onRecordButtonStateChange = (isRecording) => {
|
||||||
const recordBtn = document.getElementById('recordBtn');
|
const recordBtn = document.getElementById('recordBtn');
|
||||||
if (recordBtn) {
|
if (recordBtn) {
|
||||||
@@ -411,10 +469,10 @@ class UIController {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 连接成功
|
// Connection successful
|
||||||
this.addChatMessage('OTA连接成功,正在建立WebSocket连接...', false);
|
this.addChatMessage('OTA连接成功,正在建立WebSocket连接...', false);
|
||||||
|
|
||||||
// 更新拨号按钮状态
|
// Update dial button state
|
||||||
const dialBtn = document.getElementById('dialBtn');
|
const dialBtn = document.getElementById('dialBtn');
|
||||||
if (dialBtn) {
|
if (dialBtn) {
|
||||||
dialBtn.disabled = false;
|
dialBtn.disabled = false;
|
||||||
@@ -434,14 +492,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 +510,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;
|
||||||
@@ -462,16 +520,16 @@ class UIController {
|
|||||||
toolDiv.className = 'properties-container';
|
toolDiv.className = 'properties-container';
|
||||||
toolDiv.innerHTML = `
|
toolDiv.innerHTML = `
|
||||||
<div class="property-item">
|
<div class="property-item">
|
||||||
<input type="text" placeholder="工具名称" value="新工具">
|
<input type="text" placeholder="工具名称" value="新工具">
|
||||||
<input type="text" placeholder="工具描述" value="工具描述">
|
<input type="text" placeholder="工具描述" value="工具描述">
|
||||||
<button class="remove-property" onclick="uiController.removeMCPTool('${toolId}')">删除</button>
|
<button class="remove-property" onclick="uiController.removeMCPTool('${toolId}')">σêáΘÖñ</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
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 +537,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 +562,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 +576,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 +588,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 };
|
||||||
Reference in New Issue
Block a user