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 @@
授权码
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);