init
This commit is contained in:
16
.env.example
Normal file
16
.env.example
Normal 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
31
Dockerfile
Normal 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
76
README.md
Normal 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
206
digital-human.py
Normal 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
62
docker-compose.yml
Normal 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
7
requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Python 运行时依赖
|
||||||
|
# Dockerfile 中已优先使用清华镜像加速安装
|
||||||
|
|
||||||
|
redis==5.0.1
|
||||||
|
fastapi==0.109.2
|
||||||
|
uvicorn[standard]==0.27.1
|
||||||
|
httpx==0.26.0
|
||||||
Reference in New Issue
Block a user