This commit is contained in:
2026-04-24 10:17:38 +08:00
parent 79bc8c5120
commit 9d867520e8
2 changed files with 1 additions and 261 deletions

View File

@@ -2,8 +2,6 @@
本文档记录基于华为云 MetaStudio 智能交互 + 第三方大脑(华为云官方称为"第三方大模型")的交互数字人配置方案,智能大脑使用 Dify 平台。
> **重要公告**:华为云 MetaStudio **3D 数字人产品已停售**;**Flexus 分身数字人仅支持视频制作,不支持智能交互**。本文档智能交互部分仅涵盖 **分身数字人** 相关配置。
---
## 目录
@@ -334,7 +332,7 @@ Dify 是交互数字人的"大脑",负责知识库检索和大语言模型对
| 分身数字人 | 基于真人形象采集打造,交互自然,推荐正式客服场景 |
| 照片数字人 | 上传照片即可生成,适合快速验证 |
> **重要**:Flexus 分身数字人仅支持视频制作,**不支持智能交互**,详见 [Flexus 分身数字人限制说明](https://support.huaweicloud.com/productdesc-metastudio/metastudio_01_0022.html)。3D 数字人产品已停售,均不在可选范围内。
> **重要**:Flexus 分身数字人仅支持视频制作,**不支持智能交互**,详见 [Flexus 分身数字人限制说明](https://support.huaweicloud.com/productdesc-metastudio/metastudio_01_0022.html)。
---

View File

@@ -1,258 +0,0 @@
"""
数字人对接中转服务
-----------------
作为客户端与 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),
}
# ──────────────────────────────────────────────────────────────
# Dify 响应格式转 OpenAI 兼容格式
# ──────────────────────────────────────────────────────────────
def dify_to_openai(dify_resp: Dict[str, Any]) -> Dict[str, Any]:
"""
将 Dify /chat-messages 的响应转换为 OpenAI /chat/completions 兼容格式。
Dify 响应示例: { answer: "...", conversation_id: "...", task_id: "..." }
OpenAI 目标格式: { id, model, choices: [{ message: { role: "assistant", content: "..." } }] }
"""
return {
"id": f"chatcmpl-{dify_resp.get('task_id', 'unknown')}",
"object": "chat.completion",
"created": 0,
"model": "dify",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": dify_resp.get("answer", ""),
},
"finish_reason": "stop",
}
],
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0,
},
}
# ──────────────────────────────────────────────────────────────
# 核心转发接口
# ──────────────────────────────────────────────────────────────
@app.post("/v1/chat/completions")
async def chat_completions(request: Request):
"""
接收 OpenAI 兼容格式请求,转换为 Dify /chat-messages 格式后转发。
OpenAI 格式: { model, messages: [{role, content}], stream }
Dify 格式: { query, user, response_mode, inputs }
"""
body = await request.json()
# ── 格式转换:OpenAI -> Dify ──────────────────────────────
# 提取用户最新消息
user_content = ""
for msg in reversed(body.get("messages", [])):
if msg.get("role") == "user":
user_content = msg.get("content", "")
break
if not user_content:
raise HTTPException(status_code=400, detail="未找到用户消息")
# 生成或复用 user 标识(用于 Dify 会话上下文)
dify_user = body.get("user", "anonymous")
response_mode = "blocking" if not body.get("stream", False) else "streaming"
# 构建 Dify 请求体
dify_payload = {
"query": user_content,
"user": dify_user,
"response_mode": response_mode,
"inputs": body.get("inputs", {}),
}
# 若应用要求 inputs 字段但为空,补上空对象
if "inputs" not in dify_payload or dify_payload["inputs"] is None:
dify_payload["inputs"] = {}
# ── 检查 Redis 缓存(基于 OpenAI 原始请求体)──────────────
cache_key = get_cache_key(body)
try:
cached = get_redis().get(cache_key)
if cached:
logger.info(f"缓存命中: {cache_key}")
return dify_to_openai(json.loads(cached))
except RedisConnectionError:
logger.warning("Redis 不可用,跳过缓存读取")
# ── 转发到 Dify ─────────────────────────────────────────
headers = {
"Authorization": f"Bearer {DIFY_API_KEY}",
"Content-Type": "application/json",
}
dify_endpoint = f"{DIFY_URL.rstrip('/')}/chat-messages"
try:
async with httpx.AsyncClient(timeout=120.0) as client:
resp = await client.post(dify_endpoint, json=dify_payload, 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}")
# ── 写入缓存(存 Dify 原始格式)───────────────────────────
try:
get_redis().setex(cache_key, 300, json.dumps(result))
except RedisConnectionError:
pass
# ── 响应转 OpenAI 兼容格式 ─────────────────────────────────
return dify_to_openai(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)},
)