# 全特数字人 JSSDK 库

[![npm version](https://img.shields.io/npm/v/qt-human.svg)](https://www.npmjs.com/package/qt-human)
[![license](https://img.shields.io/npm/l/qt-human.svg)](https://github.com/quantekeji/qt-human/blob/main/LICENSE)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.5-blue.svg)](https://www.typescriptlang.org/)

一个用于 3D 数字人渲染和交互的 JavaScript SDK，提供对话、播报、语音识别、动作控制等功能。

> **最新版本**: v3.6.10-beta.1 

## 文档导航

- [语音交互标准规范](./docs/VOICE_INTERACTION_STANDARD.md)
- [VoiceConversation 使用指南](./docs/VOICE_CONVERSATION_GUIDE.md)
- [WebRTC 指南](./docs/WEBRTC_GUIDE.md)
- [Gateway 服务指南](./docs/GATEWAY_SERVICE_GUIDE.md)

## 特性

- 🎭 **3D 数字人渲染** - 基于 Three.js 的高性能 3D 渲染
- 💬 **AI 对话** - 支持流式对话，实时响应
- 🗣️ **语音合成 (TTS)** - 文本转语音，支持多种音色和模型
- 🎤 **语音识别 (ASR)** - 语音转文本，支持实时识别
- 🎬 **动作控制** - 丰富的动画和表情控制
- 📡 **Gateway 服务** - 统一的 WebSocket 网关，集成 LLM、TTS、ASR
- 📦 **TypeScript 支持** - 完整的类型定义，提供良好的开发体验
- 🎯 **事件驱动** - 完善的事件系统，方便状态监听

## 安装

```bash
# 使用 npm
npm install qt-human

# 使用 yarn
yarn add qt-human

# 使用 pnpm
pnpm add qt-human
```

## 快速开始

### 基础使用

```typescript
import Human, { EmitEvent } from 'qt-human'

// 1. 创建数字人实例
const human = new Human({
  token: 'your-auth-token',
  container: 'container-id', // DOM 元素 ID 或 HTMLElement
  characterId: 'your-character-id'
})

// 2. 监听准备就绪事件
human.on(EmitEvent.HUMAN_READY, () => {
  console.log('数字人已准备就绪')
  
  // 3. 让数字人说话
  human.speak('你好，我是数字人助手')
})

// 4. 监听错误事件
human.on(EmitEvent.HUMAN_ERROR, (error) => {
  console.error('发生错误:', error)
})
```

### TypeScript 使用

```typescript
import Human, { EmitEvent } from 'qt-human'
import type { HumanConfig } from 'qt-human'

// 使用类型定义
const config: HumanConfig = {
  token: 'your-auth-token',
  container: 'container-id',
  characterId: 'your-character-id',
  audio: {
    volume: 0.8,
    muted: false
  },
  render: {
    fps: 60,
    quality: 'high'
  }
}

const human = new Human(config)

// 类型安全的事件监听
human.on(EmitEvent.HUMAN_READY, () => {
  console.log('Ready!')
})
```

## 配置选项

### HumanConfig

```typescript
interface HumanConfig {
  // 必填：授权 Token
  token: string
  
  // 可选：渲染容器（DOM ID 或 HTMLElement）
  // 如果不提供，则只加载模型不渲染
  container?: string | HTMLElement
  
  // 可选：数字人 ID
  characterId?: string
  
  // 可选：服务配置
  server?: {
    api?: string        // API 服务地址
    gateway?: string    // Gateway WebSocket 地址（推荐）
    wss?: string        // 旧版 WebSocket 地址（将被废弃）
  }
  
  // 可选：音频配置
  audio?: {
    volume?: number   // 音量 (0-1)，默认 1.0
    muted?: boolean   // 是否静音，默认 false
  }
  
  // 可选：渲染配置
  render?: {
    fps?: number                          // 帧率，默认 30
    quality?: 'low' | 'medium' | 'high'  // 渲染质量
  }
  
  // 可选：模式
  mode?: 'normal' | 'voice' | 'render-only'
  
  // 可选：语音模式配置
  voiceModeConfig?: {
    keepActive?: boolean  // 是否保持激活状态
  }
  
  // 可选：加载动画元素
  loading?: string | HTMLElement
  
  // 可选：加载完成后是否销毁加载动画
  loadingDestroy?: boolean
  
  // 可选：调试模式
  debug?: boolean
  
  // 可选：其他配置
  options?: {
    fps?: number  // BlendShapes 每秒执行的帧数
  }
}
```

### Gateway 服务集成

从 v3.0 开始，SDK 统一使用 Gateway WebSocket 服务来处理 LLM 对话、TTS 语音合成和 ASR 语音识别功能。

```typescript
const human = new Human({
  token: 'your-auth-token',
  container: 'container-id',
  server: {
    // Gateway WebSocket 地址（推荐使用）
    gateway: 'wss://qt-gateway.quantekeji.com/api/ws',
    // API 服务地址
    api: 'https://console-api.qtworld.top'
  }
})

// 监听 Gateway 连接状态
human.on(EmitEvent.GATEWAY_CONNECTED, () => {
  console.log('Gateway 已连接')
})

human.on(EmitEvent.GATEWAY_DISCONNECTED, () => {
  console.log('Gateway 已断开')
})

human.on(EmitEvent.GATEWAY_ERROR, (error) => {
  console.error('Gateway 错误:', error)
})
```

### VoiceConversation 配置示例（高级）

如果你需要更细的语音交互控制（例如欢迎语结束后延迟恢复 ASR），可以直接使用 `VoiceConversation`：

```typescript
import { VoiceConversation } from 'qt-human'

const conversation = new VoiceConversation({
  gateway,
  renderer,
  framePlayer,
  characterId: 'your-character-id',
  // 欢迎语结束后 ASR 恢复延迟（默认 1200ms）
  welcomeAsrResumeDelayMs: 1500,
})

conversation.start()
```

## API 文档

### 核心方法

#### setCharacter

加载并显示指定的数字人模型。

```typescript
setCharacter(characterId: string, configuration?: Partial<HumanConfig>): Promise<boolean>
```

**参数:**
- `characterId` - 数字人 ID（从 Qtworld 平台获取）
- `configuration` - 可选的配置参数，用于覆盖初始配置

**返回值:**
- `Promise<boolean>` - 加载成功返回 true

**示例:**
```typescript
await human.setCharacter('your-character-id')

// 使用自定义配置
await human.setCharacter('your-character-id', {
  audio: { volume: 0.5 },
  render: { fps: 60 }
})
```

---

#### speak

让数字人播报指定的文本内容。

```typescript
speak(text: string, callback?: Function, opt?: { [key: string]: any }): Promise<StreamAudioPlayInfo>
```

**参数:**
- `text` - 要播报的文本内容
- `callback` - 可选的回调函数，接收播放事件
- `opt` - 可选配置（保留参数）

**返回值:**
- `Promise<StreamAudioPlayInfo>` - 播放信息，包含音频和 BlendShapes 数据

**示例:**
```typescript
// 基础使用
await human.speak('你好，欢迎使用数字人服务')

// 使用回调监听播放事件
await human.speak('你好', (event) => {
  console.log('播放事件:', event)
})
```

---

#### askStream

进行 AI 流式对话，支持实时响应和语音播报。

```typescript
askStream(
  params: AskParams,
  callback: (result: Result<ChatMessage[]>) => void
): Promise<Result<AskStreamResponse>>
```

**参数:**
- `params` - 对话参数
  ```typescript
  {
    reqId?: string                    // 请求 ID（可选）
    message: Message[]                // 消息列表
    conversation_id?: string          // 会话 ID（可选）
    response_mode?: 'streaming' | 'blocking'  // 响应模式
    files?: ChatFile[]                // 文件列表（可选）
  }
  ```
- `callback` - 回调函数，接收流式消息

**返回值:**
- `Promise<Result<AskStreamResponse>>` - 完整的对话响应

**示例:**
```typescript
// 单轮对话
await human.askStream(
  {
    message: [
      { role: 'user', content: '你好，请介绍一下自己' }
    ]
  },
  (result) => {
    if (result.code === 0) {
      console.log('收到消息:', result.data)
    }
  }
)

// 多轮对话（带上下文）
await human.askStream(
  {
    message: [
      { role: 'user', content: '今天天气怎么样？' },
    ],
    conversation_id: 'conv-123'
  },
  (result) => {
    console.log('AI 回复:', result.data)
  }
)
```

---

#### stop

停止当前的语音播报和动作。

```typescript
stop(): Promise<boolean>
```

**返回值:**
- `Promise<boolean>` - 停止成功返回 true

**示例:**
```typescript
// 停止当前播报
await human.stop()
```

---

### 音频控制

#### muteAudio

控制音频的静音状态。

```typescript
muteAudio(isMute: boolean = true): void
```

**参数:**
- `isMute` - true 表示静音，false 表示取消静音

**示例:**
```typescript
// 静音
human.muteAudio(true)

// 取消静音
human.muteAudio(false)
```

---

#### muteAction

控制动作的启用状态。

```typescript
muteAction(isMute: boolean = true): void
```

**参数:**
- `isMute` - true 表示禁用动作，false 表示启用动作

**示例:**
```typescript
// 禁用动作（只播放语音，不播放动画）
human.muteAction(true)

// 启用动作
human.muteAction(false)
```

---

### 语音识别

#### startVoice2Text

开始录音并进行语音识别。

```typescript
startVoice2Text(callback?: Voice2TextCallback): Promise<Result<RecorderPermission>>
```

**参数:**
- `callback` - 可选的回调函数，接收识别结果

**返回值:**
- `Promise<Result<RecorderPermission>>` - 录音权限结果

**示例:**
```typescript
// 开始录音
const result = await human.startVoice2Text((data) => {
  console.log('识别结果:', data.text)
  console.log('是否最终结果:', data.is_final)
})

if (result.code === 0) {
  console.log('录音已开始')
}
```

---

#### stopVoice2Text

停止录音并获取最终的识别结果。

```typescript
stopVoice2Text(callback?: Voice2TextCallback): Promise<Result<null>>
```

**参数:**
- `callback` - 可选的回调函数，接收最终识别结果

**返回值:**
- `Promise<Result<null>>` - 停止结果

**示例:**
```typescript
// 停止录音
await human.stopVoice2Text((data) => {
  console.log('最终识别结果:', data.text)
})
```

---

### 动作和视角控制

#### playAction

播放指定的动画动作。

```typescript
playAction(code: string, opts?: PlayActionOptions): Promise<void>
```

**参数:**
- `code` - 动画代码
- `opts` - 动画选项
  ```typescript
  {
    loop?: THREE.AnimationActionLoopStyles  // 循环模式
    repetitions?: number                     // 重复次数
  }
  ```

**示例:**
```typescript
// 播放单次动画
await human.playAction('wave')

// 循环播放动画
await human.playAction('idle', {
  loop: 2201,  // THREE.LoopRepeat
  repetitions: Infinity
})
```

---

#### setAngleView

设置相机视角。

```typescript
setAngleView(config: AngleViewConfig): void
```

**参数:**
- `config` - 视角配置
  ```typescript
  {
    camera?: {
      position?: [number, number, number]
      rotation?: [number, number, number]
      fov?: number
      duration?: number  // 动画持续时间（毫秒）
    }
    controls?: {
      target?: [number, number, number]
      enabled?: boolean
      duration?: number  // 动画持续时间（毫秒）
    }
  }
  ```

**示例:**
```typescript
// 切换到正面视角
human.setAngleView({
  camera: {
    position: [0, 1.6, 2],
    duration: 1000
  }
})
```

---

### 资源管理

#### updateToken

更新授权 Token。

```typescript
updateToken(token: string): void
```

**参数:**
- `token` - 新的授权 Token

**示例:**
```typescript
// 更新 Token
human.updateToken('new-auth-token')
```

---

#### destroy

销毁数字人实例，释放所有资源。

```typescript
destroy(): Promise<void>
```

**示例:**
```typescript
// 销毁实例
await human.destroy()
```

---

### 其他方法

#### getModelInfo

获取当前加载的模型信息。

```typescript
getModelInfo(): ModelInfo | null
```

**返回值:**
- `ModelInfo | null` - 模型信息对象，如果未加载则返回 null

---

#### speakByAudioShapes

使用自定义音频和 BlendShapes 数据驱动数字人说话。

```typescript
speakByAudioShapes(uris: string[], shapes: number[][][]): Promise<boolean>
```

**参数:**
- `uris` - 音频 URL 数组
- `shapes` - BlendShapes 数据数组

**示例:**
```typescript
await human.speakByAudioShapes(
  ['https://example.com/audio1.wav', 'https://example.com/audio2.wav'],
  [
    [[0, 0, ...], [0.1, 0.2, ...]],  // 第一段音频的 BlendShapes
    [[0, 0, ...], [0.1, 0.2, ...]]   // 第二段音频的 BlendShapes
  ]
)
```

---

#### startRender / stopRender

控制渲染的启动和停止。

```typescript
startRender(): Promise<boolean>
stopRender(): Promise<boolean>
```

**示例:**
```typescript
// 开始渲染
await human.startRender()

// 停止渲染
await human.stopRender()
```

## 事件系统

Human 类基于 Emittery 实现了完善的事件系统，您可以监听各种状态变化。

### 事件列表

#### 生命周期事件

| 事件 | 说明 | 数据类型 |
|------|------|---------|
| `HUMAN_READY` | 数字人加载完成，可以开始交互 | `void` |
| `HUMAN_ERROR` | 发生错误 | `ErrorEvent` |
| `HUMAN_LOAD_PROGRESS` | 模型加载进度更新 | `LoadProgressEvent` |
| `HUMAN_ACTION_PENDING` | 有待执行的动作 | `string[]` |
| `CORE_BUNDLES_LOADED` | 核心模型包加载完成 | `any[]` |

#### 音频事件

| 事件 | 说明 | 数据类型 |
|------|------|---------|
| `AUDIO_PLAY` | 音频开始播放 | `AudioPlayEvent` |
| `AUDIO_PAUSE` | 音频暂停 | `AudioPlayEvent` |

#### 语音识别事件

| 事件 | 说明 | 数据类型 |
|------|------|---------|
| `VOICE2TEXT` | 语音识别结果 | `Voice2TextEvent` |
| `RECORDER_START` | 开始录音 | `void` |
| `RECORDER_STOP` | 结束录音 | `void` |
| `RECORDER_MESSAGE` | 录音消息 | `any` |
| `RECORDER_SENDING` | 发送录音数据 | `void` |

#### 对话和 TTS 事件

| 事件 | 说明 | 数据类型 |
|------|------|---------|
| `CHAT_MESSAGE_BS_PLAY_START` | 对话消息的 BlendShapes 动画开始播放 | `object` |
| `CHAT_MESSAGE_BS_PLAY_END` | 对话消息的 BlendShapes 动画结束播放 | `object` |

#### Gateway 服务事件

| 事件 | 说明 | 数据类型 |
|------|------|---------|
| `GATEWAY_CONNECTED` | Gateway WebSocket 连接成功 | `void` |
| `GATEWAY_DISCONNECTED` | Gateway WebSocket 断开连接 | `void` |
| `GATEWAY_RECONNECTING` | Gateway WebSocket 正在重连 | `void` |
| `GATEWAY_MESSAGE` | Gateway 接收到消息 | `GatewayMessageEvent` |
| `GATEWAY_ERROR` | Gateway 发生错误 | `ErrorEvent` |

#### 渲染事件

| 事件 | 说明 | 数据类型 |
|------|------|---------|
| `RENDER_READY` | 渲染服务初始化完成 | `void` |
| `RENDER_ERROR` | 渲染服务发生错误 | `ErrorEvent` |
| `MODEL_LOAD_PROGRESS` | 模型加载进度更新 | `ModelLoadEvent` |
| `MODEL_LOADED` | 模型加载完成 | `ModelLoadEvent` |
| `MODEL_LOAD_FAILED` | 模型加载失败 | `ErrorEvent` |
| `ANIMATION_START` | 动画播放开始 | `AnimationEvent` |
| `ANIMATION_END` | 动画播放结束 | `AnimationEvent` |

### 事件使用示例

```typescript
import Human, { EmitEvent } from 'qt-human'

const human = new Human({
  token: 'your-token',
  container: 'container-id'
})

// 监听加载进度
human.on(EmitEvent.HUMAN_LOAD_PROGRESS, (data) => {
  const percentage = (data.progress / data.total * 100).toFixed(2)
  console.log(`加载进度: ${percentage}%`)
  
  // 更新进度条
  updateProgressBar(percentage)
})

// 监听准备就绪
human.on(EmitEvent.HUMAN_READY, () => {
  console.log('数字人已准备就绪')
  
  // 隐藏加载动画
  hideLoading()
  
  // 开始交互
  human.speak('你好，我已经准备好了')
})

// 监听错误
human.on(EmitEvent.HUMAN_ERROR, (error) => {
  console.error('错误代码:', error.code)
  console.error('错误消息:', error.message)
  
  // 显示错误提示
  showErrorMessage(error.message)
})

// 监听音频播放状态
human.on(EmitEvent.AUDIO_PLAY, () => {
  console.log('音频开始播放')
  // 显示播放图标
  showPlayingIcon()
})

human.on(EmitEvent.AUDIO_PAUSE, () => {
  console.log('音频已暂停')
  // 隐藏播放图标
  hidePlayingIcon()
})

// 监听 Gateway 连接状态
human.on(EmitEvent.GATEWAY_CONNECTED, () => {
  console.log('Gateway 服务已连接')
  showConnectionStatus('connected')
})

human.on(EmitEvent.GATEWAY_DISCONNECTED, () => {
  console.log('Gateway 服务已断开')
  showConnectionStatus('disconnected')
})

human.on(EmitEvent.GATEWAY_ERROR, (error) => {
  console.error('Gateway 错误:', error)
  showConnectionStatus('error')
})

// 监听语音识别结果
human.on(EmitEvent.VOICE2TEXT, (data) => {
  if (data.isFinal) {
    console.log('最终识别结果:', data.text)
    // 显示最终结果
    displayFinalText(data.text)
  } else {
    console.log('临时识别结果:', data.text)
    // 显示临时结果
    displayTempText(data.text)
  }
})

// 监听对话消息播放
human.on(EmitEvent.CHAT_MESSAGE_BS_PLAY_START, (data) => {
  console.log('开始播放对话:', data.content)
})

human.on(EmitEvent.CHAT_MESSAGE_BS_PLAY_END, (data) => {
  console.log('对话播放结束')
  if (data.isEnd) {
    console.log('所有对话播放完成')
  }
})
```

### 类型定义

```typescript
// 加载进度事件
interface LoadProgressEvent {
  code: 'download' | 'loaded' | 'loading'
  progress: number
  total: number
  url?: string
}

// 错误事件
interface ErrorEvent {
  code: string | number
  msg: string
  details?: any
}

// 音频播放事件
interface AudioPlayEvent {
  playing: boolean
  duration?: number
  currentTime?: number
}

// 语音识别事件
interface Voice2TextEvent {
  isFinal: boolean
  text: string
  confidence?: number
}

// Gateway 消息事件
interface GatewayMessageEvent {
  type: 'chat' | 'tts' | 'asr' | 'error'
  event: string
  data: any
  requestId?: string
}
```

## 使用示例

### 基础对话示例

```typescript
import Human, { EmitEvent } from 'qt-human'

// 创建数字人实例
const human = new Human({
  token: 'your-auth-token',
  container: 'human-container',
  characterId: 'your-character-id'
})

// 监听准备就绪
human.on(EmitEvent.HUMAN_READY, async () => {
  console.log('数字人已准备就绪')
  
  // 让数字人打招呼
  await human.speak('你好，我是数字人助手，有什么可以帮助你的吗？')
})

// 监听错误
human.on(EmitEvent.HUMAN_ERROR, (error) => {
  console.error('发生错误:', error)
})
```

### AI 对话示例

```typescript
import Human, { EmitEvent } from 'qt-human'

const human = new Human({
  token: 'your-auth-token',
  container: 'human-container',
  characterId: 'your-character-id'
})

// 单轮对话
async function chat(question: string) {
  const result = await human.askStream(
    {
      message: [
        { role: 'user', content: question }
      ]
    },
    (streamResult) => {
      // 处理流式消息
      if (streamResult.code === 0) {
        const messages = streamResult.data
        messages.forEach(msg => {
          if (msg.event === 'message') {
            console.log('AI 回复:', msg.answer)
          }
        })
      }
    }
  )
  
  if (result.code === 0) {
    console.log('对话完成')
  }
}

// 使用示例
human.on(EmitEvent.HUMAN_READY, () => {
  chat('今天天气怎么样？')
})
```

### 多轮对话示例

```typescript
import Human, { EmitEvent } from 'qt-human'

const human = new Human({
  token: 'your-auth-token',
  container: 'human-container',
  characterId: 'your-character-id'
})

// 维护对话历史
const conversationHistory: Array<{ role: 'user' | 'assistant', content: string }> = []
let conversationId = ''

async function chatWithContext(userMessage: string) {
  // 添加用户消息到历史
  conversationHistory.push({
    role: 'user',
    content: userMessage
  })
  
  const result = await human.askStream(
    {
      message: conversationHistory,
      conversation_id: conversationId
    },
    (streamResult) => {
      if (streamResult.code === 0) {
        const messages = streamResult.data
        messages.forEach(msg => {
          if (msg.event === 'message') {
            // 保存会话 ID
            if (msg.conversation_id) {
              conversationId = msg.conversation_id
            }
          }
        })
      }
    }
  )
  
  if (result.code === 0) {
    // 添加 AI 回复到历史
    // 注意：实际的回复内容需要从流式消息中收集
    console.log('对话完成')
  }
}

// 使用示例
human.on(EmitEvent.HUMAN_READY, async () => {
  await chatWithContext('你好')
  await chatWithContext('今天天气怎么样？')
  await chatWithContext('那适合户外运动吗？')
})
```

### 语音识别示例

```typescript
import Human, { EmitEvent } from 'qt-human'

const human = new Human({
  token: 'your-auth-token',
  container: 'human-container',
  characterId: 'your-character-id'
})

let recognizedText = ''

// 开始录音按钮
document.getElementById('start-record')?.addEventListener('click', async () => {
  recognizedText = ''
  
  const result = await human.startVoice2Text((data) => {
    if (data.is_final) {
      // 最终识别结果
      recognizedText = data.text
      console.log('最终识别结果:', data.text)
      
      // 显示识别结果
      document.getElementById('result')!.textContent = data.text
    } else {
      // 临时识别结果
      console.log('临时识别结果:', data.text)
      
      // 显示临时结果
      document.getElementById('temp-result')!.textContent = data.text
    }
  })
  
  if (result.code === 0) {
    console.log('开始录音')
  }
})

// 停止录音按钮
document.getElementById('stop-record')?.addEventListener('click', async () => {
  await human.stopVoice2Text((data) => {
    console.log('录音结束，最终结果:', data.text)
    
    // 使用识别结果进行对话
    if (data.text) {
      human.askStream(
        {
          message: [{ role: 'user', content: data.text }]
        },
        (result) => {
          console.log('AI 回复:', result)
        }
      )
    }
  })
})
```

### 音频和动作控制示例

```typescript
import Human, { EmitEvent } from 'qt-human'

const human = new Human({
  token: 'your-auth-token',
  container: 'human-container',
  characterId: 'your-character-id'
})

// 静音控制
document.getElementById('mute-audio')?.addEventListener('click', () => {
  human.muteAudio(true)
  console.log('已静音')
})

document.getElementById('unmute-audio')?.addEventListener('click', () => {
  human.muteAudio(false)
  console.log('已取消静音')
})

// 动作控制
document.getElementById('mute-action')?.addEventListener('click', () => {
  human.muteAction(true)
  console.log('已禁用动作')
})

document.getElementById('unmute-action')?.addEventListener('click', () => {
  human.muteAction(false)
  console.log('已启用动作')
})

// 播放自定义动画
document.getElementById('play-wave')?.addEventListener('click', () => {
  human.playAction('wave')
})

// 停止当前播报
document.getElementById('stop')?.addEventListener('click', async () => {
  await human.stop()
  console.log('已停止')
})
```

### 视角控制示例

```typescript
import Human, { EmitEvent } from 'qt-human'

const human = new Human({
  token: 'your-auth-token',
  container: 'human-container',
  characterId: 'your-character-id'
})

// 切换到正面视角
document.getElementById('front-view')?.addEventListener('click', () => {
  human.setAngleView({
    camera: {
      position: [0, 1.6, 2],
      fov: 50,
      duration: 1000  // 1秒过渡动画
    }
  })
})

// 切换到侧面视角
document.getElementById('side-view')?.addEventListener('click', () => {
  human.setAngleView({
    camera: {
      position: [2, 1.6, 1],
      fov: 50,
      duration: 1000
    }
  })
})
```

### 完整应用示例

```typescript
import Human, { EmitEvent } from 'qt-human'

class DigitalHumanApp {
  private human: Human
  private conversationHistory: Array<{ role: 'user' | 'assistant', content: string }> = []
  private conversationId = ''
  
  constructor(config: { token: string, container: string, characterId: string }) {
    this.human = new Human(config)
    this.setupEventListeners()
  }
  
  private setupEventListeners() {
    // 监听加载进度
    this.human.on(EmitEvent.HUMAN_LOAD_PROGRESS, (data) => {
      const percentage = (data.progress / data.total * 100).toFixed(2)
      this.updateLoadingProgress(percentage)
    })
    
    // 监听准备就绪
    this.human.on(EmitEvent.HUMAN_READY, () => {
      this.hideLoading()
      this.human.speak('你好，我已经准备好了，有什么可以帮助你的吗？')
    })
    
    // 监听错误
    this.human.on(EmitEvent.HUMAN_ERROR, (error) => {
      this.showError(error.message)
    })
    
    // 监听音频播放状态
    this.human.on(EmitEvent.AUDIO_PLAY, () => {
      this.showPlayingIndicator()
    })
    
    this.human.on(EmitEvent.AUDIO_PAUSE, () => {
      this.hidePlayingIndicator()
    })
  }
  
  async chat(userMessage: string) {
    // 添加用户消息
    this.conversationHistory.push({
      role: 'user',
      content: userMessage
    })
    
    // 显示用户消息
    this.addMessageToUI('user', userMessage)
    
    let aiResponse = ''
    
    const result = await this.human.askStream(
      {
        message: this.conversationHistory,
        conversation_id: this.conversationId
      },
      (streamResult) => {
        if (streamResult.code === 0) {
          streamResult.data.forEach(msg => {
            if (msg.event === 'message') {
              aiResponse += msg.answer
              this.updateAIMessage(aiResponse)
              
              if (msg.conversation_id) {
                this.conversationId = msg.conversation_id
              }
            }
          })
        }
      }
    )
    
    if (result.code === 0) {
      // 添加 AI 回复到历史
      this.conversationHistory.push({
        role: 'assistant',
        content: aiResponse
      })
    }
  }
  
  async startVoiceInput() {
    await this.human.startVoice2Text((data) => {
      if (data.is_final) {
        // 使用识别结果进行对话
        this.chat(data.text)
      } else {
        // 显示临时识别结果
        this.showTempRecognitionResult(data.text)
      }
    })
  }
  
  async stopVoiceInput() {
    await this.human.stopVoice2Text()
  }
  
  async stop() {
    await this.human.stop()
  }
  
  async destroy() {
    await this.human.destroy()
  }
  
  // UI 辅助方法
  private updateLoadingProgress(percentage: string) {
    // 更新进度条
  }
  
  private hideLoading() {
    // 隐藏加载动画
  }
  
  private showError(message: string) {
    // 显示错误提示
  }
  
  private showPlayingIndicator() {
    // 显示播放指示器
  }
  
  private hidePlayingIndicator() {
    // 隐藏播放指示器
  }
  
  private addMessageToUI(role: 'user' | 'assistant', content: string) {
    // 添加消息到 UI
  }
  
  private updateAIMessage(content: string) {
    // 更新 AI 消息
  }
  
  private showTempRecognitionResult(text: string) {
    // 显示临时识别结果
  }
}

// 使用示例
const app = new DigitalHumanApp({
  token: 'your-auth-token',
  container: 'human-container',
  characterId: 'your-character-id'
})

// 发送消息
document.getElementById('send-btn')?.addEventListener('click', () => {
  const input = document.getElementById('message-input') as HTMLInputElement
  if (input.value.trim()) {
    app.chat(input.value.trim())
    input.value = ''
  }
})

// 语音输入
document.getElementById('voice-btn')?.addEventListener('click', () => {
  app.startVoiceInput()
})

// 停止录音
document.getElementById('stop-voice-btn')?.addEventListener('click', () => {
  app.stopVoiceInput()
})
```

## 获取 Token 和 Character ID

### 获取 Character ID

1. 访问 [Qtworld 平台](https://console.quantekeji.com/login)
2. 登录您的账号
3. 在数字人管理页面复制您的数字人 ID

### 获取授权 Token

#### 方式一：通过授权接口获取（推荐）

**接口信息:**
- 请求方式: `POST`
- 接口地址: `https://console-api.qtworld.top/human-auth/get-token`

**请求参数:**
```typescript
{
  secretKey: string    // 应用秘钥（从 Qtworld 平台获取）
  visitorId: string    // 访问者 ID（需要唯一）
  visitorName: string  // 访问者名称
}
```

**响应参数:**
```typescript
{
  code: number  // 状态码，0 表示成功
  data: string  // JWT Token
  msg: string   // 响应消息
}
```

**示例代码:**

```typescript
import axios from 'axios'
import Human from 'qt-human'

async function initHuman() {
  try {
    // 1. 获取 Token（建议在服务端调用）
    const response = await axios.post(
      'https://console-api.qtworld.top/human-auth/get-token',
      {
        secretKey: 'your-secret-key',
        visitorId: 'unique-visitor-id',
        visitorName: 'visitor-name'
      }
    )
    
    if (response.data.code === 0) {
      const token = response.data.data
      
      // 2. 初始化 Human 对象
      const human = new Human({
        token,
        container: 'human-container',
        characterId: 'your-character-id'
      })
      
      return human
    } else {
      throw new Error(response.data.msg)
    }
  } catch (error) {
    console.error('初始化失败:', error)
    throw error
  }
}

// 使用
initHuman().then(human => {
  console.log('数字人初始化成功')
})
```

#### 方式二：服务端代理（推荐用于生产环境）

为了保护 `secretKey` 不被暴露在前端，建议在服务端实现一个代理接口：

**后端示例（Node.js/Express）:**

```javascript
// server.js
const express = require('express')
const axios = require('axios')

const app = express()
app.use(express.json())

app.post('/api/get-human-token', async (req, res) => {
  try {
    const { visitorId, visitorName } = req.body
    
    // 从环境变量读取 secretKey
    const secretKey = process.env.HUMAN_SECRET_KEY
    
    const response = await axios.post(
      'https://console-api.qtworld.top/human-auth/get-token',
      {
        secretKey,
        visitorId,
        visitorName
      }
    )
    
    res.json(response.data)
  } catch (error) {
    res.status(500).json({
      code: -1,
      msg: error.message
    })
  }
})

app.listen(3000)
```

**前端调用:**

```typescript
import axios from 'axios'
import Human from 'qt-human'

async function initHuman(visitorId: string, visitorName: string) {
  try {
    // 调用自己的后端接口
    const response = await axios.post('/api/get-human-token', {
      visitorId,
      visitorName
    })
    
    if (response.data.code === 0) {
      const token = response.data.data
      
      const human = new Human({
        token,
        container: 'human-container',
        characterId: 'your-character-id'
      })
      
      return human
    }
  } catch (error) {
    console.error('初始化失败:', error)
    throw error
  }
}
```

### Token 管理

#### Token 过期处理

Token 通常有效期为 2 小时，过期后需要重新获取。您可以使用 `updateToken` 方法更新 Token：

```typescript
// 定期刷新 Token
async function refreshToken(human: Human) {
  try {
    const response = await axios.post('/api/get-human-token', {
      visitorId: 'unique-visitor-id',
      visitorName: 'visitor-name'
    })
    
    if (response.data.code === 0) {
      const newToken = response.data.data
      human.updateToken(newToken)
      console.log('Token 已更新')
    }
  } catch (error) {
    console.error('Token 更新失败:', error)
  }
}

// 每 1.5 小时刷新一次 Token
setInterval(() => {
  refreshToken(human)
}, 90 * 60 * 1000)
```

## 注意事项

### 安全性

1. **不要在前端代码中硬编码 `secretKey`**
   - `secretKey` 是敏感信息，应该存储在服务端环境变量中
   - 建议通过服务端代理接口获取 Token

2. **Token 管理**
   - Token 有效期通常为 2 小时
   - 建议实现 Token 自动刷新机制
   - 不要将 Token 存储在不安全的地方（如 localStorage）

3. **访问者 ID 唯一性**
   - `visitorId` 需要保证唯一性
   - 建议使用用户 ID 或其他唯一标识

### 使用建议

1. **等待模型加载完成**
   - 在调用 `speak`、`askStream` 等方法前，确保监听到 `HUMAN_READY` 事件
   - 模型加载需要一定时间，请耐心等待

2. **资源管理**
   - 在组件销毁时调用 `destroy()` 方法释放资源
   - 避免创建多个 Human 实例导致内存泄漏

3. **错误处理**
   - 始终监听 `HUMAN_ERROR` 事件
   - 对异步方法使用 try-catch 捕获错误

4. **性能优化**
   - 根据需要调整 `fps` 参数（默认 30）
   - 使用 `muteAction` 禁用动画可以提升性能
   - 在不需要渲染时，不传入 `container` 参数

5. **网络要求**
   - 确保网络连接稳定
   - Gateway WebSocket 需要支持 WSS 协议
   - 建议在良好的网络环境下使用

### 常见问题

**Q: Token 过期后会发生什么？**

A: Token 过期后，API 调用会失败。建议实现 Token 自动刷新机制，或在错误回调中处理 Token 过期的情况。

**Q: 可以同时创建多个 Human 实例吗？**

A: 技术上可以，但不推荐。多个实例会占用大量内存和 GPU 资源，建议只创建一个实例并复用。

**Q: 如何处理网络断开的情况？**

A: SDK 内置了 Gateway 自动重连机制。您可以监听 `GATEWAY_DISCONNECTED` 和 `GATEWAY_CONNECTED` 事件来处理断线重连的 UI 提示。

**Q: 支持哪些浏览器？**

A: 支持现代浏览器：
- Chrome 90+
- Firefox 88+
- Safari 14+
- Edge 90+

**Q: 如何调试？**

A: 在配置中设置 `debug: true` 可以开启调试模式，会输出更多日志信息。

```typescript
const human = new Human({
  token: 'your-token',
  container: 'container-id',
  debug: true  // 开启调试模式
})
```


## TypeScript 类型定义

SDK 提供完整的 TypeScript 类型定义，以下是主要类型的说明。

### 配置类型

```typescript
import type { 
  HumanConfig,
  ServerConfig,
  AudioConfig,
  RenderConfig
} from 'qt-human'
```

### API 类型

```typescript
import type { 
  AskParams,
  ChatMessage,
  AskResponse,
  TTSParams,
  Voice2TextResult
} from 'qt-human'
```

### 事件类型

```typescript
import { EmitEvent } from 'qt-human'
import type { 
  LoadProgressEvent,
  ErrorEvent,
  AudioPlayEvent,
  Voice2TextEvent,
  GatewayMessageEvent
} from 'qt-human'

// 使用事件类型
human.on(EmitEvent.HUMAN_LOAD_PROGRESS, (data: LoadProgressEvent) => {
  console.log(data.progress, data.total)
})

human.on(EmitEvent.HUMAN_ERROR, (error: ErrorEvent) => {
  console.log(error.code, error.message)
})
```

## 版本历史

### v3.0.0 (当前版本)

**重大变更:**
- 🔄 重构 TypeScript 类型系统，提供完整的类型定义
- 🌐 统一使用 Gateway WebSocket 服务
- 🎯 完善事件系统，新增多个事件类型
- 📦 优化项目结构，符合 JSSDK 标准
- 🗑️ 移除旧版 WebSocket 连接代码
- ⚡ 优化 Three.js 渲染性能

**新增功能:**
- Gateway 服务集成（LLM、TTS、ASR）
- 流式对话支持
- 完整的事件系统
- TypeScript 类型导出

**废弃功能:**
- 旧版 `openVoiceInteraction` 方法（请使用 Gateway ASR 功能）
- 多个独立的 WebSocket 连接（统一使用 Gateway）

**迁移指南:**

如果您从 v2.x 升级到 v3.0，请注意以下变更：

1. **配置变更:**
```typescript
// v2.x
const human = new Human({
  token: 'xxx',
  server: {
    wss: 'ws://...'  // 旧版配置
  }
})

// v3.0
const human = new Human({
  token: 'xxx',
  server: {
    gateway: 'wss://...'  // 新版配置
  }
})
```

2. **事件名称变更:**
```typescript
// v2.x
human.on('ready', () => {})

// v3.0
import { EmitEvent } from 'qt-human'
human.on(EmitEvent.HUMAN_READY, () => {})
```

3. **类型导入:**
```typescript
// v3.0 新增
import type { HumanConfig } from 'qt-human'

const config: HumanConfig = {
  // ...
}
```

## 测试

### 测试文档

- [完整测试指南](./TESTING_GUIDE.md) - 详细的手动测试和性能测试指南
- [快速测试清单](./TEST_CHECKLIST.md) - 快速测试检查清单
- [测试实施总结](./.kiro/specs/jssdk-refactor/TESTING_IMPLEMENTATION.md) - 测试实施情况总结

### 测试工具

#### 性能监控工具

打开 `examples/performance-monitor.html` 可以实时监控 SDK 的性能指标：

- **渲染性能**: 帧率、帧时间、渲染调用次数
- **内存使用**: JS 堆大小、堆使用率、内存增长
- **网络性能**: WebSocket 状态、连接延迟、消息数量
- **系统信息**: 浏览器、WebGL 版本、GPU 信息

```bash
# 启动开发服务器
npm run dev

# 打开浏览器访问
# http://localhost:5173/examples/performance-monitor.html
```

#### 自动化测试运行器

打开 `examples/test-runner.html` 可以运行自动化测试套件：

- 初始化测试
- 模型加载测试
- 播报功能测试
- 对话功能测试
- 错误处理测试

```bash
# 打开浏览器访问
# http://localhost:5173/examples/test-runner.html
```

### 性能标准

#### 渲染性能
- 帧率 ≥ 30 FPS (配置为 30) 或 ≥ 55 FPS (配置为 60)
- 帧率稳定性：波动 < 10%
- 无明显掉帧

#### 内存使用
- 初始内存 < 200 MB
- 运行时内存增长 < 50 MB / 10分钟
- 无明显内存泄漏
- 销毁后内存释放 > 90%

#### 网络性能
- 连接延迟 < 500 ms
- 消息延迟 < 200 ms
- 重连成功率 > 95%
- 数据传输流畅无卡顿

### 浏览器兼容性

| 浏览器 | 版本 | 状态 |
|-------|------|------|
| Chrome | 90+ | ✅ 完全支持 |
| Firefox | 88+ | ✅ 完全支持 |
| Safari | 14+ | ✅ 完全支持 |
| Edge | 90+ | ✅ 完全支持 |
| iOS Safari | 14+ | ✅ 完全支持 |
| Android Chrome | 8+ | ✅ 完全支持 |

### 运行测试

```bash
# 安装依赖
npm install

# 构建项目
npm run build

# 启动开发服务器
npm run dev

```

## 许可证

ISC

## 支持

如有问题或建议，请联系：

- 邮箱: bd@quantekeji.com
- 平台: [Qtworld](https://console.quantekeji.com/login)

## 相关链接

- [Qtworld 平台](https://console.quantekeji.com/login)
- [示例项目](https://github.com/quantetech/qt-animation-scene)
