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
**维护人**:考培练系统开发团队

View File

@@ -0,0 +1,256 @@
# Dify API Keys 配置管理经验
## 📅 更新时间
2025-10-16
## 🎯 经验背景
在全链路联调阶段,发现代码中存在多处硬编码的 Dify API Keys不利于维护和安全管理。本文档总结了统一配置管理的实施经验。
## ✅ 问题发现
### 硬编码问题
**发现位置**`app/api/v1/exam.py`
```python
# ❌ 硬编码的 API Key
headers = {
"Authorization": "Bearer app-tDlrmXyS9NtWCShsOx5FH49L",
"Content-Type": "application/json",
}
```
**问题影响**
1. 更换 API Key 需要搜索全部代码
2. 敏感信息暴露在代码中
3. 不同环境无法灵活配置
4. 版本控制中包含敏感信息
## 🔧 解决方案
### 1. 配置文件集中管理
**文件**`app/core/config.py`
```python
class Settings(BaseSettings):
# Dify API 基础配置
DIFY_API_BASE: Optional[str] = Field(default="http://dify.ireborn.com.cn/v1")
# 各工作流 API Keys
DIFY_API_KEY: Optional[str] = Field(default="app-LZhZcMO6CiriLMOLB2PwUGHx") # 上传知识库
DIFY_EXAM_GENERATOR_API_KEY: str = Field(default="app-tDlrmXyS9NtWCShsOx5FH49L") # 试题生成器
DIFY_ANSWER_JUDGE_API_KEY: str = Field(default="app-FvMdrvbRBz547DVZEorgO1WT") # 答案判断器
DIFY_PRACTICE_API_KEY: Optional[str] = Field(default="app-rYP6LNM4iPmNjIHns12zFeJp") # 陪练场景提取
DIFY_PRACTICE_ANALYSIS_API_KEY: str = Field(default="app-9MWaCEiRegpYGQLov4S9oQjh") # 陪练分析报告
DIFY_COURSE_CHAT_API_KEY: str = Field(default="app-lJzD6COkL8z7Eez8t6ZrYoJS") # 与课程对话
DIFY_YANJI_ANALYSIS_API_KEY: str = Field(default="app-g0I5UT8lBB0fvuxGDOqrG8Zj") # 智能工牌分析
```
### 2. 代码重构
**重构前**
```python
headers = {
"Authorization": "Bearer app-tDlrmXyS9NtWCShsOx5FH49L",
"Content-Type": "application/json",
}
```
**重构后**
```python
from app.core.config import settings
headers = {
"Authorization": f"Bearer {settings.DIFY_EXAM_GENERATOR_API_KEY}",
"Content-Type": "application/json",
}
```
### 3. 配置验证脚本
创建验证脚本 `verify_dify_config.py`
```python
from app.core.config import settings
def verify_dify_config():
"""验证所有 Dify 配置"""
configs = [
("API Base URL", "DIFY_API_BASE", settings.DIFY_API_BASE),
("上传知识库", "DIFY_API_KEY", settings.DIFY_API_KEY),
("试题生成器", "DIFY_EXAM_GENERATOR_API_KEY", settings.DIFY_EXAM_GENERATOR_API_KEY),
# ... 其他配置项
]
all_valid = True
for name, var_name, value in configs:
if not value:
print(f"{name}: 配置缺失")
all_valid = False
else:
print(f"{name}: 已配置")
return all_valid
```
## 📋 完整工作流清单
| 工作流名称 | 配置变量 | API Key |
|-----------|---------|---------|
| 上传知识库 | `DIFY_API_KEY` | `app-LZhZcMO6CiriLMOLB2PwUGHx` |
| 试题生成器 | `DIFY_EXAM_GENERATOR_API_KEY` | `app-tDlrmXyS9NtWCShsOx5FH49L` |
| 答案判断器 | `DIFY_ANSWER_JUDGE_API_KEY` | `app-FvMdrvbRBz547DVZEorgO1WT` |
| 陪练场景提取 | `DIFY_PRACTICE_API_KEY` | `app-rYP6LNM4iPmNjIHns12zFeJp` |
| 陪练分析报告 | `DIFY_PRACTICE_ANALYSIS_API_KEY` | `app-9MWaCEiRegpYGQLov4S9oQjh` |
| 与课程对话 | `DIFY_COURSE_CHAT_API_KEY` | `app-lJzD6COkL8z7Eez8t6ZrYoJS` |
| 智能工牌分析 | `DIFY_YANJI_ANALYSIS_API_KEY` | `app-g0I5UT8lBB0fvuxGDOqrG8Zj` |
## 💡 最佳实践
### 1. 命名规范
- 统一前缀:`DIFY_`
- 功能描述:`EXAM_GENERATOR``ANSWER_JUDGE`
- 后缀:`_API_KEY`
- 示例:`DIFY_EXAM_GENERATOR_API_KEY`
### 2. 类型注解
```python
# ✅ 推荐:使用明确的类型注解
DIFY_EXAM_GENERATOR_API_KEY: str = Field(default="app-xxx")
# ⚠️ 可选配置使用 Optional
DIFY_API_KEY: Optional[str] = Field(default="app-xxx")
```
### 3. 环境变量覆盖
```bash
# .env 文件
DIFY_EXAM_GENERATOR_API_KEY=app-new-key-for-dev
DIFY_ANSWER_JUDGE_API_KEY=app-new-key-for-dev
```
### 4. 安全管理
```gitignore
# .gitignore 必须包含
.env
.env.local
.env.*.local
```
## 🔍 排查经验
### 1. 查找硬编码 API Keys
```bash
# 搜索所有 app- 开头的字符串
grep -r "app-[A-Za-z0-9]" --include="*.py" app/
# 搜索 Bearer 后跟 app-
grep -r "Bearer app-" --include="*.py" app/
```
### 2. 验证配置生效
```bash
# 运行验证脚本
python verify_dify_config.py
# 预期输出
✅ 所有 Dify 配置验证通过!
```
### 3. 调试配置加载
```python
# 临时添加调试日志
import logging
logger = logging.getLogger(__name__)
logger.debug(f"DIFY_EXAM_GENERATOR_API_KEY: {settings.DIFY_EXAM_GENERATOR_API_KEY[:20]}...")
```
## ⚠️ 注意事项
### 1. 不要提交敏感信息
```bash
# 检查暂存区
git diff --cached | grep -i "app-"
# 如果误提交,使用 git filter-branch 清理历史
```
### 2. 环境隔离
- 开发环境:使用测试 API Keys
- 生产环境:使用正式 API Keys
- 通过环境变量区分
### 3. API Key 轮换
定期轮换 API Keys步骤
1. 在 Dify 平台生成新 Key
2. 更新配置文件
3. 重启服务验证
4. 废弃旧 Key
### 4. 权限最小化
- 每个工作流使用独立 API Key
- 便于权限管理和问题追踪
- 避免一个 Key 的泄露影响所有功能
## 📊 实施效果
### 改进前
- ❌ 3 处硬编码 API Keys
- ❌ 修改需要搜索全部代码
- ❌ 无法按环境区分配置
- ❌ 安全风险高
### 改进后
- ✅ 统一在配置文件管理
- ✅ 一处修改全局生效
- ✅ 支持环境变量覆盖
- ✅ 提供验证脚本
- ✅ 完善的文档说明
## 🔗 相关文档
- [Dify API Keys 配置说明](/root/aiedu/kaopeilian-backend/docs/dify_api_keys.md)
- [Dify 系统对接分析报告](/root/aiedu/Dify系统对接分析报告.md)
- [配置更新总结](/root/aiedu/DIFY_API_KEYS_UPDATE_SUMMARY.md)
## 📝 更新日志
| 日期 | 变更内容 | 影响范围 |
|------|---------|---------|
| 2025-10-16 | 添加试题生成器和答案判断器 API Keys | `app/api/v1/exam.py` |
| 2025-10-16 | 移除硬编码,统一使用配置变量 | `app/api/v1/exam.py` |
| 2025-10-16 | 创建配置验证脚本 | 新增 `verify_dify_config.py` |
| 2025-10-16 | 创建配置文档 | 新增 `docs/dify_api_keys.md` |
## 🎓 经验总结
1. **早期规划很重要**:在项目初期就应该统一配置管理规范
2. **代码审查必不可少**:定期检查是否有新的硬编码出现
3. **自动化验证**:使用脚本自动验证配置完整性
4. **文档同步更新**:配置变更时必须同步更新文档
5. **安全意识**:敏感信息绝不提交到版本控制
## 🚀 后续优化建议
1. **CI/CD 集成**
- 在部署流程中添加配置验证步骤
- 配置缺失时阻止部署
2. **密钥管理服务**
- 考虑使用 AWS Secrets Manager 或 HashiCorp Vault
- 实现动态密钥轮换
3. **监控告警**
- API Key 即将过期时发送告警
- API 调用失败时检查 Key 是否有效
4. **权限审计**
- 定期审计 API Key 使用情况
- 发现异常调用及时处理

View File

@@ -0,0 +1,623 @@
# 考培练系统与Dify平台对接深度分析报告
## 目录
1. [系统概述](#系统概述)
2. [Dify API接口分析](#dify-api接口分析)
3. [前端页面对接实现](#前端页面对接实现)
4. [业务流程分析](#业务流程分析)
5. [技术架构图](#技术架构图)
6. [配置参数详解](#配置参数详解)
7. [数据流向分析](#数据流向分析)
8. [错误处理机制](#错误处理机制)
9. [性能优化建议](#性能优化建议)
## 系统概述
本考培练系统是一个基于 **Python + Vue3 + MySQL + FastAPI** 架构的智能教育平台与两个主要的AI平台进行深度对接
- **Dify平台**:用于动态题目生成和知识提取
- **Coze平台**用于AI陪练和智能对话
### 核心功能模块
- 动态考试题目生成基于Dify工作流
- 知识点提取与分析基于Dify工作流
- AI智能陪练基于Coze智能体
- 三轮考试机制(错题重练)
## Dify API接口分析
### 1. 主要接口端点
系统中使用了 **1个核心Dify API端点**
```
POST https://aiedu.ireborn.com.cn/v1/workflows/run
```
### 2. 使用的工作流Token
系统中发现了 **2个不同的工作流Token**
#### 2.1 动态题目生成工作流
- **Token**: `app-tDlrmXyS9NtWCShsOx5FH49L`
- **用途**: 根据考试ID和错题信息生成动态题目
- **文件位置**: `ExamsSystem/frontend/src/views/system/exams/start_exams.vue`
#### 2.2 知识提取工作流
- **Token**: `app-LZhZcMO6CiriLMOLB2PwUGHx`
- **用途**: 从考试附件中提取知识点
- **文件位置**: `ExamsSystem/frontend/src/views/system/exams/index.vue`
### 3. API请求参数详解
#### 3.1 动态题目生成API参数
```javascript
const payload = {
inputs: {
examsId: examId, // 考试ID必需
error: errorNums // 错题编号(可选,用于第二轮、第三轮)
},
response_mode: "blocking", // 同步模式
user: "abc-123" // 用户标识
};
```
**参数说明:**
- `examsId`: 当前考试的唯一标识符,用于工作流识别要生成哪个考试的题目
- `error`: 错题编号字符串,格式为逗号分隔的知识点编号,用于生成针对性的错题练习
- `response_mode`: 固定为"blocking",表示同步等待工作流执行完成
- `user`: 用户标识,固定为"abc-123"
#### 3.2 知识提取API参数
```javascript
const payload = {
inputs: {
examsTitle: exams_title, // 考试标题
file: [file], // 文件信息数组
examsId: row.id // 考试ID
},
response_mode: "blocking",
user: "abc-123"
};
// 文件对象结构
const file = {
transfer_method: "remote_url",
url: fileUrl, // 完整的文件URL
type: "document" // 文件类型
};
```
**参数说明:**
- `examsTitle`: 考试名称,帮助工作流理解文档内容的上下文
- `file`: 文件信息数组支持PDF等文档格式的知识提取
- `transfer_method`: 固定为"remote_url"表示通过URL方式传递文件
- `url`: 文件的完整访问URL支持相对路径自动补全为绝对路径
- `type`: 固定为"document",表示文档类型
### 4. API响应数据结构
#### 4.1 成功响应结构
```javascript
{
data: {
status: "succeeded", // 执行状态
outputs: {
result: [...] // 工作流输出结果
}
}
}
```
#### 4.2 题目数据结构
动态题目生成的响应数据中,`result`字段包含题目数组:
```javascript
[
{
topic: {
title: "题目内容", // 题目文本
options: {
opt1: "选项A",
opt2: "选项B",
opt3: "选项C",
opt4: "选项D"
}
},
correct: "A", // 正确答案
analysis: "解析内容", // 题目解析
know_title: "知识点编号" // 知识点标识
}
]
```
## 前端页面对接实现
### 1. 核心页面文件
#### 1.1 考试开始页面 (`start_exams.vue`)
- **路径**: `ExamsSystem/frontend/src/views/system/exams/start_exams.vue`
- **功能**: 动态题目生成、三轮考试机制、错题统计
- **关键函数**: `callDifyWorkflow()`
#### 1.2 考试管理页面 (`index.vue`)
- **路径**: `ExamsSystem/frontend/src/views/system/exams/index.vue`
- **功能**: 知识提取、考试管理
- **关键函数**: `update_know()`
#### 1.3 AI陪练页面 (`training.vue`)
- **路径**: `ExamsSystem/frontend/src/views/system/exams/training.vue`
- **功能**: 嵌入Coze聊天界面
- **实现方式**: iframe嵌入
### 2. 前端调用实现
#### 2.1 动态题目生成调用
```javascript
async function callDifyWorkflow(error = '') {
loading.value = true;
const url = "https://aiedu.ireborn.com.cn/v1/workflows/run";
const token = 'app-tDlrmXyS9NtWCShsOx5FH49L';
const payload = {
inputs: { examsId: examId },
response_mode: "blocking",
user: "abc-123"
};
// 错题重练时添加错题参数
if (error) {
payload.inputs.error = error;
}
try {
const res = await fetch(url, {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.data.status != 'succeeded') throw new Error("请求失败");
questions.value = data.data.outputs.result;
loading.value = false;
return data;
} catch (err) {
console.error("Dify 工作流调用异常:", err);
loading.value = false;
return null;
}
}
```
#### 2.2 知识提取调用
```javascript
async function update_know(row) {
// 获取考试附件信息
const exams_title = row.title || '';
const fileList = Array.isArray(row.attachmentList) ? row.attachmentList : [];
if (!fileList.length) {
proxy.$modal.msgWarning("该考试没有附件,无法提取知识!");
return;
}
// 构建文件对象
const fileUrl = fileList[0].fileUrl || fileList[0].url || '';
const file = {
transfer_method: "remote_url",
url: fileUrl.startsWith('http') ? fileUrl : `https://aiedu.ireborn.com.cn${fileUrl}`,
type: "document"
};
const payload = {
inputs: {
examsTitle: exams_title,
file: [file],
examsId: row.id
},
response_mode: "blocking",
user: "abc-123"
};
const token = "app-LZhZcMO6CiriLMOLB2PwUGHx";
const url = "https://aiedu.ireborn.com.cn/v1/workflows/run";
proxy.$modal.loading("正在提取知识,请稍候...");
try {
const res = await fetch(url, {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
if (!res.ok) {
proxy.$modal.msgError("知识提取失败!");
return;
}
const data = await res.json();
proxy.$modal.msgSuccess("知识提取成功!");
console.log("Dify知识提取结果:", data);
} catch (err) {
proxy.$modal.closeLoading();
proxy.$modal.msgError("知识提取异常!");
console.error("Dify知识提取异常:", err);
}
}
```
### 3. 前端状态管理
#### 3.1 考试状态管理
```javascript
// 核心状态变量
const questions = ref([]); // 题目数组
const loading = ref(true); // 加载状态
const currentIndex = ref(0); // 当前题目索引
const selected = ref(''); // 选中答案
const answered = ref(false); // 是否已答题
const score = ref(0); // 当前分数
const wrongQuestions = ref([]); // 错题记录
const round = ref(1); // 当前轮次 (1,2,3)
// 三轮成绩记录
const firstRoundScore = ref(0);
const firstRoundTime = ref(null);
const secondRoundScore = ref(0);
const secondRoundTime = ref(null);
const thirdRoundScore = ref(0);
const thirdRoundTime = ref(null);
```
#### 3.2 轮次流转逻辑
```javascript
// 第二轮:基于第一轮错题
function restartWithWrongQuestions() {
const errorNums = wrongQuestions.value.map(item => item.title).join(',');
// 重置状态
currentIndex.value = 0;
score.value = 0;
wrongQuestions.value = [];
round.value = 2;
// 调用Dify生成针对性题目
callDifyWorkflow(errorNums);
}
// 第三轮:基于第二轮错题
function restartWithThirdQuestions() {
const errorNums = wrongQuestions.value.map(item => item.title).join(',');
// 重置状态
currentIndex.value = 0;
score.value = 0;
thirdWrongQuestions.value = [...wrongQuestions.value];
wrongQuestions.value = [];
round.value = 3;
// 调用Dify生成针对性题目
callDifyWorkflow(errorNums);
}
```
## 业务流程分析
### 1. 动态考试流程
```mermaid
graph TD
A[用户选择考试] --> B[获取考试ID]
B --> C[调用Dify工作流]
C --> D[生成第一轮题目]
D --> E[用户答题]
E --> F[记录错题]
F --> G{是否完成所有题目}
G -->|否| E
G -->|是| H[显示第一轮成绩]
H --> I{用户选择第二轮}
I -->|是| J[传递错题信息给Dify]
J --> K[生成第二轮针对性题目]
K --> L[用户答题]
L --> M[记录错题]
M --> N{是否完成所有题目}
N -->|否| L
N -->|是| O[显示第二轮成绩]
O --> P{用户选择第三轮}
P -->|是| Q[传递第二轮错题给Dify]
Q --> R[生成第三轮题目]
R --> S[完成三轮考试]
S --> T[保存最终成绩]
```
### 2. 知识提取流程
```mermaid
graph TD
A[管理员上传考试附件] --> B[点击知识提取按钮]
B --> C[获取附件URL]
C --> D[构建文件对象]
D --> E[调用Dify知识提取工作流]
E --> F[Dify处理PDF文档]
F --> G[提取知识点]
G --> H[返回提取结果]
H --> I[前端显示成功消息]
```
### 3. AI陪练流程
```mermaid
graph TD
A[用户进入陪练页面] --> B[iframe加载Coze聊天界面]
B --> C[用户发送语音/文本]
C --> D[Coze智能体处理]
D --> E[返回AI回复]
E --> F[支持语音合成]
F --> G[用户继续对话]
G --> C
```
## 技术架构图
### 1. 整体架构
```mermaid
graph TB
subgraph "前端层 (Vue3)"
A1[考试管理页面]
A2[动态考试页面]
A3[AI陪练页面]
end
subgraph "后端层 (FastAPI)"
B1[考试管理API]
B2[成绩管理API]
B3[文件管理API]
end
subgraph "AI平台层"
C1[Dify工作流]
C2[Coze智能体]
end
subgraph "数据层"
D1[MySQL数据库]
D2[文件存储]
end
A1 --> B1
A2 --> C1
A3 --> C2
A2 --> B2
B1 --> D1
B2 --> D1
B3 --> D2
C1 --> D2
```
### 2. Dify集成架构
```mermaid
graph LR
subgraph "前端"
A[Vue组件]
end
subgraph "Dify平台"
B[工作流引擎]
C[题目生成工作流]
D[知识提取工作流]
end
A -->|HTTP POST| B
B --> C
B --> D
C -->|题目数据| A
D -->|知识点数据| A
```
## 配置参数详解
### 1. 系统配置
#### 1.1 域名配置
- **主域名**: `https://aiedu.ireborn.com.cn`
- **API端点**: `/v1/workflows/run`
- **文件服务**: `/dev-api/profile/upload/`
#### 1.2 工作流配置
| 功能 | Token | Bot ID | 用途 |
|------|-------|--------|------|
| 题目生成 | app-tDlrmXyS9NtWCShsOx5FH49L | - | 根据考试ID和错题生成动态题目 |
| 知识提取 | app-LZhZcMO6CiriLMOLB2PwUGHx | - | 从PDF文档中提取知识点 |
| 高情商回复 | - | 7509380917472280617 | AI智能回复 |
| 咨询师陪练 | - | 7509379008556089379 | 语音陪练 |
| 动态考题 | - | 7509379046204162074 | 动态题目生成 |
### 2. 环境配置
#### 2.1 前端配置 (`vite.config.js`)
```javascript
server: {
host: '0.0.0.0',
port: 80,
proxy: {
'/dev-api': {
target: 'https://aiedu.ireborn.com.cn',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/dev-api/, '')
}
}
}
```
#### 2.2 后端配置 (`config/env.py`)
```python
app_host: str = 'https://aiedu.ireborn.com.cn/'
```
## 数据流向分析
### 1. 题目生成数据流
```
用户操作 → Vue组件状态 → Dify API调用 → 工作流处理 → 题目数据返回 → 前端渲染
```
**详细流程:**
1. 用户点击开始考试
2. 获取URL参数中的`examId`
3. 调用`callDifyWorkflow(examId)`
4. 发送POST请求到Dify工作流
5. 工作流根据`examsId`生成题目
6. 返回JSON格式的题目数组
7. 前端解析数据并渲染题目界面
### 2. 错题重练数据流
```
错题收集 → 错题编号拼接 → Dify API调用(带error参数) → 针对性题目生成 → 前端渲染
```
**详细流程:**
1. 第一轮答题过程中收集错题
2. 将错题的`know_title`字段拼接成字符串
3. 调用`callDifyWorkflow(errorNums)`
4. Dify工作流根据错题信息生成针对性题目
5. 返回专门针对薄弱知识点的题目
6. 前端进入第二轮/第三轮答题模式
### 3. 知识提取数据流
```
文件上传 → 附件URL获取 → Dify API调用 → PDF解析 → 知识点提取 → 结果返回
```
**详细流程:**
1. 管理员在考试管理页面上传PDF附件
2. 系统生成文件访问URL
3. 点击"知识提取"按钮触发`update_know()`
4. 构建包含文件URL的请求参数
5. 调用Dify知识提取工作流
6. 工作流下载并解析PDF文档
7. 提取关键知识点并返回结果
## 错误处理机制
### 1. API调用错误处理
#### 1.1 网络错误处理
```javascript
try {
const res = await fetch(url, options);
const data = await res.json();
// 处理成功响应
} catch (err) {
console.error("Dify 工作流调用异常:", err);
loading.value = false;
return null;
}
```
#### 1.2 业务错误处理
```javascript
if (data.data.status != 'succeeded') {
throw new Error("请求失败");
}
```
#### 1.3 用户提示机制
```javascript
// 成功提示
proxy.$modal.msgSuccess("知识提取成功!");
// 警告提示
proxy.$modal.msgWarning("该考试没有附件,无法提取知识!");
// 错误提示
proxy.$modal.msgError("知识提取失败!");
// 加载提示
proxy.$modal.loading("正在提取知识,请稍候...");
proxy.$modal.closeLoading();
```
### 2. 数据验证机制
#### 2.1 前端验证
- 检查考试ID是否存在
- 验证附件列表是否为空
- 确认URL格式正确性
#### 2.2 响应数据验证
- 检查`data.data.status`是否为"succeeded"
- 验证`data.data.outputs.result`是否存在
- 确保题目数据结构完整
## 性能优化建议
### 1. 前端优化
#### 1.1 请求优化
- **缓存机制**: 对相同考试ID的题目进行本地缓存
- **请求去重**: 防止用户快速点击导致的重复请求
- **超时处理**: 设置合理的请求超时时间
#### 1.2 用户体验优化
- **加载状态**: 显示详细的加载进度和状态
- **错误重试**: 提供手动重试机制
- **离线支持**: 缓存已生成的题目支持离线答题
### 2. 后端优化
#### 2.1 API性能
- **连接池**: 使用HTTP连接池减少连接开销
- **异步处理**: 对于知识提取等耗时操作使用异步处理
- **结果缓存**: 缓存Dify工作流的执行结果
#### 2.2 监控告警
- **API监控**: 监控Dify API的响应时间和成功率
- **错误日志**: 记录详细的错误日志便于问题排查
- **性能指标**: 统计题目生成时间和知识提取效率
### 3. Dify工作流优化
#### 3.1 工作流设计
- **参数验证**: 在工作流中添加输入参数验证
- **错误处理**: 完善工作流内部的错误处理逻辑
- **性能调优**: 优化工作流的执行效率
#### 3.2 资源管理
- **并发控制**: 控制同时执行的工作流数量
- **资源限制**: 设置合理的内存和CPU使用限制
- **成本优化**: 监控和优化AI模型的调用成本
## 总结
本考培练系统通过与Dify平台的深度对接实现了智能化的题目生成和知识提取功能。系统采用了成熟的技术架构具备良好的扩展性和可维护性。主要特点包括
1. **智能题目生成**: 基于考试内容和学员错题情况动态生成个性化题目
2. **三轮考试机制**: 通过多轮练习帮助学员巩固薄弱知识点
3. **知识自动提取**: 从PDF文档中自动提取关键知识点
4. **AI智能陪练**: 结合Coze平台提供语音陪练功能
系统在实现上注重用户体验和错误处理,具备较强的实用性和稳定性。建议在后续开发中进一步优化性能和扩展功能模块。
---
**文档版本**: v1.0
**生成时间**: 2025年9月20日
**分析范围**: ExamsSystem、coze-chat-backend、coze-chat-frontend模块

View File

@@ -0,0 +1,69 @@
# Dify工作流集成文档
**版本:** v2.0
**状态:** ✅ 已完成并验证
**最后更新:** 2025-10-12
---
## 文件说明
### 核心文档
| 文件 | 说明 |
|------|------|
| `考试工作流-最终版.md` | 考试工作流完整实现(推荐阅读) |
| `知识拆解工作流.md` | 知识点分析工作流配置 |
| `试题生成器的核心提示词与输出示例.md` | Dify提示词和返回格式参考 |
| `考试工作流联调文档.md` | 详细联调文档(备查) |
| `考试工作流联调-原版.md` | 原始需求(禁止修改) |
### 数据库API服务
| 文件 | 说明 |
|------|------|
| `数据库api 服务/README.md` | 快速配置指南 |
| `数据库api 服务/openapi_sql_executor.json` | OpenAPI Schema文件 |
---
## 快速开始
### 1. 考试工作流
阅读:`考试工作流-最终版.md`
**关键信息:**
- 试题生成器Token`app-tDlrmXyS9NtWCShsOx5FH49L`
- 答案判断器Token`app-FvMdrvbRBz547DVZEorgO1WT`
- 后端接口:`/api/v1/exams/*`
- 测试页面:`http://localhost:3001/trainee/exam?courseId=1`
### 2. 知识拆解工作流
阅读:`知识拆解工作流.md`
**关键信息:**
- API Key`app-LZhZcMO6CiriLMOLB2PwUGHx`
- 响应模式streaming无需轮询
- 状态映射succeeded→分析完成failed→分析失败
### 3. 数据库API服务
阅读:`数据库api 服务/README.md`
**关键信息:**
- API Key`dify-2025-kaopeilian`
- 端点:`/sql/execute-simple`
- 服务器:`http://120.79.247.16:8000/api/v1`
---
## 验证状态
- ✅ 考试工作流:三轮考试流程完整可用
- ✅ 知识拆解工作流:单个/批量分析正常
- ✅ 数据库API服务SQL执行器正常工作
---
**文档维护:** 开发团队
**技术支持:** 参考 `考培练系统规划/全链路联调/联调经验汇总.md`

View File

@@ -0,0 +1,500 @@
# Dify 对话流 API 官方文档
> 文档来源https://dify.ireborn.com.cn/app/4bea851a-7f24-47bd-9d0b-1d74f69ba603/develop
> 导出时间2025-10-14
## 工作流编排对话型应用 API
对话应用支持会话持久化,可将之前的聊天记录作为上下文进行回答,可适用于聊天/客服 AI 等。
### 基础 URL
```
http://dify.ireborn.com.cn/v1
```
### 鉴权
Service API 使用 `API-Key` 进行鉴权。
**强烈建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。**
所有 API 请求都应在 `Authorization` HTTP Header 中包含您的 `API-Key`,如下所示:
```
Authorization: Bearer {API_KEY}
```
---
## POST /chat-messages - 发送对话消息
创建会话消息。
### Request Body
| Name | Type | Description |
|------|------|-------------|
| `query` | string | 用户输入/提问内容。 |
| `inputs` | object | 允许传入 App 定义的各变量值。inputs 参数包含了多组键值对Key/Value pairs每组的键对应一个特定变量每组的值则是该变量的具体值。如果变量是文件类型请指定一个包含以下 `files` 中所述键的对象。默认 `{}` |
| `response_mode` | string | `streaming` 流式模式(推荐)。基于 SSE[Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events))实现类似打字机输出方式的流式返回。<br>`blocking` 阻塞模式,等待执行完毕后返回结果。(请求若流程较长可能会被中断)。由于 Cloudflare 限制,请求会在 100 秒超时无返回后中断。 |
| `user` | string | 用户标识,用于定义终端用户的身份,方便检索、统计。由开发者定义规则,需保证用户标识在应用内唯一。服务 API 不会共享 WebApp 创建的对话。 |
| `conversation_id` | string | (选填)会话 ID需要基于之前的聊天记录继续对话必须传之前消息的 conversation_id。 |
| `files` | array[object] | 文件列表,适用于传入文件结合文本理解并回答问题,仅当模型支持 Vision/Video 能力时可用。<br><br>**文件类型:**<br>- `document`: TXT, MD, MARKDOWN, MDX, PDF, HTML, XLSX, XLS, VTT, PROPERTIES, DOC, DOCX, CSV, EML, MSG, PPTX, PPT, XML, EPUB<br>- `image`: JPG, JPEG, PNG, GIF, WEBP, SVG<br>- `audio`: MP3, M4A, WAV, WEBM, MPGA<br>- `video`: MP4, MOV, MPEG, WEBM<br>- `custom`: 其他文件类型<br><br>**transfer_method (string)** 传递方式:<br>- `remote_url`: 文件地址<br>- `local_file`: 上传文件<br><br>- `url`: 文件地址(仅当传递方式为 remote_url 时)<br>- `upload_file_id`: 上传文件 ID仅当传递方式为 local_file 时) |
| `auto_generate_name` | bool | (选填)自动生成标题,默认 true。若设置为 false则可通过调用会话重命名接口并设置 auto_generate 为 true 实现异步生成标题。 |
| `workflow_id` | string | 选填工作流ID用于指定特定版本如果不提供则使用默认的已发布版本。 |
| `trace_id` | string | 选填链路追踪ID。适用于与业务系统已有的trace组件打通实现端到端分布式追踪等场景。如果未指定系统会自动生成trace_id。支持以下三种方式传递具体优先级依次为<br>1. Header通过 HTTP Header X-Trace-Id 传递,优先级最高。<br>2. Query 参数:通过 URL 查询参数 trace_id 传递。<br>3. Request Body通过请求体字段 trace_id 传递(即本字段)。 |
### Response
`response_mode``blocking` 时,返回 **ChatCompletionResponse** object。
`response_mode``streaming` 时,返回 **ChunkChatCompletionResponse** object 流式序列。
#### ChatCompletionResponse阻塞模式
返回完整的 App 结果Content-Type 为 `application/json`
| 字段 | 类型 | 描述 |
|------|------|------|
| `event` | string | 事件类型,固定为 `message` |
| `task_id` | string | 任务 ID用于请求跟踪和下方的停止响应接口 |
| `id` | string | 唯一ID |
| `message_id` | string | 消息唯一 ID |
| `conversation_id` | string | 会话 ID |
| `mode` | string | App 模式,固定为 `chat` |
| `answer` | string | 完整回复内容 |
| `metadata` | object | 元数据 |
| `usage` | Usage | 模型用量信息 |
| `retriever_resources` | array[RetrieverResource] | 引用和归属分段列表 |
| `created_at` | int | 消息创建时间戳1705395332 |
#### ChunkChatCompletionResponse流式模式
返回 App 输出的流式块Content-Type 为 `text/event-stream`
每个流式块均为 `data:` 开头,块之间以 `\n\n` 即两个换行符分隔,如下所示:
```
data: {"event": "message", "task_id": "900bbd43-dc0b-4383-a372-aa6e6c414227", "id": "663c5084-a254-4040-8ad3-51f2a3c1a77c", "answer": "Hi", "created_at": 1705398420}\n\n
```
**流式块中根据 `event` 不同,结构也不同:**
##### event: workflow_started
workflow 开始执行
| 字段 | 类型 | 描述 |
|------|------|------|
| `task_id` | string | 任务 ID用于请求跟踪和下方的停止响应接口 |
| `workflow_run_id` | string | workflow 执行 ID |
| `event` | string | 固定为 `workflow_started` |
| `data` | object | 详细内容 |
| `data.id` | string | workflow 执行 ID |
| `data.workflow_id` | string | 关联 Workflow ID |
| `data.created_at` | timestamp | 开始时间 |
##### event: node_started
node 开始执行
| 字段 | 类型 | 描述 |
|------|------|------|
| `task_id` | string | 任务 ID用于请求跟踪和下方的停止响应接口 |
| `workflow_run_id` | string | workflow 执行 ID |
| `event` | string | 固定为 `node_started` |
| `data` | object | 详细内容 |
| `data.id` | string | workflow 执行 ID |
| `data.node_id` | string | 节点 ID |
| `data.node_type` | string | 节点类型 |
| `data.title` | string | 节点名称 |
| `data.index` | int | 执行序号,用于展示 Tracing Node 顺序 |
| `data.predecessor_node_id` | string | 前置节点 ID用于画布展示执行路径 |
| `data.inputs` | object | 节点中所有使用到的前置节点变量内容 |
| `data.created_at` | timestamp | 开始时间 |
##### event: node_finished
node 执行结束,成功失败同一事件中不同状态
| 字段 | 类型 | 描述 |
|------|------|------|
| `task_id` | string | 任务 ID用于请求跟踪和下方的停止响应接口 |
| `workflow_run_id` | string | workflow 执行 ID |
| `event` | string | 固定为 `node_finished` |
| `data` | object | 详细内容 |
| `data.id` | string | node 执行 ID |
| `data.node_id` | string | 节点 ID |
| `data.index` | int | 执行序号,用于展示 Tracing Node 顺序 |
| `data.predecessor_node_id` | string | optional 前置节点 ID用于画布展示执行路径 |
| `data.inputs` | object | 节点中所有使用到的前置节点变量内容 |
| `data.process_data` | json | Optional 节点过程数据 |
| `data.outputs` | json | Optional 输出内容 |
| `data.status` | string | 执行状态 running / succeeded / failed / stopped |
| `data.error` | string | Optional 错误原因 |
| `data.elapsed_time` | float | Optional 耗时(s) |
| `data.execution_metadata` | json | 元数据 |
| `data.execution_metadata.total_tokens` | int | optional 总使用 tokens |
| `data.execution_metadata.total_price` | decimal | optional 总费用 |
| `data.execution_metadata.currency` | string | optional 货币,如 USD / RMB |
| `data.created_at` | timestamp | 开始时间 |
##### event: workflow_finished
workflow 执行结束,成功失败同一事件中不同状态
| 字段 | 类型 | 描述 |
|------|------|------|
| `task_id` | string | 任务 ID用于请求跟踪和下方的停止响应接口 |
| `workflow_run_id` | string | workflow 执行 ID |
| `event` | string | 固定为 `workflow_finished` |
| `data` | object | 详细内容 |
| `data.id` | string | workflow 执行 ID |
| `data.workflow_id` | string | 关联 Workflow ID |
| `data.status` | string | 执行状态 running / succeeded / failed / stopped |
| `data.outputs` | json | Optional 输出内容 |
| `data.error` | string | Optional 错误原因 |
| `data.elapsed_time` | float | Optional 耗时(s) |
| `data.total_tokens` | int | Optional 总使用 tokens |
| `data.total_steps` | int | 总步数(冗余),默认 0 |
| `data.created_at` | timestamp | 开始时间 |
| `data.finished_at` | timestamp | 结束时间 |
##### event: message
LLM 返回文本块事件,即:完整的文本以分块的方式输出。
| 字段 | 类型 | 描述 |
|------|------|------|
| `task_id` | string | 任务 ID用于请求跟踪和下方的停止响应接口 |
| `message_id` | string | 消息唯一 ID |
| `conversation_id` | string | 会话 ID |
| `answer` | string | LLM 返回文本块内容 |
| `created_at` | int | 创建时间戳1705395332 |
##### event: message_file
文件事件,表示有新文件需要展示
| 字段 | 类型 | 描述 |
|------|------|------|
| `id` | string | 文件唯一ID |
| `type` | string | 文件类型目前仅为image |
| `belongs_to` | string | 文件归属user或assistant该接口返回仅为 assistant |
| `url` | string | 文件访问地址 |
| `conversation_id` | string | 会话ID |
##### event: message_end
消息结束事件,收到此事件则代表流式返回结束。
| 字段 | 类型 | 描述 |
|------|------|------|
| `task_id` | string | 任务 ID用于请求跟踪和下方的停止响应接口 |
| `message_id` | string | 消息唯一 ID |
| `conversation_id` | string | 会话 ID |
| `metadata` | object | 元数据 |
| `usage` | Usage | 模型用量信息 |
| `retriever_resources` | array[RetrieverResource] | 引用和归属分段列表 |
##### event: tts_message
TTS 音频流事件语音合成输出。内容是Mp3格式的音频块使用 base64 编码后的字符串,播放的时候直接解码即可。(开启自动播放才有此消息)
| 字段 | 类型 | 描述 |
|------|------|------|
| `task_id` | string | 任务 ID用于请求跟踪和下方的停止响应接口 |
| `message_id` | string | 消息唯一 ID |
| `audio` | string | 语音合成之后的音频块使用 Base64 编码之后的文本内容,播放的时候直接 base64 解码送入播放器即可 |
| `created_at` | int | 创建时间戳1705395332 |
##### event: tts_message_end
TTS 音频流结束事件,收到这个事件表示音频流返回结束。
| 字段 | 类型 | 描述 |
|------|------|------|
| `task_id` | string | 任务 ID用于请求跟踪和下方的停止响应接口 |
| `message_id` | string | 消息唯一 ID |
| `audio` | string | 结束事件是没有音频的,所以这里是空字符串 |
| `created_at` | int | 创建时间戳1705395332 |
##### event: message_replace
消息内容替换事件。开启内容审查和审查输出内容时,若命中了审查条件,则会通过此事件替换消息内容为预设回复。
| 字段 | 类型 | 描述 |
|------|------|------|
| `task_id` | string | 任务 ID用于请求跟踪和下方的停止响应接口 |
| `message_id` | string | 消息唯一 ID |
| `conversation_id` | string | 会话 ID |
| `answer` | string | 替换内容(直接替换 LLM 所有回复文本) |
| `created_at` | int | 创建时间戳1705395332 |
##### event: error
流式输出过程中出现的异常会以 stream event 形式输出,收到异常事件后即结束。
| 字段 | 类型 | 描述 |
|------|------|------|
| `task_id` | string | 任务 ID用于请求跟踪和下方的停止响应接口 |
| `message_id` | string | 消息唯一 ID |
| `status` | int | HTTP 状态码 |
| `code` | string | 错误码 |
| `message` | string | 错误消息 |
##### event: ping
每 10s 一次的 ping 事件,保持连接存活。
### Errors
- `404` - 对话不存在
- `400, invalid_param` - 传入参数异常
- `400, app_unavailable` - App 配置不可用
- `400, provider_not_initialize` - 无可用模型凭据配置
- `400, provider_quota_exceeded` - 模型调用额度不足
- `400, model_currently_not_support` - 当前模型不可用
- `400, workflow_not_found` - 指定的工作流版本未找到
- `400, draft_workflow_error` - 无法使用草稿工作流版本
- `400, workflow_id_format_error` - 工作流ID格式错误需要UUID格式
- `400, completion_request_error` - 文本生成失败
- `500` - 服务内部异常
### Request 示例
```bash
curl -X POST 'http://dify.ireborn.com.cn/v1/chat-messages' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{
"inputs": {},
"query": "What are the specs of the iPhone 13 Pro Max?",
"response_mode": "streaming",
"conversation_id": "",
"user": "abc-123",
"files": [
{
"type": "image",
"transfer_method": "remote_url",
"url": "https://cloud.dify.ai/logo/logo-site.png"
}
]
}'
```
### Response 示例
#### 阻塞模式
```json
{
"event": "message",
"task_id": "c3800678-a077-43df-a102-53f23ed20b88",
"id": "9da23599-e713-473b-982c-4328d4f5c78a",
"message_id": "9da23599-e713-473b-982c-4328d4f5c78a",
"conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2",
"mode": "chat",
"answer": "iPhone 13 Pro Max specs are listed here:...",
"metadata": {
"usage": {
"prompt_tokens": 1033,
"prompt_unit_price": "0.001",
"prompt_price_unit": "0.001",
"prompt_price": "0.0010330",
"completion_tokens": 128,
"completion_unit_price": "0.002",
"completion_price_unit": "0.001",
"completion_price": "0.0002560",
"total_tokens": 1161,
"total_price": "0.0012890",
"currency": "USD",
"latency": 0.7682376249867957
},
"retriever_resources": [
{
"position": 1,
"dataset_id": "101b4c97-fc2e-463c-90b1-5261a4cdcafb",
"dataset_name": "iPhone",
"document_id": "8dd1ad74-0b5f-4175-b735-7d98bbbb4e00",
"document_name": "iPhone List",
"segment_id": "ed599c7f-2766-4294-9d1d-e5235a61270a",
"score": 0.98457545,
"content": "\"Model\",\"Release Date\",\"Display Size\",\"Resolution\",\"Processor\",\"RAM\",\"Storage\",\"Camera\",\"Battery\",\"Operating System\"\n\"iPhone 13 Pro Max\",\"September 24, 2021\",\"6.7 inch\",\"1284 x 2778\",\"Hexa-core (2x3.23 GHz Avalanche + 4x1.82 GHz Blizzard)\",\"6 GB\",\"128, 256, 512 GB, 1TB\",\"12 MP\",\"4352 mAh\",\"iOS 15\""
}
]
},
"created_at": 1705407629
}
```
#### 流式模式
```
data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "created_at": 1679586595}}
data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}}
data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}}
data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}}
data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": " I", "created_at": 1679586595}
data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": "'m", "created_at": 1679586595}
data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": " glad", "created_at": 1679586595}
data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": " to", "created_at": 1679586595}
data: {"event": "message", "message_id" : "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": " meet", "created_at": 1679586595}
data: {"event": "message", "message_id" : "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": " you", "created_at": 1679586595}
data: {"event": "message_end", "id": "5e52ce04-874b-4d27-9045-b3bc80def685", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "metadata": {"usage": {"prompt_tokens": 1033, "prompt_unit_price": "0.001", "prompt_price_unit": "0.001", "prompt_price": "0.0010330", "completion_tokens": 135, "completion_unit_price": "0.002", "completion_price_unit": "0.001", "completion_price": "0.0002700", "total_tokens": 1168, "total_price": "0.0013030", "currency": "USD", "latency": 1.381760165997548}, "retriever_resources": [{"position": 1, "dataset_id": "101b4c97-fc2e-463c-90b1-5261a4cdcafb", "dataset_name": "iPhone", "document_id": "8dd1ad74-0b5f-4175-b735-7d98bbbb4e00", "document_name": "iPhone List", "segment_id": "ed599c7f-2766-4294-9d1d-e5235a61270a", "score": 0.98457545, "content": "\"Model\",\"Release Date\",\"Display Size\",\"Resolution\",\"Processor\",\"RAM\",\"Storage\",\"Camera\",\"Battery\",\"Operating System\"\n\"iPhone 13 Pro Max\",\"September 24, 2021\",\"6.7 inch\",\"1284 x 2778\",\"Hexa-core (2x3.23 GHz Avalanche + 4x1.82 GHz Blizzard)\",\"6 GB\",\"128, 256, 512 GB, 1TB\",\"12 MP\",\"4352 mAh\",\"iOS 15\""}]}}
data: {"event": "tts_message", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"}
data: {"event": "tts_message_end", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": ""}
```
---
## POST /files/upload - 上传文件
上传文件并在发送消息时使用,可实现图文多模态理解。支持您的应用程序所支持的所有格式。上传的文件仅供当前终端用户使用。
该接口需使用 `multipart/form-data` 进行请求。
### Request Body
| Name | Type | Description |
|------|------|-------------|
| `file` | file | 要上传的文件。 |
| `user` | string | 用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。 |
### Response
成功上传后,服务器会返回文件的 ID 和相关信息。
| 字段 | 类型 | 描述 |
|------|------|------|
| `id` | uuid | ID |
| `name` | string | 文件名 |
| `size` | int | 文件大小byte |
| `extension` | string | 文件后缀 |
| `mime_type` | string | 文件 mime-type |
| `created_by` | uuid | 上传人 ID |
| `created_at` | timestamp | 上传时间 |
### Errors
- `400, no_file_uploaded` - 必须提供文件
- `400, too_many_files` - 目前只接受一个文件
- `400, unsupported_preview` - 该文件不支持预览
- `400, unsupported_estimate` - 该文件不支持估算
- `413, file_too_large` - 文件太大
- `415, unsupported_file_type` - 不支持的扩展名,当前只接受文档类文件
- `503, s3_connection_failed` - 无法连接到 S3 服务
- `503, s3_permission_denied` - 无权限上传文件到 S3
- `503, s3_file_too_large` - 文件超出 S3 大小限制
### Request 示例
```bash
curl -X POST 'http://dify.ireborn.com.cn/v1/files/upload' \
--header 'Authorization: Bearer {api_key}' \
--form 'file=@localfile;type=image/[png|jpeg|jpg|webp|gif]' \
--form 'user=abc-123'
```
### Response 示例
```json
{
"id": "72fa9618-8f89-4a37-9b33-7e1178a24a67",
"name": "example.png",
"size": 1024,
"extension": "png",
"mime_type": "image/png",
"created_by": 123,
"created_at": 1577836800
}
```
---
## POST /chat-messages/:task_id/stop - 停止响应
仅支持流式模式。
### Path
| 参数 | 类型 | 描述 |
|------|------|------|
| `task_id` | string | 任务 ID可在流式返回 Chunk 中获取 |
### Request Body
| Name | Type | Description |
|------|------|-------------|
| `user` | string | Required 用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。API 无法访问 WebApp 创建的会话。 |
### Response
| 字段 | 类型 | 描述 |
|------|------|------|
| `result` | string | 固定返回 success |
### Request 示例
```bash
curl -X POST 'http://dify.ireborn.com.cn/v1/chat-messages/:task_id/stop' \
-H 'Authorization: Bearer {api_key}' \
-H 'Content-Type: application/json' \
--data-raw '{
"user": "abc-123"
}'
```
### Response 示例
```json
{
"result": "success"
}
```
---
## 其他接口
文档还包含以下接口(此处仅列出标题,详细内容请查看原始文档):
- POST /messages/:message_id/feedbacks - 消息反馈(点赞)
- GET /app/feedbacks - 获取APP的消息点赞和反馈
- GET /messages/{message_id}/suggested - 获取下一轮建议问题列表
- GET /messages - 获取会话历史消息
- GET /conversations - 获取会话列表
- DELETE /conversations/:conversation_id - 删除会话
- POST /conversations/:conversation_id/name - 会话重命名
- GET /conversations/:conversation_id/variables - 获取对话变量
- PUT /conversations/:conversation_id/variables/:variable_id - 更新对话变量
- POST /audio-to-text - 语音转文字
- POST /text-to-audio - 文字转语音
- GET /info - 获取应用基本信息
- GET /parameters - 获取应用参数
- GET /meta - 获取应用Meta信息
- GET /site - 获取应用 WebApp 设置
- GET /apps/annotations - 获取标注列表
- POST /apps/annotations - 创建标注
- PUT /apps/annotations/{annotation_id} - 更新标注
- DELETE /apps/annotations/{annotation_id} - 删除标注
- POST /apps/annotation-reply/{action} - 标注回复初始设置
- GET /apps/annotation-reply/{action}/status/{job_id} - 查询标注回复初始设置任务状态
---
## 关键说明
### 流式模式事件流程
对于工作流编排的对话应用,典型的事件流程如下:
1. `workflow_started` - 工作流开始(包含 conversation_id
2. `node_started` - 节点开始执行
3. `node_finished` - 节点执行完成(可能包含输出数据)
4. `workflow_finished` - 工作流完成(包含最终输出)
5. `message` - LLM 文本块(逐字返回,可能有多个)
6. `message_end` - 消息结束
### conversation_id 管理
- **首次对话**:不传 `conversation_id`,系统会在 `workflow_started` 事件中返回新的 `conversation_id`
- **续接对话**:传入之前获取的 `conversation_id`,保持上下文连续性
### 重要注意事项
1. API Key 必须放在后端,不要暴露在客户端
2. 流式模式使用 SSEServer-Sent Events协议
3. 每个事件块以 `data:` 开头,块之间用 `\n\n` 分隔
4. Cloudflare 有 100 秒超时限制(阻塞模式)
5. 流式模式每 10 秒发送一次 ping 事件保持连接

View File

@@ -0,0 +1,283 @@
# 与课程对话功能实施总结
> 完成时间2025-10-14
> 功能状态:✅ 已完成实施
## 📋 功能概述
基于 Dify 对话流实现了与课程的智能对话功能,用户可以在课程中心点击"对话"按钮,与课程内容进行智能问答互动。
## 🎯 技术方案
### 架构设计
```
前端 chat-course.vue
↓ (SSE)
后端 /api/v1/course/chat
↓ (HTTP Stream)
Dify 对话流 API
```
### 核心特性
1. **流式响应**:使用 SSEServer-Sent Events实现实时对话
2. **会话管理**conversation_id 由前端管理,支持多轮对话
3. **无持久化**:对话历史由 Dify 托管,系统不存储
4. **纯文本**:当前版本仅支持文本对话
## 📁 已修改文件
### 后端
1. **配置文件**`kaopeilian-backend/app/core/config.py`
- 添加 `DIFY_COURSE_CHAT_API_KEY = "app-lJzD6COkL8z7Eez8t6ZrYoJS"`
2. **API 接口**`kaopeilian-backend/app/api/v1/course_chat.py`(新建)
- `POST /api/v1/course/chat` - 与课程对话接口
- 实现 SSE 流式代理
- 事件转换Dify → 前端友好格式
3. **路由注册**`kaopeilian-backend/app/api/v1/__init__.py`
- 注册 `course_chat_router``/course` 前缀
### 前端
1. **API 封装**`kaopeilian-frontend/src/api/courseChat.ts`(新建)
- `courseChatApi.sendMessage()` - 发送消息并返回 ReadableStream
- TypeScript 类型定义:`CourseChatEvent`
2. **对话页面**`kaopeilian-frontend/src/views/trainee/chat-course.vue`
- 删除 Coze 集成代码
- 改用 Dify 对话流
- 前端管理 `conversationId`
- SSE 事件处理conversation_started / message_content / message_end
### 测试
1. **测试脚本**`test_course_chat.py`(新建)
- 测试登录 → 首次对话 → 续接对话
- 验证 SSE 事件流
- 验证会话管理
## 🔄 SSE 事件流程
### Dify 原始事件
```
workflow_started → node_finished → workflow_finished → message_end
```
### 后端转换后的事件
```json
// 1. 会话开始(首次对话)
{"event": "conversation_started", "conversation_id": "xxx"}
// 2. 消息块(逐字返回,实现打字机效果)
{"event": "message_chunk", "chunk": "这"}
{"event": "message_chunk", "chunk": "门"}
{"event": "message_chunk", "chunk": "课"}
...
// 3. 消息结束
{"event": "message_end"}
// 4. 错误(如有)
{"event": "error", "message": "错误信息"}
```
## 📊 数据流
### 首次对话
```
用户输入问题
前端调用 courseChatApi.sendMessage({course_id, query})
后端转发到 Dify (无 conversation_id)
Dify 创建新会话
SSE: conversation_started → 前端保存 conversation_id
SSE: message_content → 前端显示答案
SSE: message_end → 对话完成
```
### 续接对话
```
用户输入问题
前端调用 courseChatApi.sendMessage({course_id, query, conversation_id})
后端转发到 Dify (带 conversation_id)
Dify 基于上下文回答
SSE: message_content → 前端显示答案
SSE: message_end → 对话完成
```
## 🧪 测试步骤
### 1. 启动服务
```bash
# 后端
cd kaopeilian-backend
docker-compose up -d
# 前端
cd kaopeilian-frontend
npm run dev
```
### 2. 运行测试脚本
```bash
python test_course_chat.py
```
### 3. 手动测试
1. 登录系统
2. 进入课程中心http://localhost:3001/trainee/course-center
3. 点击课程卡片的"对话"按钮
4. 输入问题并发送
5. 验证:
- AI 回复显示正常
- 可以进行多轮对话
- 点击"清空对话"后会话重置
## 🎨 UI 特性
- ✅ 欢迎界面(首次进入时显示)
- ✅ 快速提问(预设问题点击发送)
- ✅ 消息加载动画(三个点跳动)
- ✅ 消息复制功能
- ✅ 消息收藏功能
- ✅ 侧边栏(知识要点、对话历史)
- ✅ 响应式设计(移动端适配)
## ⚠️ 注意事项
### 1. conversation_id 管理
- 前端使用 `ref<string>` 保存
- 页面刷新后丢失(符合"每次进入对话页面都创建新会话"的需求)
- 点击"清空对话"时重置
### 2. 流式打字机效果
- ✅ 已实现Dify streaming 模式支持 `event: message` 逐字返回
- 前端通过 `message_chunk` 事件逐字追加文本
- 实现类似 ChatGPT 的实时打字效果
### 3. 超时设置
- 后端180 秒httpx.Timeout
- 前端:依赖浏览器默认(通常无限制)
### 4. 错误处理
- 网络错误:显示友好提示
- Dify API 错误:记录日志并返回错误事件
- 解析错误:跳过当前行,继续处理
## 📝 API 接口文档
### POST /api/v1/course/chat
**请求体:**
```json
{
"course_id": 1,
"query": "这门课程讲什么?",
"conversation_id": "可选,续接对话时传入"
}
```
**响应SSE**
```
data: {"event":"conversation_started","conversation_id":"xxx"}
data: {"event":"message_content","answer":"这门课程..."}
data: {"event":"message_end"}
```
## 🔍 关键代码片段
### 后端 SSE 生成
```python
async def generate_stream():
async with client.stream("POST", url, headers=headers, json=payload) as response:
async for line in response.aiter_lines():
if line.startswith("data: "):
event_data = json.loads(line[6:])
# 处理事件...
yield f"data: {json.dumps(frontend_event)}\n\n"
```
### 前端 SSE 消费
```typescript
const stream = await courseChatApi.sendMessage({...})
const reader = stream.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const text = decoder.decode(value)
// 解析 SSE 事件...
}
```
## ✅ 验收标准
- [x] 后端配置添加完成
- [x] 后端 API 接口实现
- [x] 后端路由注册
- [x] 前端 API 封装
- [x] 前端页面改造
- [x] SSE 流式响应正常
- [x] 会话管理正常
- [x] 错误处理完善
- [x] 无 linter 错误
- [x] 测试脚本创建
## 🚀 下一步
### 可选优化
1. **Markdown 渲染**:如果 Dify 返回 Markdown 格式,前端可添加 Markdown 渲染器
2. **会话持久化**:如需要持久化历史,可在后端存储 conversation_id 与用户/课程的映射
3. **实时打字效果**:如 Dify 支持逐字返回,可修改事件处理逻辑
4. **语音对话**:未来可集成语音输入/输出
## 📚 相关文档
- [Dify 对话流 API 文档](./Dify对话流API文档.md)
- [实施计划](../../../------dify---.plan.md)
- [规范与约定-团队基线](../../规范与约定-团队基线.md)
- [联调经验汇总](../../联调经验汇总.md)
## 🎉 总结
成功将与课程对话功能从 Coze 迁移到 Dify 对话流,实现了:
✅ 完整的 SSE 流式对话
✅ 会话持续性conversation_id 管理)
✅ 前后端解耦API 代理模式)
✅ 良好的错误处理
✅ 友好的用户界面
代码质量:无 linter 错误,代码结构清晰,注释完善。

View File

@@ -0,0 +1,76 @@
# 数据库API服务配置指南
**用途:** Dify工作流访问考培练系统数据库
---
## 快速配置
### 服务器信息
- **地址:** http://120.79.247.16:8000/api/v1
- **备用:** http://aiedu.ireborn.com.cn/api/v1
### Dify配置步骤
1. **导入OpenAPI Schema**
- 文件:`openapi_sql_executor.json`
- 位置:工具 → 导入OpenAPI
2. **配置认证**
- 鉴权类型:请求头
- 头部前缀Custom
- 键:`X-API-Key`
- 值:`dify-2025-kaopeilian`
3. **选择端点**
- `/sql/execute-simple`
---
## 常用SQL语句
### 查询知识点
```sql
SELECT kp.id, kp.name, kp.description, kp.topic_relation
FROM knowledge_points kp
INNER JOIN course_materials cm ON kp.material_id = cm.id
WHERE kp.course_id = ? AND kp.is_deleted = FALSE AND cm.is_deleted = FALSE
ORDER BY RAND() LIMIT 10
```
### 查询岗位信息
```sql
SELECT id, name, description, skills, level
FROM positions
WHERE id = ? AND is_deleted = FALSE
```
---
## 测试验证
```bash
curl -X POST http://120.79.247.16:8000/api/v1/sql/execute-simple \
-H "X-API-Key: dify-2025-kaopeilian" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT COUNT(*) as total FROM users"}'
```
**预期结果:**
```json
{
"code": 200,
"message": "SQL 执行成功",
"data": {
"type": "query",
"columns": ["total"],
"rows": [{"total": 8}],
"row_count": 1
}
}
```
---
**最后更新:** 2025-10-12

View File

@@ -0,0 +1,664 @@
{
"openapi": "3.1.0",
"info": {
"title": "KaoPeiLian SQL Executor API",
"description": "SQL 执行器 API专门为 Dify 平台集成设计,支持对考陪练系统数据库执行查询和写入操作。\n\n## 主要功能\n- 执行 SQL 查询和写入操作\n- 支持参数化查询防止 SQL 注入\n- 获取数据库表列表和表结构\n- SQL 语句验证\n\n## 安全说明\n所有接口都需要 JWT Bearer Token 认证。请先通过登录接口获取访问令牌。",
"version": "1.0.0",
"contact": {
"name": "KaoPeiLian Tech Support",
"email": "support@kaopeilian.com"
}
},
"servers": [
{
"url": "http://120.79.247.16:8000/api/v1",
"description": "考陪练系统服务器"
},
{
"url": "http://aiedu.ireborn.com.cn/api/v1",
"description": "域名访问"
}
],
"security": [
{
"bearerAuth": []
}
],
"paths": {
"/auth/login": {
"post": {
"tags": ["认证"],
"summary": "用户登录",
"description": "获取访问令牌,用于后续 API 调用",
"security": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LoginRequest"
},
"examples": {
"admin": {
"summary": "管理员登录",
"value": {
"username": "admin",
"password": "admin123"
}
}
}
}
}
},
"responses": {
"200": {
"description": "登录成功",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LoginResponse"
}
}
}
},
"401": {
"description": "用户名或密码错误",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
},
"/sql/execute": {
"post": {
"tags": ["SQL执行器"],
"summary": "执行 SQL 语句",
"description": "执行查询或写入 SQL 语句。\n\n**查询操作**: SELECT, SHOW, DESCRIBE\n**写入操作**: INSERT, UPDATE, DELETE, CREATE, ALTER, DROP\n\n支持参数化查询使用 `:param_name` 格式定义参数。",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SqlExecuteRequest"
},
"examples": {
"simpleQuery": {
"summary": "简单查询",
"value": {
"sql": "SELECT id, username, role FROM users LIMIT 5"
}
},
"parameterizedQuery": {
"summary": "参数化查询",
"value": {
"sql": "SELECT * FROM courses WHERE category = :category AND status = :status",
"params": {
"category": "护肤",
"status": "active"
}
}
},
"insertData": {
"summary": "插入数据",
"value": {
"sql": "INSERT INTO knowledge_points (title, content, course_id) VALUES (:title, :content, :course_id)",
"params": {
"title": "面部护理基础",
"content": "面部护理的基本步骤...",
"course_id": 1
}
}
}
}
}
}
},
"responses": {
"200": {
"description": "SQL 执行成功",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/QueryResponse"
},
{
"$ref": "#/components/schemas/ExecuteResponse"
}
]
},
"examples": {
"queryResult": {
"summary": "查询结果",
"value": {
"code": 200,
"message": "SQL 执行成功",
"data": {
"type": "query",
"columns": ["id", "username", "role"],
"rows": [
{
"id": 1,
"username": "admin",
"role": "admin"
},
{
"id": 2,
"username": "user1",
"role": "trainee"
}
],
"row_count": 2
}
}
},
"executeResult": {
"summary": "写入结果",
"value": {
"code": 200,
"message": "SQL 执行成功",
"data": {
"type": "execute",
"affected_rows": 1,
"success": true
}
}
}
}
}
}
},
"400": {
"description": "请求参数错误",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"401": {
"description": "未认证或认证失败",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"500": {
"description": "SQL 执行错误",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
},
"/sql/validate": {
"post": {
"tags": ["SQL执行器"],
"summary": "验证 SQL 语法",
"description": "验证 SQL 语句的语法正确性,不执行实际操作",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SqlValidateRequest"
}
}
}
},
"responses": {
"200": {
"description": "验证完成",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidateResponse"
}
}
}
}
}
}
},
"/sql/tables": {
"get": {
"tags": ["SQL执行器"],
"summary": "获取表列表",
"description": "获取数据库中所有表的列表",
"responses": {
"200": {
"description": "成功获取表列表",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TablesResponse"
}
}
}
},
"401": {
"description": "未认证",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
},
"/sql/table/{table_name}/schema": {
"get": {
"tags": ["SQL执行器"],
"summary": "获取表结构",
"description": "获取指定表的结构信息,包括字段名、类型、约束等",
"parameters": [
{
"name": "table_name",
"in": "path",
"required": true,
"description": "表名",
"schema": {
"type": "string",
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
},
"example": "users"
}
],
"responses": {
"200": {
"description": "成功获取表结构",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TableSchemaResponse"
}
}
}
},
"400": {
"description": "无效的表名",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"401": {
"description": "未认证",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
}
},
"components": {
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
"description": "使用登录接口返回的 access_token。\n格式: Bearer {access_token}"
}
},
"schemas": {
"LoginRequest": {
"type": "object",
"required": ["username", "password"],
"properties": {
"username": {
"type": "string",
"description": "用户名",
"example": "admin"
},
"password": {
"type": "string",
"format": "password",
"description": "密码",
"example": "admin123"
}
}
},
"LoginResponse": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"example": 200
},
"message": {
"type": "string",
"example": "登录成功"
},
"data": {
"type": "object",
"properties": {
"user": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"example": 1
},
"username": {
"type": "string",
"example": "admin"
},
"role": {
"type": "string",
"example": "admin"
}
}
},
"token": {
"type": "object",
"properties": {
"access_token": {
"type": "string",
"description": "JWT 访问令牌",
"example": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
},
"token_type": {
"type": "string",
"example": "bearer"
},
"expires_in": {
"type": "integer",
"description": "过期时间(秒)",
"example": 1800
}
}
}
}
}
}
},
"SqlExecuteRequest": {
"type": "object",
"required": ["sql"],
"properties": {
"sql": {
"type": "string",
"description": "要执行的 SQL 语句",
"example": "SELECT * FROM users WHERE role = :role"
},
"params": {
"type": "object",
"description": "SQL 参数字典,键为参数名,值为参数值",
"additionalProperties": true,
"example": {
"role": "admin"
}
}
}
},
"SqlValidateRequest": {
"type": "object",
"required": ["sql"],
"properties": {
"sql": {
"type": "string",
"description": "要验证的 SQL 语句",
"example": "SELECT * FROM users"
}
}
},
"QueryResponse": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"example": 200
},
"message": {
"type": "string",
"example": "SQL 执行成功"
},
"data": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["query"],
"example": "query"
},
"columns": {
"type": "array",
"items": {
"type": "string"
},
"description": "列名数组",
"example": ["id", "username", "role"]
},
"rows": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
},
"description": "查询结果行"
},
"row_count": {
"type": "integer",
"description": "返回的行数",
"example": 5
}
}
}
}
},
"ExecuteResponse": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"example": 200
},
"message": {
"type": "string",
"example": "SQL 执行成功"
},
"data": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["execute"],
"example": "execute"
},
"affected_rows": {
"type": "integer",
"description": "影响的行数",
"example": 1
},
"success": {
"type": "boolean",
"example": true
}
}
}
}
},
"ValidateResponse": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"example": 200
},
"message": {
"type": "string",
"example": "SQL 验证完成"
},
"data": {
"type": "object",
"properties": {
"valid": {
"type": "boolean",
"description": "SQL 是否有效",
"example": true
},
"warnings": {
"type": "array",
"items": {
"type": "string"
},
"description": "警告信息列表",
"example": ["包含危险操作: DROP"]
},
"sql_type": {
"type": "string",
"description": "SQL 类型",
"example": "SELECT"
}
}
}
}
},
"TablesResponse": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"example": 200
},
"message": {
"type": "string",
"example": "获取表列表成功"
},
"data": {
"type": "object",
"properties": {
"tables": {
"type": "array",
"items": {
"type": "string"
},
"description": "表名列表",
"example": ["users", "courses", "exams"]
},
"count": {
"type": "integer",
"description": "表的数量",
"example": 20
}
}
}
}
},
"TableSchemaResponse": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"example": 200
},
"message": {
"type": "string",
"example": "获取表结构成功"
},
"data": {
"type": "object",
"properties": {
"table_name": {
"type": "string",
"example": "users"
},
"columns": {
"type": "array",
"items": {
"type": "object",
"properties": {
"field": {
"type": "string",
"description": "字段名",
"example": "id"
},
"type": {
"type": "string",
"description": "字段类型",
"example": "int(11)"
},
"null": {
"type": "string",
"enum": ["YES", "NO"],
"description": "是否可为空",
"example": "NO"
},
"key": {
"type": "string",
"description": "键类型PRI, UNI, MUL",
"example": "PRI"
},
"default": {
"type": "string",
"nullable": true,
"description": "默认值",
"example": null
},
"extra": {
"type": "string",
"description": "额外信息",
"example": "auto_increment"
}
}
}
},
"column_count": {
"type": "integer",
"description": "列的数量",
"example": 10
}
}
}
}
},
"ErrorResponse": {
"type": "object",
"properties": {
"detail": {
"type": "string",
"description": "错误详情",
"example": "SQL 执行失败: You have an error in your SQL syntax"
}
}
}
}
},
"tags": [
{
"name": "认证",
"description": "用户认证相关接口"
},
{
"name": "SQL执行器",
"description": "SQL 执行和管理相关接口"
}
]
}

View File

@@ -0,0 +1,52 @@
# 知识拆解工作流
**工作流名称:** upload_knowldge
**功能:** 上传资料并提炼知识点
---
## 配置信息
**API端点**
```
URL: http://dify.ireborn.com.cn/v1/workflows/run
API Key: app-LZhZcMO6CiriLMOLB2PwUGHx
```
**请求参数:**
```json
{
"inputs": {
"file": {
"type": "document",
"transfer_method": "local_file",
"upload_file_id": "dify_file_id"
},
"course_name": "课程名称",
"course_id": "1",
"material_id": "16"
},
"response_mode": "streaming",
"user": "kaopeilian"
}
```
---
## 实现方式
### Streaming模式直接完成
- 使用 `response_mode: "streaming"`
- 后端完整处理SSE流至 `workflow_finished`
- 前端180秒超时等待最终状态
- 无需轮询
### 状态映射
- `running` → 分析中
- `succeeded` → 分析完成(刷新知识点)
- `failed` → 分析失败
- `stopped` → 分析已停止
---
**最后更新:** 2025-10-12

View File

@@ -0,0 +1,224 @@
# 考试工作流-最终版
**版本:** v2.0
**状态:** ✅ 已完成并验证
**最后更新:** 2025-10-12
---
## 一、功能概述
### 考试页面
- **URL**`http://localhost:3001/trainee/exam?courseId=1`
- **流程**:三轮考试(正式考试 + 两次错题重考)
- **试题生成**动态调用Dify工作流预计1-3分钟
### 支持的题型
1. 单选题 - 点击立即判断
2. 多选题 - 点击提交判断
3. 判断题 - 点击立即判断
4. 填空题 - AI语义判断
5. 问答题 - AI语义判断
---
## 二、Dify工作流配置
### 工作流1试题生成器
**API配置**
```
URL: http://dify.ireborn.com.cn/v1/workflows/run
Token: app-tDlrmXyS9NtWCShsOx5FH49L
User: kaopeilian
Mode: streaming
```
**输入参数:**
```json
{
"course_id": 1,
"position_id": 3,
"single_choice_count": 4,
"multiple_choice_count": 2,
"true_false_count": 1,
"fill_blank_count": 2,
"essay_count": 1,
"difficulty_level": 3
}
```
**第二、三轮增加:**
```json
{
"mistake_records": "[{\"question_id\":null,\"knowledge_point_id\":456,...}]"
}
```
⚠️ **重要**:第一轮不传`mistake_records`参数第二三轮传入JSON字符串格式
### 工作流2答案判断器
**API配置**
```
URL: http://dify.ireborn.com.cn/v1/workflows/run
Token: app-FvMdrvbRBz547DVZEorgO1WT
User: kaopeilian
Mode: streaming
```
**输入参数:**
```json
{
"question": "题目内容",
"correct_answer": "正确答案",
"user_answer": "用户答案",
"analysis": "答案解析"
}
```
**返回字段:** `result``is_correct`(值为"正确"/"错误"或true/false
---
## 三、后端API接口
### 接口列表
| 接口 | 方法 | 功能 |
|------|------|------|
| `/api/v1/exams/generate` | POST | 生成试题 |
| `/api/v1/exams/judge-answer` | POST | 判断主观题答案 |
| `/api/v1/exams/record-mistake` | POST | 记录错题 |
| `/api/v1/exams/mistakes` | GET | 获取错题记录 |
**文件位置:** `kaopeilian-backend/app/api/v1/exam.py`
### 关键实现
**获取岗位ID**
```python
# 从用户岗位分配信息中自动获取
position_member = await db.execute(
select(PositionMember).where(
PositionMember.user_id == current_user.id,
PositionMember.is_deleted == False
)
)
position_id = position_member.position_id if position_member else 1
```
**创建Exam记录**
```python
exam = Exam(
user_id=current_user.id,
course_id=request.course_id,
exam_name=f"课程{request.course_id}考试",
status="started"
)
await db.commit()
return exam.id # 使用真实自增ID
```
---
## 四、前端实现要点
### 数据格式转换
**Dify格式 → 前端格式:**
```typescript
{
type: "single_choice" "single",
topic.title title,
topic.options options[],
correct correctAnswer,
analysis explanation,
knowledge_point_id parseInt()
}
```
### 三轮考试流程
```typescript
// 第一轮不传mistake_records
await generateExam({
course_id: 1,
// 不包含 mistake_records
})
// 第二、三轮:传入上一轮错题
const mistakes = await getMistakes(lastExamId)
await generateExam({
course_id: 1,
mistake_records: JSON.stringify(mistakes.data.data.mistakes)
})
```
---
## 五、关键配置
### HTTP超时设置
```typescript
// 前端
generateExam: timeout 300000 // 5分钟
judgeAnswer: timeout 60000 // 1分钟
// 后端
httpx.AsyncClient(timeout=300.0) // 试题生成
httpx.AsyncClient(timeout=60.0) // 答案判断
```
### 数据库表
**exam_mistakes表**
- user_id, exam_id必填
- question_id, knowledge_point_id可空
- question_content, correct_answer, user_answer
---
## 六、重要经验
### FastAPI路由顺序
```python
# ✅ 正确顺序
@router.get("/mistakes") # 具体路由在前
@router.get("/{exam_id}") # 动态路由在后
```
### ResponseModel使用
```python
# ✅ 统一使用ResponseModel
@router.get("/mistakes", response_model=ResponseModel[GetMistakesResponse])
async def get_mistakes(...):
return ResponseModel(code=200, data=GetMistakesResponse(...))
```
### 数据库ID生成
- ❌ 不使用时间戳13位数字超出INT范围
- ✅ 使用数据库自增ID
### Axios响应访问
```typescript
// ✅ 正确路径
response.data.code
response.data.data.result
response.data.data.exam_id
```
---
## 七、测试账号
- **用户名:** admin
- **密码:** admin123
- **测试课程:** courseId=1
---
**文档维护:** AI助手
**最后验证:** 2025-10-12三轮考试流程验证成功

View File

@@ -0,0 +1,164 @@
# 考试工作流联调文档
**版本:** v2.0
**状态:** ✅ 已完成
**最后更新:** 2025-10-12
---
## 一、基本信息
### 考试页面
- **URL** `http://localhost:3001/trainee/exam?courseId=1`
- **流程:** 三轮考试(正式考试 + 两次错题重考)
### 两个Dify工作流
#### 工作流1试题生成器
- **API** `http://dify.ireborn.com.cn/v1/workflows/run`
- **Token** `app-tDlrmXyS9NtWCShsOx5FH49L`
- **功能:** 根据课程知识点生成试题
#### 工作流2答案判断器
- **API** `http://dify.ireborn.com.cn/v1/workflows/run`
- **Token** `app-FvMdrvbRBz547DVZEorgO1WT`
- **功能:** 判断填空题和问答题答案
---
## 二、API接口
### 后端接口列表
| 接口 | 方法 | 功能 |
|------|------|------|
| `/api/v1/exams/generate` | POST | 生成试题 |
| `/api/v1/exams/judge-answer` | POST | 判断主观题答案 |
| `/api/v1/exams/record-mistake` | POST | 记录错题 |
| `/api/v1/exams/mistakes` | GET | 获取错题记录 |
**文件:** `kaopeilian-backend/app/api/v1/exam.py`
---
## 三、参数说明
### 生成试题参数
**第一轮不传mistake_records**
```json
{
"course_id": 1,
"single_choice_count": 4,
"multiple_choice_count": 2,
"true_false_count": 1,
"fill_blank_count": 2,
"essay_count": 1,
"difficulty_level": 3
}
```
**第二、三轮(传入错题记录):**
```json
{
"course_id": 1,
"mistake_records": "[{\"question_id\":null,\"knowledge_point_id\":456,...}]",
"single_choice_count": 2,
...
}
```
⚠️ **关键**
- 第一轮:完全不传`mistake_records`参数
- 第二三轮传入JSON字符串格式
### 判断答案参数
```json
{
"question": "题目内容",
"correct_answer": "正确答案",
"user_answer": "用户答案",
"analysis": "答案解析"
}
```
**返回:** `result: "正确"/"错误"``is_correct: true/false`
---
## 四、数据库表
### exam_mistakes错题记录表
**核心字段:**
- user_id, exam_id必填外键CASCADE
- question_id, knowledge_point_id可空SET NULL
- question_content, correct_answer, user_answer
**索引:** user_id, exam_id, knowledge_point_id
---
## 五、三轮考试流程
```
第一轮
↓ 答错N题
├─ 记录错题到数据库
└─ 获取错题记录 → 第二轮
第二轮(针对第一轮错题)
↓ 答错M题
├─ 记录错题到数据库
└─ 获取错题记录 → 第三轮
第三轮(针对第二轮错题)
↓ 完成
└─ 显示最终成绩
```
---
## 六、关键技术点
### 1. 路由顺序
```python
# ✅ 正确
@router.get("/mistakes") # 具体路由在前
@router.get("/{exam_id}") # 动态路由在后
```
### 2. 数据格式转换
**Dify → 前端:**
```
single_choice → single
multiple_choice → multiple
true_false → judge
fill_blank → blank
essay → essay
```
### 3. 超时配置
- 试题生成300秒5分钟
- 答案判断60秒1分钟
---
## 七、测试验证
**测试账号:** admin / admin123
**测试课程:** courseId=1
**验证要点:**
- ✅ 试题成功生成
- ✅ 所有题型正常答题
- ✅ 错题正确记录
- ✅ AI判断正常工作
- ✅ 三轮流程完整
---
**文档维护:** 开发团队
**参考:** `试题生成器的核心提示词与输出示例.md`

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,78 @@
**知识拆解 (Dify)**:管理员上传课程文件(如 PDF、WordDify 工作流会自动启动,对文档进行深度分析、拆解、提炼,形成结构化的知识点,写入数据库,为后续的动态考试和课程问答提供数据基础。当然也可手动在课程编辑页面的学习资料与知识点管理中,点击重新分析。
注意是每上传一个文件就启动一次
要启动的是一个 dify 工作流
api 服务器http://dify.ireborn.com.cn/v1
api 密钥app-LZhZcMO6CiriLMOLB2PwUGHx
workflow_id80cc2d27-f028-4bf1-9ac0-59742ae1cdab
api 文档地址(可用 mcp 工具查看https://dify.ireborn.com.cn/app/6713de7f-d98d-4f0a-8e1d-5ad8b4496211/develop
需要提交的必填参数(全部必填):
- file管理员上传的课程文件单个文件需上传
- course_name即课程主题
- course_id即课程 id
- material_id资料ID
该工作流会拆解知识点后直接写入数据库
## 实现经验2025-09-23
### 核心实现
- 后端:`app/services/ai/knowledge_analysis.py` - 知识点分析服务
- API`POST /api/v1/courses/{id}/reanalyze` - 重新分析接口
- 前端:课程编辑页面添加"重新分析"按钮,上传资料后自动触发
### 关键技术点
1. **文件处理**先上传文件到Dify获取file_id再调用工作流
2. **调用格式**
```python
# 1. 上传文件
POST /files/upload (multipart/form-data)
# 2. 调用工作流
POST /workflows/run (JSON格式使用upload_file_id)
```
3. **工作流参数修正file 为单对象)**
```json
{
"inputs": {
"file": {"type": "document", "transfer_method": "local_file", "upload_file_id": "file_id"},
"course_name": "课程标题",
"course_id": ID,
"material_id": ID
},
"response_mode": "blocking",
"user": "system_user_{course_id}"
}
```
4. **异步处理**使用BackgroundTasks避免阻塞用户操作
5. **日志规范**使用f-string格式避免关键字参数
6. **网络配置**将dify.ireborn.com.cn加入no proxy列表
### 验证完成
- ✅ 前端按钮正常工作API调用成功返回200
- ✅ 后台任务正常执行文件上传到Dify成功
- ✅ Dify工作流成功触发workflow_run_id已生成
- ❌ Dify工作流回调失败尝试调用 `https://aiedu.ireborn.com.cn/dev-api/system/knowledge`
### 关键发现2025-09-23
**问题**: Dify工作流成功启动但执行失败
- 工作流ID: `80cc2d27-f028-4bf1-9ac0-59742ae1cdab`
- 错误: `Reached maximum retries (0) for URL http://localhost:8000/api/v1/system/knowledge`(示例)
**原因**: Dify工作流配置的回调URL需与当前环境一致在本地联调时应使用本地地址
**解决方案**:
1. 修改Dify工作流配置将回调URL改为本地地址: `http://localhost:8000/api/v1/system/knowledge`
2. 或者在公网环境部署API端点供Dify回调
**已创建回调API**: `POST /api/v1/system/knowledge` - 接收Dify工作流的知识点数据
workflow_id: 80cc2d27-f028-4bf1-9ac0-59742ae1cdab