重构Dify客户端:改为通过FastAPI代理并使用JWT认证

主要变更:
- 修改 dify-client.server.ts 使用 JWT 认证通过 FastAPI 后端代理访问 Dify API
- 所有 Dify API 路由(chat-messages, parameters, conversations, messages)添加 JWT 获取和传递逻辑
- API_URL 从直连 Dify 改为 FastAPI 后端的 /dify 路由
- 增强 JWT 认证失败的错误处理(返回401状态码)
- 添加详细的日志输出,便于调试

安全提升:
- DIFY_API_KEY 从前端移至后端,不再暴露在客户端代码
- 使用统一的 JWT 认证体系,提高系统安全性

文档:
- 新增 dify-proxy-backend-integration.md - 后端对接文档(包含完整 FastAPI 实现示例)
- 新增 dify-frontend-modification-summary.md - 前端修改总结
- 新增 CLAUDE.md - 项目架构说明文档

影响范围:
- app/services/dify-client.server.ts - 核心服务层
- app/routes/api.chat-messages.tsx - 聊天消息
- app/routes/api.parameters.tsx - 应用参数
- app/routes/api.conversations.tsx - 会话列表
- app/routes/api.messages.tsx - 消息历史
- app/routes/api.conversations.$id.tsx - 删除会话
- app/routes/api.conversations.$id.name.tsx - 重命名会话

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-30 09:47:48 +08:00
parent 064f05ffa5
commit c4c08cb59b
11 changed files with 2036 additions and 60 deletions
+821
View File
@@ -0,0 +1,821 @@
# Dify 代理服务后端对接文档
## 📋 文档说明
本文档描述了前端 Remix 应用如何通过 FastAPI 后端代理访问 Dify AI 服务,以及后端需要实现的接口规范。
**更新时间**: 2025-01-XX
**前端修改版本**: v2.0 - JWT 认证版本
---
## 🎯 架构变更
### **旧架构**
```
前端 Remix App → 直接调用 Dify API (使用 API KEY)
```
### **新架构**
```
前端 Remix App → FastAPI 后端 /dify 路由 (使用 JWT) → Dify API (使用 API KEY)
```
---
## 🔐 认证流程说明
### **前端侧**
1. 用户登录后获得 `frontendJWT` (前端JWT)
2. 所有 Dify 相关请求携带 `Authorization: Bearer {frontendJWT}`
3. JWT 由 OAuth2.0 + 自定义签名生成,包含用户信息
### **后端侧**
1. 接收前端请求,验证 `frontendJWT` 是否有效
2. JWT 验证通过后,使用配置的 `DIFY_API_KEY` 调用真正的 Dify API
3. 将 Dify API 的响应原样返回给前端
### **关键配置迁移**
前端原本使用的配置:
```bash
# .env (前端配置 - 已不再使用)
NEXT_PUBLIC_APP_ID=http://nas.7bm.co:12980/app/46539478-3281-4e98-a445-6da9dc078e95/configuration
NEXT_PUBLIC_APP_KEY=app-N3su9tKyMMnqxt2EMgOkVof7
```
**现在这两个配置应该移到 FastAPI 后端**
```python
# FastAPI 配置 (后端配置)
DIFY_API_URL = "http://nas.7bm.co:12980/v1" # Dify API 基础URL
DIFY_API_KEY = "app-N3su9tKyMMnqxt2EMgOkVof7" # Dify API Key
DIFY_APP_ID = "46539478-3281-4e98-a445-6da9dc078e95" # Dify App ID
```
---
## 🛣️ API 路由规范
### **基础路由前缀**
```
http://{FASTAPI_HOST}:{PORT}/dify
```
### **需要实现的路由列表**
| 路由 | 方法 | 说明 | 前端调用频率 |
|------|------|------|--------------|
| `/dify/parameters` | GET | 获取应用参数 | 初始化时 |
| `/dify/conversations` | GET | 获取会话列表 | 每次打开聊天 |
| `/dify/messages` | GET | 获取会话消息历史 | 切换会话时 |
| `/dify/chat-messages` | POST | 发送聊天消息 | 用户发送消息时 |
| `/dify/conversations/{id}/name` | POST | 重命名会话 | 用户重命名时 |
| `/dify/conversations/{id}` | DELETE | 删除会话 | 用户删除时 |
| `/dify/messages/{id}/feedbacks` | POST | 消息反馈(点赞/点踩) | 用户反馈时 |
---
## 📡 详细接口规范
### **1. 获取应用参数**
#### 前端请求
```http
GET http://{FASTAPI_HOST}/dify/parameters
Authorization: Bearer {frontendJWT}
```
#### 后端处理逻辑
```python
@app.get("/dify/parameters")
async def get_parameters(
authorization: str = Header(None)
):
# 1. 验证 JWT
jwt_token = authorization.replace("Bearer ", "")
user_info = verify_jwt(jwt_token) # 你们的JWT验证函数
if not user_info:
raise HTTPException(status_code=401, detail="JWT认证失败,请重新登录")
# 2. 调用 Dify API
headers = {
"Authorization": f"Bearer {DIFY_API_KEY}",
"Content-Type": "application/json"
}
response = requests.get(
f"{DIFY_API_URL}/parameters",
headers=headers
)
# 3. 返回 Dify 响应
return response.json()
```
#### Dify API 响应示例
```json
{
"opening_statement": "你好!我是AI助手...",
"suggested_questions": ["问题1", "问题2"],
"speech_to_text": { "enabled": false },
"retriever_resource": { "enabled": false },
"annotation_reply": { "enabled": false },
"user_input_form": [],
"file_upload": {
"image": {
"enabled": false,
"number_limits": 3,
"transfer_methods": ["remote_url", "local_file"]
}
},
"system_parameters": {}
}
```
---
### **2. 获取会话列表**
#### 前端请求
```http
GET http://{FASTAPI_HOST}/dify/conversations?user={user_id}&limit=100&first_id=
Authorization: Bearer {frontendJWT}
```
**查询参数**
- `user`: 用户标识(从 JWT 中可提取)
- `limit`: 返回数量限制(默认100
- `first_id`: 分页游标(可选)
#### 后端处理逻辑
```python
@app.get("/dify/conversations")
async def get_conversations(
user: str,
limit: int = 100,
first_id: str = "",
authorization: str = Header(None)
):
# 1. 验证 JWT
jwt_token = authorization.replace("Bearer ", "")
user_info = verify_jwt(jwt_token)
if not user_info:
raise HTTPException(status_code=401, detail="JWT认证失败,请重新登录")
# 2. 调用 Dify API
headers = {
"Authorization": f"Bearer {DIFY_API_KEY}",
"Content-Type": "application/json"
}
params = {
"user": user,
"limit": limit,
"first_id": first_id
}
response = requests.get(
f"{DIFY_API_URL}/conversations",
headers=headers,
params=params
)
# 3. 返回 Dify 响应
return response.json()
```
#### Dify API 响应示例
```json
{
"data": [
{
"id": "conv-123456",
"name": "会话标题",
"inputs": {},
"status": "normal",
"introduction": "",
"created_at": 1234567890,
"updated_at": 1234567890
}
],
"has_more": false,
"limit": 100
}
```
---
### **3. 获取会话消息历史**
#### 前端请求
```http
GET http://{FASTAPI_HOST}/dify/messages?user={user_id}&conversation_id={conv_id}&limit=20&last_id=
Authorization: Bearer {frontendJWT}
```
**查询参数**
- `user`: 用户标识
- `conversation_id`: 会话ID(必填)
- `limit`: 返回数量限制(默认20
- `last_id`: 分页游标(可选)
#### 后端处理逻辑
```python
@app.get("/dify/messages")
async def get_messages(
user: str,
conversation_id: str,
limit: int = 20,
last_id: str = "",
authorization: str = Header(None)
):
# 验证 JWT 和调用 Dify API(同上)
# ...
params = {
"user": user,
"conversation_id": conversation_id,
"limit": limit,
"last_id": last_id
}
response = requests.get(
f"{DIFY_API_URL}/messages",
headers=headers,
params=params
)
return response.json()
```
---
### **4. 发送聊天消息(流式响应)⭐**
#### 前端请求
```http
POST http://{FASTAPI_HOST}/dify/chat-messages
Authorization: Bearer {frontendJWT}
Content-Type: application/json
{
"inputs": {},
"query": "",
"user": "user_app-id:session-id",
"response_mode": "streaming",
"conversation_id": "conv-123456",
"files": []
}
```
**请求体参数**
- `inputs`: 输入变量(对象)
- `query`: 用户消息内容(必填)
- `user`: 用户标识(必填)
- `response_mode`: 响应模式,`"streaming"``"blocking"`
- `conversation_id`: 会话ID(可选,不传则创建新会话)
- `files`: 上传的文件列表(可选)
#### 后端处理逻辑(重要!)
```python
from fastapi import StreamingResponse
import httpx
@app.post("/dify/chat-messages")
async def create_chat_message(
request: Request,
authorization: str = Header(None)
):
# 1. 验证 JWT
jwt_token = authorization.replace("Bearer ", "")
user_info = verify_jwt(jwt_token)
if not user_info:
raise HTTPException(status_code=401, detail="JWT认证失败,请重新登录")
# 2. 获取请求体
body = await request.json()
response_mode = body.get("response_mode", "streaming")
# 3. 调用 Dify API
headers = {
"Authorization": f"Bearer {DIFY_API_KEY}",
"Content-Type": "application/json"
}
# 4. 根据响应模式处理
if response_mode == "streaming":
# 流式响应 - 直接转发流
async def stream_response():
async with httpx.AsyncClient() as client:
async with client.stream(
"POST",
f"{DIFY_API_URL}/chat-messages",
headers=headers,
json=body,
timeout=60.0
) as response:
async for chunk in response.aiter_bytes():
yield chunk
return StreamingResponse(
stream_response(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Access-Control-Allow-Origin": "*"
}
)
else:
# 非流式响应
response = requests.post(
f"{DIFY_API_URL}/chat-messages",
headers=headers,
json=body
)
return response.json()
```
#### Dify API 流式响应格式
```
data: {"event": "message", "message_id": "msg-123", "conversation_id": "conv-123", "answer": "你好"}
data: {"event": "message", "message_id": "msg-123", "conversation_id": "conv-123", "answer": ""}
data: {"event": "message_end", "id": "msg-123", "metadata": {...}}
data: {"event": "workflow_finished", "data": {...}}
```
---
### **5. 重命名会话**
#### 前端请求
```http
POST http://{FASTAPI_HOST}/dify/conversations/{conversation_id}/name
Authorization: Bearer {frontendJWT}
Content-Type: application/json
{
"name": "",
"auto_generate": false,
"user": "user_app-id:session-id"
}
```
#### 后端处理逻辑
```python
@app.post("/dify/conversations/{conversation_id}/name")
async def rename_conversation(
conversation_id: str,
request: Request,
authorization: str = Header(None)
):
# 验证 JWT
jwt_token = authorization.replace("Bearer ", "")
user_info = verify_jwt(jwt_token)
if not user_info:
raise HTTPException(status_code=401, detail="JWT认证失败,请重新登录")
# 获取请求体
body = await request.json()
# 调用 Dify API
headers = {
"Authorization": f"Bearer {DIFY_API_KEY}",
"Content-Type": "application/json"
}
response = requests.post(
f"{DIFY_API_URL}/conversations/{conversation_id}/name",
headers=headers,
json=body
)
return response.json()
```
---
### **6. 删除会话**
#### 前端请求
```http
DELETE http://{FASTAPI_HOST}/dify/conversations/{conversation_id}
Authorization: Bearer {frontendJWT}
Content-Type: application/json
{
"user": "user_app-id:session-id"
}
```
#### 后端处理逻辑
```python
@app.delete("/dify/conversations/{conversation_id}")
async def delete_conversation(
conversation_id: str,
request: Request,
authorization: str = Header(None)
):
# 验证 JWT
jwt_token = authorization.replace("Bearer ", "")
user_info = verify_jwt(jwt_token)
if not user_info:
raise HTTPException(status_code=401, detail="JWT认证失败,请重新登录")
# 获取请求体
body = await request.json()
# 调用 Dify API
headers = {
"Authorization": f"Bearer {DIFY_API_KEY}",
"Content-Type": "application/json"
}
response = requests.delete(
f"{DIFY_API_URL}/conversations/{conversation_id}",
headers=headers,
json=body
)
return response.json()
```
---
### **7. 消息反馈(点赞/点踩)**
#### 前端请求
```http
POST http://{FASTAPI_HOST}/dify/messages/{message_id}/feedbacks
Authorization: Bearer {frontendJWT}
Content-Type: application/json
{
"rating": "like",
"user": "user_app-id:session-id"
}
```
**请求体参数**
- `rating`: 评价类型,`"like"` | `"dislike"` | `null`
- `user`: 用户标识
---
## 🔧 完整后端实现示例(FastAPI)
```python
from fastapi import FastAPI, Header, HTTPException, Request
from fastapi.responses import StreamingResponse
import httpx
import requests
from typing import Optional
app = FastAPI()
# 配置
DIFY_API_URL = "http://nas.7bm.co:12980/v1"
DIFY_API_KEY = "app-N3su9tKyMMnqxt2EMgOkVof7"
DIFY_APP_ID = "46539478-3281-4e98-a445-6da9dc078e95"
# JWT 验证函数(使用你们现有的JWT验证逻辑)
def verify_jwt(token: str) -> Optional[dict]:
"""
验证 JWT Token
返回: 用户信息字典,如果验证失败返回 None
"""
try:
# 这里使用你们现有的 JWT 验证逻辑
# payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
# return payload
pass
except Exception as e:
print(f"JWT 验证失败: {e}")
return None
# 通用 Dify 请求包装器
async def dify_request(
method: str,
endpoint: str,
authorization: str,
params: dict = None,
json_data: dict = None,
stream: bool = False
):
"""通用 Dify API 请求处理"""
# 验证 JWT
jwt_token = authorization.replace("Bearer ", "") if authorization else None
if not jwt_token:
raise HTTPException(status_code=401, detail="缺少认证令牌")
user_info = verify_jwt(jwt_token)
if not user_info:
raise HTTPException(status_code=401, detail="JWT认证失败,请重新登录")
# 构建 Dify 请求头
headers = {
"Authorization": f"Bearer {DIFY_API_KEY}",
"Content-Type": "application/json"
}
url = f"{DIFY_API_URL}/{endpoint.lstrip('/')}"
if stream:
# 流式响应
async def stream_response():
async with httpx.AsyncClient() as client:
async with client.stream(
method,
url,
headers=headers,
params=params,
json=json_data,
timeout=60.0
) as response:
async for chunk in response.aiter_bytes():
yield chunk
return StreamingResponse(
stream_response(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive"
}
)
else:
# 普通响应
response = requests.request(
method,
url,
headers=headers,
params=params,
json=json_data
)
if response.status_code >= 400:
raise HTTPException(
status_code=response.status_code,
detail=response.text
)
return response.json()
# 路由实现
@app.get("/dify/parameters")
async def get_parameters(authorization: str = Header(None)):
return await dify_request("GET", "/parameters", authorization)
@app.get("/dify/conversations")
async def get_conversations(
user: str,
limit: int = 100,
first_id: str = "",
authorization: str = Header(None)
):
params = {"user": user, "limit": limit, "first_id": first_id}
return await dify_request("GET", "/conversations", authorization, params=params)
@app.get("/dify/messages")
async def get_messages(
user: str,
conversation_id: str,
limit: int = 20,
last_id: str = "",
authorization: str = Header(None)
):
params = {
"user": user,
"conversation_id": conversation_id,
"limit": limit,
"last_id": last_id
}
return await dify_request("GET", "/messages", authorization, params=params)
@app.post("/dify/chat-messages")
async def create_chat_message(
request: Request,
authorization: str = Header(None)
):
body = await request.json()
response_mode = body.get("response_mode", "streaming")
stream = (response_mode == "streaming")
return await dify_request(
"POST",
"/chat-messages",
authorization,
json_data=body,
stream=stream
)
@app.post("/dify/conversations/{conversation_id}/name")
async def rename_conversation(
conversation_id: str,
request: Request,
authorization: str = Header(None)
):
body = await request.json()
return await dify_request(
"POST",
f"/conversations/{conversation_id}/name",
authorization,
json_data=body
)
@app.delete("/dify/conversations/{conversation_id}")
async def delete_conversation(
conversation_id: str,
request: Request,
authorization: str = Header(None)
):
body = await request.json()
return await dify_request(
"DELETE",
f"/conversations/{conversation_id}",
authorization,
json_data=body
)
@app.post("/dify/messages/{message_id}/feedbacks")
async def message_feedback(
message_id: str,
request: Request,
authorization: str = Header(None)
):
body = await request.json()
return await dify_request(
"POST",
f"/messages/{message_id}/feedbacks",
authorization,
json_data=body
)
```
---
## ⚠️ 重要注意事项
### **1. JWT 验证**
- 必须验证 JWT 的签名和过期时间
- JWT 验证失败时返回 **401 状态码**
- 错误信息格式:`{"error": "JWT认证失败,请重新登录"}`
### **2. 流式响应处理**
- `/chat-messages` 接口必须支持流式响应(SSE
- 响应头必须包含:
```
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
```
- 不要缓冲流式响应,直接转发给前端
### **3. 错误处理**
所有错误响应应遵循统一格式:
```json
{
"error": "错误描述信息"
}
```
常见错误码:
- `400`: 请求参数错误
- `401`: JWT 认证失败
- `403`: 权限不足
- `500`: 服务器内部错误
### **4. CORS 配置**
如果前后端分离部署,需要配置 CORS:
```python
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173", "http://your-frontend-domain"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
```
### **5. 日志记录**
建议记录以下信息:
- 请求时间、用户ID、接口路径
- JWT 验证结果
- Dify API 调用结果
- 错误信息和堆栈
---
## 🧪 测试验证
### **测试工具**
推荐使用以下工具测试:
- **Postman** - API 接口测试
- **curl** - 命令行测试
- **httpie** - 友好的命令行工具
### **测试步骤**
#### 1. 获取 JWT Token
从前端登录后,在浏览器开发者工具中获取 `frontendJWT`
```javascript
// 在浏览器控制台执行
console.log(document.cookie);
```
#### 2. 测试基础接口
```bash
# 测试获取参数
curl -X GET "http://localhost:8000/dify/parameters" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
# 测试获取会话列表
curl -X GET "http://localhost:8000/dify/conversations?user=test_user&limit=10" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
```
#### 3. 测试流式聊天
```bash
# 测试发送消息(流式)
curl -X POST "http://localhost:8000/dify/chat-messages" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"inputs": {},
"query": "你好",
"user": "test_user",
"response_mode": "streaming"
}'
```
---
## 📞 联系方式
如有疑问或需要协助,请联系:
- **前端开发**: [你的联系方式]
- **文档位置**: `E:\A_Wrok\Porject\docreview\docs\dify-proxy-backend-integration.md`
- **前端代码仓库**: [仓库地址]
---
## 📚 附录
### **相关文档**
- [Dify 官方 API 文档](https://docs.dify.ai/v/zh-hans/guides/application-publishing/developing-with-apis)
- [JWT 认证实现文档](./JWT_IMPLEMENTATION.md)
- [FastAPI 官方文档](https://fastapi.tiangolo.com/zh/)
### **配置清单**
#### 后端需要的环境变量
```bash
# Dify API 配置
DIFY_API_URL=http://nas.7bm.co:12980/v1
DIFY_API_KEY=app-N3su9tKyMMnqxt2EMgOkVof7
DIFY_APP_ID=46539478-3281-4e98-a445-6da9dc078e95
# JWT 验证配置(使用现有的)
JWT_SECRET=your-jwt-secret
JWT_ALGORITHM=HS256
```
#### 前端配置(已修改)
```bash
# API Base URL - 指向 FastAPI 后端
API_BASE_URL=http://172.16.0.55:8000
# Dify 配置已移除,不再使用
# NEXT_PUBLIC_APP_ID=... (已废弃)
# NEXT_PUBLIC_APP_KEY=... (已废弃)
```
---
## ✅ 检查清单
后端实现完成后,请确认:
- [ ] 所有 7 个 Dify API 路由已实现
- [ ] JWT 验证逻辑已集成
- [ ] 流式响应(SSE)正常工作
- [ ] 错误处理返回正确的状态码
- [ ] CORS 配置已启用(如需要)
- [ ] 日志记录已完善
- [ ] 已使用 Postman/curl 测试所有接口
- [ ] 与前端联调测试通过
---
**祝开发顺利!** 🎉