This commit is contained in:
2026-04-22 14:07:23 +08:00
commit 0f765281cb
6 changed files with 398 additions and 0 deletions

16
.env.example Normal file
View File

@@ -0,0 +1,16 @@
# ============================================================
# 数字人对接中转服务 - 环境变量配置
# 复制此文件为 .env 并填写实际值
# ============================================================
# Dify 服务地址(必填)
# 例如:https://atwdify.antiwearvalve.com/v1
DIFY_URL=https://your-dify-domain.com/v1
# Dify API 密钥(必填)
# 例如:app-PDkSHiHZsZIluoZF7N2OlNZM
DIFY_API_KEY=app-xxxxxxxxxxxxxxxxxxxx
# Redis 配置(可选,有默认值)
REDIS_HOST=redis
REDIS_PORT=6379

31
Dockerfile Normal file
View File

@@ -0,0 +1,31 @@
# ============================================================
# 数字人对接中转服务 - Dockerfile
# 基础镜像:Python 3.11 slim(Debian-based,体积适中)
# ============================================================
FROM python:3.11-slim
# ── 安装系统依赖 ──────────────────────────────────────────
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
redis-tools \
&& rm -rf /var/lib/apt/lists/*
# ── 设置工作目录 ──────────────────────────────────────────
WORKDIR /app
# ── 安装 Python 运行时依赖 ────────────────────────────────
# 优先使用清华镜像加速,镜像不可用时自动回退到官方 PyPI
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt \
|| pip install --no-cache-dir -r requirements.txt
# ── 复制应用代码 ──────────────────────────────────────────
COPY digital-human.py .
# ── 暴露端口 ──────────────────────────────────────────────
EXPOSE 8000
# ── 启动命令 ──────────────────────────────────────────────
# 使用 uvicorn 运行 FastAPI 应用,监听 0.0.0.0:8000
CMD ["uvicorn", "digital-human:app", "--host", "0.0.0.0", "--port", "8000"]

76
README.md Normal file
View File

@@ -0,0 +1,76 @@
# 数字人对接中转服务
基于 Docker Compose 部署的数字人对接中转服务,连接客户端与 Dify LLM 平台。
## 架构
```
Client --> digital-human (8000) --> Dify LLM Platform
|
+--> Redis (6379)
```
## 快速部署
### 1. 配置环境变量
```bash
cp .env.example .env
# 编辑 .env,填入 DIFY_URL 和 DIFY_API_KEY
```
### 2. 启动服务
```bash
docker compose up -d
```
### 3. 验证服务
```bash
# 健康检查
curl http://localhost:8000/health
# 转发测试(chat/completions)
curl -X POST http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "your-model",
"messages": [{"role": "user", "content": "你好"}]
}'
```
## 端口说明
| 端口 | 服务 | 说明 |
|:---|:---|:---|
| 8000 | digital-human | 中转服务,对外暴露 |
| 6379 | Redis | 仅容器内部使用 |
## 环境变量
| 变量 | 必填 | 默认值 | 说明 |
|:---|:---|:---|:---|
| `DIFY_URL` | 是 | - | Dify 服务地址,如 `https://xxx.com/v1` |
| `DIFY_API_KEY` | 是 | - | Dify API 密钥 |
| `REDIS_HOST` | 否 | `redis` | Redis 主机(Docker Compose 内部 DNS) |
| `REDIS_PORT` | 否 | `6379` | Redis 端口 |
## 生产部署注意事项
1. **防火墙**:确保服务器 TCP 8000 端口对外部开放
2. **HTTPS**:生产环境建议在 `digital-human` 前加一层 Nginx/Caddy 反向代理, termination TLS
3. **环境变量**:不要将密钥提交到代码仓库,`.env` 已加入 `.gitignore`
4. **Redis**:生产环境可移除内建 Redis,连接外部 Redis 集群
## 目录结构
```
.
├── docker-compose.yml # 容器编排配置
├── Dockerfile # 应用镜像构建
├── digital-human.py # FastAPI 中转服务源码
├── requirements.txt # Python 依赖
├── .env.example # 环境变量示例
└── README.md # 本文件
```

206
digital-human.py Normal file
View File

@@ -0,0 +1,206 @@
"""
数字人对接中转服务
-----------------
作为客户端与 Dify LLM 平台之间的中间转发层,处理请求路由、
认证转发及 Redis 缓存,提升响应效率并保护后端 API Key。
环境变量:
DIFY_URL - Dify 服务地址,如 https://atwdify.antiwearvalve.com/v1
DIFY_API_KEY - Dify API 密钥,如 app-PDkSHiHZsZIluoZF7N2OlNZM
REDIS_HOST - Redis 主机地址,默认 localhost
REDIS_PORT - Redis 端口,默认 6379
"""
import os
import json
import hashlib
import logging
from contextlib import asynccontextmanager
from typing import Any, Dict, Optional
import httpx
import redis
from redis.exceptions import ConnectionError as RedisConnectionError
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
# ──────────────────────────────────────────────────────────────
# 日志配置
# ──────────────────────────────────────────────────────────────
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s - %(message)s",
)
logger = logging.getLogger("digital-human")
# ──────────────────────────────────────────────────────────────
# 应用配置(从环境变量读取)
# ──────────────────────────────────────────────────────────────
DIFY_URL: str = os.getenv("DIFY_URL", "")
DIFY_API_KEY: str = os.getenv("DIFY_API_KEY", "")
REDIS_HOST: str = os.getenv("REDIS_HOST", "localhost")
REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379"))
# 验证必填配置
if not DIFY_URL or not DIFY_API_KEY:
logger.warning(
"DIFY_URL 或 DIFY_API_KEY 未配置,"
"部分功能可能不可用。请通过环境变量设置。"
)
# ──────────────────────────────────────────────────────────────
# Redis 客户端(懒加载)
# ──────────────────────────────────────────────────────────────
_redis_client: Optional[redis.Redis] = None
def get_redis() -> redis.Redis:
"""获取 Redis 客户端实例(延迟初始化)。"""
global _redis_client
if _redis_client is None:
_redis_client = redis.Redis(
host=REDIS_HOST,
port=REDIS_PORT,
decode_responses=True,
socket_connect_timeout=5,
)
return _redis_client
def get_cache_key(payload: Dict[str, Any]) -> str:
"""根据请求体生成 Redis 缓存键。"""
raw = json.dumps(payload, sort_keys=True)
return f"dh:cache:{hashlib.md5(raw.encode()).hexdigest()}"
# ──────────────────────────────────────────────────────────────
# FastAPI 应用生命周期
# ──────────────────────────────────────────────────────────────
@asynccontextmanager
async def lifespan(app: FastAPI):
"""应用启动 / 关闭钩子。"""
logger.info("数字人对接服务启动中...")
logger.info(f" Dify URL : {DIFY_URL}")
logger.info(f" Redis : {REDIS_HOST}:{REDIS_PORT}")
# 尝试连接 Redis(仅记录,不阻塞启动)
try:
get_redis().ping()
logger.info("Redis 连接成功")
except RedisConnectionError as e:
logger.warning(f"Redis 连接失败(服务将继续运行): {e}")
yield
logger.info("数字人对接服务已关闭")
app = FastAPI(
title="数字人对接中转服务",
version="1.0.0",
description="对接 Dify LLM 平台的数字人应用中转服务",
lifespan=lifespan,
)
# ──────────────────────────────────────────────────────────────
# 健康检查
# ──────────────────────────────────────────────────────────────
@app.get("/")
async def root():
"""服务健康检查端点。"""
return {
"status": "ok",
"service": "digital-human-relay",
"version": "1.0.0",
}
@app.get("/health")
async def health_check():
"""详细健康检查(包含 Redis 状态)。"""
redis_ok = False
try:
get_redis().ping()
redis_ok = True
except Exception:
pass
return {
"status": "healthy" if redis_ok else "degraded",
"redis": "connected" if redis_ok else "disconnected",
"dify_configured": bool(DIFY_URL and DIFY_API_KEY),
}
# ──────────────────────────────────────────────────────────────
# 核心转发接口
# ──────────────────────────────────────────────────────────────
@app.post("/v1/chat/completions")
async def chat_completions(request: Request):
"""
转发 chat/completions 请求到 Dify。
支持缓存(可选):相同请求体会命中 Redis 缓存。
"""
# 读取请求体
body = await request.json()
# 检查 Redis 缓存
try:
r = get_redis()
cache_key = get_cache_key(body)
cached = r.get(cache_key)
if cached:
logger.info(f"缓存命中: {cache_key}")
return json.loads(cached)
except RedisConnectionError:
logger.warning("Redis 不可用,跳过缓存读取")
# 构建转发请求头
headers = {
"Authorization": f"Bearer {DIFY_API_KEY}",
"Content-Type": "application/json",
}
# 转发到 Dify
dify_endpoint = f"{DIFY_URL.rstrip('/')}/chat/completions"
try:
async with httpx.AsyncClient(timeout=60.0) as client:
resp = await client.post(
dify_endpoint,
json=body,
headers=headers,
)
resp.raise_for_status()
result = resp.json()
except httpx.HTTPStatusError as e:
logger.error(f"Dify 返回错误状态码 {e.response.status_code}: {e.response.text}")
raise HTTPException(
status_code=e.response.status_code,
detail=f"Dify 请求失败: {e.response.text}",
)
except httpx.RequestError as e:
logger.error(f"无法连接 Dify 服务: {e}")
raise HTTPException(status_code=502, detail=f"无法连接 Dify 服务: {e}")
# 写入 Redis 缓存(TTL = 300 秒)
try:
r = get_redis()
r.setex(cache_key, 300, json.dumps(result))
except RedisConnectionError:
pass
return result
# ──────────────────────────────────────────────────────────────
# 错误处理器
# ──────────────────────────────────────────────────────────────
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
logger.exception(f"未处理的异常: {exc}")
return JSONResponse(
status_code=500,
content={"error": "Internal Server Error", "detail": str(exc)},
)

62
docker-compose.yml Normal file
View File

@@ -0,0 +1,62 @@
# ============================================================
# 数字人对接中转服务 - docker-compose.yml
#
# 使用方法:
# 1. 将 DIFY_URL 和 DIFY_API_KEY 填入 .env 文件
# 2. 执行: docker compose up -d
# 3. 验证: curl http://localhost:8000
# ============================================================
services:
# ── Redis 缓存服务 ──────────────────────────────────────
redis:
image: redis:7-alpine
container_name: digital-human-redis
restart: unless-stopped
# 生产环境建议取消注释以下 networks 和 ports 配置
# networks:
# - digital-human-net
# ports:
# - "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
# ── 数字人对接中转服务 ──────────────────────────────────
digital-human:
build:
context: .
dockerfile: Dockerfile
container_name: digital-human-relay
restart: unless-stopped
ports:
- "8000:8000"
environment:
# Dify 服务地址,如 https://atwdify.antiwearvalve.com/v1
DIFY_URL: "${DIFY_URL}"
# Dify API 密钥,如 app-PDkSHiHZsZIluoZF7N2OlNZM
DIFY_API_KEY: "${DIFY_API_KEY}"
# Redis 连接地址(服务名方式,Docker Compose 内部 DNS)
REDIS_HOST: "redis"
REDIS_PORT: "6379"
depends_on:
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
# networks:
# - digital-human-net
# ============================================================
# 网络配置(生产环境取消注释)
# ============================================================
# networks:
# digital-human-net:
# driver: bridge

7
requirements.txt Normal file
View File

@@ -0,0 +1,7 @@
# Python 运行时依赖
# Dockerfile 中已优先使用清华镜像加速安装
redis==5.0.1
fastapi==0.109.2
uvicorn[standard]==0.27.1
httpx==0.26.0