/** * 华为交互数字人 - HwICSUiSdk 集成示例 */ import { HwICSUiSdk } from '../hwICSUiSdk/HwICSUiSdk.esm.js'; import { fetchOnceCode } from './api.js'; const ICSEventKey = { ERROR: 'error', JOB_INFO_CHANGE: 'jobInfoChange', SPEAKING_START: 'speakingStart', SPEAKING_STOP: 'speakingStop', SPEECH_RECOGNIZED: 'speechRecognized', SEMANTIC_RECOGNIZED: 'semanticRecognized', ENTER_SLEEP: 'enterSleep', ENTER_ACTIVE: 'enterActive', LANGUAGE_INFO_CHANGE: 'languageInfoChange', JOB_END_MENTION: 'jobEndMention', JOB_END: 'jobEnd' }; const InteractionMode = { AUDIO: 'AUDIO', TEXT: 'TEXT' }; const ConfigKey = { ENABLE_CAPTION: 'enableCaption', ENABLE_CHAT_BTN: 'enableChatBtn', ENABLE_HOT_ISSUES: 'enableHotIssues', ENABLE_WEAK_ERROR_INFO: 'enableWeakErrorInfo', SHOW_LANGUAGE_UI: 'showLanguageUI', ENABLE_DEVICE_SETTING: 'enableDeviceSetting', ENABLE_NETWORK_CHECK: 'enableNetworkCheck', AUTO_PLAY_REMOTE_VIDEO: 'autoPlayRemoteVideo', DEFAULT_MUTE_REMOTE_AUDIO: 'defaultMuteRemoteAudio' }; const state = { isInitialized: false, isChatting: false, isSpeaking: false, isMuted: false, jobInfo: null, currentInteractionMode: InteractionMode.AUDIO }; const elements = { serverAddress: document.getElementById('serverAddress'), robotId: document.getElementById('robotId'), onceCode: document.getElementById('onceCode'), initBtn: document.getElementById('initBtn'), destroyBtn: document.getElementById('destroyBtn'), getOnceCodeBtn: document.getElementById('getOnceCodeBtn'), clearLogBtn: document.getElementById('clearLogBtn'), fullscreenBtn: document.getElementById('fullscreenBtn'), toggleConfigBtn: document.getElementById('toggleConfigBtn'), toggleCaptionBtn: document.getElementById('toggleCaptionBtn'), toggleLogBtn: document.getElementById('toggleLogBtn'), applyCaptionStyleBtn: document.getElementById('applyCaptionStyleBtn'), captionFontSize: document.getElementById('captionFontSize'), logLevel: document.getElementById('logLevel'), digitalHumanBox: document.getElementById('digitalHumanBox'), logContainer: document.getElementById('logContainer'), connectionStatus: document.getElementById('connectionStatus'), sdkVersion: document.getElementById('sdkVersion') }; const fullscreenState = { isFullscreen: false, sdkInitialized: false }; const panelState = { configPanel: false, captionPanel: false, logPanel: false }; const logger = { log(level, message, data = null) { const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false }); const entry = document.createElement('div'); entry.className = 'log-entry'; const levelClass = level.toLowerCase(); const displayMessage = data ? `${message} ${JSON.stringify(data, null, 2)}` : message; entry.innerHTML = ` [${timestamp}] ${level} ${displayMessage} `; elements.logContainer.appendChild(entry); elements.logContainer.scrollTop = elements.logContainer.scrollHeight; console[level === 'ERROR' ? 'error' : level === 'WARN' ? 'warn' : 'log'](message, data); }, debug(message, data) { this.log('DEBUG', message, data); }, info(message, data) { this.log('INFO', message, data); }, warn(message, data) { this.log('WARN', message, data); }, error(message, data) { this.log('ERROR', message, data); } }; const eventListeners = { [ICSEventKey.ERROR]: (error) => { logger.error('SDK 错误', error); updateConnectionStatus(false); }, [ICSEventKey.JOB_INFO_CHANGE]: (jobInfo) => { logger.info('任务信息变更', jobInfo); state.jobInfo = jobInfo; }, [ICSEventKey.SPEAKING_START]: () => { logger.info('数字人开始说话'); state.isSpeaking = true; updateUIState(); }, [ICSEventKey.SPEAKING_STOP]: () => { logger.info('数字人停止说话'); state.isSpeaking = false; updateUIState(); }, [ICSEventKey.SPEECH_RECOGNIZED]: (data) => { logger.info('语音识别结果', data); }, [ICSEventKey.SEMANTIC_RECOGNIZED]: (data) => { logger.info('语义识别结果', data); }, [ICSEventKey.ENTER_SLEEP]: () => { logger.info('数字人进入休眠状态'); }, [ICSEventKey.ENTER_ACTIVE]: () => { logger.info('数字人进入激活状态'); }, [ICSEventKey.LANGUAGE_INFO_CHANGE]: (langInfo) => { logger.info('语言信息变更', langInfo); }, [ICSEventKey.JOB_END_MENTION]: (data) => { logger.warn('任务即将结束', data); }, [ICSEventKey.JOB_END]: (data) => { logger.info('任务已结束', data); state.isChatting = false; state.jobInfo = null; updateConnectionStatus(false); updateUIState(); } }; function updateUIState() { const { isInitialized } = state; elements.initBtn.disabled = isInitialized; elements.destroyBtn.disabled = !isInitialized; } function updateConnectionStatus(connected) { const status = elements.connectionStatus; if (connected) { status.textContent = '已连接'; status.className = 'status connected'; } else { status.textContent = '未连接'; status.className = 'status disconnected'; } } function togglePanel(panelId) { const panel = document.getElementById(panelId); console.log('切换面板:', panelId, '找到元素:', !!panel); if (!panel) return; const isVisible = panel.classList.contains('visible'); console.log('当前可见:', isVisible); Object.keys(panelState).forEach(key => { const el = document.getElementById(key); if (el) el.classList.remove('visible'); panelState[key] = false; }); document.querySelectorAll('.toolbar-btn').forEach(btn => btn.classList.remove('active')); if (!isVisible) { panel.classList.add('visible'); panelState[panelId] = true; console.log('面板已显示:', panelId); const btnMap = { configPanel: 'toggleConfigBtn', captionPanel: 'toggleCaptionBtn', logPanel: 'toggleLogBtn' }; const btnId = btnMap[panelId]; if (btnId && elements[btnId]) { elements[btnId].classList.add('active'); } } } async function initializeSDK() { try { const serverAddress = elements.serverAddress.value.trim(); const robotId = elements.robotId.value.trim(); let onceCode = elements.onceCode.value.trim(); if (!serverAddress) { logger.error('请输入服务器地址'); return; } if (!onceCode) { logger.info('授权码为空,正在自动获取...'); try { onceCode = await fetchOnceCode(); elements.onceCode.value = onceCode; logger.info('授权码自动获取成功'); } catch (err) { logger.error('授权码自动获取失败', err.message); return; } } logger.info('正在初始化 SDK...'); const isSupported = await HwICSUiSdk.checkBrowserSupport(true); if (!isSupported) { logger.error('当前浏览器不支持该功能'); return; } const logLevel = elements.logLevel.value; HwICSUiSdk.setLogLevel(logLevel); HwICSUiSdk.addEventListeners(eventListeners); const createParam = { serverAddress, containerId: 'digitalHumanBox', robotId: robotId || undefined, onceCode: onceCode || undefined, config: { [ConfigKey.ENABLE_CAPTION]: true, [ConfigKey.ENABLE_CHAT_BTN]: true, [ConfigKey.ENABLE_HOT_ISSUES]: true, [ConfigKey.ENABLE_WEAK_ERROR_INFO]: false, [ConfigKey.SHOW_LANGUAGE_UI]: false, [ConfigKey.ENABLE_DEVICE_SETTING]: false, [ConfigKey.ENABLE_NETWORK_CHECK]: false, [ConfigKey.AUTO_PLAY_REMOTE_VIDEO]: true, [ConfigKey.DEFAULT_MUTE_REMOTE_AUDIO]: false }, logLevel }; await HwICSUiSdk.create(createParam); state.isInitialized = true; fullscreenState.sdkInitialized = true; updateConnectionStatus(true); updateUIState(); elements.sdkVersion.textContent = `v${HwICSUiSdk.version}`; logger.info('SDK 初始化成功', { version: HwICSUiSdk.version }); rebindDynamicButtons(); setTimeout(calculateScale, 500); } catch (error) { logger.error('SDK 初始化失败', error); state.isInitialized = false; updateUIState(); } } async function destroySDK() { try { logger.info('正在销毁 SDK...'); await HwICSUiSdk.destroy(); state.isInitialized = false; state.isChatting = false; state.isSpeaking = false; state.jobInfo = null; fullscreenState.sdkInitialized = false; updateConnectionStatus(false); updateUIState(); elements.digitalHumanBox.innerHTML = `
按 ESC 退出全屏
🤖

点击右下角 ⚙️ 按钮初始化 SDK

`; logger.info('SDK 已销毁'); } catch (error) { logger.error('销毁 SDK 失败', error); } } async function handleGetOnceCode() { try { elements.getOnceCodeBtn.disabled = true; elements.getOnceCodeBtn.textContent = '🔄 获取中...'; logger.info('正在获取授权码...'); const onceCode = await fetchOnceCode(); elements.onceCode.value = onceCode; logger.info('授权码获取成功'); } catch (error) { logger.error('获取授权码失败', error.message); } finally { elements.getOnceCodeBtn.disabled = false; elements.getOnceCodeBtn.textContent = '🔑 获取'; } } function clearLogs() { elements.logContainer.innerHTML = ''; logger.info('日志已清空'); } function applyCaptionStyle() { const fontSize = elements.captionFontSize.value; const captionContainer = document.querySelector('.caption-container-hub'); if (!captionContainer) { logger.warn('字幕容器未找到'); return; } captionContainer.style.fontSize = `${fontSize}px`; const textElements = captionContainer.querySelectorAll('*'); textElements.forEach(el => { el.style.fontSize = `${fontSize}px`; }); logger.info('字幕样式已应用', { fontSize }); } function initCaptionDragAndResize(container) { container.dataset.dragInitialized = 'true'; let isDragging = false; let isResizing = false; let startX, startY, startLeft, startTop, startWidth, startHeight; let resizeHandle = container.querySelector('.caption-resize-handle'); if (!resizeHandle) { resizeHandle = document.createElement('div'); resizeHandle.className = 'caption-resize-handle'; resizeHandle.style.position = 'absolute'; resizeHandle.style.right = '0'; resizeHandle.style.bottom = '0'; resizeHandle.style.width = '24px'; resizeHandle.style.height = '24px'; resizeHandle.style.cursor = 'nwse-resize'; resizeHandle.style.background = 'linear-gradient(135deg, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0.95) 50%, rgba(255,255,255,0.8) 100%)'; resizeHandle.style.borderRadius = '0 0 8px 0'; resizeHandle.style.zIndex = '10000'; resizeHandle.style.borderRight = '3px solid rgba(0, 123, 255, 0.7)'; resizeHandle.style.borderBottom = '3px solid rgba(0, 123, 255, 0.7)'; resizeHandle.style.boxShadow = 'inset -2px -2px 4px rgba(0,0,0,0.1), 0 0 8px rgba(0, 123, 255, 0.2)'; resizeHandle.style.opacity = '0'; resizeHandle.style.transition = 'opacity 0.2s ease'; resizeHandle.title = '拖拽调整大小'; container.appendChild(resizeHandle); } container.addEventListener('mouseenter', () => { resizeHandle.style.opacity = '1'; }); container.addEventListener('mouseleave', () => { if (!isResizing) { resizeHandle.style.opacity = '0'; } }); const onContainerMouseDown = (e) => { if (e.target === resizeHandle || resizeHandle.contains(e.target)) return; isDragging = true; startX = e.clientX; startY = e.clientY; const rect = container.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; e.preventDefault(); e.stopPropagation(); container.style.borderColor = 'rgba(0, 123, 255, 0.6)'; container.style.boxShadow = '0 4px 12px rgba(0, 123, 255, 0.3)'; }; const onResizeHandleMouseDown = (e) => { isResizing = true; startX = e.clientX; startY = e.clientY; startWidth = container.offsetWidth; startHeight = container.offsetHeight; e.preventDefault(); e.stopPropagation(); container.style.borderColor = 'rgba(0, 123, 255, 0.8)'; container.style.boxShadow = '0 4px 12px rgba(0, 123, 255, 0.4)'; }; const onMouseMove = (e) => { if (!isDragging && !isResizing) return; e.preventDefault(); if (isDragging) { const dx = e.clientX - startX; const dy = e.clientY - startY; container.style.left = `${startLeft + dx}px`; container.style.top = `${startTop + dy}px`; } if (isResizing) { const dx = e.clientX - startX; const dy = e.clientY - startY; const newWidth = Math.max(200, startWidth + dx); const newHeight = Math.max(100, startHeight + dy); container.style.width = `${newWidth}px`; container.style.height = `${newHeight}px`; container.style.maxHeight = `${newHeight}px`; } }; const onMouseUp = () => { if (isDragging || isResizing) { container.style.borderColor = 'transparent'; container.style.boxShadow = 'none'; resizeHandle.style.opacity = '0'; } isDragging = false; isResizing = false; }; container.addEventListener('mousedown', onContainerMouseDown); resizeHandle.addEventListener('mousedown', onResizeHandleMouseDown); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); logger.info('字幕容器拖拽和缩放已启用'); } function watchCaptionContainer() { const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList') { const captionContainer = document.querySelector('.caption-container-hub'); if (captionContainer && !captionContainer.dataset.dragInitialized) { logger.info('检测到字幕容器,自动启用拖拽功能'); captionContainer.style.position = 'fixed'; captionContainer.style.cursor = 'move'; captionContainer.style.userSelect = 'none'; captionContainer.style.zIndex = '9999'; initCaptionDragAndResize(captionContainer); } } } }); observer.observe(document.body, { childList: true, subtree: true }); logger.info('已启动字幕容器监听'); } function calculateScale() { const container = elements.digitalHumanBox; const sdkContainer = container.querySelector('.digital-intelligent-interaction'); if (!sdkContainer) return; sdkContainer.style.transform = 'none'; } function toggleFullscreen() { if (!fullscreenState.sdkInitialized) { logger.warn('请先初始化 SDK'); return; } fullscreenState.isFullscreen = !fullscreenState.isFullscreen; if (fullscreenState.isFullscreen) { elements.digitalHumanBox.classList.add('fullscreen'); elements.fullscreenBtn.innerHTML = '⛶'; elements.fullscreenBtn.title = '退出全屏'; logger.info('进入全屏模式'); } else { elements.digitalHumanBox.classList.remove('fullscreen'); elements.fullscreenBtn.innerHTML = '⛶'; elements.fullscreenBtn.title = '全屏显示'; logger.info('退出全屏模式'); } setTimeout(calculateScale, 100); } function handleFullscreenKeydown(e) { if (e.key === 'Escape' && fullscreenState.isFullscreen) { toggleFullscreen(); } } function initScaling() { const resizeObserver = new ResizeObserver(() => { if (fullscreenState.sdkInitialized) { calculateScale(); } }); resizeObserver.observe(elements.digitalHumanBox); window.addEventListener('keydown', handleFullscreenKeydown); } function bindEvents() { elements.initBtn.addEventListener('click', initializeSDK); elements.destroyBtn.addEventListener('click', destroySDK); elements.getOnceCodeBtn.addEventListener('click', handleGetOnceCode); elements.applyCaptionStyleBtn.addEventListener('click', applyCaptionStyle); elements.clearLogBtn.addEventListener('click', clearLogs); elements.logLevel.addEventListener('change', () => { if (state.isInitialized) { HwICSUiSdk.setLogLevel(elements.logLevel.value); logger.info(`日志级别已切换为: ${elements.logLevel.value}`); } }); elements.toggleConfigBtn.addEventListener('click', () => togglePanel('configPanel')); elements.toggleCaptionBtn.addEventListener('click', () => togglePanel('captionPanel')); elements.toggleLogBtn.addEventListener('click', () => togglePanel('logPanel')); elements.fullscreenBtn.addEventListener('click', toggleFullscreen); document.querySelectorAll('.panel-close').forEach(btn => { btn.addEventListener('click', () => { const panelId = btn.dataset.panel; if (panelId) togglePanel(panelId); }); }); window.addEventListener('beforeunload', () => { if (state.isInitialized) { HwICSUiSdk.destroy(); } }); } function rebindDynamicButtons() { const fullscreenBtn = document.getElementById('fullscreenBtn'); if (fullscreenBtn) { fullscreenBtn.removeEventListener('click', toggleFullscreen); fullscreenBtn.addEventListener('click', toggleFullscreen); } } function init() { logger.info('应用初始化完成'); logger.info('HwICSUiSdk 版本', { version: HwICSUiSdk.version, buildTime: HwICSUiSdk.buildTime }); bindEvents(); updateUIState(); initScaling(); watchCaptionContainer(); } init();