Files
leaudit-platform-frontend/docs/dify-proxy-backend-integration.md
TanWenyan c4c08cb59b 重构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>
2025-10-30 09:47:48 +08:00

20 KiB
Raw Permalink Blame History

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 的响应原样返回给前端

关键配置迁移

前端原本使用的配置:

# .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" | null
  • user: 用户标识

🔧 完整后端实现示例(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 测试所有接口
  • 与前端联调测试通过

祝开发顺利! 🎉