c4c08cb59b
主要变更: - 修改 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>
20 KiB
20 KiB
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)
🔐 认证流程说明
前端侧
- 用户登录后获得
frontendJWT(前端JWT) - 所有 Dify 相关请求携带
Authorization: Bearer {frontendJWT} - JWT 由 OAuth2.0 + 自定义签名生成,包含用户信息
后端侧
- 接收前端请求,验证
frontendJWT是否有效 - JWT 验证通过后,使用配置的
DIFY_API_KEY调用真正的 Dify API - 将 Dify API 的响应原样返回给前端
关键配置迁移
前端原本使用的配置:
# .env (前端配置 - 已不再使用)
NEXT_PUBLIC_APP_ID=http://nas.7bm.co:12980/app/46539478-3281-4e98-a445-6da9dc078e95/configuration
NEXT_PUBLIC_APP_KEY=app-N3su9tKyMMnqxt2EMgOkVof7
现在这两个配置应该移到 FastAPI 后端:
# 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. 获取应用参数
前端请求
GET http://{FASTAPI_HOST}/dify/parameters
Authorization: Bearer {frontendJWT}
后端处理逻辑
@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 响应示例
{
"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. 获取会话列表
前端请求
GET http://{FASTAPI_HOST}/dify/conversations?user={user_id}&limit=100&first_id=
Authorization: Bearer {frontendJWT}
查询参数:
user: 用户标识(从 JWT 中可提取)limit: 返回数量限制(默认100)first_id: 分页游标(可选)
后端处理逻辑
@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 响应示例
{
"data": [
{
"id": "conv-123456",
"name": "会话标题",
"inputs": {},
"status": "normal",
"introduction": "",
"created_at": 1234567890,
"updated_at": 1234567890
}
],
"has_more": false,
"limit": 100
}
3. 获取会话消息历史
前端请求
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: 分页游标(可选)
后端处理逻辑
@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. 发送聊天消息(流式响应)⭐
前端请求
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: 上传的文件列表(可选)
后端处理逻辑(重要!)
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. 重命名会话
前端请求
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"
}
后端处理逻辑
@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. 删除会话
前端请求
DELETE http://{FASTAPI_HOST}/dify/conversations/{conversation_id}
Authorization: Bearer {frontendJWT}
Content-Type: application/json
{
"user": "user_app-id:session-id"
}
后端处理逻辑
@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. 消息反馈(点赞/点踩)
前端请求
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"|nulluser: 用户标识
🔧 完整后端实现示例(FastAPI)
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. 错误处理
所有错误响应应遵循统一格式:
{
"error": "错误描述信息"
}
常见错误码:
400: 请求参数错误401: JWT 认证失败403: 权限不足500: 服务器内部错误
4. CORS 配置
如果前后端分离部署,需要配置 CORS:
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:
// 在浏览器控制台执行
console.log(document.cookie);
2. 测试基础接口
# 测试获取参数
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. 测试流式聊天
# 测试发送消息(流式)
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 配置
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
前端配置(已修改)
# 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 测试所有接口
- 与前端联调测试通过
祝开发顺利! 🎉