- 新增 server 目录包含后端服务,支持通过 AK/SK 或用户名密码认证获取授权码 - 添加前端 API 模块实现授权码获取功能 - 更新 UI 增加获取授权码按钮和输入框布局 - 完善 README 文档说明后端配置和启动流程
483 lines
14 KiB
JavaScript
483 lines
14 KiB
JavaScript
/**
|
|
* 华为交互数字人 - HwICSUiSdk 集成示例
|
|
*
|
|
* 功能说明:
|
|
* 1. SDK 初始化和销毁
|
|
* 2. 数字人创建和显示
|
|
* 3. 语音/文本对话交互
|
|
* 4. 事件监听和日志记录
|
|
*/
|
|
|
|
// 导入 HwICSUiSdk
|
|
import { HwICSUiSdk, ICSEventKey, InteractionMode, ConfigKey } from '../hwICSUiSdk/HwICSUiSdk.esm.js';
|
|
import { fetchOnceCode } from './api.js';
|
|
|
|
// ===== 全局状态 =====
|
|
const state = {
|
|
isInitialized: false,
|
|
isChatting: false,
|
|
isSpeaking: false,
|
|
isMuted: false,
|
|
jobInfo: null,
|
|
currentInteractionMode: InteractionMode.AUDIO
|
|
};
|
|
|
|
// ===== DOM 元素引用 =====
|
|
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'),
|
|
startChatBtn: document.getElementById('startChatBtn'),
|
|
stopChatBtn: document.getElementById('stopChatBtn'),
|
|
startSpeakBtn: document.getElementById('startSpeakBtn'),
|
|
stopSpeakBtn: document.getElementById('stopSpeakBtn'),
|
|
sendTextBtn: document.getElementById('sendTextBtn'),
|
|
interruptBtn: document.getElementById('interruptBtn'),
|
|
toggleAudioBtn: document.getElementById('toggleAudioBtn'),
|
|
clearLogBtn: document.getElementById('clearLogBtn'),
|
|
|
|
// 输入框
|
|
textInput: document.getElementById('textInput'),
|
|
logLevel: document.getElementById('logLevel'),
|
|
|
|
// 显示区域
|
|
digitalHumanBox: document.getElementById('digitalHumanBox'),
|
|
logContainer: document.getElementById('logContainer'),
|
|
connectionStatus: document.getElementById('connectionStatus'),
|
|
sdkVersion: document.getElementById('sdkVersion')
|
|
};
|
|
|
|
// ===== 日志工具 =====
|
|
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 = `
|
|
<span class="log-time">[${timestamp}]</span>
|
|
<span class="log-level ${levelClass}">${level}</span>
|
|
<span class="log-message">${displayMessage}</span>
|
|
`;
|
|
|
|
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); }
|
|
};
|
|
|
|
// ===== SDK 事件监听配置 =====
|
|
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();
|
|
}
|
|
};
|
|
|
|
// ===== UI 状态更新 =====
|
|
function updateUIState() {
|
|
const { isInitialized, isChatting, isSpeaking } = state;
|
|
|
|
elements.initBtn.disabled = isInitialized;
|
|
elements.destroyBtn.disabled = !isInitialized;
|
|
|
|
elements.startChatBtn.disabled = !isInitialized || isChatting;
|
|
elements.stopChatBtn.disabled = !isInitialized || !isChatting;
|
|
|
|
elements.startSpeakBtn.disabled = !isInitialized || !isChatting || isSpeaking;
|
|
elements.stopSpeakBtn.disabled = !isInitialized || !isChatting || !isSpeaking;
|
|
|
|
elements.textInput.disabled = !isInitialized || !isChatting;
|
|
elements.sendTextBtn.disabled = !isInitialized || !isChatting;
|
|
|
|
elements.interruptBtn.disabled = !isInitialized || !isChatting;
|
|
elements.toggleAudioBtn.disabled = !isInitialized;
|
|
|
|
if (isChatting) {
|
|
elements.digitalHumanBox.classList.add('active');
|
|
} else {
|
|
elements.digitalHumanBox.classList.remove('active');
|
|
}
|
|
}
|
|
|
|
function updateConnectionStatus(connected) {
|
|
const status = elements.connectionStatus;
|
|
if (connected) {
|
|
status.textContent = '已连接';
|
|
status.className = 'status connected';
|
|
} else {
|
|
status.textContent = '未连接';
|
|
status.className = 'status disconnected';
|
|
}
|
|
}
|
|
|
|
// ===== SDK 操作 =====
|
|
|
|
async function initializeSDK() {
|
|
try {
|
|
const serverAddress = elements.serverAddress.value.trim();
|
|
const robotId = elements.robotId.value.trim();
|
|
const onceCode = elements.onceCode.value.trim();
|
|
|
|
if (!serverAddress) {
|
|
logger.error('请输入服务器地址');
|
|
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]: true,
|
|
[ConfigKey.SHOW_LANGUAGE_UI]: true,
|
|
[ConfigKey.ENABLE_DEVICE_SETTING]: true,
|
|
[ConfigKey.ENABLE_NETWORK_CHECK]: true,
|
|
[ConfigKey.AUTO_PLAY_REMOTE_VIDEO]: true,
|
|
[ConfigKey.DEFAULT_MUTE_REMOTE_AUDIO]: false
|
|
},
|
|
logLevel
|
|
};
|
|
|
|
await HwICSUiSdk.create(createParam);
|
|
|
|
state.isInitialized = true;
|
|
updateConnectionStatus(true);
|
|
updateUIState();
|
|
|
|
elements.sdkVersion.textContent = `v${HwICSUiSdk.version}`;
|
|
|
|
logger.info('SDK 初始化成功', { version: HwICSUiSdk.version });
|
|
|
|
} 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;
|
|
|
|
updateConnectionStatus(false);
|
|
updateUIState();
|
|
|
|
elements.digitalHumanBox.innerHTML = `
|
|
<div class="placeholder">
|
|
<span class="placeholder-icon">🤖</span>
|
|
<p>请先初始化 SDK 并创建数字人</p>
|
|
</div>
|
|
`;
|
|
|
|
logger.info('SDK 已销毁');
|
|
|
|
} catch (error) {
|
|
logger.error('销毁 SDK 失败', error);
|
|
}
|
|
}
|
|
|
|
async function startChat() {
|
|
try {
|
|
logger.info('正在开始对话...');
|
|
|
|
const result = await HwICSUiSdk.startChat({
|
|
interactionMode: InteractionMode.AUDIO,
|
|
wakeupType: undefined,
|
|
preCheckPermissions: true
|
|
});
|
|
|
|
if (result.result) {
|
|
state.isChatting = true;
|
|
logger.info('对话已开始', { chatId: result.chatId, sessionId: result.sessionId });
|
|
updateUIState();
|
|
} else {
|
|
logger.error('开始对话失败', result);
|
|
}
|
|
|
|
} catch (error) {
|
|
logger.error('开始对话出错', error);
|
|
}
|
|
}
|
|
|
|
async function stopChat() {
|
|
try {
|
|
logger.info('正在停止对话...');
|
|
|
|
const result = await HwICSUiSdk.stopChat();
|
|
|
|
if (result.result) {
|
|
state.isChatting = false;
|
|
state.isSpeaking = false;
|
|
logger.info('对话已停止');
|
|
updateUIState();
|
|
} else {
|
|
logger.error('停止对话失败', result);
|
|
}
|
|
|
|
} catch (error) {
|
|
logger.error('停止对话出错', error);
|
|
}
|
|
}
|
|
|
|
async function startUserSpeak() {
|
|
try {
|
|
logger.info('开始语音识别...');
|
|
|
|
const result = await HwICSUiSdk.startUserSpeak({
|
|
preCheckPermissions: true
|
|
});
|
|
|
|
if (result.result) {
|
|
logger.info('语音识别已启动');
|
|
} else {
|
|
logger.error('启动语音识别失败', result);
|
|
}
|
|
|
|
} catch (error) {
|
|
logger.error('启动语音识别出错', error);
|
|
}
|
|
}
|
|
|
|
async function stopUserSpeak() {
|
|
try {
|
|
logger.info('停止语音识别...');
|
|
|
|
const result = await HwICSUiSdk.stopUserSpeak();
|
|
|
|
if (result.result) {
|
|
logger.info('语音识别已停止');
|
|
} else {
|
|
logger.error('停止语音识别失败', result);
|
|
}
|
|
|
|
} catch (error) {
|
|
logger.error('停止语音识别出错', error);
|
|
}
|
|
}
|
|
|
|
async function sendTextMessage() {
|
|
const text = elements.textInput.value.trim();
|
|
|
|
if (!text) {
|
|
logger.warn('请输入文本内容');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
logger.info('发送文本消息...', { text });
|
|
|
|
const result = await HwICSUiSdk.sendTextQuestion({
|
|
text,
|
|
questionParam: undefined
|
|
});
|
|
|
|
if (result.result) {
|
|
logger.info('消息发送成功', { chatId: result.chatId });
|
|
elements.textInput.value = '';
|
|
} else {
|
|
logger.error('发送消息失败', result);
|
|
}
|
|
|
|
} catch (error) {
|
|
logger.error('发送消息出错', error);
|
|
}
|
|
}
|
|
|
|
async function interruptSpeaking() {
|
|
try {
|
|
logger.info('打断说话...');
|
|
|
|
const result = await HwICSUiSdk.interruptSpeaking();
|
|
|
|
if (result.result) {
|
|
logger.info('已打断说话');
|
|
} else {
|
|
logger.error('打断说话失败', result);
|
|
}
|
|
|
|
} catch (error) {
|
|
logger.error('打断说话出错', error);
|
|
}
|
|
}
|
|
|
|
async function toggleAudio() {
|
|
try {
|
|
if (state.isMuted) {
|
|
const result = await HwICSUiSdk.unmuteRemoteAudio();
|
|
if (result) {
|
|
state.isMuted = false;
|
|
elements.toggleAudioBtn.innerHTML = '<span class="icon">🔊</span> 静音';
|
|
logger.info('已取消静音');
|
|
}
|
|
} else {
|
|
const result = await HwICSUiSdk.muteRemoteAudio();
|
|
if (result) {
|
|
state.isMuted = true;
|
|
elements.toggleAudioBtn.innerHTML = '<span class="icon">🔇</span> 取消静音';
|
|
logger.info('已静音');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logger.error('切换音频状态出错', 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 bindEvents() {
|
|
elements.initBtn.addEventListener('click', initializeSDK);
|
|
elements.destroyBtn.addEventListener('click', destroySDK);
|
|
elements.getOnceCodeBtn.addEventListener('click', handleGetOnceCode);
|
|
|
|
elements.startChatBtn.addEventListener('click', startChat);
|
|
elements.stopChatBtn.addEventListener('click', stopChat);
|
|
|
|
elements.startSpeakBtn.addEventListener('click', startUserSpeak);
|
|
elements.stopSpeakBtn.addEventListener('click', stopUserSpeak);
|
|
|
|
elements.sendTextBtn.addEventListener('click', sendTextMessage);
|
|
elements.textInput.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') {
|
|
sendTextMessage();
|
|
}
|
|
});
|
|
|
|
elements.interruptBtn.addEventListener('click', interruptSpeaking);
|
|
elements.toggleAudioBtn.addEventListener('click', toggleAudio);
|
|
|
|
elements.clearLogBtn.addEventListener('click', clearLogs);
|
|
elements.logLevel.addEventListener('change', () => {
|
|
if (state.isInitialized) {
|
|
HwICSUiSdk.setLogLevel(elements.logLevel.value);
|
|
logger.info(`日志级别已切换为: ${elements.logLevel.value}`);
|
|
}
|
|
});
|
|
|
|
window.addEventListener('beforeunload', () => {
|
|
if (state.isInitialized) {
|
|
HwICSUiSdk.destroy();
|
|
}
|
|
});
|
|
}
|
|
|
|
// ===== 初始化 =====
|
|
function init() {
|
|
logger.info('应用初始化完成');
|
|
logger.info('HwICSUiSdk 版本', { version: HwICSUiSdk.version, buildTime: HwICSUiSdk.buildTime });
|
|
|
|
bindEvents();
|
|
updateUIState();
|
|
}
|
|
|
|
init();
|