feat: 初始化考培练系统项目

- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
111
2026-01-24 19:33:28 +08:00
commit 998211c483
1197 changed files with 228429 additions and 0 deletions

View File

@@ -0,0 +1,434 @@
# Coze API 使用文档
## 一、概述
Coze是字节跳动推出的AI对话平台提供强大的Bot开发和对话管理能力。本文档整理了考培练系统陪练功能需要使用的核心API。
### 官方资源
- **官方文档**: https://www.coze.cn/open/docs/developer_guides/chat_v3
- **Python SDK**: https://github.com/coze-dev/coze-py
- **API域名**: https://api.coze.cn (中国区)
### 重要提示
⚠️ **从GitHub获取的源码和示例默认使用 `COZE_COM_BASE_URL`,使用前必须改为 `COZE_CN_BASE_URL`**
## 二、认证方式
### 2.1 个人访问令牌 (Personal Access Token - 推荐)
**获取方式**
1. 访问 https://www.coze.cn/open/oauth/pats
2. 创建新的个人访问令牌
3. 设置名称、有效期和权限
4. 保存令牌(仅显示一次)
**使用示例**
```python
from cozepy import Coze, TokenAuth, COZE_CN_BASE_URL
coze = Coze(
auth=TokenAuth(token="pat_Sa5OiuUl0gDflnKstQTToIz0sSMshBV06diX0owOeuI1ZK1xDLH5YZH9fSeuKLIi"),
base_url=COZE_CN_BASE_URL # 重要:使用中国区域名
)
```
### 2.2 OAuth JWT认证 (生产环境推荐)
```python
from cozepy import Coze, JWTAuth, COZE_CN_BASE_URL
from pathlib import Path
coze = Coze(
auth=JWTAuth(
client_id="your_client_id",
private_key=Path("private_key.pem").read_text(),
public_key_id="your_public_key_id",
ttl=900 # Token有效期
),
base_url=COZE_CN_BASE_URL
)
```
## 三、核心API功能
### 3.1 Bot对话 (Chat API)
#### 流式对话 (推荐)
**功能说明**实时流式返回AI响应适合陪练对话场景
**示例代码**
```python
from cozepy import Coze, TokenAuth, Message, ChatEventType, COZE_CN_BASE_URL
coze = Coze(auth=TokenAuth(token="your_token"), base_url=COZE_CN_BASE_URL)
# 创建流式对话
stream = coze.chat.stream(
bot_id='7560643598174683145', # 陪练Bot ID
user_id='user_123', # 用户ID业务系统的用户标识
additional_messages=[
Message.build_user_question_text("你好,我想练习轻医美产品咨询"),
],
conversation_id='conv_abc', # 可选关联对话ID
)
# 处理流式事件
for event in stream:
if event.event == ChatEventType.CONVERSATION_MESSAGE_DELTA:
# 消息增量(实时打字效果)
print(event.message.content, end="", flush=True)
elif event.event == ChatEventType.CONVERSATION_MESSAGE_COMPLETED:
# 消息完成
print("\n消息完成")
elif event.event == ChatEventType.CONVERSATION_CHAT_COMPLETED:
# 对话完成
print("Token用量:", event.chat.usage.token_count)
break
elif event.event == ChatEventType.CONVERSATION_CHAT_FAILED:
# 对话失败
print("对话失败:", event.chat.last_error)
break
```
#### 非流式对话
```python
chat = coze.chat.create(
bot_id='bot_id',
user_id='user_id',
additional_messages=[
Message.build_user_question_text('你好')
]
)
print(chat.content)
```
### 3.2 对话管理 (Conversation API)
#### 创建对话
```python
# 创建新对话
conversation = coze.conversations.create()
print("对话ID:", conversation.id)
```
#### 获取对话列表
```python
# 获取Bot的对话列表
conversations = coze.conversations.list(
bot_id='bot_id',
page_num=1,
page_size=20
)
for conv in conversations.items:
print(f"对话ID: {conv.id}, 创建时间: {conv.created_at}")
```
#### 删除对话
```python
# 删除指定对话
coze.conversations.delete(conversation_id='conversation_id')
```
### 3.3 消息历史
#### 获取对话消息
```python
# 获取指定对话的消息列表
messages = coze.conversations.messages.list(
conversation_id='conversation_id',
page_num=1,
page_size=50
)
for msg in messages.items:
print(f"{msg.role}: {msg.content}")
```
### 3.4 中断对话
```python
# 中断正在进行的对话
result = coze.chat.cancel(
conversation_id='conversation_id',
chat_id='chat_id'
)
```
### 3.5 文件上传 (可选)
```python
from pathlib import Path
# 上传文件(如音频文件)
uploaded_file = coze.files.upload(file=Path('audio.wav'))
print("文件ID:", uploaded_file.id)
# 在消息中使用文件
from cozepy import MessageObjectString
message = Message.build_user_question_objects([
MessageObjectString.build_audio(file_id=uploaded_file.id)
])
```
## 四、事件类型说明
### 4.1 ChatEventType枚举
| 事件类型 | 说明 | 用途 |
|---------|------|------|
| `CONVERSATION_CHAT_CREATED` | 对话创建 | 获取chat_id和conversation_id |
| `CONVERSATION_MESSAGE_DELTA` | 消息增量 | 实时显示打字效果 |
| `CONVERSATION_MESSAGE_COMPLETED` | 消息完成 | 显示完整消息 |
| `CONVERSATION_CHAT_COMPLETED` | 对话完成 | 统计Token用量、清理状态 |
| `CONVERSATION_CHAT_FAILED` | 对话失败 | 错误处理、用户提示 |
| `CONVERSATION_AUDIO_DELTA` | 音频增量 | 实时语音播放(语音对话) |
### 4.2 事件对象结构
```python
# 消息增量事件
event.event == ChatEventType.CONVERSATION_MESSAGE_DELTA
event.message.content # 消息内容增量
event.message.role # 消息角色user/assistant
# 对话完成事件
event.event == ChatEventType.CONVERSATION_CHAT_COMPLETED
event.chat.id # 对话ID
event.chat.conversation_id # 会话ID
event.chat.usage.token_count # Token用量
event.chat.usage.input_count # 输入Token数
event.chat.usage.output_count # 输出Token数
# 对话失败事件
event.event == ChatEventType.CONVERSATION_CHAT_FAILED
event.chat.last_error # 错误信息
```
## 五、消息构建方法
### 5.1 文本消息
```python
from cozepy import Message
# 用户问题
user_msg = Message.build_user_question_text("你好,我想了解产品")
# 助手回答
assistant_msg = Message.build_assistant_answer("好的,我来为您介绍")
```
### 5.2 多轮对话
```python
# 构建对话历史
messages = [
Message.build_user_question_text("第一个问题"),
Message.build_assistant_answer("第一个回答"),
Message.build_user_question_text("第二个问题"),
]
stream = coze.chat.stream(
bot_id='bot_id',
user_id='user_id',
additional_messages=messages
)
```
## 六、错误处理
### 6.1 常见错误
```python
from cozepy.exception import CozePyError
try:
chat = coze.chat.create(bot_id='bot_id', user_id='user_id')
except CozePyError as e:
print(f"Coze API错误: {e}")
# 处理错误
```
### 6.2 超时配置
```python
import httpx
from cozepy import Coze, TokenAuth, SyncHTTPClient
# 自定义超时设置
http_client = SyncHTTPClient(timeout=httpx.Timeout(
timeout=180.0, # 总超时
connect=5.0 # 连接超时
))
coze = Coze(
auth=TokenAuth(token="your_token"),
base_url=COZE_CN_BASE_URL,
http_client=http_client
)
```
## 七、调试技巧
### 7.1 日志配置
```python
import logging
from cozepy import setup_logging
# 启用DEBUG日志
setup_logging(level=logging.DEBUG)
```
### 7.2 获取LogID
```python
# 每个请求都有唯一的logid用于排查问题
bot = coze.bots.retrieve(bot_id='bot_id')
print("LogID:", bot.response.logid)
stream = coze.chat.stream(bot_id='bot_id', user_id='user_id')
print("LogID:", stream.response.logid)
```
## 八、最佳实践
### 8.1 陪练对话场景建议
1. **使用流式响应**:提供更好的用户体验
2. **传递对话上下文**:使用`conversation_id`保持多轮对话
3. **合理设置超时**陪练对话建议180秒超时
4. **错误重试机制**:网络波动时自动重试
5. **Token计数统计**监控API使用成本
### 8.2 用户ID设计
```python
# 推荐使用业务系统的用户ID
user_id = f"trainee_{user.id}" # trainee_123
# 对话ID可包含场景信息
conversation_id = f"practice_{scene_id}_{user_id}_{timestamp}"
```
### 8.3 场景参数传递
可以通过Bot的系统提示词Prompt或参数传递场景信息
```python
# 方式1在用户消息中包含场景背景
scene_context = """
场景:轻医美产品咨询
背景客户是30岁女性关注面部抗衰
AI角色扮演挑剔的客户对价格敏感
"""
stream = coze.chat.stream(
bot_id='bot_id',
user_id='user_id',
additional_messages=[
Message.build_user_question_text(scene_context + "\n\n开始陪练")
]
)
```
## 九、性能优化
### 9.1 连接复用
```python
# 全局初始化一次Coze客户端
coze = Coze(auth=TokenAuth(token="your_token"), base_url=COZE_CN_BASE_URL)
# 多次调用复用连接
def handle_chat(user_id, message):
stream = coze.chat.stream(bot_id='bot_id', user_id=user_id, ...)
return stream
```
### 9.2 异步并发
```python
from cozepy import AsyncCoze, AsyncTokenAuth
import asyncio
async_coze = AsyncCoze(
auth=AsyncTokenAuth(token="your_token"),
base_url=COZE_CN_BASE_URL
)
async def concurrent_chats():
tasks = [
async_coze.chat.create(bot_id='bot_id', user_id=f'user_{i}')
for i in range(10)
]
results = await asyncio.gather(*tasks)
return results
```
## 十、陪练系统专用配置
### 10.1 配置信息
```python
# 考培练系统陪练Bot配置
COZE_API_BASE = "https://api.coze.cn"
COZE_API_TOKEN = "pat_Sa5OiuUl0gDflnKstQTToIz0sSMshBV06diX0owOeuI1ZK1xDLH5YZH9fSeuKLIi"
COZE_PRACTICE_BOT_ID = "7560643598174683145"
```
### 10.2 FastAPI集成示例
```python
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from cozepy import Coze, TokenAuth, Message, ChatEventType, COZE_CN_BASE_URL
import json
app = FastAPI()
coze = Coze(auth=TokenAuth(token="your_token"), base_url=COZE_CN_BASE_URL)
@app.post("/api/v1/practice/start")
async def start_practice(user_id: str, message: str):
"""开始陪练对话SSE流式返回"""
def generate_stream():
stream = coze.chat.stream(
bot_id=COZE_PRACTICE_BOT_ID,
user_id=user_id,
additional_messages=[Message.build_user_question_text(message)]
)
for event in stream:
if event.event == ChatEventType.CONVERSATION_MESSAGE_DELTA:
yield f"event: message.delta\ndata: {json.dumps({'content': event.message.content})}\n\n"
elif event.event == ChatEventType.CONVERSATION_CHAT_COMPLETED:
yield f"event: done\ndata: [DONE]\n\n"
return StreamingResponse(
generate_stream(),
media_type="text/event-stream"
)
```
## 十一、参考资料
- **Coze Python SDK GitHub**: https://github.com/coze-dev/coze-py
- **示例代码目录**: `参考代码/coze-py-main/examples/`
- **后端参考实现**: `参考代码/coze-chat-backend/main.py`
- **官方文档**: https://www.coze.cn/open/docs
---
**文档维护**:本文档基于 Coze Python SDK v0.19.0 编写最后更新时间2025-10-13

View File

@@ -0,0 +1,132 @@
# Coze-Chat 集成方案分析
## 现状分析
### 技术栈差异
- **主系统**Vue3 + TypeScript + Element Plus
- **Coze-Chat**React 18 + TypeScript + Ant Design
### 功能定位
Coze-Chat 是考培练系统的智能对话模块,提供:
- 智能体列表展示
- 实时流式对话
- 语音输入输出
- 会话管理
## 集成方案对比
### 方案一:独立服务部署(推荐短期方案)
**优势**
- 无需重写代码,立即可用
- 保持模块独立性和稳定性
- 部署灵活,可独立扩展
**实施方式**
1. 将 Coze-Chat 作为独立微服务部署在独立容器
2. 通过 API Gateway 统一入口
3. 主系统通过 iframe 或 API 调用集成
**配置示例**
```yaml
# docker-compose.yml
services:
coze-service:
build: ./参考代码/coze-chat-系统/coze-chat-backend
ports:
- "8001:8000"
coze-frontend:
build: ./参考代码/coze-chat-系统/coze-chat-frontend
ports:
- "3002:80"
```
### 方案二:逐步迁移到 Vue3推荐长期方案
**优势**
- 统一技术栈,降低维护成本
- 更好的集成体验
- 统一的组件库和样式
**实施计划**
1. **第一阶段**API 层对接
- 保留 Coze 后端服务
- 在 Vue3 中创建对话组件
- 复用现有 API 接口
2. **第二阶段**:功能迁移
- 智能体列表页面
- 对话界面
- 语音功能模块
3. **第三阶段**:完全整合
- 统一用户系统
- 统一权限管理
- 统一样式主题
## 推荐实施路径
### 短期1-2周
1. 保持 Coze-Chat 作为独立服务
2. 在主系统中通过 iframe 嵌入关键页面
3. 统一认证 Token 传递
### 中期1-2月
1. 抽取 Coze API 服务层
2. 在 Vue3 中实现核心对话组件
3. 逐步替换 React 页面
### 长期3-6月
1. 完全迁移到 Vue3
2. 优化集成体验
3. 统一技术栈
## 技术要点
### API 对接
```javascript
// Vue3 中调用 Coze API
import { cozeApi } from '@/api/coze'
export const cozeService = {
// 获取智能体列表
async getBots() {
return await cozeApi.get('/agent/v1/cozechat/bots')
},
// 创建对话
async createChat(data) {
return await cozeApi.post('/agent/v1/cozechat/create-chat-stream', data)
}
}
```
### iframe 集成
```vue
<template>
<div class="coze-container">
<iframe
:src="cozeUrl"
:style="{ width: '100%', height: '100%', border: 'none' }"
@load="onCozeLoaded"
/>
</div>
</template>
<script setup>
const cozeUrl = computed(() => {
const token = useAuthStore().token
return `http://localhost:3002?token=${token}`
})
</script>
```
## 结论
建议采用**渐进式迁移策略**
1. 短期保持独立部署,通过 iframe 集成
2. 中期开始组件级迁移
3. 长期实现完全整合
这样既能快速上线,又为未来的技术栈统一留出空间。

View File

@@ -0,0 +1,222 @@
# 陪练功能开发文档
## 📚 文档导航
本目录包含考培练系统AI陪练功能的完整开发文档。
---
## 🎯 快速开始
### 新成员必读
1. **⚠️核心差异点速查.md**5分钟- 与参考代码的关键不同
2. **基础信息.md**5分钟- 功能概述和配置信息
3. **README.md**(本文档)- 文档导航
### 开发人员
**后端开发**
1. Coze-API文档.md - Coze API使用指南
2. 陪练功能API接口规范.md - 14个API接口定义
3. 陪练分析报告-数据结构与Dify规范.md - Dify工作流规范
**前端开发**
1. 参考代码分析-Training模块.md - React到Vue3迁移方案
2. 陪练功能API接口规范.md - 前端API调用
3. 陪练功能数据流程图.md - 数据流程
---
## ✅ 完成报告
### 已完成功能
1. **✅陪练中心入口开发完成报告.md** - 预设场景陪练(文本模式)
2. **✅语音陪练功能完成报告.md** - 语音对话功能
3. **✅课程中心陪练入口开发完成报告.md** - 课程场景提取
4. **✅陪练分析报告功能完成报告.md** - AI分析报告
---
## 📖 技术文档
### 核心技术文档
**Coze集成**
- Coze-API文档.md - Coze WebSocket API完整使用指南
- ⚠️核心差异点速查.md - 场景提示词构建要点
**API规范**
- 陪练功能API接口规范.md - 14个API接口详细定义
- 陪练分析报告-数据结构与Dify规范.md - Dify工作流输入输出
**数据流程**
- 陪练功能数据流程图.md - 完整流程图和时序图
- 陪练功能技术方案.md - 技术架构和实现方案
**参考代码**
- 参考代码分析-Training模块.md - React实现深度解析和Vue3迁移
**基础信息**
- 基础信息.md - 功能概述、配置信息、参考代码位置
---
## 🎯 核心功能
### 1. 语音陪练对话
- 前端@coze/api直连wss://ws.coze.cn
- 实时语音识别和播放
- 双方字幕实时显示
- 语音/文本模式切换
**技术**WsChatClient + Agora SDK
### 2. 对话历史保存
- 实时保存到MySQLpractice_dialogues表
- 会话管理practice_sessions表
- 异步保存,不阻塞对话
### 3. AI分析报告
- Dify工作流生成分析
- 5个维度评分
- 6个能力评估
- 对话标注(亮点/金牌话术)
- 改进建议
**Dify API Key**app-9MWaCEiRegpYGQLov4S9oQjh
### 4. 报告展示
- practice-report.vue页面
- 合并数据库对话+Dify标注
- 雷达图可视化
- 对话筛选功能
### 5. 陪练记录
- practice-records.vue页面
- 列表查询和筛选
- 统计数据展示
---
## 📊 数据库表
### 4张表
1. **practice_scenes** - 陪练场景5个预设场景
2. **practice_sessions** - 陪练会话(记录时长、轮次)
3. **practice_dialogues** - 对话记录(逐条保存)
4. **practice_reports** - 分析报告JSON存储
---
## 🔌 API接口
### 14个接口
**场景管理**2个
- GET /practice/scenes
- GET /practice/scenes/{id}
**对话管理**3个
- POST /practice/startSSE
- POST /practice/interrupt
- POST /practice/conversation/create
**会话管理**7个
- POST /practice/sessions/create
- POST /practice/dialogues/save
- POST /practice/sessions/{id}/end
- POST /practice/sessions/{id}/analyze
- GET /practice/reports/{id}
- GET /practice/sessions/list
- GET /practice/stats
**场景提取**1个
- POST /practice/extract-scene
---
## ⚠️ 关键要点
### 场景提示词(最重要)
考培练系统与参考代码的核心差异:
- **首次消息必须包含完整场景设定**
- 使用Markdown格式组织
- 后续消息只发送用户输入
- 详见:⚠️核心差异点速查.md
### Dify对话标注格式
```json
{
"dialogue_annotations": [
{"sequence": 1, "tags": ["金牌话术"], "comment": "..."}
]
}
```
**重要**
- 完整对话来自数据库
- Dify只返回标注sequence+tags+comment
- 后端自动合并
### 语音识别技巧
- server_vad模式
- 说完话保持静音500ms
- 环境必须安静
---
## 📝 开发状态
### 已完成100%
- ✅ 语音陪练对话
- ✅ 对话历史保存
- ✅ AI分析报告
- ✅ 报告页面展示
- ✅ 陪练记录管理
### 测试通过
- ✅ 语音连接和播放
- ✅ 用户语音识别
- ✅ 对话实时保存
- ✅ Dify分析报告生成
- ✅ 对话标注匹配
- ✅ 报告页面数据展示
---
## 🎓 核心经验
1. **前端直连Coze** - 比后端中转简单高效
2. **使用官方SDK** - @coze/api稳定可靠
3. **对话数据分离** - 数据库存完整对话Dify只做标注
4. **异步保存策略** - 保存失败不影响对话继续
5. **合理的数据结构** - 两张表分离(会话+对话)便于查询
---
## 📞 技术支持
遇到问题时查阅:
1. **场景不生效** → 核心差异点速查.md
2. **语音无法识别** → Coze-API文档.mdVAD配置
3. **API对接问题** → 陪练功能API接口规范.md
4. **Dify格式问题** → 陪练分析报告-数据结构与Dify规范.md
---
**文档维护**:考培练系统开发团队
**最后更新**2025-10-13
**版本**v2.0(完整版)

View File

@@ -0,0 +1,334 @@
# ⚠️ 核心差异点速查卡
> 考培练系统 vs 参考代码的关键不同
## 🎯 最重要的差异:场景提示词
### 参考代码(简单对话)
```typescript
// 直接发送用户消息
await startChatStream({
content: "你好", // 仅用户输入
bot_id: "7509379008556089379",
user_id: "user_123"
})
```
### 考培练系统(场景驱动)
```typescript
// 首次消息:场景信息 + 用户输入
await fetch('/api/v1/practice/start', {
method: 'POST',
body: JSON.stringify({
// ⚠️ 完整场景信息
scene_name: "初次电话拜访客户",
scene_background: "你是一名销售专员...",
scene_ai_role: "AI扮演一位忙碌的采购经理...",
scene_objectives: ["学会专业开场白", "建立信任"],
scene_keywords: ["开场白", "需求挖掘"],
// 用户输入
user_message: "您好我是XX公司的销售顾问",
// ⚠️ 首次标记
is_first: true
})
})
// 后续消息:仅用户输入
await fetch('/api/v1/practice/start', {
method: 'POST',
body: JSON.stringify({
user_message: "我们提供轻医美整体解决方案",
conversation_id: "conv_abc123", // 保持上下文
is_first: false // 不再包含场景信息
})
})
```
## 📝 后端场景提示词构建
### 标准模板Python
```python
if request.is_first:
scene_prompt = f"""
# 陪练场景设定
## 场景名称
{request.scene_name}
## 场景描述
{request.scene_description}
## 场景背景
{request.scene_background}
## AI角色要求
{request.scene_ai_role}
## 练习目标
{chr(10).join(f"{i+1}. {obj}" for i, obj in enumerate(request.scene_objectives))}
## 关键词
{', '.join(request.scene_keywords)}
---
现在开始陪练对话。请你严格按照上述场景设定扮演角色,与学员进行实战对话练习。
不要提及"场景设定""角色扮演"等元信息,直接进入角色开始对话。
学员的第一句话:{request.user_message}
"""
# 发送给Coze
stream = coze.chat.stream(
bot_id=COZE_PRACTICE_BOT_ID,
user_id=user_id,
additional_messages=[Message.build_user_question_text(scene_prompt)]
)
else:
# 后续消息直接发送
stream = coze.chat.stream(
bot_id=COZE_PRACTICE_BOT_ID,
user_id=user_id,
additional_messages=[Message.build_user_question_text(request.user_message)],
conversation_id=request.conversation_id # 使用同一个对话ID
)
```
## 🔄 完整对话流程对比
### 参考代码流程
```
用户输入 "你好"
发送给Coze: "你好"
AI回复: "你好我是AI助手..."
用户继续输入...
```
### 考培练系统流程
```
用户在场景确认页面点击"开始陪练"
用户输入第一句话 "您好我是XX公司的销售顾问"
后端构建完整提示词:
"""
# 陪练场景设定
## 场景名称: 初次电话拜访客户
## 场景背景: 你是一名销售专员...
## AI角色要求: AI扮演一位忙碌的采购经理...
## 练习目标:
1. 学会专业的电话开场白
2. 快速建立信任关系
---
现在开始陪练。学员第一句话您好我是XX公司的销售顾问
"""
发送给Coze: 完整提示词Markdown格式
AI理解场景后回复: "喂?什么事?我现在很忙..."(扮演采购经理)
用户继续输入 "我想占用您几分钟..."
发送给Coze: "我想占用您几分钟..."(仅用户输入,不含场景)
AI继续扮演角色回复...
```
## 💡 为什么这样设计?
### 1. AI角色一致性
- **问题**如果不传场景信息AI可能无法理解要扮演什么角色
- **解决**首次消息包含完整场景设定让AI明确角色
### 2. 对话上下文保持
- **机制**使用conversation_id续接对话
- **效果**后续消息AI会记住场景设定无需重复发送
### 3. 性能优化
- **首次**:发送完整提示词(~500字符
- **后续**:仅发送用户输入(~50字符
- **节省**减少Token消耗提升响应速度
### 4. 用户体验
- **界面显示**:用户仅看到自己的输入,不看到场景设定文本
- **沉浸感**:用户专注于对话,不被技术细节干扰
## 📋 实现检查清单
### 后端实现
- [ ] 判断is_first标记
- [ ] 构建Markdown格式场景提示词
- [ ] 拼接user_message
- [ ] 发送给Coze
- [ ] 保存conversation_id
### 前端实现
- [ ] 页面加载时获取场景信息
- [ ] 首次发送时携带完整场景参数
- [ ] 设置is_first=true
- [ ] 保存返回的conversation_id
- [ ] 后续消息仅发送user_message和conversation_id
- [ ] 设置is_first=false
### 测试验证
- [ ] 首次消息AI回复符合场景角色
- [ ] 后续消息AI持续扮演角色
- [ ] conversation_id正确续接
- [ ] 场景信息不显示在消息列表中
## 🚨 常见错误
### ❌ 错误1每次都发送场景信息
```python
# 错误示范
stream = coze.chat.stream(
additional_messages=[Message.build_user_question_text(
scene_prompt + user_message # 每次都拼接浪费Token
)]
)
```
### ✅ 正确:仅首次发送
```python
# 正确示范
if is_first:
message_content = scene_prompt + user_message
else:
message_content = user_message
stream = coze.chat.stream(
additional_messages=[Message.build_user_question_text(message_content)],
conversation_id=conversation_id # 关键:保持上下文
)
```
### ❌ 错误2不保存conversation_id
```python
# 错误示范:每次创建新对话
stream = coze.chat.stream(
bot_id=bot_id,
user_id=user_id,
additional_messages=[...]
# 没有传conversation_idAI会忘记场景
)
```
### ✅ 正确:保持对话连续性
```python
# 正确示范
stream = coze.chat.stream(
bot_id=bot_id,
user_id=user_id,
additional_messages=[...],
conversation_id=saved_conversation_id # 使用同一个ID
)
```
### ❌ 错误3前端显示场景提示词
```javascript
// 错误示范:用户看到场景设定文本
messageList.value.push({
role: 'user',
content: scenePrompt + userInput // 显示完整提示词
})
```
### ✅ 正确:仅显示用户输入
```javascript
// 正确示范:用户仅看到自己的输入
messageList.value.push({
role: 'user',
content: userInput // 仅显示用户输入
})
// 后端负责拼接场景信息
```
## 📊 数据流示例
### 首次消息数据流
```
前端发送:
{
scene_name: "初次电话拜访",
scene_background: "...",
scene_ai_role: "...",
scene_objectives: [...],
user_message: "您好",
is_first: true
}
后端构建:
"""
# 陪练场景设定
## 场景名称: 初次电话拜访
...
学员第一句话:您好
"""
发送给Coze:
Message.build_user_question_text(完整提示词)
前端显示:
用户: 您好
AI: 喂?什么事?
```
### 后续消息数据流
```
前端发送:
{
user_message: "我想占用您几分钟",
conversation_id: "conv_abc123",
is_first: false
}
后端直接发送:
Message.build_user_question_text("我想占用您几分钟")
发送给Coze:
使用conversation_id, AI记住之前的场景
前端显示:
用户: 我想占用您几分钟
AI: 好吧,说吧,你有什么产品?
```
## 🎓 学习建议
### 新手开发者
1. 先理解参考代码的基本对话流程
2. 再理解场景提示词的必要性
3. 对比两种实现方式的差异
4. 按照文档实现考培练版本
### 有经验开发者
1. 快速浏览参考代码结构
2. 重点关注场景提示词构建逻辑
3. 理解is_first标记的作用
4. 直接开始实现
## 📞 技术支持
遇到问题时查阅:
1. **场景不生效** → 检查is_first标记和提示词构建
2. **AI忘记角色** → 检查conversation_id是否正确续接
3. **Token消耗大** → 检查是否每次都发送场景信息
4. **前端显示问题** → 检查消息列表是否只显示user_message
---
**版本**v1.0
**更新**2025-10-13
**维护**:考培练系统开发团队
**快速链接**
- [完整技术方案](./陪练功能技术方案.md)
- [API接口规范](./陪练功能API接口规范.md)
- [参考代码分析](./参考代码分析-Training模块.md)
- [开发任务清单](./📋开发任务清单.md)

View File

@@ -0,0 +1,303 @@
# ✅ 陪练功能完整开发报告
**完成时间**2025-10-13
**完成度**100%
**测试状态**:✅ 全部通过
---
## 🎉 功能总览
### 完成的5大模块
1. **语音陪练对话** - 实时WebSocket语音交互
2. **对话历史保存** - MySQL数据库持久化
3. **AI分析报告** - Dify智能分析和评分
4. **报告页面展示** - 完整的可视化展示
5. **陪练记录管理** - 历史记录查询和统计
---
## 📊 核心功能
### 1. 语音陪练对话(文本+语音双模式)
**文本模式**
- SSE流式对话
- 场景提示词驱动
- conversation_id保持上下文
**语音模式**
- 前端@coze/api直连wss://ws.coze.cn
- Agora SDK自动处理音频
- 实时语音识别VAD
- 流式语音播放
- 双方字幕显示
**技术架构**
```
前端 @coze/api → wss://ws.coze.cn
```
**关键配置**
- allowPersonalAccessTokenInBrowser: true
- audioMutedDefault: false
- playbackVolumeDefault: 1.0
### 2. 对话历史保存
**实时保存策略**
- 用户语音识别完成→立即保存
- AI回复完成→立即保存
- 异步保存,失败只记录日志
- sequence连续递增
**数据库表**
- practice_sessions会话元信息
- practice_dialogues对话详细记录
### 3. AI分析报告
**Dify工作流**
- API Key: app-9MWaCEiRegpYGQLov4S9oQjh
- 输入: dialogue_historyJSON数组
- 输出: 分析结果dialogue_annotations格式
**分析内容**
- 综合得分0-100分
- 5个维度打分开场技巧、需求挖掘、产品介绍、异议处理、成交技巧
- 6个能力评估沟通表达、倾听理解、情绪控制、专业知识、销售技巧、应变能力
- 对话标注sequence+tags+comment
- 3-5条改进建议含具体示例
**对话复盘逻辑**
```
完整对话(数据库)+ Dify标注sequence匹配= 对话复盘
```
### 4. 报告页面展示
**页面**practice-report.vue
**展示内容**
- 会话信息(日期、时长、轮次)
- 综合评分圆环图
- 5个维度进度条
- 6个能力雷达图
- 完整对话复盘(标注亮点/金牌话术)
- 改进建议列表
**筛选功能**
- 全部对话
- 亮点话术
- 金牌话术
### 5. 陪练记录管理
**页面**practice-records.vue
**功能**
- 陪练记录列表(分页)
- 统计数据(总次数、平均分、总时长、本月进步)
- 关键词搜索
- 场景类型筛选
- 时间范围筛选
- 分数范围筛选
---
## 📁 数据库设计
### 4张表
```sql
-- 陪练场景表5个预设场景
practice_scenes (id, name, type, difficulty, background, ai_role, objectives, keywords...)
-- 陪练会话表
practice_sessions (session_id, user_id, scene_id, start_time, end_time, duration_seconds, turns, status...)
-- 对话记录表
practice_dialogues (session_id, speaker, content, timestamp, sequence...)
-- 分析报告表
practice_reports (session_id, total_score, score_breakdown, ability_dimensions, dialogue_review, suggestions...)
```
---
## 🔌 API接口14个
### 场景管理2个
- GET /practice/scenes - 场景列表
- GET /practice/scenes/{id} - 场景详情
### 对话管理3个
- POST /practice/start - 开始对话SSE
- POST /practice/interrupt - 中断对话
- POST /practice/conversation/create - 创建对话
### 会话管理7个
- POST /practice/sessions/create - 创建会话
- POST /practice/dialogues/save - 保存对话
- POST /practice/sessions/{id}/end - 结束会话
- POST /practice/sessions/{id}/analyze - 生成报告
- GET /practice/reports/{id} - 获取报告
- GET /practice/sessions/list - 记录列表
- GET /practice/stats - 统计数据
### 场景提取1个
- POST /practice/extract-scene - 从课程提取场景
---
## 🎓 核心技术
### 前端直连Coze
```typescript
import { WsChatClient } from '@coze/api/ws-tools'
const client = new WsChatClient({
token: 'pat_xxx',
baseWsURL: 'wss://ws.coze.cn',
allowPersonalAccessTokenInBrowser: true,
botId: '7560643598174683145'
})
await client.connect()
client.setPlaybackVolume(1)
client.sendTextMessage(scenePrompt)
```
### Dify对话标注
**输入**
```json
{
"inputs": {
"dialogue_history": "[{\"speaker\":\"user\",\"content\":\"...\"}]"
}
}
```
**输出**
```json
{
"dialogue_annotations": [
{"sequence": 1, "tags": ["金牌话术"], "comment": "开场专业"}
]
}
```
### 对话合并
```python
# 数据库查询完整对话
dialogues = SELECT * FROM practice_dialogues
# 按sequence匹配Dify标注
for dialogue in dialogues:
annotation = annotations_map.get(dialogue.sequence)
dialogue_review.append({
"content": dialogue.content, # 来自数据库
"tags": annotation.tags, # 来自Dify
"comment": annotation.comment # 来自Dify
})
```
---
## 📈 性能指标
| 功能 | 指标 |
|-----|------|
| WebSocket连接 | <2秒 |
| 语音识别 | <2秒 |
| 对话保存 | <100ms |
| Dify分析 | 10-15秒 |
| 报告查询 | <300ms |
---
## 🎊 开发成果
### 前端文件5个
1. src/utils/cozeVoiceClient.ts
2. src/components/VoiceChat.vue
3. src/components/TextChat.vue
4. src/views/trainee/practice-report.vue数据对接
5. src/views/trainee/practice-records.vue数据对接
### 后端文件5个
1. app/models/practice.py4个模型
2. app/schemas/practice.py20个Schema
3. app/services/coze_service.py
4. app/services/dify_practice_service.py
5. app/api/v1/practice.py14个接口
### 配置文件
1. package.json - 添加@coze/api依赖
2. app/core/config.py - Coze和Dify配置
---
## 🔑 关键经验
1. **架构选择**:前端直连优于后端中转
2. **官方SDK优先**@coze/api比自己实现可靠
3. **数据分离**对话存数据库Dify做标注
4. **异步保存**:不阻塞用户体验
5. **合理设计**:两张表分离便于查询
---
## 🚀 使用方式
### 完整流程
```
1. 陪练中心 → 选择场景 → 开始陪练
2. 语音对话(实时保存到数据库)
3. 点击"保存并查看分析报告"Dify分析
4. 查看完整分析报告5维度+6能力+对话复盘+建议)
5. 陪练记录页面(查看历史+统计)
```
### 访问地址
- 陪练中心http://localhost:3001/trainee/ai-practice-center
- 陪练记录http://localhost:3001/trainee/practice-records
- 分析报告http://localhost:3001/trainee/practice-report/{sessionId}
---
## ⚠️ 重要规范
### Dify约束
- dialogue_annotations的sequence必须在1到实际对话数范围内
- tags只有两种["亮点话术"]或["金牌话术"]
- 不要返回不存在的sequence
### VAD使用
- 说完话保持静音500ms
- 环境安静
- 说话清晰
---
**开发团队**:考培练系统开发组
**完成日期**2025-10-13
**文档版本**v2.0(最终版)
**🎊 陪练功能完整开发圆满完成!**

View File

@@ -0,0 +1,296 @@
# 陪练功能基础信息
## 一、功能概述
### 业务场景
考培练系统为轻医美连锁品牌提供AI陪练功能通过模拟真实客户场景让学员进行实战对话练习。
### 两种陪练入口
#### 1. 陪练中心(直接模式)
- **URL**: http://localhost:3001/trainee/ai-practice-center
- **流程**: 选择预设场景 → 直接调用Coze开始陪练
- **场景来源**: 数据库中的预设场景practice_scenes表
#### 2. 课程中心(课程模式)
- **URL**: http://localhost:3001/trainee/course-center
- **流程**: 点击课程陪练 → Dify提取场景 → Coze开始陪练
- **场景来源**: Dify根据课程内容动态生成
## 二、Coze配置信息
### 认证方式(更新于 2025-11-16
系统使用 **OAuth认证JWT + 私钥签名)**不再使用Personal Access Token (PAT)。
### OAuth配置后端环境变量
```bash
# OAuth认证配置
COZE_OAUTH_CLIENT_ID=1114009328887
COZE_OAUTH_PUBLIC_KEY_ID=GGs9pw0BDHx2k9vGGehUyRgKV-PyUWLBncDs-YNNN_I
COZE_OAUTH_PRIVATE_KEY_PATH=/app/secrets/coze_private_key.pem
COZE_PRACTICE_BOT_ID=7560643598174683145
# API配置关键必须使用中国区
COZE_API_BASE=https://api.coze.cn
```
### 私钥文件位置
- **宿主机**: `/root/aiedu/kaopeilian-backend/secrets/coze_private_key.pem`
- **容器内**: `/app/secrets/coze_private_key.pem`
- **权限**: 600仅所有者可读写
- **Docker挂载**: `./kaopeilian-backend/secrets:/app/secrets:ro`(只读)
### 重要提示
⚠️ **JWTAuth必须指定 `base_url='https://api.coze.cn'`,否则会使用国际版导致认证失败**
### 官方资源
- **API文档**: https://www.coze.cn/open/docs/developer_guides/chat_v3
- **OAuth文档**: https://www.coze.cn/open/docs/developer_guides/oauth
- **Python SDK**: https://github.com/coze-dev/coze-py
- **SDK版本**: cozepy>=0.19.0
### Bot信息
- **Bot ID**: 7560643598174683145
- **应用ID**: 1114009328887
- **功能**: AI模拟客户陪练支持多轮对话
- **特性**: 实时流式响应,可自定义场景角色
## 三、Dify配置信息
### API配置
```python
DIFY_API_BASE = "http://dify.ireborn.com.cn/v1"
DIFY_PRACTICE_API_KEY = "app-rYP6LNM4iPmNjIHns12zFeJp"
DIFY_PRACTICE_WORKFLOW_ID = "待确认" # 需要从Dify创建工作流后获取
```
### 调用示例
```bash
curl -X POST 'http://dify.ireborn.com.cn/v1/workflows/run' \
--header 'Authorization: Bearer app-rYP6LNM4iPmNjIHns12zFeJp' \
--header 'Content-Type: application/json' \
--data-raw '{
"inputs": {
"course_id": "5"
},
"response_mode": "streaming",
"user": "kaopeilian"
}'
```
### 工作流说明
- **输入参数**: course_id课程ID
- **响应模式**: streaming流式返回
- **输出格式**: JSON场景数据包含name、description、background、ai_role、objectives等
## 四、技术架构
### 核心技术栈
- **前端**: Vue3 + Element Plus
- **后端**: Python + FastAPI
- **数据库**: MySQL 8.0(存储场景数据)
- **AI对话**: Coze Bot字节跳动
- **场景提取**: Dify工作流
- **通信协议**: SSE (Server-Sent Events)
### 数据存储策略
- **场景数据**: 存储在MySQL的practice_scenes表
- **对话历史**: 由Coze平台管理不存储在本地数据库
## 五、规划文档
### 已完成文档
1.**Coze-API文档.md** - Coze API核心使用方法
2.**陪练功能技术方案.md** - 完整技术架构和实现方案
3.**陪练功能API接口规范.md** - 前后端API接口详细规范
4.**陪练功能数据流程图.md** - 数据流程和时序图
5.**规范与约定-团队基线.md** - 更新了陪练功能相关规范
### 文档位置
所有规划文档位于:`考培练系统规划/全链路联调/Ai工作流/coze/`
## 六、参考代码
### 可用参考代码位置
- **Coze Python SDK**: `参考代码/coze-py-main/`
- 示例代码:`examples/chat_stream.py`(流式对话)
- 示例代码:`examples/chat_simple_audio.py`(音频对话)
- **Coze后端参考**: `参考代码/coze-chat-backend/`
- `main.py` - FastAPI集成示例
- `auth.py` - Coze认证方式
- `config.py` - 配置管理
- **Coze前端参考**: `参考代码/coze-chat-frontend/`
- React + TypeScript实现
- SSE流式对话处理示例
### 已有页面
- **陪练中心页面**: `kaopeilian-frontend/src/views/trainee/ai-practice-center.vue`(前端已实现)
- **场景管理页面**: `kaopeilian-frontend/src/views/manager/practice-scene-management.vue`(前端已实现)
- **对话页面**: 待开发可参考Training模块
### 核心参考代码(新发现)
- **Training模块**: `参考代码/coze-chat-frontend/src/pages/Training/`
- `index.tsx` - 主入口20行
- `TextChat.tsx` - 文本对话实现68行
- `VoiceChat.tsx` - 语音对话实现225行
- **`TrainingStore.ts` - 核心状态管理525行** ⭐ 重点参考
**TrainingStore.ts核心功能**
- ✅ SSE流式对话处理使用@ant-design/x的XStream
- ✅ WebSocket语音对话使用@coze/api/ws-tools
- ✅ 消息列表管理(增量更新、删除、重新生成)
- ✅ 对话状态管理6种状态未连接、连接中、已连接、聆听、回复中、错误
- ✅ 文件上传支持
- ✅ 错误处理和重试
- ✅ AbortController中断控制
## 七、开发路线图
### 第一阶段数据库与基础API2天
- [ ] 创建practice_scenes表
- [ ] 插入初始场景数据
- [ ] 实现场景管理APICRUD
### 第二阶段前端管理页面2天
- [ ] 场景管理页面开发
- [ ] 表单验证与提交
- [ ] 列表筛选与分页
### 第三阶段Coze集成3天
- [ ] Coze SDK集成
- [ ] 流式对话API实现
- [ ] 陪练对话页面开发
### 第四阶段Dify集成2天
- [ ] Dify场景提取API
- [ ] 课程中心集成
### 第五阶段联调与优化2天
- [ ] 端到端测试
- [ ] 错误处理完善
- [ ] 性能优化
**预计总工期**: 11个工作日
## 八、关键技术点
### ⚠️ 场景提示词构建(核心差异)
**与参考代码的关键不同**
- **参考代码**用户直接与Bot对话无场景概念
- **考培练系统**:首次消息必须包含完整场景设定
**实现方式**
```python
# 后端:首次消息时构建场景提示词
if is_first:
scene_prompt = f"""
# 陪练场景设定
## 场景名称
{scene_name}
## 场景背景
{scene_background}
## AI角色要求
{scene_ai_role}
## 练习目标
{chr(10).join(f"{i+1}. {obj}" for i, obj in enumerate(scene_objectives))}
---
现在开始陪练对话。请严格按照上述设定扮演角色。
学员的第一句话:{user_message}
"""
# 发送完整提示词给Coze
Message.build_user_question_text(scene_prompt)
else:
# 后续消息仅发送用户输入
Message.build_user_question_text(user_message)
```
**为什么这样设计**
1. 让AI明确理解要扮演的角色
2. 保持对话上下文使用conversation_id
3. 后续消息无需重复场景信息
4. 用户界面不显示场景设定文本
### SSE流式通信
```javascript
// 前端处理SSE事件
const response = await fetch('/api/v1/practice/start', {
method: 'POST',
body: JSON.stringify({
scene_name: scene.name,
scene_background: scene.background,
scene_ai_role: scene.ai_role,
scene_objectives: scene.objectives,
user_message: userInput,
is_first: true // 标记首次消息
})
})
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)
// 解析 event: xxx\ndata: {...}\n\n 格式
}
```
### Coze流式对话
```python
# 后端调用Coze
from cozepy import Coze, TokenAuth, Message, ChatEventType
stream = coze.chat.stream(
bot_id=COZE_PRACTICE_BOT_ID,
user_id=user_id,
additional_messages=[Message.build_user_question_text(message)],
conversation_id=conversation_id # 保持对话上下文
)
for event in stream:
if event.event == ChatEventType.CONVERSATION_MESSAGE_DELTA:
# 发送SSE增量事件
yield f"event: message.delta\ndata: {json.dumps({'content': event.message.content})}\n\n"
```
## 九、注意事项
### 安全性
- Token不要提交到版本控制
- 使用环境变量管理敏感配置
- API调用添加速率限制
### 性能
- SSE连接超时设置为180秒
- 数据库查询使用索引
- 前端消息列表使用虚拟滚动
### 用户体验
- 实现打字效果message.delta
如有技术问题,请参考:
1. Coze API文档`Coze-API文档.md`
2. 技术方案文档:`陪练功能技术方案.md`
3. API接口规范`陪练功能API接口规范.md`
4. 数据流程图:`陪练功能数据流程图.md`
5. OAuth认证规范`../../规范与约定-团队基线.md`
6. 完整迁移报告:`/root/aiedu/OAUTH_MIGRATION_SUCCESS.md`
---
**文档版本**: v2.0
**最后更新**: 2025-11-16
**维护人**: 考培练系统开发团队
**重大更新**: OAuth认证迁移完成

View File

@@ -0,0 +1,183 @@
# Coze 工作流运行 API 文档
## 接口说明
执行已发布的工作流。此接口为非流式响应模式,对于支持流式输出的节点,应使用流式响应接口。
## 播课工作流配置信息
- **workflow_id**: `7561161554420482088`
- **space_id**: `7474971491470688296`
- **个人令牌**: 同陪练功能使用的 COZE_API_TOKEN
- **输入参数**: `course_id`(字符串类型)
- **输出结果**: mp3 音频文件 URL
## 基础信息
| 项目 | 内容 |
|------|------|
| 请求方式 | POST |
| 请求地址 | `https://api.coze.cn/v1/workflow/run` |
| 权限 | run确保个人令牌开通了 run 权限) |
| 超时时间 | 默认 10 分钟,建议控制在 5 分钟以内 |
## 限制说明
| 限制项 | 说明 |
|--------|------|
| 工作流发布状态 | 必须为已发布状态 |
| 请求大小上限 | 20 MB |
| 超时时间 | 未开启异步时为 10 分钟,开启异步后为 24 小时 |
## 请求参数
### Header
| 参数 | 取值 | 说明 |
|------|------|------|
| Authorization | Bearer $Access_Token | Personal Access Token 认证 |
| Content-Type | application/json | 请求正文格式 |
### Body
| 参数 | 类型 | 是否必选 | 说明 |
|------|------|----------|------|
| workflow_id | String | 必选 | 待执行的 Workflow ID |
| parameters | Map | 可选 | 工作流开始节点的输入参数JSON 格式 |
| bot_id | String | 可选 | 需要关联的智能体 ID |
| app_id | String | 可选 | 关联的扣子应用 ID |
| ext | JSON Map | 可选 | 额外字段经纬度、user_id 等) |
| is_async | Boolean | 可选 | 是否异步运行(仅付费版可用) |
## 播课工作流请求示例
```json
{
"workflow_id": "7561161554420482088",
"parameters": {
"course_id": "5"
}
}
```
## 返回参数
| 参数 | 类型 | 说明 |
|------|------|------|
| code | Long | 调用状态码0 表示成功 |
| data | String | 工作流执行结果,通常为 JSON 序列化字符串 |
| msg | String | 状态信息,失败时包含详细错误信息 |
| execute_id | String | 异步执行的事件 ID |
| debug_url | String | 调试页面 URL7天有效期 |
| usage | Object | Token 使用情况 |
| detail | Object | 请求详情(包含 logid |
### Usage 对象
| 参数 | 类型 | 说明 |
|------|------|------|
| input_count | Integer | 输入 Token 数 |
| output_count | Integer | 输出 Token 数 |
| token_count | Integer | Token 总量 |
## 播课工作流返回示例
```json
{
"code": 0,
"data": "{\"mp3_url\":\"https://example.com/broadcast/course_5.mp3\"}",
"debug_url": "https://www.coze.cn/work_flow?execute_id=741364789030728****&space_id=7474971491470688296&workflow_id=7561161554420482088",
"msg": "",
"usage": {
"input_count": 120,
"token_count": 350,
"output_count": 230
}
}
```
## 使用 cozepy SDK 调用示例
### Python SDK0.2.0版本)
```python
from cozepy import Coze, TokenAuth
from app.core.config import settings
import json
async def generate_broadcast(course_id: int) -> str:
"""调用 Coze 工作流生成播课音频"""
# 初始化 Coze 客户端
coze = Coze(
auth=TokenAuth(token=settings.COZE_API_TOKEN),
base_url=settings.COZE_API_BASE
)
try:
# 调用工作流
result = await coze.workflows.runs.create(
workflow_id=settings.COZE_BROADCAST_WORKFLOW_ID,
parameters={"course_id": str(course_id)}
)
# 解析返回结果
if result.code == 0:
data = json.loads(result.data)
mp3_url = data.get("mp3_url")
return mp3_url
else:
raise Exception(f"工作流执行失败: {result.msg}")
except Exception as e:
logger.error(f"调用 Coze 工作流失败: {e}")
raise
```
## 错误处理
### 常见错误码
| code | 说明 | 处理方式 |
|------|------|---------|
| 0 | 成功 | 正常处理 |
| 400 | 参数错误 | 检查 workflow_id 和 parameters |
| 4200 | 工作流未发布 | 确保工作流已发布 |
| 500 | 服务器错误 | 记录 logid 并重试 |
| 6003 | 异步运行需付费版 | 使用同步模式或升级账号 |
### 错误日志记录
```python
logger.error(
f"Coze工作流执行失败",
extra={
"workflow_id": workflow_id,
"course_id": course_id,
"logid": result.detail.logid if hasattr(result, 'detail') else None,
"error_code": result.code,
"error_msg": result.msg
}
)
```
## 注意事项
1. **超时配置**:播课音频生成通常需要 10 分钟,建议设置超时时间为 180 秒
2. **Token 认证**:使用 Personal Access Token与陪练功能共用同一个 token
3. **参数类型**course_id 需要转换为字符串类型传递
4. **返回解析**data 字段为 JSON 字符串,需要反序列化后提取 mp3_url
5. **调试支持**:返回的 debug_url 可用于查看工作流执行详情7天有效期
6. **异步模式**:免费版不支持异步模式,使用同步调用即可
## 参考链接
- Coze 官方文档https://www.coze.cn/open/docs/developer_guides/workflow_run
- cozepy SDK 文档https://github.com/coze-dev/coze-py
---
**文档版本**: v1.0
**创建日期**: 2025-10-14
**维护人**: 考培练系统开发团队

View File

@@ -0,0 +1,69 @@
# 播课功能
## 功能概述
管理员在课程编辑页点击"生成播课"按钮,触发 Coze 工作流生成综合播课音频。学员可在课程中心点击"播课"按钮播放。
## 核心策略
**触发即可Coze工作流直接写数据库**
- 后端API只负责触发Coze工作流立即返回
- Coze工作流生成音频后直接执行SQL更新数据库
- 前端刷新页面即可看到最新结果,无需轮询
## 架构设计
1. **管理员触发**:课程编辑页点击"生成播课"
2. **后端触发**:调用 Coze API立即返回
3. **Coze工作流**:生成音频 + 直接写数据库
4. **用户查看**:刷新页面查看结果
## 技术要点
- 使用 `asyncio.to_thread()` 避免同步SDK阻塞
- Coze工作流配置MySQL连接端口3307
- 数据库只需2个字段`broadcast_audio_url``broadcast_generated_at`
- 极简架构:无需后台任务、状态轮询、状态字段
## 文档说明
- **Coze工作流运行API文档.md** - Coze API调用规范
- **播课功能API接口规范.md** - 后端API接口定义
- **播课功能技术方案.md** - 技术架构和实现细节
- **播课功能验收清单.md** - 功能验收标准
## 数据库配置
Coze工作流连接生产环境数据库
```
主机120.79.247.16 或 aiedu.ireborn.com.cn
端口3307容器映射端口
数据库kaopeilian
用户root
密码nj861021
```
SQL更新语句
```sql
UPDATE courses
SET
broadcast_audio_url = '{mp3_url}',
broadcast_generated_at = NOW(),
updated_at = NOW()
WHERE id = {course_id}
```
## 验证状态
✅ 已完成端到端联调
✅ Coze工作流成功写入远程数据库
✅ 无linter错误
✅ 生产环境验证通过
---
**实施日期**: 2025-10-14
**最后更新**: 2025-10-14

View File

@@ -0,0 +1,137 @@
# 播课功能 API 接口规范
## 设计原则
**触发即可Coze工作流直接写数据库**
- 管理员点击"生成播课"触发Coze工作流
- 后端API只负责触发立即返回
- Coze工作流生成完成后直接更新数据库无需后端介入
## API 端点
### 1. 触发播课生成
**接口**: `POST /api/v1/courses/{course_id}/generate-broadcast`
**权限**: manager、admin
**请求参数**:
- Path: `course_id` (int) - 课程ID
**响应数据**:
```json
{
"code": 200,
"message": "播课生成已启动",
"data": {
"message": "播课生成工作流已启动,生成完成后将自动更新"
}
}
```
**说明**:
- 立即返回,不等待生成完成
- Coze工作流会异步处理并直接写数据库
---
### 2. 获取播课信息
**接口**: `GET /api/v1/courses/{course_id}/broadcast`
**权限**: 所有登录用户
**请求参数**:
- Path: `course_id` (int) - 课程ID
**响应数据**:
```json
{
"code": 200,
"message": "success",
"data": {
"has_broadcast": true,
"mp3_url": "https://example.com/broadcast.mp3",
"generated_at": "2025-10-14T12:00:00Z"
}
}
```
**字段说明**:
- `has_broadcast` (boolean): 是否有播课
- `mp3_url` (string): 播课音频URL
- `generated_at` (string): 生成时间
---
## 数据库表结构
### courses 表
播课相关字段:
| 字段名 | 类型 | 说明 |
|--------|------|------|
| broadcast_audio_url | varchar(500) | 播课音频URL |
| broadcast_generated_at | datetime | 播课生成时间 |
**更新方式**: Coze工作流直接执行SQL更新
**SQL示例**:
```sql
UPDATE courses
SET
broadcast_audio_url = '{mp3_url}',
broadcast_generated_at = NOW(),
updated_at = NOW()
WHERE id = {course_id}
```
---
## Coze 工作流配置
### 数据库连接信息
- **主机**: `aiedu.ireborn.com.cn`
- **端口**: `3306`
- **数据库**: `kaopeilian`
- **用户**: `root`
- **密码**: `Kaopeilian2025!@#`
### 工作流参数
- **workflow_id**: `7561161554420482088`
- **space_id**: `7474971491470688296`
- **输入参数**: `course_id` (字符串)
- **输出**: 无(直接写数据库)
---
## 前端交互流程
1. **触发生成**
- 管理员点击"生成播课"按钮
- 调用 `POST /generate-broadcast`
- 显示提示:"播课生成已启动,完成后会自动更新"
2. **查看结果**
- 用户刷新页面或重新进入
- 调用 `GET /broadcast` 获取最新状态
- 如果 `has_broadcast=true` 则显示"已生成"和播放链接
---
## 错误处理
| HTTP 状态码 | 说明 | 处理方式 |
|------------|------|---------|
| 404 | 课程不存在 | 提示用户 |
| 500 | 触发失败 | 提示用户稍后重试 |
---
## 日期
创建: 2025-10-14
更新: 2025-10-14 - 简化为触发模式

View File

@@ -0,0 +1,296 @@
# 播课功能技术方案
## 一、需求背景
课程管理员需要为课程生成播课音频,学员可以在课程卡片上点击"播课"按钮收听课程内容,支持音频播放控制。
## 二、核心设计
### 2.1 触发方式
- **管理员手动触发**:在课程编辑页的"学习资料与知识点"选项卡,点击"生成播课"按钮
- **生成范围**:一个课程生成一个综合播课音频(多个资料合并生成)
- **存储方式**:只存储 mp3 URL不存储在本地服务器
### 2.2 技术选型
| 技术项 | 选型 | 说明 |
|--------|------|------|
| AI 工作流 | Coze 工作流 | workflow_id: 7561161554420482088 |
| SDK | cozepy 0.2.0 | 已安装,与陪练功能共用 |
| 认证方式 | Personal Access Token | 与陪练功能共用 token |
| 数据库字段 | `broadcast_audio_url` | 存储 mp3 URL |
| 前端播放器 | HTML5 audio | 原生支持,无需额外依赖 |
### 2.3 数据流程
```
管理员点击"生成播课"
→ 前端调用后端 API
→ 后端调用 Coze 工作流
→ Coze 生成 mp3 音频
→ 返回 mp3 URL
→ 后端保存到数据库
→ 前端显示成功提示
学员点击"播课"
→ 前端查询播课信息
→ 跳转到播放页面
→ 加载并播放音频
```
## 三、数据库设计
### 3.1 courses 表新增字段
```sql
ALTER TABLE courses
ADD COLUMN broadcast_audio_url VARCHAR(500) NULL COMMENT '播课音频URL',
ADD COLUMN broadcast_generated_at DATETIME NULL COMMENT '播课生成时间';
```
**字段说明**
- `broadcast_audio_url`: 存储 Coze 工作流返回的 mp3 文件 URL
- `broadcast_generated_at`: 记录生成时间,用于判断是否需要重新生成
**索引**: 无需额外索引(查询通过主键 course_id
## 四、后端架构
### 4.1 配置层
**文件**: `app/core/config.py`
```python
class Settings(BaseSettings):
# 播课工作流配置
COZE_BROADCAST_WORKFLOW_ID: str = Field(default="7561161554420482088")
COZE_BROADCAST_SPACE_ID: str = Field(default="7474971491470688296")
```
### 4.2 服务层
**文件**: `app/services/coze_broadcast_service.py`
**职责**
- 封装 Coze 工作流调用逻辑
- 解析返回的 mp3 URL
- 错误处理和日志记录
**关键方法**
```python
class CozeBroadcastService:
async def generate_broadcast(self, course_id: int) -> str:
"""
调用 Coze 工作流生成播课音频
Args:
course_id: 课程ID
Returns:
mp3 音频文件 URL
Raises:
CozeError: Coze API 调用失败
ValueError: 返回结果解析失败
"""
```
### 4.3 API 层
**文件**: `app/api/v1/broadcast.py`
**接口1**: 生成播课
- 路径: `POST /api/v1/courses/{course_id}/generate-broadcast`
- 权限: manager, admin
- 超时: 180秒
- 功能: 调用工作流生成音频,更新数据库
**接口2**: 获取播课信息
- 路径: `GET /api/v1/courses/{course_id}/broadcast`
- 权限: trainee, manager, admin
- 功能: 查询课程是否有播课以及播课 URL
### 4.4 Schema 定义
```python
class GenerateBroadcastResponse(BaseModel):
"""生成播课响应"""
mp3_url: str = Field(..., description="播课音频URL")
generated_at: datetime = Field(..., description="生成时间")
class BroadcastInfo(BaseModel):
"""播课信息"""
has_broadcast: bool = Field(..., description="是否有播课")
mp3_url: Optional[str] = Field(None, description="播课音频URL")
generated_at: Optional[datetime] = Field(None, description="生成时间")
```
## 五、前端架构
### 5.1 类型定义
**文件**: `src/types/broadcast.ts`
```typescript
export interface BroadcastInfo {
has_broadcast: boolean
mp3_url?: string
generated_at?: string
}
export interface GenerateBroadcastResponse {
mp3_url: string
generated_at: string
}
```
### 5.2 API 封装
**文件**: `src/api/broadcast.ts`
```typescript
export const broadcastApi = {
generate(courseId: number): Promise<ResponseData<GenerateBroadcastResponse>>
getInfo(courseId: number): Promise<ResponseData<BroadcastInfo>>
}
```
### 5.3 页面改造
**1. 课程编辑页** (`src/views/manager/edit-course.vue`)
新增功能:
- 在"学习资料与知识点"选项卡添加"生成播课"按钮
- 查询并显示播课信息(是否已生成、生成时间)
- 生成中显示 Loading 状态
- 生成成功后显示"重新生成"按钮
**2. 播放页面** (`src/views/trainee/broadcast-course.vue`)
播放器功能:
- 播放/暂停控制
- 进度条拖动
- 当前时间/总时长显示
- 播放速度调节1.0x, 1.25x, 1.5x, 2.0x
**3. 课程卡片** (`src/views/trainee/course-center.vue`)
修改播课按钮点击事件:
- 查询播课信息
- 如无播课,提示"该课程暂无播课内容"
- 如有播课,跳转到播放页面
## 六、交互流程
### 6.1 生成播课流程
```
1. 管理员进入课程编辑页
2. 切换到"学习资料与知识点"选项卡
3. 点击"生成播课"按钮
4. 前端调用 POST /api/v1/courses/{id}/generate-broadcast
5. 后端调用 Coze 工作流约10分钟
6. Coze 返回 mp3 URL
7. 后端更新数据库 broadcast_audio_url 和 broadcast_generated_at
8. 前端显示"生成成功"提示
9. 按钮文本变为"重新生成",显示生成时间
```
### 6.2 播放流程
```
1. 学员在课程中心查看课程卡片
2. 点击"播课"按钮
3. 前端调用 GET /api/v1/courses/{id}/broadcast
4. 如 has_broadcast=false显示"暂无播课内容"
5. 如 has_broadcast=true跳转到播放页面
6. 播放页面加载 mp3 音频
7. 学员使用播放器控制播放
```
## 七、性能与安全
### 7.1 性能优化
- **超时配置**: 前后端统一设置 180 秒超时
- **缓存策略**: 音频 URL 从数据库读取,无需额外缓存
- **CDN 加速**: mp3 文件由 Coze 托管,自带 CDN 加速
### 7.2 错误处理
| 场景 | 处理方式 |
|------|---------|
| 工作流调用超时 | 提示"生成超时,请稍后重试" |
| 工作流执行失败 | 记录 logid提示"生成失败,请联系管理员" |
| mp3 URL 解析失败 | 记录错误日志,提示"结果解析失败" |
| 播放加载失败 | 提示"音频加载失败,请检查网络" |
### 7.3 日志记录
```python
# 生成开始
logger.info(f"开始生成播课: course_id={course_id}, user_id={user_id}")
# 生成成功
logger.info(f"播课生成成功: course_id={course_id}, mp3_url={mp3_url}, duration={duration}")
# 生成失败
logger.error(
f"播课生成失败: course_id={course_id}",
extra={"logid": logid, "error": str(error)}
)
```
## 八、测试策略
### 8.1 单元测试
- CozeBroadcastService 工作流调用测试
- broadcast API 接口测试
- 前端 broadcastApi 封装测试
### 8.2 集成测试
- 管理员生成播课端到端测试
- 学员播放端到端测试
- 错误场景测试(无播课、生成失败、播放失败)
### 8.3 性能测试
- 工作流调用耗时(预期 10 分钟左右)
- 前端音频加载时间
- 并发生成请求测试
## 九、部署与监控
### 9.1 配置检查
部署前确认以下配置:
- ✅ COZE_API_TOKEN 正确配置
- ✅ COZE_BROADCAST_WORKFLOW_ID = 7561161554420482088
- ✅ COZE_BROADCAST_SPACE_ID = 7474971491470688296
- ✅ 工作流已在 Coze 平台发布
### 9.2 监控指标
- 生成成功率(目标 >95%
- 平均生成时长(预期 10 分钟)
- 播放失败率(目标 <5%
- Coze API 调用错误率
## 十、后续优化方向
1. **自动生成**:资料上传后自动触发生成(需评估资源消耗)
2. **进度提示**:显示生成进度(需 Coze 工作流支持进度回调)
3. **音频预览**:生成后在编辑页预览播放
4. **历史版本**:保存多个版本的播课音频
5. **字幕支持**:工作流返回字幕文件,前端同步显示
---
**文档版本**: v1.0
**创建日期**: 2025-10-14
**维护人**: 考培练系统开发团队

View File

@@ -0,0 +1,923 @@
# 陪练功能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
**维护人**:考培练系统开发团队

View File

@@ -0,0 +1,720 @@
# 考培练系统 - AI陪练功能技术方案
## 一、需求概述
### 1.1 业务背景
考培练系统为轻医美连锁品牌提供员工培训服务AI陪练功能旨在通过模拟真实客户场景让学员进行实战对话练习提升销售和服务能力。
### 1.2 核心功能
1. **陪练场景管理**:管理员可创建和管理各类陪练场景
2. **场景化陪练**:学员选择预设场景进行针对性练习
3. **课程关联陪练**:基于课程内容动态生成陪练场景
4. **实时对话**流式AI对话模拟真实交互
5. **对话管理**对话历史由Coze管理前端可查询
### 1.3 用户角色
- **管理员 (Manager)**:管理陪练场景(增删改查、启用/禁用)
- **学员 (Trainee)**:选择场景并进行陪练对话
## 二、技术架构
### 2.1 技术选型
| 组件 | 技术 | 说明 |
|------|------|------|
| 前端 | Vue3 + Element Plus | 已有技术栈 |
| 后端 | Python + FastAPI | 已有技术栈 |
| 数据库 | MySQL 8.0 | 存储场景数据 |
| AI对话 | Coze Bot | 字节跳动AI对话平台 |
| 场景提取 | Dify工作流 | 从课程提取场景信息 |
| 通信协议 | SSE (Server-Sent Events) | 流式对话 |
### 2.2 系统架构图
```
┌─────────────────────────────────────────────────────────────┐
│ 前端 (Vue3) │
│ ┌──────────────────┐ ┌────────────────┐ ┌────────────┐ │
│ │ 陪练中心页面 │ │ 陪练对话页面 │ │ 场景管理 │ │
│ │ (场景列表) │ │ (实时对话) │ │ (Manager) │ │
│ └──────────────────┘ └────────────────┘ └────────────┘ │
└───────────┬────────────────────┬────────────────┬───────────┘
│ │ │
│ HTTP/SSE │ SSE │ HTTP
│ │ │
┌───────────▼────────────────────▼────────────────▼───────────┐
│ 后端 (FastAPI) │
│ ┌──────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ 场景管理API │ │ Coze集成 │ │ Dify集成 │ │
│ │ (CRUD) │ │ (对话流式) │ │ (场景提取) │ │
│ └──────────────┘ └─────────────┘ └─────────────────┘ │
└───────────┬────────────────┬────────────────┬───────────────┘
│ │ │
┌────▼────┐ ┌─────▼──────┐ ┌────▼─────┐
│ MySQL │ │ Coze API │ │ Dify API │
│(场景库) │ │(AI对话) │ │(场景生成)│
└─────────┘ └────────────┘ └──────────┘
```
## 三、数据库设计
### 3.1 陪练场景表 (practice_scenes)
```sql
CREATE TABLE `practice_scenes` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(200) NOT NULL COMMENT '场景名称',
`description` TEXT COMMENT '场景描述',
`type` VARCHAR(50) NOT NULL COMMENT '场景类型: phone/face/complaint/after-sales/product-intro',
`difficulty` VARCHAR(50) NOT NULL COMMENT '难度等级: beginner/junior/intermediate/senior/expert',
`status` VARCHAR(20) DEFAULT 'active' COMMENT '状态: active/inactive',
-- 场景详细配置
`background` TEXT COMMENT '场景背景设定',
`ai_role` TEXT COMMENT 'AI角色描述',
`objectives` JSON COMMENT '练习目标数组 ["目标1", "目标2"]',
`keywords` JSON COMMENT '关键词数组 ["关键词1", "关键词2"]',
-- 统计信息
`duration` INT DEFAULT 10 COMMENT '预计时长(分钟)',
`usage_count` INT DEFAULT 0 COMMENT '使用次数',
`rating` DECIMAL(3,1) DEFAULT 0.0 COMMENT '评分',
-- 审计字段
`created_by` INT COMMENT '创建人ID',
`updated_by` INT COMMENT '更新人ID',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`is_deleted` BOOLEAN DEFAULT FALSE,
`deleted_at` DATETIME,
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_type (type),
INDEX idx_difficulty (difficulty),
INDEX idx_status (status),
INDEX idx_is_deleted (is_deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='陪练场景表';
```
**设计说明**
- 对话历史不存储在数据库由Coze平台管理
- 场景数据支持JSON类型便于扩展
- 软删除设计,保留历史数据
- 统计字段用于数据分析
### 3.2 初始数据示例
```sql
INSERT INTO `practice_scenes` (name, description, type, difficulty, status, background, ai_role, objectives, keywords, duration) VALUES
('初次电话拜访客户', '模拟首次通过电话联系潜在客户的场景', 'phone', 'beginner', 'active',
'你是一名销售专员,需要通过电话联系一位从未接触过的潜在客户...',
'AI扮演一位忙碌且略显不耐烦的采购经理...',
'["学会专业的电话开场白", "快速建立信任关系", "有效探询客户需求"]',
'["开场白", "需求挖掘", "时间管理"]', 10),
('处理价格异议', '练习如何应对客户对产品价格的质疑和异议', 'face', 'intermediate', 'active',
'客户对你的产品很感兴趣,但认为价格太高...',
'AI扮演一位精明的客户对价格非常敏感...',
'["掌握价值塑造技巧", "学会处理价格异议", "提升谈判能力"]',
'["异议处理", "价值塑造", "谈判技巧"]', 15);
```
## 四、后端API设计
### 4.1 技术配置
```python
# app/config/settings.py
# Coze配置
COZE_API_BASE = "https://api.coze.cn"
COZE_API_TOKEN = "pat_Sa5OiuUl0gDflnKstQTToIz0sSMshBV06diX0owOeuI1ZK1xDLH5YZH9fSeuKLIi"
COZE_PRACTICE_BOT_ID = "7560643598174683145"
# Dify配置
DIFY_API_BASE = "http://dify.ireborn.com.cn/v1"
DIFY_PRACTICE_API_KEY = "app-rYP6LNM4iPmNjIHns12zFeJp"
DIFY_PRACTICE_WORKFLOW_ID = "待确认" # 需要从Dify获取实际工作流ID
```
### 4.2 Coze客户端初始化
```python
# app/services/coze_service.py
from cozepy import Coze, TokenAuth, COZE_CN_BASE_URL
from app.config.settings import settings
class CozeService:
def __init__(self):
self.client = Coze(
auth=TokenAuth(token=settings.COZE_API_TOKEN),
base_url=COZE_CN_BASE_URL
)
self.bot_id = settings.COZE_PRACTICE_BOT_ID
def create_stream_chat(self, user_id: str, message: str, conversation_id: str = None):
"""创建流式对话"""
from cozepy import Message
stream = self.client.chat.stream(
bot_id=self.bot_id,
user_id=user_id,
additional_messages=[Message.build_user_question_text(message)],
conversation_id=conversation_id
)
return stream
```
### 4.3 API接口清单
#### 4.3.1 场景管理接口 (Manager)
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 获取场景列表 | GET | `/api/v1/manager/practice-scenes` | 分页、筛选 |
| 获取场景详情 | GET | `/api/v1/manager/practice-scenes/{id}` | 单个场景 |
| 创建场景 | POST | `/api/v1/manager/practice-scenes` | 新增场景 |
| 更新场景 | PUT | `/api/v1/manager/practice-scenes/{id}` | 修改场景 |
| 删除场景 | DELETE | `/api/v1/manager/practice-scenes/{id}` | 软删除 |
| 切换状态 | PUT | `/api/v1/manager/practice-scenes/{id}/toggle-status` | 启用/禁用 |
#### 4.3.2 学员查询接口 (Trainee)
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 获取可用场景 | GET | `/api/v1/practice/scenes` | 仅返回active状态 |
| 获取场景详情 | GET | `/api/v1/practice/scenes/{id}` | 单个场景详情 |
#### 4.3.3 陪练对话接口
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 开始陪练 | POST | `/api/v1/practice/start` | SSE流式返回 |
| 中断对话 | POST | `/api/v1/practice/interrupt` | 中断当前对话 |
| 获取对话列表 | GET | `/api/v1/practice/conversations` | Coze管理的对话 |
#### 4.3.4 Dify场景提取接口
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 提取场景 | POST | `/api/v1/practice/extract-scene` | 从课程提取场景 |
### 4.4 核心接口实现
#### 场景列表(带分页筛选)
```python
@router.get("/manager/practice-scenes", response_model=ResponseModel[PaginatedResponse[PracticeSceneResponse]])
async def list_practice_scenes(
page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100),
type: Optional[str] = Query(None),
difficulty: Optional[str] = Query(None),
status: Optional[str] = Query(None),
search: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取陪练场景列表(分页、筛选)"""
query = select(PracticeScene).where(PracticeScene.is_deleted == False)
# 筛选条件
if type:
query = query.where(PracticeScene.type == type)
if difficulty:
query = query.where(PracticeScene.difficulty == difficulty)
if status:
query = query.where(PracticeScene.status == status)
if search:
query = query.where(
or_(
PracticeScene.name.contains(search),
PracticeScene.description.contains(search)
)
)
# 分页
total = await db.scalar(select(func.count()).select_from(query.subquery()))
scenes = await db.scalars(
query.offset((page - 1) * size).limit(size).order_by(PracticeScene.created_at.desc())
)
return ResponseModel.success(
PaginatedResponse(
items=list(scenes),
total=total,
page=page,
page_size=size
)
)
```
#### 开始陪练SSE流式
```python
from fastapi.responses import StreamingResponse
from cozepy import ChatEventType
import json
@router.post("/practice/start")
async def start_practice(
request: StartPracticeRequest,
current_user: User = Depends(get_current_user),
coze_service: CozeService = Depends(get_coze_service)
):
"""开始陪练对话SSE流式返回"""
# ⚠️ 关键差异场景信息必须作为第一条消息发给Coze
# 与参考代码不同我们需要在首次对话时将完整场景信息发送给AI
if request.is_first:
scene_context = f"""
# 陪练场景设定
## 场景名称
{request.scene_name}
## 场景背景
{request.scene_background}
## AI角色要求
{request.scene_ai_role}
## 练习目标
{chr(10).join(f"{i+1}. {obj}" for i, obj in enumerate(request.scene_objectives))}
## 关键词
{', '.join(request.scene_keywords) if hasattr(request, 'scene_keywords') else ''}
---
现在开始陪练对话。请你严格按照上述场景设定扮演角色,与学员进行实战对话练习。
学员的第一句话:{request.user_message}
"""
user_message = scene_context
else:
user_message = request.user_message
def generate_stream():
try:
stream = coze_service.create_stream_chat(
user_id=str(current_user.id),
message=user_message,
conversation_id=request.conversation_id
)
for event in stream:
if event.event == ChatEventType.CONVERSATION_CHAT_CREATED:
yield f"event: conversation.chat.created\ndata: {json.dumps({'conversation_id': request.conversation_id, 'chat_id': stream.response.logid})}\n\n"
elif event.event == ChatEventType.CONVERSATION_MESSAGE_DELTA:
yield f"event: message.delta\ndata: {json.dumps({'content': event.message.content})}\n\n"
elif event.event == ChatEventType.CONVERSATION_MESSAGE_COMPLETED:
yield f"event: message.completed\ndata: {json.dumps({'content': event.message.content})}\n\n"
elif event.event == ChatEventType.CONVERSATION_CHAT_COMPLETED:
yield f"event: conversation.completed\ndata: {json.dumps({'token_count': event.chat.usage.token_count})}\n\n"
break
elif event.event == ChatEventType.CONVERSATION_CHAT_FAILED:
yield f"event: error\ndata: {json.dumps({'error': str(event.chat.last_error)})}\n\n"
break
yield f"event: done\ndata: [DONE]\n\n"
except Exception as e:
logger.error(f"陪练对话失败: {e}")
yield f"event: error\ndata: {json.dumps({'error': str(e)})}\n\n"
return StreamingResponse(
generate_stream(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no"
}
)
```
#### Dify场景提取
```python
import httpx
@router.post("/practice/extract-scene", response_model=ResponseModel[ExtractSceneResponse])
async def extract_scene(
request: ExtractSceneRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""从课程提取陪练场景"""
# 获取课程信息
course = await db.get(Course, request.course_id)
if not course:
raise HTTPException(status_code=404, detail="课程不存在")
# 调用Dify工作流
url = f"{settings.DIFY_API_BASE}/workflows/run"
headers = {
"Authorization": f"Bearer {settings.DIFY_PRACTICE_API_KEY}",
"Content-Type": "application/json"
}
payload = {
"inputs": {"course_id": str(request.course_id)},
"response_mode": "streaming",
"user": "kaopeilian"
}
aggregated_text = ""
workflow_run_id = ""
async with httpx.AsyncClient(timeout=180.0) as client:
async with client.stream("POST", url, json=payload, headers=headers) as response:
async for line in response.aiter_lines():
if not line or not line.startswith("data: "):
continue
try:
event_data = json.loads(line[6:])
event_type = event_data.get("event")
if event_type == "workflow_started":
workflow_run_id = event_data["workflow_run_id"]
logger.info(f"Dify工作流启动: {workflow_run_id}")
elif event_type == "text_chunk":
aggregated_text += event_data["data"]["text"]
elif event_type == "workflow_finished":
logger.info("Dify工作流完成")
break
except Exception as e:
logger.error(f"解析Dify事件失败: {e}")
# 解析场景数据
scene_data = json.loads(aggregated_text)
return ResponseModel.success(ExtractSceneResponse(**scene_data))
```
## 五、前端实现方案
### 5.1 路由配置
```javascript
// src/router/trainee.ts
{
path: 'ai-practice-center',
name: 'AIPracticeCenter',
component: () => import('@/views/trainee/ai-practice-center.vue'),
meta: { title: 'AI陪练中心', requiresAuth: true }
},
{
path: 'ai-practice',
name: 'AIPractice',
component: () => import('@/views/trainee/ai-practice.vue'),
meta: { title: 'AI陪练对话', requiresAuth: true }
}
// src/router/manager.ts
{
path: 'practice-scene-management',
name: 'PracticeSceneManagement',
component: () => import('@/views/manager/practice-scene-management.vue'),
meta: { title: '陪练场景管理', requiresAuth: true, role: 'manager' }
}
```
### 5.2 陪练中心页面实现要点
**页面文件**`src/views/trainee/ai-practice-center.vue`
**核心功能**
1. 场景筛选(类型、难度、关键词)
2. 场景卡片展示
3. 场景详情查看
4. 开始陪练按钮
**关键代码**
```javascript
// 获取场景列表
const fetchScenes = async () => {
try {
const response = await practiceApi.getScenes({
page: currentPage.value,
size: pageSize.value,
type: filterForm.type,
difficulty: filterForm.difficulty,
search: filterForm.search
})
sceneList.value = response.data.items
total.value = response.data.total
} catch (error) {
ElMessage.error('获取场景列表失败')
}
}
// 开始陪练
const startPractice = (scene) => {
router.push({
name: 'AIPractice',
query: {
sceneId: scene.id,
sceneName: scene.name,
mode: 'direct' // 直接模式
}
})
}
```
### 5.3 陪练对话页面实现要点
**页面文件**`src/views/trainee/ai-practice.vue`
**核心功能**
1. SSE流式接收AI响应
2. 消息列表展示(用户/AI
3. 输入框和发送按钮
4. 中断对话功能
5. **场景信息首次发送**(关键功能)
**⚠️ 与参考代码的关键差异**
- 参考代码:场景信息不传递,直接开始对话
- 考培练系统:**场景信息必须作为第一条消息发送给Coze**让AI理解角色设定
**SSE处理示例**
```javascript
const startConversation = async (userMessage) => {
try {
isLoading.value = true
// ⚠️ 关键is_first标记决定是否在消息中包含场景信息
const isFirstMessage = messages.value.length === 0
const response = await fetch('/api/v1/practice/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getToken()}`
},
body: JSON.stringify({
scene_id: route.query.sceneId,
scene_name: currentScene.value.name,
scene_description: currentScene.value.description,
scene_background: currentScene.value.background,
scene_ai_role: currentScene.value.ai_role,
scene_objectives: currentScene.value.objectives,
scene_keywords: currentScene.value.keywords,
user_message: userMessage,
conversation_id: conversationId.value,
is_first: isFirstMessage // 首次消息会将场景信息拼接到user_message
})
})
const reader = response.body.getReader()
const decoder = new TextDecoder()
let aiMessage = {
role: 'assistant',
content: '',
timestamp: Date.now()
}
messages.value.push(aiMessage)
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: ', ''))
if (event === 'message.delta') {
aiMessage.content += data.content
} else if (event === 'conversation.completed') {
console.log('对话完成Token用量:', data.token_count)
} else if (event === 'error') {
ElMessage.error(data.error)
}
}
}
} catch (error) {
ElMessage.error('发送消息失败')
} finally {
isLoading.value = false
}
}
```
### 5.4 课程中心集成
**文件**`src/views/trainee/course-center.vue`
**修改点**
1. 课程卡片增加"陪练"按钮
2. 点击按钮调用Dify提取场景
3. 提取成功后跳转到对话页面
```javascript
// 点击陪练按钮
const handlePractice = async (course) => {
try {
ElMessage.info('正在提取陪练场景...')
// 调用Dify提取场景
const response = await practiceApi.extractScene({
course_id: course.id
})
// 跳转到对话页面
router.push({
name: 'AIPractice',
query: {
mode: 'course', // 课程模式
courseId: course.id,
sceneData: JSON.stringify(response.data)
}
})
} catch (error) {
ElMessage.error('提取场景失败')
}
}
```
## 六、两种陪练入口对比
| 对比项 | 陪练中心入口 | 课程中心入口 |
|--------|------------|------------|
| **场景来源** | 数据库预设场景 | Dify动态生成 |
| **适用场景** | 通用陪练练习 | 课程相关练习 |
| **流程** | 选择场景 → 对话 | 提取场景 → 对话 |
| **场景质量** | 人工设计,质量稳定 | AI生成依赖课程质量 |
| **灵活性** | 固定场景 | 动态适配课程内容 |
| **实现复杂度** | 简单 | 中等需要Dify集成 |
## 七、开发排期建议
### 第一阶段数据库与基础API2天
- 创建practice_scenes表
- 插入5-10条初始场景数据
- 实现场景管理APICRUD
- 实现学员查询API
### 第二阶段前端管理页面2天
- 场景管理页面开发
- 表单验证与提交
- 列表筛选与分页
### 第三阶段Coze集成3天
- Coze SDK集成
- 流式对话API实现
- 对话管理接口
- 陪练中心页面开发
- 陪练对话页面开发
### 第四阶段Dify集成2天
- Dify场景提取API
- 课程中心集成
- 场景数据转换
### 第五阶段联调与优化2天
- 端到端测试
- 错误处理完善
- 性能优化
- 用户体验优化
**总计约11个工作日**
## 八、风险与应对
### 8.1 技术风险
| 风险 | 影响 | 应对措施 |
|------|------|---------|
| Coze API不稳定 | 对话中断 | 实现重试机制,超时提示 |
| Dify场景质量差 | 练习效果差 | 人工审核机制,场景优化 |
| SSE连接中断 | 用户体验差 | 断线重连,状态恢复 |
| Token用量超限 | 成本问题 | 监控Token使用设置限额 |
### 8.2 业务风险
| 风险 | 影响 | 应对措施 |
|------|------|---------|
| 场景设计不合理 | 练习无效 | 场景评分反馈,持续优化 |
| AI回复不够真实 | 体验差 | 优化Bot Prompt多轮测试 |
| 用户使用频率低 | ROI低 | 增加趣味性,积分激励 |
## 九、监控与维护
### 9.1 关键指标
- API响应时间
- SSE连接成功率
- 对话完成率
- Token使用量
- 场景使用次数
- 用户活跃度
### 9.2 日志记录
```python
logger.info(f"用户{user_id}开始陪练,场景{scene_id}")
logger.info(f"Coze对话创建成功logid={stream.response.logid}")
logger.info(f"对话完成Token用量={token_count}")
logger.error(f"陪练对话失败: {error}")
```
### 9.3 异常告警
- Coze API调用失败超过阈值
- Dify工作流超时频繁
- SSE连接异常率超标
## 十、附录
### 10.1 参考文档
- [Coze API文档](./Coze-API文档.md)
- [陪练功能API接口规范](./陪练功能API接口规范.md)
- [陪练功能数据流程图](./陪练功能数据流程图.md)
### 10.2 示例代码位置
- Coze Python SDK`参考代码/coze-py-main/`
- 后端参考实现:`参考代码/coze-chat-backend/main.py`
- 前端陪练页面:`kaopeilian-frontend/src/views/trainee/ai-practice-center.vue`
### 10.3 配置信息
```python
# 陪练系统专用配置
COZE_API_BASE = "https://api.coze.cn"
COZE_API_TOKEN = "pat_Sa5OiuUl0gDflnKstQTToIz0sSMshBV06diX0owOeuI1ZK1xDLH5YZH9fSeuKLIi"
COZE_PRACTICE_BOT_ID = "7560643598174683145"
DIFY_API_BASE = "http://dify.ireborn.com.cn/v1"
DIFY_PRACTICE_API_KEY = "app-rYP6LNM4iPmNjIHns12zFeJp"
DIFY_PRACTICE_WORKFLOW_ID = "待确认"
```
---
**文档版本**v1.0
**最后更新**2025-10-13
**维护人**:考培练系统开发团队

View File

@@ -0,0 +1,760 @@
# 陪练功能数据流程图
## 一、总体流程概览
```
┌─────────────────────────────────────────────────────────────────┐
│ 考培练系统 │
│ │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ 陪练中心入口 │ │ 课程中心入口 │ │
│ │ (直接模式) │ │ (课程模式) │ │
│ └───────┬────────┘ └───────┬────────┘ │
│ │ │ │
│ │ 选择场景 │ 提取场景 │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 陪练对话页面 (统一入口) │ │
│ │ - 实时SSE对话 │ │
│ │ - 消息展示 │ │
│ │ - 对话控制 │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
## 二、陪练中心入口流程(直接模式)
### 2.1 场景选择流程
```
┌──────┐
│ 学员 │
└───┬──┘
│ 1. 访问陪练中心
┌─────────────────────────┐
│ 陪练中心页面 │
│ /trainee/ai-practice-center │
└───────┬─────────────────┘
│ 2. 获取场景列表
┌───────────────────────────┐
│ GET /api/v1/practice/scenes│
│ 参数:筛选条件、分页 │
└───────┬───────────────────┘
│ 3. 查询数据库
┌──────────────────────┐
│ MySQL: practice_scenes│
│ WHERE status='active' │
│ AND is_deleted=false│
└───────┬──────────────┘
│ 4. 返回场景列表
┌──────────────────────┐
│ 场景卡片展示 │
│ - 名称、描述 │
│ - 类型、难度标签 │
│ - 统计信息 │
└───────┬──────────────┘
│ 5. 点击"开始陪练"
┌──────────────────────┐
│ 跳转到对话页面 │
│ 携带场景参数 │
└──────────────────────┘
```
### 2.2 对话流程
```
┌──────┐
│ 学员 │
└───┬──┘
│ 1. 进入对话页面
┌───────────────────────────┐
│ 陪练对话页面 │
│ /trainee/ai-practice │
│ query: sceneId, mode=direct│
└───────┬───────────────────┘
│ 2. 加载场景详情
┌─────────────────────────────┐
│ GET /api/v1/practice/scenes/1│
└───────┬─────────────────────┘
│ 3. 返回场景完整信息
│ (background, ai_role, objectives)
┌──────────────────────┐
│ 显示场景背景和目标 │
└───────┬──────────────┘
│ 4. 用户输入第一条消息
┌────────────────────────────┐
│ POST /api/v1/practice/start│
│ 请求体: │
│ - scene_id: 1 │
│ - scene_name │
│ - scene_description │
│ - scene_background │
│ - scene_ai_role │
│ - scene_objectives │
│ - scene_keywords │
│ - user_message │
│ - is_first: true │
└───────┬───────────────────┘
│ 5. ⚠️ 后端构建完整场景提示词
│ (关键步骤)
┌──────────────────────────────┐
│ 构建Markdown格式场景文本
│ # 陪练场景设定 │
│ ## 场景名称 │
│ 初次电话拜访客户 │
│ ## 场景背景 │
│ 你是一名销售专员... │
│ ## AI角色要求 │
│ AI扮演一位忙碌的采购经理... │
│ ## 练习目标 │
│ 1. 学会专业的电话开场白 │
│ 2. 快速建立信任关系 │
│ --- │
│ 现在开始陪练对话... │
│ 学员的第一句话:您好... │
└───────┬──────────────────────┘
│ 6. 发送完整提示词给Coze
┌──────────────────────────┐
│ Python FastAPI后端 │
│ coze_service.py │
└───────┬─────────────────┘
│ 7. 调用Coze API发送场景提示词
┌─────────────────────────────┐
│ Coze Chat API (流式) │
│ https://api.coze.cn │
│ Bot ID: 7560643598174683145 │
│ │
│ Message.build_user_question_text(│
│ 完整的场景提示词 Markdown │
│ ) │
└───────┬─────────────────────┘
│ 8. Coze AI理解场景并生成回复
│ (AI根据场景扮演角色)
┌──────────────────────────┐
│ AI推理生成对话内容 │
│ - 理解场景背景 │
│ - 扮演指定角色 │
│ - 符合难度设定 │
└───────┬─────────────────┘
│ 9. SSE流式返回
┌──────────────────────────┐
│ SSE事件流 │
│ event: message.delta │
│ data: {"content":"您"} │
│ │
│ event: message.delta │
│ data: {"content":"好"} │
│ │
│ event: message.completed │
│ event: done │
└───────┬─────────────────┘
│ 10. 前端实时显示
┌──────────────────────┐
│ AI消息打字效果展示 │
│ (AI已理解场景角色) │
└───────┬──────────────┘
│ 11. 用户继续对话(后续消息)
┌────────────────────────────┐
│ POST /api/v1/practice/start│
│ 请求体: │
│ - user_message │
│ - conversation_id │
│ - is_first: false │
│ (⚠️不再包含场景信息) │
└───────┬───────────────────┘
│ 12. 直接发送用户消息
│ (重复步骤7-11)
┌──────────────────────┐
│ 多轮对话交互 │
│ 使用conversation_id │
│ 保持对话上下文 │
│ AI持续扮演角色 │
└──────────────────────┘
```
## 三、课程中心入口流程(课程模式)
### 3.1 场景提取流程
```
┌──────┐
│ 学员 │
└───┬──┘
│ 1. 访问课程中心
┌─────────────────────────┐
│ 课程中心页面 │
│ /trainee/course-center │
└───────┬─────────────────┘
│ 2. 点击课程卡片"陪练"按钮
┌───────────────────────────────┐
│ POST /api/v1/practice/extract-scene│
│ 请求体: │
│ { "course_id": 5 } │
└───────┬───────────────────────────┘
│ 3. 调用Dify工作流
┌──────────────────────────────────┐
│ Dify API (流式) │
│ POST /v1/workflows/run │
│ URL: dify.ireborn.com.cn │
│ API Key: app-rYP6LNM4iPmNjIHns... │
│ Workflow ID: 待确认 │
└───────┬──────────────────────────┘
│ 4. Dify从数据库获取课程信息
┌──────────────────────────┐
│ MySQL: courses │
│ - 课程名称 │
│ - 课程描述 │
│ - 关联的知识点 │
│ - 学习资料 │
└───────┬─────────────────┘
│ 5. Dify AI分析生成场景
│ (大模型推理)
┌──────────────────────────┐
│ Dify SSE事件流 │
│ event: workflow_started │
│ event: text_chunk │
│ event: workflow_finished │
└───────┬─────────────────┘
│ 6. 后端消费SSE聚合结果
┌──────────────────────────┐
│ 解析场景JSON数据 │
│ { │
│ name: "场景名称", │
│ description: "...", │
│ background: "...", │
│ ai_role: "...", │
│ objectives: [...], │
│ keywords: [...] │
│ } │
└───────┬─────────────────┘
│ 7. 返回场景数据给前端
┌──────────────────────────┐
│ 前端接收场景数据 │
└───────┬─────────────────┘
│ 8. 跳转到对话页面
┌──────────────────────────┐
│ /trainee/ai-practice │
│ query: │
│ - mode=course │
│ - courseId=5 │
│ - sceneData=JSON.stringify(scene)│
└──────────────────────────┘
```
### 3.2 课程模式对话流程
课程模式的对话流程与直接模式基本相同,区别在于:
1. 场景数据来自URL参数Dify生成而非数据库查询
2. 首次消息会带上课程上下文信息
```
┌──────┐
│ 学员 │
└───┬──┘
│ 1. 从URL解析场景数据
┌──────────────────────────┐
│ const sceneData = │
│ JSON.parse( │
│ route.query.sceneData│
│ ) │
└───────┬─────────────────┘
│ 2. 显示Dify生成的场景
┌──────────────────────┐
│ 场景背景和目标展示 │
│ (来自Dify) │
└───────┬──────────────┘
│ 3-9. 对话流程
│ (同直接模式)
┌──────────────────────┐
│ SSE流式对话交互 │
└──────────────────────┘
```
## 四、场景管理流程Manager
### 4.1 创建场景流程
```
┌──────┐
│管理员 │
└───┬──┘
│ 1. 访问场景管理页面
┌────────────────────────────────┐
│ 场景管理页面 │
│ /manager/practice-scene-management│
└───────┬────────────────────────┘
│ 2. 点击"新增场景"
┌──────────────────────┐
│ 打开创建场景表单 │
│ - 场景名称 │
│ - 场景描述 │
│ - 类型、难度 │
│ - 场景背景 │
│ - AI角色描述 │
│ - 练习目标(数组) │
│ - 关键词(数组) │
└───────┬──────────────┘
│ 3. 填写并提交表单
┌──────────────────────────────┐
│ POST /api/v1/manager/practice-scenes│
│ 请求体CreateSceneRequest │
└───────┬──────────────────────┘
│ 4. 后端验证数据
┌──────────────────────┐
│ 字段验证: │
│ - 必填字段检查 │
│ - 长度限制 │
│ - 类型枚举验证 │
│ - 名称唯一性 │
└───────┬──────────────┘
│ 5. 写入数据库
┌──────────────────────┐
│ INSERT INTO │
│ practice_scenes │
│ VALUES (...) │
└───────┬──────────────┘
│ 6. 返回创建的场景
┌──────────────────────┐
│ 前端刷新列表 │
│ 提示创建成功 │
└──────────────────────┘
```
### 4.2 场景列表查询流程
```
┌──────┐
│管理员 │
└───┬──┘
│ 1. 访问场景管理页面
┌────────────────────────────────┐
│ GET /api/v1/manager/practice-scenes│
│ 参数: │
│ - page, size (分页) │
│ - type, difficulty (筛选) │
│ - search (关键词) │
└───────┬────────────────────────┘
│ 2. 构建查询条件
┌──────────────────────────┐
│ SELECT * FROM │
│ practice_scenes │
│ WHERE is_deleted = false │
│ AND type = ? │
│ AND difficulty = ? │
│ AND name LIKE ? │
│ ORDER BY created_at DESC │
│ LIMIT ? OFFSET ? │
└───────┬─────────────────┘
│ 3. 返回分页数据
┌──────────────────────────┐
│ { │
│ items: [...], │
│ total: 15, │
│ page: 1, │
│ page_size: 20 │
│ } │
└───────┬─────────────────┘
│ 4. 前端渲染列表
┌──────────────────────────┐
│ 场景列表展示 │
│ - 筛选器 │
│ - 场景卡片 │
│ - 分页器 │
│ - 操作按钮 │
└──────────────────────────┘
```
## 五、SSE流式通信详细流程
### 5.1 SSE连接建立
```
┌────────┐ ┌────────┐ ┌──────────┐
│ 前端 │ │ 后端 │ │ Coze API │
└───┬────┘ └───┬────┘ └─────┬────┘
│ │ │
│ 1. fetch POST /practice/start │
├─────────────────────────>│ │
│ │ │
│ │ 2. 构建场景上下文 │
│ │ (background + ai_role) │
│ │ │
│ │ 3. coze.chat.stream() │
│ ├───────────────────────────>│
│ │ │
│ │ │ 4. 开始流式处理
│ │<───────────────────────────┤
│ │ event: conversation.chat.created
│<─────────────────────────┤ │
│ 5. 设置conversation_id │ │
│ │ │
│ │<───────────────────────────┤
│<─────────────────────────┤ event: message.delta │
│ 6. 追加内容: "您" │ data: {"content":"您"} │
│ │ │
│ │<───────────────────────────┤
│<─────────────────────────┤ event: message.delta │
│ 7. 追加内容: "好" │ data: {"content":"好"} │
│ │ │
│ │<───────────────────────────┤
│<─────────────────────────┤ event: message.delta │
│ 8. 追加内容: "" │ data: {"content":""} │
│ │ │
│ ... (持续接收增量) ... │
│ │ │
│ │<───────────────────────────┤
│<─────────────────────────┤ event: message.completed │
│ 9. 消息完成标记 │ │
│ │ │
│ │<───────────────────────────┤
│<─────────────────────────┤ event: conversation.completed│
│ 10. 对话完成 │ data: {"token_count":156} │
│ │ │
│<─────────────────────────┤ │
│ event: done │ │
│ 11. 关闭SSE连接 │ │
│ │ │
```
### 5.2 SSE错误处理流程
```
┌────────┐ ┌────────┐ ┌──────────┐
│ 前端 │ │ 后端 │ │ Coze API │
└───┬────┘ └───┬────┘ └─────┬────┘
│ │ │
│ 1. SSE连接正常 │ │
│ │ │
│ │ │ 2. 网络错误
│ │<───────────────────────────┤
│ │ Exception: Network Error │
│ │ │
│ │ 3. 捕获异常 │
│<─────────────────────────┤ │
│ event: error │ │
│ data: {"error":"..."} │ │
│ │ │
│ 4. 前端显示错误提示 │ │
│ ElMessage.error(...) │ │
│ │ │
│ 5. 关闭SSE连接 │ │
│ │ │
```
## 六、数据流转汇总
### 6.1 数据流向图
```
┌─────────────────────────────────────────────────────────────────┐
│ 数据流向总览 │
└─────────────────────────────────────────────────────────────────┘
用户输入
前端Vue页面
├─ 陪练中心: 从MySQL读取场景 ────> practice_scenes表
│ │
│ ▼
└─ 课程中心: Dify提取场景 ────> courses表 + knowledge_points表
Dify工作流分析
生成场景JSON
场景数据
前端对话页面
│ 用户消息 + 场景上下文
后端FastAPI
│ 构建完整Prompt
Coze Chat API
│ AI推理
SSE流式响应
├─ message.delta (增量)
├─ message.completed (完成)
└─ conversation.completed (结束)
前端实时展示
└─ 对话历史由Coze管理不存MySQL
```
### 6.2 核心数据对象流转
```
1. 场景数据 (PracticeScene)
MySQL practice_scenes表
Python Model (SQLAlchemy)
Pydantic Schema
JSON Response
TypeScript Interface
Vue Reactive Data
2. 对话消息 (Message)
用户输入 (Frontend)
HTTP Request Body
Python Request Model
Coze Message Object
Coze API Request
AI Response (SSE Stream)
Frontend Message Array
3. Dify场景 (ExtractedScene)
Course Data (MySQL)
Dify Workflow Input
AI Analysis
Workflow Output (JSON)
Python Response Model
Frontend Scene Object
```
## 七、关键时序说明
### 7.1 用户完整陪练流程时序
```
时刻T0: 用户访问陪练中心
↓ (立即)
时刻T1: 加载场景列表 (MySQL查询 ~100ms)
↓ (用户浏览场景 ~30s)
时刻T2: 用户选择场景
↓ (立即)
时刻T3: 跳转到对话页面
↓ (立即)
时刻T4: 加载场景详情 (MySQL查询 ~100ms)
↓ (用户阅读背景 ~20s)
时刻T5: 用户输入第一条消息
↓ (立即)
时刻T6: 发送SSE请求到后端 (网络延迟 ~50ms)
时刻T7: 后端调用Coze API (网络延迟 ~100ms)
时刻T8: 开始接收AI响应流 (首字节延迟 ~500ms)
↓ (流式传输 ~2-5s)
时刻T9: AI回复完成
↓ (用户阅读 ~10s)
时刻T10: 用户继续输入下一条消息
↓ (重复T5-T10)
```
### 7.2 Dify场景提取时序
```
时刻T0: 用户点击课程"陪练"按钮
↓ (立即)
时刻T1: 前端显示"提取场景中..."
↓ (立即)
时刻T2: 发送HTTP请求到后端 (网络延迟 ~50ms)
时刻T3: 后端调用Dify工作流 (网络延迟 ~100ms)
时刻T4: Dify查询数据库 (MySQL ~200ms)
时刻T5: Dify AI分析推理 (大模型 ~10-30s)
时刻T6: 后端接收完整场景数据
↓ (立即)
时刻T7: 返回给前端 (网络延迟 ~50ms)
↓ (立即)
时刻T8: 跳转到对话页面
```
## 八、异常流程处理
### 8.1 网络异常流程
```
正常流程
发送SSE请求
├─ 成功 ──> 接收流式数据
└─ 失败 ──> catch Error
判断错误类型
├─ NetworkError ──> 提示"网络错误,请检查连接"
│ └─> 提供重试按钮
├─ TimeoutError ──> 提示"请求超时,请稍后重试"
│ └─> 自动重试(最多3次)
└─ ServerError ──> 提示"服务器错误,请联系管理员"
└─> 记录错误日志
```
### 8.2 Coze API异常流程
```
后端调用Coze
coze.chat.stream()
├─ 成功 ──> 正常SSE流
└─ 失败 ──> catch CozePyError
判断错误
├─ Token过期 ──> 刷新Token + 重试
├─ 余额不足 ──> 通知管理员 + 提示用户稍后重试
├─ Bot不存在 ──> 检查Bot配置 + 使用备用Bot
└─ API限流 ──> 等待重试 + 记录日志
```
## 九、性能优化要点
### 9.1 数据库查询优化
```
场景列表查询
├─ 使用索引: idx_type, idx_difficulty, idx_status
├─ 分页限制: LIMIT + OFFSET
├─ 只查必要字段: SELECT id, name, description, type...
└─ 缓存热门场景 (可选)
```
### 9.2 SSE连接优化
```
SSE流式传输
├─ 使用HTTP/2 (多路复用)
├─ 合理设置超时: 180秒
├─ 心跳保活: ping event
└─ 断线重连: 保存conversation_id
```
### 9.3 前端渲染优化
```
消息列表展示
├─ 虚拟滚动 (消息数 > 100)
├─ 防抖输入: debounce 300ms
├─ 消息缓存: localStorage
└─ 懒加载历史消息
```
---
**文档版本**v1.0
**最后更新**2025-10-13
**维护人**:考培练系统开发团队