feat: 初始化考培练系统项目
- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
434
docs/规划/全链路联调/Ai工作流/coze/Coze-API文档.md
Normal file
434
docs/规划/全链路联调/Ai工作流/coze/Coze-API文档.md
Normal 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
|
||||
|
||||
132
docs/规划/全链路联调/Ai工作流/coze/Coze集成方案.md
Normal file
132
docs/规划/全链路联调/Ai工作流/coze/Coze集成方案.md
Normal 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. 长期实现完全整合
|
||||
|
||||
这样既能快速上线,又为未来的技术栈统一留出空间。
|
||||
222
docs/规划/全链路联调/Ai工作流/coze/README.md
Normal file
222
docs/规划/全链路联调/Ai工作流/coze/README.md
Normal 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. 对话历史保存
|
||||
|
||||
- 实时保存到MySQL(practice_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/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
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键要点
|
||||
|
||||
### 场景提示词(最重要)
|
||||
|
||||
考培练系统与参考代码的核心差异:
|
||||
- **首次消息必须包含完整场景设定**
|
||||
- 使用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文档.md(VAD配置)
|
||||
3. **API对接问题** → 陪练功能API接口规范.md
|
||||
4. **Dify格式问题** → 陪练分析报告-数据结构与Dify规范.md
|
||||
|
||||
---
|
||||
|
||||
**文档维护**:考培练系统开发团队
|
||||
**最后更新**:2025-10-13
|
||||
**版本**:v2.0(完整版)
|
||||
334
docs/规划/全链路联调/Ai工作流/coze/⚠️核心差异点速查.md
Normal file
334
docs/规划/全链路联调/Ai工作流/coze/⚠️核心差异点速查.md
Normal 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_id,AI会忘记场景
|
||||
)
|
||||
```
|
||||
|
||||
### ✅ 正确:保持对话连续性
|
||||
```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)
|
||||
|
||||
303
docs/规划/全链路联调/Ai工作流/coze/✅陪练功能完整开发报告.md
Normal file
303
docs/规划/全链路联调/Ai工作流/coze/✅陪练功能完整开发报告.md
Normal 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_history(JSON数组)
|
||||
- 输出: 分析结果(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.py(4个模型)
|
||||
2. app/schemas/practice.py(20个Schema)
|
||||
3. app/services/coze_service.py
|
||||
4. app/services/dify_practice_service.py
|
||||
5. app/api/v1/practice.py(14个接口)
|
||||
|
||||
### 配置文件
|
||||
|
||||
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(最终版)
|
||||
|
||||
**🎊 陪练功能完整开发圆满完成!**
|
||||
|
||||
296
docs/规划/全链路联调/Ai工作流/coze/基础信息.md
Normal file
296
docs/规划/全链路联调/Ai工作流/coze/基础信息.md
Normal 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中断控制
|
||||
|
||||
## 七、开发路线图
|
||||
|
||||
### 第一阶段:数据库与基础API(2天)
|
||||
- [ ] 创建practice_scenes表
|
||||
- [ ] 插入初始场景数据
|
||||
- [ ] 实现场景管理API(CRUD)
|
||||
|
||||
### 第二阶段:前端管理页面(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认证迁移完成
|
||||
183
docs/规划/全链路联调/Ai工作流/coze/播课/Coze工作流运行API文档.md
Normal file
183
docs/规划/全链路联调/Ai工作流/coze/播课/Coze工作流运行API文档.md
Normal 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 | 调试页面 URL(7天有效期) |
|
||||
| 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 SDK(0.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
|
||||
**维护人**: 考培练系统开发团队
|
||||
|
||||
69
docs/规划/全链路联调/Ai工作流/coze/播课/README.md
Normal file
69
docs/规划/全链路联调/Ai工作流/coze/播课/README.md
Normal 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
|
||||
137
docs/规划/全链路联调/Ai工作流/coze/播课/播课功能API接口规范.md
Normal file
137
docs/规划/全链路联调/Ai工作流/coze/播课/播课功能API接口规范.md
Normal 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 - 简化为触发模式
|
||||
296
docs/规划/全链路联调/Ai工作流/coze/播课/播课功能技术方案.md
Normal file
296
docs/规划/全链路联调/Ai工作流/coze/播课/播课功能技术方案.md
Normal 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
|
||||
**维护人**: 考培练系统开发团队
|
||||
|
||||
923
docs/规划/全链路联调/Ai工作流/coze/陪练功能API接口规范.md
Normal file
923
docs/规划/全链路联调/Ai工作流/coze/陪练功能API接口规范.md
Normal 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
|
||||
**维护人**:考培练系统开发团队
|
||||
|
||||
720
docs/规划/全链路联调/Ai工作流/coze/陪练功能技术方案.md
Normal file
720
docs/规划/全链路联调/Ai工作流/coze/陪练功能技术方案.md
Normal 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集成) |
|
||||
|
||||
## 七、开发排期建议
|
||||
|
||||
### 第一阶段:数据库与基础API(2天)
|
||||
- 创建practice_scenes表
|
||||
- 插入5-10条初始场景数据
|
||||
- 实现场景管理API(CRUD)
|
||||
- 实现学员查询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
|
||||
**维护人**:考培练系统开发团队
|
||||
|
||||
760
docs/规划/全链路联调/Ai工作流/coze/陪练功能数据流程图.md
Normal file
760
docs/规划/全链路联调/Ai工作流/coze/陪练功能数据流程图.md
Normal 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
|
||||
**维护人**:考培练系统开发团队
|
||||
|
||||
256
docs/规划/全链路联调/Ai工作流/dify/Dify_API_Keys_配置管理经验.md
Normal file
256
docs/规划/全链路联调/Ai工作流/dify/Dify_API_Keys_配置管理经验.md
Normal 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 使用情况
|
||||
- 发现异常调用及时处理
|
||||
|
||||
623
docs/规划/全链路联调/Ai工作流/dify/Dify系统对接分析报告.md
Normal file
623
docs/规划/全链路联调/Ai工作流/dify/Dify系统对接分析报告.md
Normal 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模块
|
||||
69
docs/规划/全链路联调/Ai工作流/dify/README.md
Normal file
69
docs/规划/全链路联调/Ai工作流/dify/README.md
Normal 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`
|
||||
|
||||
500
docs/规划/全链路联调/Ai工作流/dify/对话流/Dify对话流API文档.md
Normal file
500
docs/规划/全链路联调/Ai工作流/dify/对话流/Dify对话流API文档.md
Normal 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. 流式模式使用 SSE(Server-Sent Events)协议
|
||||
3. 每个事件块以 `data:` 开头,块之间用 `\n\n` 分隔
|
||||
4. Cloudflare 有 100 秒超时限制(阻塞模式)
|
||||
5. 流式模式每 10 秒发送一次 ping 事件保持连接
|
||||
|
||||
283
docs/规划/全链路联调/Ai工作流/dify/对话流/与课程对话功能实施总结.md
Normal file
283
docs/规划/全链路联调/Ai工作流/dify/对话流/与课程对话功能实施总结.md
Normal file
@@ -0,0 +1,283 @@
|
||||
# 与课程对话功能实施总结
|
||||
|
||||
> 完成时间:2025-10-14
|
||||
> 功能状态:✅ 已完成实施
|
||||
|
||||
## 📋 功能概述
|
||||
|
||||
基于 Dify 对话流实现了与课程的智能对话功能,用户可以在课程中心点击"对话"按钮,与课程内容进行智能问答互动。
|
||||
|
||||
## 🎯 技术方案
|
||||
|
||||
### 架构设计
|
||||
|
||||
```
|
||||
前端 chat-course.vue
|
||||
↓ (SSE)
|
||||
后端 /api/v1/course/chat
|
||||
↓ (HTTP Stream)
|
||||
Dify 对话流 API
|
||||
```
|
||||
|
||||
### 核心特性
|
||||
|
||||
1. **流式响应**:使用 SSE(Server-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 错误,代码结构清晰,注释完善。
|
||||
|
||||
76
docs/规划/全链路联调/Ai工作流/dify/数据库api 服务/README.md
Normal file
76
docs/规划/全链路联调/Ai工作流/dify/数据库api 服务/README.md
Normal 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
|
||||
|
||||
664
docs/规划/全链路联调/Ai工作流/dify/数据库api 服务/openapi_sql_executor.json
Normal file
664
docs/规划/全链路联调/Ai工作流/dify/数据库api 服务/openapi_sql_executor.json
Normal 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 执行和管理相关接口"
|
||||
}
|
||||
]
|
||||
}
|
||||
52
docs/规划/全链路联调/Ai工作流/dify/知识拆解工作流.md
Normal file
52
docs/规划/全链路联调/Ai工作流/dify/知识拆解工作流.md
Normal 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
|
||||
224
docs/规划/全链路联调/Ai工作流/dify/考试工作流-最终版.md
Normal file
224
docs/规划/全链路联调/Ai工作流/dify/考试工作流-最终版.md
Normal 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(三轮考试流程验证成功)
|
||||
|
||||
164
docs/规划/全链路联调/Ai工作流/dify/考试工作流联调文档.md
Normal file
164
docs/规划/全链路联调/Ai工作流/dify/考试工作流联调文档.md
Normal 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`
|
||||
124
docs/规划/全链路联调/Ai工作流/dify/试题生成器的核心提示词与输出示例.md
Normal file
124
docs/规划/全链路联调/Ai工作流/dify/试题生成器的核心提示词与输出示例.md
Normal file
File diff suppressed because one or more lines are too long
78
docs/规划/全链路联调/Ai工作流/知识点拆解工作流.md
Normal file
78
docs/规划/全链路联调/Ai工作流/知识点拆解工作流.md
Normal file
@@ -0,0 +1,78 @@
|
||||
**知识拆解 (Dify)**:管理员上传课程文件(如 PDF、Word)后,Dify 工作流会自动启动,对文档进行深度分析、拆解、提炼,形成结构化的知识点,写入数据库,为后续的动态考试和课程问答提供数据基础。当然也可手动在课程编辑页面的学习资料与知识点管理中,点击重新分析。
|
||||
注意是每上传一个文件就启动一次
|
||||
|
||||
要启动的是一个 dify 工作流
|
||||
api 服务器:http://dify.ireborn.com.cn/v1
|
||||
api 密钥:app-LZhZcMO6CiriLMOLB2PwUGHx
|
||||
workflow_id:80cc2d27-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
|
||||
Reference in New Issue
Block a user