diff --git a/main/xiaozhi-server/test/favicon.ico b/main/xiaozhi-server/test/favicon.ico new file mode 100644 index 00000000..5f285a58 Binary files /dev/null and b/main/xiaozhi-server/test/favicon.ico differ diff --git a/main/xiaozhi-server/test/js/config/default-mcp-tools.json b/main/xiaozhi-server/test/js/config/default-mcp-tools.json index f4620a1a..2d44f2dc 100644 --- a/main/xiaozhi-server/test/js/config/default-mcp-tools.json +++ b/main/xiaozhi-server/test/js/config/default-mcp-tools.json @@ -68,37 +68,5 @@ "brightness": "${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" - ] - } } ] \ No newline at end of file diff --git a/main/xiaozhi-server/test/js/core/audio/recorder.js b/main/xiaozhi-server/test/js/core/audio/recorder.js index c7d95790..9c744cf6 100644 --- a/main/xiaozhi-server/test/js/core/audio/recorder.js +++ b/main/xiaozhi-server/test/js/core/audio/recorder.js @@ -174,7 +174,6 @@ export class AudioRecorder { if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { try { this.websocket.send(opusData.buffer); - log(`已发送Opus帧,大小: ${opusData.length} 字节`, 'debug'); } catch (error) { log(`WebSocket发送错误: ${error.message}`, 'error'); } diff --git a/main/xiaozhi-server/test/js/core/audio/recorder.test.browser.js b/main/xiaozhi-server/test/js/core/audio/recorder.test.browser.js deleted file mode 100644 index 38f5e46e..00000000 --- a/main/xiaozhi-server/test/js/core/audio/recorder.test.browser.js +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Audio recording module tests - Browser compatible version - * Test microphone availability detection functionality - * - * This version works without Vitest - uses the simple test framework from test-runner.html - */ - -import { checkMicrophoneAvailability, isHttpNonLocalhost } from './recorder.js'; - -describe('Microphone Availability Detection', () => { - beforeEach(() => { - // Reset mocks before each test - vi.clearAllMocks(); - }); - - /** - * Test checkMicrophoneAvailability function - success case - */ - test('should return true when microphone is available', async () => { - // Mock navigator.mediaDevices.getUserMedia to return a successful stream - const mockTrack = { stop: vi.fn() }; - const mockStream = { getTracks: () => [mockTrack] }; - navigator.mediaDevices.getUserMedia = vi.fn().mockResolvedValue(mockStream); - const result = await checkMicrophoneAvailability(); - expect(result).toBe(true); - expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalledWith({ audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 16000, channelCount: 1 } }); - expect(mockTrack.stop).toHaveBeenCalled(); - }); - - /** - * Test checkMicrophoneAvailability function - failure case - */ - test('should return false when microphone is not available', async () => { - // Mock getUserMedia to throw an error - navigator.mediaDevices.getUserMedia = vi.fn().mockRejectedValue(new Error('Permission denied')); - const result = await checkMicrophoneAvailability(); - expect(result).toBe(false); - expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalled(); - }); - - /** - * Test checkMicrophoneAvailability function - browser not supported - */ - test('should return false when browser does not support getUserMedia', async () => { - // Mock navigator.mediaDevices.getUserMedia to be undefined - const originalGetUserMedia = navigator.mediaDevices.getUserMedia; - navigator.mediaDevices.getUserMedia = undefined; - const result = await checkMicrophoneAvailability(); - expect(result).toBe(false); - // Restore - navigator.mediaDevices.getUserMedia = originalGetUserMedia; - }); - - /** - * Test isHttpNonLocalhost function - HTTP non-localhost - * Note: window.location properties are read-only in browsers, so we test the logic indirectly - */ - test('should return true for HTTP non-localhost access', () => { - // Since window.location is read-only, we'll test by checking the actual implementation - // This test verifies the function works correctly with the current location - // In a real browser environment, this would test against actual location - const result = isHttpNonLocalhost(); - // Just verify the function runs without error - expect(typeof result).toBe('boolean'); - }); - - /** - * Test isHttpNonLocalhost function - localhost should return false - * Note: window.location properties are read-only in browsers - */ - test('should return false for localhost', () => { - // Test the logic by checking if current location is localhost - const result = isHttpNonLocalhost(); - // If we're on localhost, result should be false - if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { - expect(result).toBe(false); - } else { - // Otherwise just verify function returns boolean - expect(typeof result).toBe('boolean'); - } - }); - - /** - * Test isHttpNonLocalhost function - 127.0.0.1 should return false - * Note: window.location properties are read-only in browsers - */ - test('should return false for 127.0.0.1', () => { - // Test the logic by checking if current location is 127.0.0.1 - const result = isHttpNonLocalhost(); - // If we're on 127.0.0.1, result should be false - if (window.location.hostname === '127.0.0.1') { - expect(result).toBe(false); - } else { - // Otherwise just verify function returns boolean - expect(typeof result).toBe('boolean'); - } - }); - - /** - * Test isHttpNonLocalhost function - private IP should return false - * Note: window.location properties are read-only in browsers - */ - test('should return false for private IP addresses', () => { - // Test the logic by checking if current location is a private IP - const result = isHttpNonLocalhost(); - const hostname = window.location.hostname; - const isPrivateIP = hostname.startsWith('192.168.') || hostname.startsWith('10.') || hostname.startsWith('172.'); - if (isPrivateIP && window.location.protocol === 'http:') { - expect(result).toBe(false); - } else { - // Otherwise just verify function returns boolean - expect(typeof result).toBe('boolean'); - } - }); - - /** - * Test isHttpNonLocalhost function - HTTPS should return false - * Note: window.location properties are read-only in browsers - */ - test('should return false for HTTPS protocol', () => { - // Test the logic by checking if current protocol is HTTPS - const result = isHttpNonLocalhost(); - // If we're on HTTPS, result should be false - if (window.location.protocol === 'https:') { - expect(result).toBe(false); - } else { - // Otherwise just verify function returns boolean - expect(typeof result).toBe('boolean'); - } - }); -}); diff --git a/main/xiaozhi-server/test/js/core/audio/recorder.test.js b/main/xiaozhi-server/test/js/core/audio/recorder.test.js deleted file mode 100644 index c160e5f2..00000000 --- a/main/xiaozhi-server/test/js/core/audio/recorder.test.js +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Audio recording module tests - * Test microphone availability detection functionality - */ - -import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'; -import { checkMicrophoneAvailability, isHttpNonLocalhost } from './recorder.js'; - -describe('Microphone Availability Detection', () => { - beforeEach(() => { - // Reset mocks before each test - vi.clearAllMocks(); - }); - - /** - * Test checkMicrophoneAvailability function - success case - */ - test('should return true when microphone is available', async () => { - // Mock navigator.mediaDevices.getUserMedia to return a successful stream - const mockTrack = { stop: vi.fn() }; - const mockStream = { getTracks: () => [mockTrack] }; - global.navigator.mediaDevices.getUserMedia = vi.fn().mockResolvedValue(mockStream); - const result = await checkMicrophoneAvailability(); - expect(result).toBe(true); - expect(global.navigator.mediaDevices.getUserMedia).toHaveBeenCalledWith({ audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 16000, channelCount: 1 } }); - expect(mockTrack.stop).toHaveBeenCalled(); - }); - - /** - * Test checkMicrophoneAvailability function - failure case - */ - test('should return false when microphone is not available', async () => { - // Mock getUserMedia to throw an error - global.navigator.mediaDevices.getUserMedia = vi.fn().mockRejectedValue(new Error('Permission denied')); - const result = await checkMicrophoneAvailability(); - expect(result).toBe(false); - expect(global.navigator.mediaDevices.getUserMedia).toHaveBeenCalled(); - }); - - /** - * Test checkMicrophoneAvailability function - browser not supported - */ - test('should return false when browser does not support getUserMedia', async () => { - // Mock navigator without mediaDevices - const originalMediaDevices = global.navigator.mediaDevices; - delete global.navigator.mediaDevices; - const result = await checkMicrophoneAvailability(); - expect(result).toBe(false); - // Restore - global.navigator.mediaDevices = originalMediaDevices; - }); - - /** - * Test isHttpNonLocalhost function - HTTP non-localhost - */ - test('should return true for HTTP non-localhost access', () => { - // Mock window.location for HTTP non-localhost - Object.defineProperty(window, 'location', { value: { protocol: 'http:', hostname: 'example.com' }, writable: true, configurable: true }); - const result = isHttpNonLocalhost(); - expect(result).toBe(true); - }); - - /** - * Test isHttpNonLocalhost function - localhost should return false - */ - test('should return false for localhost', () => { - Object.defineProperty(window, 'location', { value: { protocol: 'http:', hostname: 'localhost' }, writable: true, configurable: true }); - const result = isHttpNonLocalhost(); - expect(result).toBe(false); - }); - - /** - * Test isHttpNonLocalhost function - 127.0.0.1 should return false - */ - test('should return false for 127.0.0.1', () => { - Object.defineProperty(window, 'location', { value: { protocol: 'http:', hostname: '127.0.0.1' }, writable: true, configurable: true }); - const result = isHttpNonLocalhost(); - expect(result).toBe(false); - }); - - /** - * Test isHttpNonLocalhost function - private IP should return false - */ - test('should return false for private IP addresses', () => { - const privateIPs = ['192.168.1.100', '10.0.0.1', '172.16.0.1']; - privateIPs.forEach(ip => { - Object.defineProperty(window, 'location', { value: { protocol: 'http:', hostname: ip }, writable: true, configurable: true }); - const result = isHttpNonLocalhost(); - expect(result).toBe(false); - }); - }); - - /** - * Test isHttpNonLocalhost function - HTTPS should return false - */ - test('should return false for HTTPS protocol', () => { - Object.defineProperty(window, 'location', { value: { protocol: 'https:', hostname: 'example.com' }, writable: true, configurable: true }); - const result = isHttpNonLocalhost(); - expect(result).toBe(false); - }); -}); diff --git a/main/xiaozhi-server/test/js/core/mcp/tools.js b/main/xiaozhi-server/test/js/core/mcp/tools.js index 02a3e09e..c7070a83 100644 --- a/main/xiaozhi-server/test/js/core/mcp/tools.js +++ b/main/xiaozhi-server/test/js/core/mcp/tools.js @@ -305,43 +305,6 @@ export function getMcpTools() { return mcpTools.map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema })); } -/** - * 执行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}` }; - } -} - /** * 执行工具调用 */ @@ -351,10 +314,6 @@ export function executeMcpTool(toolName, toolArgs) { log(`未找到工具: ${toolName}`, 'error'); return { success: false, error: `未知工具: ${toolName}` }; } - // 处理Live2D动作工具 - if (toolName.startsWith('live2d.')) { - return executeLive2DAction(toolName, toolArgs); - } // 如果有模拟返回结果,使用它 if (tool.mockResponse) { // 替换模板变量 diff --git a/main/xiaozhi-server/test/js/core/mcp/tools.test.browser.js b/main/xiaozhi-server/test/js/core/mcp/tools.test.browser.js deleted file mode 100644 index 44043745..00000000 --- a/main/xiaozhi-server/test/js/core/mcp/tools.test.browser.js +++ /dev/null @@ -1,98 +0,0 @@ -/** - * MCP工具模块测试 - Browser compatible version - * 测试Live2D动作工具执行功能 - * - * This version works without Vitest - uses the simple test framework from test-runner.html - */ - -import { executeMcpTool, initMcpTools, setWebSocket } from './tools.js'; - -describe('Live2D Action Tools', () => { - let mockLive2DManager, originalChatApp; - - beforeEach(() => { - // Reset mocks before each test - vi.clearAllMocks(); - // Save original chatApp - originalChatApp = window.chatApp; - // Mock Live2D manager - mockLive2DManager = { motion: vi.fn() }; - // Setup window.chatApp - window.chatApp = { live2dManager: mockLive2DManager }; - // Mock localStorage - localStorage.getItem = vi.fn(() => null); - // Mock fetch for default-mcp-tools.json - globalThis.fetch = vi.fn(() => Promise.resolve({ json: () => Promise.resolve([{ name: 'live2d.smile', description: 'Make the virtual human smile', inputSchema: { type: 'object', properties: {} } }, { name: 'live2d.wave', description: 'Make the virtual human wave', inputSchema: { type: 'object', properties: {} } }, { name: 'live2d.action', description: 'Trigger a specified action', inputSchema: { type: 'object', properties: { action: { type: 'string' } }, required: ['action'] } }]) })); - // Mock DOM elements - ensure all required elements exist - const mockContainer = { innerHTML: '', appendChild: vi.fn(), textContent: '' }; - document.getElementById = vi.fn((id) => { - // Return mock elements for all IDs that tools.js might access - if (id === 'mcpToolsContainer' || id === 'mcpPropertiesContainer') return mockContainer; - if (id === 'mcpToolsCount') return { textContent: '' }; - // Return null for other elements (they're checked with if statements) - return null; - }); - }); - - afterEach(() => { - // Clean up - window.chatApp = originalChatApp; - }); - - /** - * 测试 executeMcpTool - smile 动作 - */ - test('should execute Live2D smile action', async () => { - await initMcpTools(); - const result = executeMcpTool('live2d.smile', {}); - expect(result.success).toBe(true); - expect(result.action).toBe('FlickUp'); - expect(result.tool).toBe('live2d.smile'); - expect(mockLive2DManager.motion).toHaveBeenCalledWith('FlickUp'); - }); - - /** - * 测试 executeMcpTool - wave 动作 - */ - test('should execute Live2D wave action', async () => { - await initMcpTools(); - const result = executeMcpTool('live2d.wave', {}); - expect(result.success).toBe(true); - expect(result.action).toBe('Tap'); - expect(result.tool).toBe('live2d.wave'); - expect(mockLive2DManager.motion).toHaveBeenCalledWith('Tap'); - }); - - /** - * 测试 executeMcpTool - 通用动作工具 - */ - test('should handle generic action tool', async () => { - await initMcpTools(); - const result = executeMcpTool('live2d.action', { action: 'FlickDown' }); - expect(result.success).toBe(true); - expect(result.action).toBe('FlickDown'); - expect(result.tool).toBe('live2d.action'); - expect(mockLive2DManager.motion).toHaveBeenCalledWith('FlickDown'); - }); - - /** - * 测试 executeMcpTool - Live2D管理器未初始化 - */ - test('should handle missing Live2D manager gracefully', async () => { - await initMcpTools(); - window.chatApp = null; - const result = executeMcpTool('live2d.smile', {}); - expect(result.success).toBe(false); - expect(result.error).toContain('Live2D管理器未初始化'); - }); - - /** - * 测试 executeMcpTool - 未知的工具 - */ - test('should handle unknown tool gracefully', async () => { - await initMcpTools(); - const result = executeMcpTool('unknown.tool', {}); - expect(result.success).toBe(false); - expect(result.error).toContain('未知工具'); - }); -}); diff --git a/main/xiaozhi-server/test/js/core/mcp/tools.test.js b/main/xiaozhi-server/test/js/core/mcp/tools.test.js deleted file mode 100644 index 237b1e28..00000000 --- a/main/xiaozhi-server/test/js/core/mcp/tools.test.js +++ /dev/null @@ -1,133 +0,0 @@ -/** - * MCP工具模块测试 - * 测试Live2D动作工具执行功能 - */ - -import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'; -import { executeMcpTool, initMcpTools, setWebSocket } from './tools.js'; - -describe('Live2D Action Tools', () => { - let mockLive2DManager; - - beforeEach(() => { - // Reset mocks before each test - vi.clearAllMocks(); - // Mock Live2D manager - mockLive2DManager = { motion: vi.fn() }; - // Setup window.chatApp - global.window.chatApp = { live2dManager: mockLive2DManager }; - // Mock localStorage - global.localStorage.getItem = vi.fn(() => null); - // Mock fetch for default-mcp-tools.json - global.fetch = vi.fn(() => Promise.resolve({ json: () => Promise.resolve([{ name: 'live2d.smile', description: 'Make the virtual human smile', inputSchema: { type: 'object', properties: {} } }, { name: 'live2d.wave', description: 'Make the virtual human wave', inputSchema: { type: 'object', properties: {} } }, { name: 'live2d.action', description: 'Trigger a specified action', inputSchema: { type: 'object', properties: { action: { type: 'string' } }, required: ['action'] } }]) })); - // Mock DOM elements - global.document.getElementById = vi.fn((id) => { - if (id === 'mcpToolsContainer') { - return { innerHTML: '', appendChild: vi.fn() }; - } - if (id === 'mcpToolsCount') { - return { textContent: '' }; - } - return null; - }); - }); - - afterEach(() => { - // Clean up - global.window.chatApp = null; - }); - - /** - * 测试 executeMcpTool - smile 动作 - */ - test('should execute Live2D smile action', async () => { - await initMcpTools(); - const result = executeMcpTool('live2d.smile', {}); - expect(result.success).toBe(true); - expect(result.action).toBe('FlickUp'); - expect(result.tool).toBe('live2d.smile'); - expect(mockLive2DManager.motion).toHaveBeenCalledWith('FlickUp'); - }); - - /** - * 测试 executeMcpTool - wave 动作 - */ - test('should execute Live2D wave action', async () => { - await initMcpTools(); - const result = executeMcpTool('live2d.wave', {}); - expect(result.success).toBe(true); - expect(result.action).toBe('Tap'); - expect(result.tool).toBe('live2d.wave'); - expect(mockLive2DManager.motion).toHaveBeenCalledWith('Tap'); - }); - - /** - * 测试 executeMcpTool - 通用动作工具 - */ - test('should handle generic action tool', async () => { - await initMcpTools(); - const result = executeMcpTool('live2d.action', { action: 'FlickDown' }); - expect(result.success).toBe(true); - expect(result.action).toBe('FlickDown'); - expect(result.tool).toBe('live2d.action'); - expect(mockLive2DManager.motion).toHaveBeenCalledWith('FlickDown'); - }); - - /** - * 测试 executeMcpTool - Live2D管理器未初始化 - */ - test('should handle missing Live2D manager gracefully', async () => { - await initMcpTools(); - global.window.chatApp = null; - const result = executeMcpTool('live2d.smile', {}); - expect(result.success).toBe(false); - expect(result.error).toContain('Live2D管理器未初始化'); - }); - - /** - * 测试 executeMcpTool - 未知的动作 - */ - test('should handle unknown action gracefully', async () => { - await initMcpTools(); - // Add an unknown live2d tool to the list - const tools = await global.fetch().then(res => res.json()); - tools.push({ name: 'live2d.unknown', description: 'Unknown action', inputSchema: { type: 'object', properties: {} } }); - global.fetch = vi.fn(() => Promise.resolve({ json: () => Promise.resolve(tools) })); - await initMcpTools(); - const result = executeMcpTool('live2d.unknown', {}); - expect(result.success).toBe(false); - expect(result.error).toContain('未知的动作'); - }); - - /** - * 测试 executeMcpTool - 未知的工具 - */ - test('should handle unknown tool gracefully', async () => { - await initMcpTools(); - const result = executeMcpTool('unknown.tool', {}); - expect(result.success).toBe(false); - expect(result.error).toContain('未知工具'); - }); - - /** - * 测试 executeMcpTool - 其他动作映射 - */ - test('should handle other action mappings', async () => { - await initMcpTools(); - const actionMappings = [{ tool: 'live2d.happy', expectedAction: 'FlickUp' }, { tool: 'live2d.sad', expectedAction: 'FlickDown' }, { tool: 'live2d.tap', expectedAction: 'Tap' }, { tool: 'live2d.tapBody', expectedAction: 'Tap@Body' }, { tool: 'live2d.flick', expectedAction: 'Flick' }, { tool: 'live2d.flickBody', expectedAction: 'Flick@Body' }, { tool: 'live2d.flickUp', expectedAction: 'FlickUp' }, { tool: 'live2d.flickDown', expectedAction: 'FlickDown' }]; - // Add these tools to the mock - const tools = await global.fetch().then(res => res.json()); - actionMappings.forEach(mapping => { - tools.push({ name: mapping.tool, description: `Test ${mapping.tool}`, inputSchema: { type: 'object', properties: {} } }); - }); - global.fetch = vi.fn(() => Promise.resolve({ json: () => Promise.resolve(tools) })); - await initMcpTools(); - for (const mapping of actionMappings) { - mockLive2DManager.motion.mockClear(); - const result = executeMcpTool(mapping.tool, {}); - expect(result.success).toBe(true); - expect(result.action).toBe(mapping.expectedAction); - expect(mockLive2DManager.motion).toHaveBeenCalledWith(mapping.expectedAction); - } - }); -}); diff --git a/main/xiaozhi-server/test/js/core/network/websocket.js b/main/xiaozhi-server/test/js/core/network/websocket.js index 1392781f..ed72b5ab 100644 --- a/main/xiaozhi-server/test/js/core/network/websocket.js +++ b/main/xiaozhi-server/test/js/core/network/websocket.js @@ -304,7 +304,6 @@ export class WebSocketHandler { let arrayBuffer; if (data instanceof ArrayBuffer) { arrayBuffer = data; - log(`收到ArrayBuffer音频数据,大小: ${data.byteLength}字节`, 'debug'); } else if (data instanceof Blob) { arrayBuffer = await data.arrayBuffer(); log(`收到Blob音频数据,大小: ${arrayBuffer.byteLength}字节`, 'debug'); @@ -392,7 +391,7 @@ export class WebSocketHandler { this.websocket.onerror = (error) => { log(`WebSocket错误: ${error.message || '未知错误'}`, 'error'); - + uiController.addChatMessage(`⚠️ WebSocket错误: ${error.message || '未知错误'}`, false); if (this.onConnectionStateChange) { this.onConnectionStateChange(false); } diff --git a/main/xiaozhi-server/test/js/ui/controller.js b/main/xiaozhi-server/test/js/ui/controller.js index e28cac4c..651de9d3 100644 --- a/main/xiaozhi-server/test/js/ui/controller.js +++ b/main/xiaozhi-server/test/js/ui/controller.js @@ -1,8 +1,8 @@ // UI controller module import { loadConfig, saveConfig } from '../config/manager.js'; +import { getAudioPlayer } from '../core/audio/player.js'; import { getAudioRecorder } from '../core/audio/recorder.js'; import { getWebSocketHandler } from '../core/network/websocket.js'; -import { getAudioPlayer } from '../core/audio/player.js'; // UI controller class class UIController { @@ -292,12 +292,7 @@ class UIController { // 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(); @@ -369,10 +364,20 @@ class UIController { // Start AI chat session after connection startAIChatSession() { this.addChatMessage('连接成功,开始聊天吧~😊', false); - // Start recording - const recordBtn = document.getElementById('recordBtn'); - if (recordBtn) { - recordBtn.click(); + // Check microphone availability and show error messages if needed + if (!window.microphoneAvailable) { + if (window.isHttpNonLocalhost) { + 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(); + } } } @@ -418,13 +423,39 @@ class UIController { // Get WebSocket handler instance 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(); 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) { @@ -434,34 +465,6 @@ class UIController { window.microphoneAvailable = false; } - // Register connection state callback - wsHandler.onConnectionStateChange = (isConnected) => { - this.updateConnectionUI(isConnected); - this.updateDialButton(isConnected); - }; - - // Register chat message callback - wsHandler.onChatMessage = (text, isUser) => { - this.addChatMessage(text, isUser); - }; - - // Register record button state callback - 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 = '录音'; - } - } - }; - - // Connection successful - this.addChatMessage('OTA连接成功,正在建立WebSocket连接...', false); - // Update dial button state const dialBtn = document.getElementById('dialBtn'); if (dialBtn) { @@ -595,4 +598,4 @@ class UIController { export const uiController = new UIController(); // Export class for module usage -export { UIController }; \ No newline at end of file +export { UIController }; diff --git a/main/xiaozhi-server/test/test-runner.html b/main/xiaozhi-server/test/test-runner.html deleted file mode 100644 index b8b1b92e..00000000 --- a/main/xiaozhi-server/test/test-runner.html +++ /dev/null @@ -1,59 +0,0 @@ - - -
- - -Browser-based test runner for xiaozhi test modules
-