From 0f765281cb2f194abac8d687f74e2505bd458cc3 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Wed, 22 Apr 2026 14:07:23 +0800 Subject: [PATCH] init --- .env.example | 16 ++++ Dockerfile | 31 +++++++ README.md | 76 +++++++++++++++++ digital-human.py | 206 +++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 62 ++++++++++++++ requirements.txt | 7 ++ 6 files changed, 398 insertions(+) create mode 100644 .env.example create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 digital-human.py create mode 100644 docker-compose.yml create mode 100644 requirements.txt diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f6fd913 --- /dev/null +++ b/.env.example @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bfad86f --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..d107fab --- /dev/null +++ b/README.md @@ -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 # 本文件 +``` diff --git a/digital-human.py b/digital-human.py new file mode 100644 index 0000000..8dc0e36 --- /dev/null +++ b/digital-human.py @@ -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)}, + ) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..cad82aa --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..50d423a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +# Python 运行时依赖 +# Dockerfile 中已优先使用清华镜像加速安装 + +redis==5.0.1 +fastapi==0.109.2 +uvicorn[standard]==0.27.1 +httpx==0.26.0