diff --git a/README.md b/README.md index 8f375f0..9b44df0 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,12 @@ antiwear-web/ │ ├── HwICSUiSdk.d.ts # TypeScript 类型定义 │ ├── HwICSUiSdk.css # SDK 样式 │ └── ... +├── server/ # 后端服务 +│ ├── index.js # 后端主入口 +│ └── package.json # 后端依赖 ├── scripts/ -│ └── main.js # 主逻辑代码 +│ ├── main.js # 主逻辑代码 +│ └── api.js # 前端 API 调用模块 ├── styles/ │ └── main.css # 自定义样式 ├── index.html # 主页面 @@ -23,28 +27,63 @@ antiwear-web/ ### 1. 环境要求 +- Node.js 14+ (后端服务) - 现代浏览器(Chrome、Firefox、Edge、Safari 最新版本) - 支持 WebRTC 和 WebSocket - 需要麦克风权限(用于语音交互) -### 2. 配置参数 +### 2. 后端服务配置 -#### 默认参数(已预填) -| 参数 | 默认值 | -|------|--------| -| 服务器地址 | `metastudio-api.cn-north-4.myhuaweicloud.com` | -| 机器人ID | `2c9084bf9c9cc1c0019db2c4ca2a5c89` | +#### 方式一:使用 AK/SK 认证(推荐) -#### 需要填写的参数 -| 参数 | 说明 | 必填 | -|------|------|------| -| 一次性授权码 | 用于身份验证的临时授权码 | 是 | +设置环境变量: +```bash +# Windows +set HUAWEI_AK=your_access_key +set HUAWEI_SK=your_secret_key +set HUAWEI_PROJECT_ID=your_project_id +``` -> **如何获取授权码**:通过华为云 MetaStudio 控制台或 API 获取一次性授权码(有效期5分钟) +```bash +# Linux/Mac +export HUAWEI_AK=your_access_key +export HUAWEI_SK=your_secret_key +export HUAWEI_PROJECT_ID=your_project_id +``` -### 3. 启动项目 +#### 方式二:使用用户名密码认证 -由于使用了 ES Modules,需要通过本地服务器运行: +```bash +# Windows +set HUAWEI_USERNAME=your_username +set HUAWEI_PASSWORD=your_password +set HUAWEI_DOMAIN_NAME=your_domain_name +set HUAWEI_PROJECT_ID=your_project_id +``` + +```bash +# Linux/Mac +export HUAWEI_USERNAME=your_username +export HUAWEI_PASSWORD=your_password +export HUAWEI_DOMAIN_NAME=your_domain_name +export HUAWEI_PROJECT_ID=your_project_id +``` + +### 3. 启动服务 + +#### 启动后端服务 + +```bash +cd server +npm install +npm start +``` + +后端服务将在 `http://localhost:3001` 启动。 + +#### 启动前端项目 + +在另一个终端窗口: ```bash # 使用 Python 3 @@ -58,11 +97,21 @@ npx http-server -p 8080 然后在浏览器中访问 `http://localhost:8080` -### 4. 使用流程 +### 4. 配置参数 -1. **获取授权码**:通过华为云控制台获取一次性授权码 -2. **填写配置**:在页面中输入授权码 -3. **初始化 SDK**:点击 "初始化 SDK" 开始使用 +#### 默认参数(已预填) +| 参数 | 默认值 | +|------|--------| +| 服务器地址 | `metastudio-api.cn-north-4.myhuaweicloud.com` | +| 机器人ID | `2c9084bf9c9cc1c0019db2c4ca2a5c89` | +| 项目ID | `c67bf5e7d1f84d20a6eefff2c1ee9cd4` | + +### 5. 使用流程 + +1. **启动后端服务**:`cd server && npm start` +2. **启动前端服务器**:`python -m http.server 8080` +3. **获取授权码**:点击页面上的 "🔑 获取授权码" 按钮 +4. **初始化 SDK**:点击 "初始化 SDK" 开始使用 ## 📖 功能说明 @@ -105,55 +154,36 @@ SDK 提供了丰富的事件回调: ## 🔧 开发指南 -### 引入 SDK +### 前端 API 调用 -```javascript -import { HwICSUiSdk, ICSEventKey, InteractionMode, ConfigKey } from './hwICSUiSdk/HwICSUiSdk.esm.js'; +后端服务提供了以下接口: + +#### 获取一次性授权码 +``` +POST /api/once-code ``` -### 初始化 SDK - -```javascript -await HwICSUiSdk.create({ - serverAddress: 'your-server-address', - containerId: 'digitalHumanBox', - robotId: 'your-robot-id', - onceCode: 'your-once-code', - config: { - [ConfigKey.ENABLE_CAPTION]: true, - [ConfigKey.ENABLE_CHAT_BTN]: true - } -}); +响应示例: +```json +{ + "once_code": "OATAXWpTNDPGjVyzxNLKvumcv297Q7uUz5CCltLtFDE1uqnPAPmS4jm61XnMQGuV" +} ``` -### 注册事件监听 - -```javascript -HwICSUiSdk.addEventListeners({ - [ICSEventKey.SPEAKING_START]: () => { - console.log('数字人开始说话'); - }, - [ICSEventKey.SPEECH_RECOGNIZED]: (data) => { - console.log('识别结果:', data); - } -}); +错误响应: +```json +{ + "error_code": "CONFIG_MISSING", + "error_msg": "请配置环境变量 (AK/SK 或用户名密码)" +} ``` -### 开始对话 +### 后端接口 -```javascript -await HwICSUiSdk.startChat({ - interactionMode: InteractionMode.AUDIO -}); -``` - -### 发送文本消息 - -```javascript -await HwICSUiSdk.sendTextQuestion({ - text: '你好,数字人!' -}); -``` +| 接口 | 方法 | 说明 | +|------|------|------| +| /api/once-code | POST | 获取一次性授权码 | +| /health | GET | 健康检查 | ## ⚙️ 配置选项 @@ -176,6 +206,7 @@ SDK 支持多种配置项: 2. **浏览器权限**:首次使用需要授权麦克风和摄像头权限 3. **资源释放**:页面卸载时务必调用 `destroy()` 方法释放资源 4. **错误处理**:建议对所有异步操作添加错误处理 +5. **后端服务**:获取授权码需要启动后端服务并配置正确的环境变量 ## 🔍 调试技巧 diff --git a/index.html b/index.html index bf332c8..3f71438 100644 --- a/index.html +++ b/index.html @@ -44,8 +44,11 @@

授权码

- - 通过华为云控制台获取,有效期5分钟,只能使用一次 +
+ + +
+ 有效期5分钟,只能使用一次
diff --git a/scripts/api.js b/scripts/api.js new file mode 100644 index 0000000..3194f51 --- /dev/null +++ b/scripts/api.js @@ -0,0 +1,18 @@ +const API_BASE = 'http://localhost:3001'; + +export async function fetchOnceCode() { + const response = await fetch(`${API_BASE}/api/once-code`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error_msg || '获取授权码失败'); + } + + const data = await response.json(); + return data.once_code; +} diff --git a/scripts/main.js b/scripts/main.js index d28c7d9..244bfc9 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -10,6 +10,7 @@ // 导入 HwICSUiSdk import { HwICSUiSdk, ICSEventKey, InteractionMode, ConfigKey } from '../hwICSUiSdk/HwICSUiSdk.esm.js'; +import { fetchOnceCode } from './api.js'; // ===== 全局状态 ===== const state = { @@ -31,6 +32,7 @@ const elements = { // 按钮 initBtn: document.getElementById('initBtn'), destroyBtn: document.getElementById('destroyBtn'), + getOnceCodeBtn: document.getElementById('getOnceCodeBtn'), startChatBtn: document.getElementById('startChatBtn'), stopChatBtn: document.getElementById('stopChatBtn'), startSpeakBtn: document.getElementById('startSpeakBtn'), @@ -408,6 +410,24 @@ async function toggleAudio() { } } +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('日志已清空'); @@ -417,6 +437,7 @@ function clearLogs() { 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); diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..01d5e7c --- /dev/null +++ b/server/index.js @@ -0,0 +1,197 @@ +const express = require('express'); +const cors = require('cors'); +const crypto = require('crypto'); + +const app = express(); +const PORT = 3001; + +app.use(cors()); +app.use(express.json()); + +const METASTUDIO_API_BASE = 'https://metastudio.cn-north-4.myhuaweicloud.com'; +const PROJECT_ID = process.env.HUAWEI_PROJECT_ID || 'c67bf5e7d1f84d20a6eefff2c1ee9cd4'; +const IAM_TOKEN_URL = 'https://iam.cn-north-4.myhuaweicloud.com/v3/auth/tokens'; + +async function getIamToken() { + const username = process.env.HUAWEI_USERNAME; + const password = process.env.HUAWEI_PASSWORD; + const domainName = process.env.HUAWEI_DOMAIN_NAME || domainName; + const region = 'cn-north-4'; + + if (!username || !password || !domainName) { + throw new Error('请配置环境变量: HUAWEI_USERNAME, HUAWEI_PASSWORD, HUAWEI_DOMAIN_NAME'); + } + + const requestBody = { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "name": username, + "password": password, + "domain": { + "name": domainName + } + } + } + }, + "scope": { + "project": { + "name": region + } + } + } + }; + + const response = await fetch(IAM_TOKEN_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestBody) + }); + + if (!response.ok) { + throw new Error(`IAM认证失败: ${response.status}`); + } + + const token = response.headers.get('X-Subject-Token'); + return token; +} + +function buildAKSKSignature(method, host, uri, query, date, headers) { + const secretKey = process.env.HUAWEI_SECRET_KEY; + if (!secretKey) { + throw new Error('请配置环境变量: HUAWEI_SECRET_KEY'); + } + + const signingKey = 'sdk-hmac-sha256'; + const dateShort = date.toISOString().slice(0, 10).replace(/-/g, ''); + + const canonicalRequestParts = [ + method.toUpperCase(), + uri, + query || '', + `${headers.toLowerCase()}\n${host.toLowerCase()}` + ]; + const canonicalRequest = canonicalRequestParts.join('\n'); + + const stringToSign = [ + signingKey, + date, + `${dateShort}/${region}/metastudio/request`, + crypto.createHash('sha256').update(canonicalRequest).digest('hex') + ].join('\n'); + + const signature = crypto + .createHmac('sha256', secretKey) + .update(stringToSign) + .digest('base64'); + + return { + signature, + stringToSign, + dateShort + }; +} + +async function createOnceCodeWithAKSK() { + const accessKey = process.env.HUAWEI_AK; + const secretKey = process.env.HUAWEI_SK; + const region = 'cn-north-4'; + + if (!accessKey || !secretKey) { + throw new Error('请配置环境变量: HUAWEI_AK 和 HUAWEI_SK'); + } + + const host = 'metastudio.cn-north-4.myhuaweicloud.com'; + const uri = `/v1/${PROJECT_ID}/digital-human-chat/once-code`; + const method = 'POST'; + const date = new Date().toUTCString(); + const query = ''; + + const { signature, stringToSign, dateShort } = buildAKSKSignature( + method, host, uri, query, date, '' + ); + + const authHeader = `SDK-HMAC-SHA256 Access=${accessKey}, SignedHeaders=host;x-sdk-date, Signature=${signature}`; + + const response = await fetch(`${METASTUDIO_API_BASE}${uri}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Sdk-Date': date, + 'Authorization': authHeader, + 'X-Project-Id': PROJECT_ID + }, + body: JSON.stringify({}) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`API调用失败: ${response.status} - ${errorText}`); + } + + const data = await response.json(); + return data.once_code; +} + +app.post('/api/once-code', async (req, res) => { + try { + let onceCode; + + if (process.env.HUAWEI_AK && process.env.HUAWEI_SK) { + console.log('使用 AK/SK 认证获取授权码...'); + onceCode = await createOnceCodeWithAKSK(); + } else if (process.env.HUAWEI_USERNAME && process.env.HUAWEI_PASSWORD) { + console.log('使用 Token 认证获取授权码...'); + const token = await getIamToken(); + + const response = await fetch( + `${METASTUDIO_API_BASE}/v1/${PROJECT_ID}/digital-human-chat/once-code`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Auth-Token': token, + 'X-Project-Id': PROJECT_ID + }, + body: JSON.stringify({}) + } + ); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`API调用失败: ${response.status} - ${errorText}`); + } + + const data = await response.json(); + onceCode = data.once_code; + } else { + return res.status(400).json({ + error_code: 'CONFIG_MISSING', + error_msg: '请配置环境变量 (AK/SK 或用户名密码)' + }); + } + + res.json({ once_code: onceCode }); + } catch (error) { + console.error('获取授权码失败:', error); + res.status(500).json({ + error_code: 'INTERNAL_ERROR', + error_msg: error.message + }); + } +}); + +app.get('/health', (req, res) => { + res.json({ status: 'ok' }); +}); + +app.listen(PORT, () => { + console.log(`后端服务已启动: http://localhost:${PORT}`); + console.log('支持的认证方式:'); + console.log(' 1. AK/SK 认证: 设置 HUAWEI_AK 和 HUAWEI_SK'); + console.log(' 2. Token 认证: 设置 HUAWEI_USERNAME, HUAWEI_PASSWORD, HUAWEI_DOMAIN_NAME'); +}); diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..adbe1b6 --- /dev/null +++ b/server/package.json @@ -0,0 +1,13 @@ +{ + "name": "antiwear-server", + "version": "1.0.0", + "description": "华为交互数字人后端服务", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "express": "^4.18.2", + "cors": "^2.8.5" + } +} diff --git a/styles/main.css b/styles/main.css index ee5cf7b..71e0baa 100644 --- a/styles/main.css +++ b/styles/main.css @@ -248,6 +248,15 @@ section h2 { padding-right: 80px; } +.input-with-button { + display: flex; + gap: var(--spacing-sm); +} + +.input-with-button input { + flex: 1; +} + .field-status { position: absolute; right: var(--spacing-sm);