# 陪练功能API接口规范

## 一、通用规范

### 1.1 请求格式

**Base URL**：
- 开发环境：`http://localhost:8000`
- 生产环境：`https://aiedu.ireborn.com.cn`

**请求头**：
```http
Content-Type: application/json; charset=utf-8
Authorization: Bearer <access_token>
```

### 1.2 响应格式

**统一响应结构**：
```json
{
  "code": 200,
  "message": "success",
  "data": {},
  "request_id": "uuid"
}
```

**分页响应结构**：
```json
{
  "code": 200,
  "message": "success",
  "data": {
    "items": [],
    "total": 100,
    "page": 1,
    "page_size": 20,
    "pages": 5
  }
}
```

### 1.3 状态码约定

| HTTP状态码 | code | 说明 |
|-----------|------|------|
| 200 | 200 | 成功 |
| 400 | 400 | 请求参数错误 |
| 401 | 401 | 未授权（token无效或过期） |
| 403 | 403 | 无权限 |
| 404 | 404 | 资源不存在 |
| 500 | 500 | 服务器错误 |

## 二、数据模型

### 2.1 场景类型枚举 (SceneType)

```typescript
type SceneType = 'phone' | 'face' | 'complaint' | 'after-sales' | 'product-intro'
```

| 值 | 说明 |
|----|------|
| phone | 电话销售 |
| face | 面对面销售 |
| complaint | 客户投诉 |
| after-sales | 售后服务 |
| product-intro | 产品介绍 |

### 2.2 难度等级枚举 (Difficulty)

```typescript
type Difficulty = 'beginner' | 'junior' | 'intermediate' | 'senior' | 'expert'
```

| 值 | 说明 |
|----|------|
| beginner | 入门 |
| junior | 初级 |
| intermediate | 中级 |
| senior | 高级 |
| expert | 专家 |

### 2.3 场景状态枚举 (SceneStatus)

```typescript
type SceneStatus = 'active' | 'inactive'
```

| 值 | 说明 |
|----|------|
| active | 启用 |
| inactive | 禁用 |

### 2.4 陪练场景对象 (PracticeScene)

```typescript
interface PracticeScene {
  id: number
  name: string
  description: string
  type: SceneType
  difficulty: Difficulty
  status: SceneStatus
  background: string
  ai_role: string
  objectives: string[]
  keywords: string[]
  duration: number
  usage_count: number
  rating: number
  created_by: number
  updated_by: number
  created_at: string
  updated_at: string
}
```

## 三、场景管理接口（Manager）

### 3.1 获取场景列表

**接口**：`GET /api/v1/manager/practice-scenes`

**权限**：manager、admin

**请求参数**：
| 参数 | 类型 | 必填 | 说明 | 示例 |
|------|------|------|------|------|
| page | number | 否 | 页码，默认1 | 1 |
| size | number | 否 | 每页数量，默认20 | 20 |
| type | string | 否 | 场景类型筛选 | phone |
| difficulty | string | 否 | 难度筛选 | intermediate |
| status | string | 否 | 状态筛选 | active |
| search | string | 否 | 关键词搜索（名称、描述） | 销售 |

**请求示例**：
```http
GET /api/v1/manager/practice-scenes?page=1&size=20&type=phone&difficulty=beginner
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

**响应示例**：
```json
{
  "code": 200,
  "message": "success",
  "data": {
    "items": [
      {
        "id": 1,
        "name": "初次电话拜访客户",
        "description": "模拟首次通过电话联系潜在客户的场景",
        "type": "phone",
        "difficulty": "beginner",
        "status": "active",
        "background": "你是一名销售专员...",
        "ai_role": "AI扮演一位忙碌的采购经理...",
        "objectives": ["学会专业的电话开场白", "快速建立信任关系"],
        "keywords": ["开场白", "需求挖掘"],
        "duration": 10,
        "usage_count": 256,
        "rating": 4.5,
        "created_at": "2024-01-15T10:30:00",
        "updated_at": "2024-03-18T15:20:00"
      }
    ],
    "total": 15,
    "page": 1,
    "page_size": 20,
    "pages": 1
  }
}
```

### 3.2 获取场景详情

**接口**：`GET /api/v1/manager/practice-scenes/{id}`

**权限**：manager、admin

**路径参数**：
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | number | 是 | 场景ID |

**请求示例**：
```http
GET /api/v1/manager/practice-scenes/1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

**响应示例**：
```json
{
  "code": 200,
  "message": "success",
  "data": {
    "id": 1,
    "name": "初次电话拜访客户",
    "description": "模拟首次通过电话联系潜在客户的场景",
    "type": "phone",
    "difficulty": "beginner",
    "status": "active",
    "background": "你是一名销售专员，需要通过电话联系一位从未接触过的潜在客户。客户是某公司的采购经理，你需要在短时间内引起他的兴趣。",
    "ai_role": "AI扮演一位忙碌且略显不耐烦的采购经理，对推销电话比较抵触，但如果销售人员能够快速切入他的需求点，他会愿意继续交谈。",
    "objectives": ["学会专业的电话开场白", "快速建立信任关系", "有效探询客户需求", "预约下次沟通时间"],
    "keywords": ["开场白", "需求挖掘", "时间管理", "预约技巧"],
    "duration": 10,
    "usage_count": 256,
    "rating": 4.5,
    "created_by": 1,
    "updated_by": 1,
    "created_at": "2024-01-15T10:30:00",
    "updated_at": "2024-03-18T15:20:00"
  }
}
```

### 3.3 创建场景

**接口**：`POST /api/v1/manager/practice-scenes`

**权限**：manager、admin

**请求体**：
```typescript
interface CreateSceneRequest {
  name: string                 // 必填，最长200字符
  description: string          // 必填，场景描述
  type: SceneType             // 必填
  difficulty: Difficulty      // 必填
  status?: SceneStatus        // 可选，默认active
  background: string          // 必填，场景背景
  ai_role: string             // 必填，AI角色描述
  objectives: string[]        // 必填，练习目标数组
  keywords: string[]          // 必填，关键词数组
  duration?: number           // 可选，预计时长（分钟），默认10
}
```

**请求示例**：
```http
POST /api/v1/manager/practice-scenes
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

{
  "name": "产品功能介绍",
  "description": "练习向客户详细介绍产品功能特点",
  "type": "product-intro",
  "difficulty": "junior",
  "status": "active",
  "background": "客户对你们的产品有一定了解，现在希望你详细介绍产品的核心功能和优势。",
  "ai_role": "AI扮演一位专业的采购人员，会提出具体的技术问题和需求，希望了解产品的详细功能。",
  "objectives": ["清晰介绍产品功能", "突出产品优势", "回答技术问题"],
  "keywords": ["产品介绍", "功能展示", "优势分析"],
  "duration": 12
}
```

**响应示例**：
```json
{
  "code": 200,
  "message": "创建场景成功",
  "data": {
    "id": 6,
    "name": "产品功能介绍",
    "description": "练习向客户详细介绍产品功能特点",
    "type": "product-intro",
    "difficulty": "junior",
    "status": "active",
    "background": "客户对你们的产品有一定了解...",
    "ai_role": "AI扮演一位专业的采购人员...",
    "objectives": ["清晰介绍产品功能", "突出产品优势", "回答技术问题"],
    "keywords": ["产品介绍", "功能展示", "优势分析"],
    "duration": 12,
    "usage_count": 0,
    "rating": 0.0,
    "created_at": "2025-10-13T18:30:00"
  }
}
```

### 3.4 更新场景

**接口**：`PUT /api/v1/manager/practice-scenes/{id}`

**权限**：manager、admin

**路径参数**：
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | number | 是 | 场景ID |

**请求体**：与创建场景相同，所有字段可选

**请求示例**：
```http
PUT /api/v1/manager/practice-scenes/6
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

{
  "name": "产品功能详细介绍",
  "duration": 15,
  "status": "active"
}
```

**响应示例**：
```json
{
  "code": 200,
  "message": "更新场景成功",
  "data": {
    "id": 6,
    "name": "产品功能详细介绍",
    "duration": 15,
    "updated_at": "2025-10-13T19:00:00"
  }
}
```

### 3.5 删除场景

**接口**：`DELETE /api/v1/manager/practice-scenes/{id}`

**权限**：manager、admin

**路径参数**：
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | number | 是 | 场景ID |

**请求示例**：
```http
DELETE /api/v1/manager/practice-scenes/6
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

**响应示例**：
```json
{
  "code": 200,
  "message": "删除场景成功",
  "data": {
    "id": 6
  }
}
```

### 3.6 切换场景状态

**接口**：`PUT /api/v1/manager/practice-scenes/{id}/toggle-status`

**权限**：manager、admin

**路径参数**：
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | number | 是 | 场景ID |

**请求示例**：
```http
PUT /api/v1/manager/practice-scenes/6/toggle-status
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

**响应示例**：
```json
{
  "code": 200,
  "message": "场景状态已切换",
  "data": {
    "id": 6,
    "status": "inactive"
  }
}
```

## 四、学员查询接口（Trainee）

### 4.1 获取可用场景列表

**接口**：`GET /api/v1/practice/scenes`

**权限**：trainee、manager、admin

**请求参数**：
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| page | number | 否 | 页码，默认1 |
| size | number | 否 | 每页数量，默认20 |
| type | string | 否 | 场景类型筛选 |
| difficulty | string | 否 | 难度筛选 |
| search | string | 否 | 关键词搜索 |

**说明**：仅返回status=active的场景

**请求示例**：
```http
GET /api/v1/practice/scenes?page=1&size=20&type=phone
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

**响应示例**：同管理接口的列表响应

### 4.2 获取场景详情

**接口**：`GET /api/v1/practice/scenes/{id}`

**权限**：trainee、manager、admin

**路径参数**：
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | number | 是 | 场景ID |

**请求示例**：
```http
GET /api/v1/practice/scenes/1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

**响应示例**：同管理接口的详情响应

## 五、陪练对话接口

### 5.1 开始陪练对话

**接口**：`POST /api/v1/practice/start`

**权限**：trainee、manager、admin

**协议**：SSE (Server-Sent Events)

**请求体**：
```typescript
interface StartPracticeRequest {
  scene_id?: number           // 场景ID（陪练中心模式，可选）
  scene_name: string          // 场景名称（必填）
  scene_description?: string  // 场景描述（可选）
  scene_background: string    // 场景背景（必填）
  scene_ai_role: string       // AI角色（必填）
  scene_objectives: string[]  // 练习目标（必填）
  scene_keywords?: string[]   // 关键词（可选）
  user_message: string        // 用户消息（必填）
  conversation_id?: string    // 对话ID（续接对话时必填）
  is_first: boolean          // 是否首次消息（必填）
}
```

**⚠️ 重要说明**：
- **首次消息 (is_first=true)**：后端会将场景信息（background, ai_role, objectives等）拼接到user_message前面，作为完整的系统提示发送给Coze，让AI理解角色设定
- **后续消息 (is_first=false)**：仅发送user_message，不再重复场景信息

**请求示例**：
**首次消息请求示例**：
```http
POST /api/v1/practice/start
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

{
  "scene_id": 1,
  "scene_name": "初次电话拜访客户",
  "scene_description": "模拟首次通过电话联系潜在客户的场景",
  "scene_background": "你是一名销售专员，需要通过电话联系一位从未接触过的潜在客户。客户是某公司的采购经理，你需要在短时间内引起他的兴趣。",
  "scene_ai_role": "AI扮演一位忙碌且略显不耐烦的采购经理，对推销电话比较抵触，但如果销售人员能够快速切入他的需求点，他会愿意继续交谈。",
  "scene_objectives": ["学会专业的电话开场白", "快速建立信任关系", "有效探询客户需求"],
  "scene_keywords": ["开场白", "需求挖掘", "时间管理"],
  "user_message": "您好，我是XX公司的销售顾问，想占用您几分钟时间",
  "is_first": true
}
```

**后端处理**：首次消息时，后端会将场景信息构建为完整的场景设定文本，拼接到user_message前面发送给Coze。

**实际发送给Coze的内容**：
```
# 陪练场景设定

## 场景名称
初次电话拜访客户

## 场景背景
你是一名销售专员，需要通过电话联系一位从未接触过的潜在客户...

## AI角色要求
AI扮演一位忙碌且略显不耐烦的采购经理...

## 练习目标
1. 学会专业的电话开场白
2. 快速建立信任关系
3. 有效探询客户需求

## 关键词
开场白, 需求挖掘, 时间管理

---

现在开始陪练对话。请你严格按照上述场景设定扮演角色，与学员进行实战对话练习。

学员的第一句话：您好，我是XX公司的销售顾问，想占用您几分钟时间
```

**后续消息请求示例**：
```http
POST /api/v1/practice/start
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

{
  "user_message": "我们公司提供的是轻医美整体解决方案",
  "conversation_id": "conv_abc123",
  "is_first": false
}
```

**后端处理**：后续消息直接发送user_message，不再拼接场景信息。

**SSE事件格式**：

#### 事件1：对话创建
```
event: conversation.chat.created
data: {"conversation_id":"conv_abc123","chat_id":"chat_xyz789"}
```

#### 事件2：消息增量（实时打字）
```
event: message.delta
data: {"content":"您"}

event: message.delta
data: {"content":"好"}

event: message.delta
data: {"content":"，"}
```

#### 事件3：消息完成
```
event: message.completed
data: {"content":"您好，我现在很忙，你有什么事吗？"}
```

#### 事件4：对话完成
```
event: conversation.completed
data: {"token_count":156}
```

#### 事件5：结束标记
```
event: done
data: [DONE]
```

#### 事件6：错误
```
event: error
data: {"error":"对话失败: Network Error"}
```

**前端处理示例**：
```javascript
const response = await fetch('/api/v1/practice/start', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`
  },
  body: JSON.stringify(requestData)
})

const reader = response.body.getReader()
const decoder = new TextDecoder()

while (true) {
  const { value, done } = await reader.read()
  if (done) break
  
  const chunk = decoder.decode(value)
  const lines = chunk.split('\n\n')
  
  for (const line of lines) {
    if (!line.trim() || !line.startsWith('event: ')) continue
    
    const [eventLine, dataLine] = line.split('\n')
    const event = eventLine.replace('event: ', '')
    const data = JSON.parse(dataLine.replace('data: ', ''))
    
    switch (event) {
      case 'conversation.chat.created':
        conversationId.value = data.conversation_id
        break
      case 'message.delta':
        aiMessage.content += data.content
        break
      case 'message.completed':
        console.log('消息完成')
        break
      case 'conversation.completed':
        console.log('Token用量:', data.token_count)
        break
      case 'error':
        ElMessage.error(data.error)
        break
    }
  }
}
```

### 5.2 中断对话

**接口**：`POST /api/v1/practice/interrupt`

**权限**：trainee、manager、admin

**请求体**：
```typescript
interface InterruptPracticeRequest {
  conversation_id: string
  chat_id: string
}
```

**请求示例**：
```http
POST /api/v1/practice/interrupt
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

{
  "conversation_id": "conv_abc123",
  "chat_id": "chat_xyz789"
}
```

**响应示例**：
```json
{
  "code": 200,
  "message": "对话已中断",
  "data": {
    "conversation_id": "conv_abc123",
    "chat_id": "chat_xyz789"
  }
}
```

### 5.3 获取对话列表

**接口**：`GET /api/v1/practice/conversations`

**权限**：trainee、manager、admin

**请求参数**：
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| page | number | 否 | 页码，默认1 |
| size | number | 否 | 每页数量，默认20 |

**请求示例**：
```http
GET /api/v1/practice/conversations?page=1&size=20
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

**响应示例**：
```json
{
  "code": 200,
  "message": "success",
  "data": {
    "items": [
      {
        "id": "conv_abc123",
        "name": "初次电话拜访客户 - 2025-10-13",
        "created_at": 1697184000000
      }
    ],
    "has_more": false,
    "page": 1,
    "size": 20
  }
}
```

## 六、Dify场景提取接口

### 6.1 从课程提取场景

**接口**：`POST /api/v1/practice/extract-scene`

**权限**：trainee、manager、admin

**请求体**：
```typescript
interface ExtractSceneRequest {
  course_id: number
}
```

**请求示例**：
```http
POST /api/v1/practice/extract-scene
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

{
  "course_id": 5
}
```

**响应示例**：
```json
{
  "code": 200,
  "message": "场景提取成功",
  "data": {
    "scene": {
      "name": "轻医美产品咨询陪练",
      "description": "模拟客户咨询轻医美产品的场景，重点练习产品介绍和价格谈判技巧",
      "type": "product-intro",
      "difficulty": "intermediate",
      "background": "客户是一位30岁女性，对面部抗衰项目感兴趣，之前了解过玻尿酸填充，现在想详细咨询你们的产品和价格。",
      "ai_role": "AI扮演一位对轻医美有一定了解的客户，关注产品安全性和性价比，会提出具体的技术问题和价格异议。",
      "objectives": [
        "了解客户具体需求和预算",
        "专业介绍产品成分和效果",
        "处理价格异议，强调价值",
        "建立客户信任"
      ],
      "keywords": ["抗衰", "玻尿酸", "价格", "安全性"]
    },
    "workflow_run_id": "wf_run_abc123",
    "task_id": "task_xyz789"
  }
}
```

## 七、错误码说明

| code | message | 说明 | 处理建议 |
|------|---------|------|---------|
| 200 | success | 成功 | 正常处理 |
| 400 | 参数错误 | 请求参数不合法 | 检查参数格式 |
| 401 | 未授权 | token无效或过期 | 重新登录 |
| 403 | 无权限 | 当前角色无权限 | 提示用户无权限 |
| 404 | 资源不存在 | 场景不存在 | 返回列表页 |
| 409 | 资源冲突 | 场景名称重复 | 提示修改名称 |
| 500 | 服务器错误 | 内部错误 | 提示稍后重试 |
| 503 | 服务不可用 | Coze/Dify服务异常 | 提示稍后重试 |

## 八、附录

### 8.1 完整TypeScript类型定义

```typescript
// 场景类型
type SceneType = 'phone' | 'face' | 'complaint' | 'after-sales' | 'product-intro'
type Difficulty = 'beginner' | 'junior' | 'intermediate' | 'senior' | 'expert'
type SceneStatus = 'active' | 'inactive'

// 场景对象
interface PracticeScene {
  id: number
  name: string
  description: string
  type: SceneType
  difficulty: Difficulty
  status: SceneStatus
  background: string
  ai_role: string
  objectives: string[]
  keywords: string[]
  duration: number
  usage_count: number
  rating: number
  created_by: number
  updated_by: number
  created_at: string
  updated_at: string
}

// API响应
interface ResponseModel<T = any> {
  code: number
  message: string
  data: T
  request_id?: string
}

interface PaginatedResponse<T> {
  items: T[]
  total: number
  page: number
  page_size: number
  pages: number
}

// 请求对象
interface CreateSceneRequest {
  name: string
  description: string
  type: SceneType
  difficulty: Difficulty
  status?: SceneStatus
  background: string
  ai_role: string
  objectives: string[]
  keywords: string[]
  duration?: number
}

interface StartPracticeRequest {
  scene_id?: number
  scene_name: string
  scene_background: string
  scene_ai_role: string
  scene_objectives: string[]
  user_message: string
  conversation_id?: string
  is_first: boolean
}

interface ExtractSceneRequest {
  course_id: number
}

// SSE事件
interface SSEEvent {
  event: 'conversation.chat.created' | 'message.delta' | 'message.completed' | 'conversation.completed' | 'error' | 'done'
  data: any
}
```

### 8.2 API调用示例（完整）

```javascript
// src/api/practice.ts
import request from '@/utils/request'

export const practiceApi = {
  // 获取场景列表
  getScenes(params) {
    return request.get('/api/v1/practice/scenes', { params })
  },
  
  // 获取场景详情
  getSceneDetail(id) {
    return request.get(`/api/v1/practice/scenes/${id}`)
  },
  
  // 开始陪练（SSE流式）
  startPractice(data) {
    // SSE需要特殊处理，不能用普通的request
    return fetch(`${import.meta.env.VITE_API_BASE_URL}/api/v1/practice/start`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${getToken()}`
      },
      body: JSON.stringify(data)
    })
  },
  
  // 中断对话
  interruptPractice(data) {
    return request.post('/api/v1/practice/interrupt', data)
  },
  
  // 获取对话列表
  getConversations(params) {
    return request.get('/api/v1/practice/conversations', { params })
  },
  
  // 提取场景
  extractScene(data) {
    return request.post('/api/v1/practice/extract-scene', data)
  }
}

// 管理接口
export const practiceManagerApi = {
  // 获取场景列表
  getScenes(params) {
    return request.get('/api/v1/manager/practice-scenes', { params })
  },
  
  // 创建场景
  createScene(data) {
    return request.post('/api/v1/manager/practice-scenes', data)
  },
  
  // 更新场景
  updateScene(id, data) {
    return request.put(`/api/v1/manager/practice-scenes/${id}`, data)
  },
  
  // 删除场景
  deleteScene(id) {
    return request.delete(`/api/v1/manager/practice-scenes/${id}`)
  },
  
  // 切换状态
  toggleStatus(id) {
    return request.put(`/api/v1/manager/practice-scenes/${id}/toggle-status`)
  }
}
```

---

**文档版本**：v1.0
**最后更新**：2025-10-13
**维护人**：考培练系统开发团队

