feat(chat): support AGNES image/video models in AI chat - backend _generate_media (images/generations + async videos polling) and frontend MarkdownContent media rendering + model capability badges; nginx proxy_read_timeout raised to 600s for long media generation

This commit is contained in:
qyyfw committed 2026-07-10 15:47:03 +08:00
1 parent 774f1648dc
commit d62848ee98
3 files changed
+2370 -2128

No files matched your search

+112 -1
View File
@@ -12,6 +12,7 @@ import uuid
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
import re import re
import json import json
import asyncio
import math import math
import httpx import httpx
import os import os
@@ -40,6 +41,108 @@ def cst_now():
return datetime.now(CST_TZ) return datetime.now(CST_TZ)
def _html_escape(s: str) -> str:
return (str(s).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
.replace('"', "&quot;").replace("'", "&#39;"))
async def _generate_media(db, ai_model, prompt: str, user_id: str = None) -> str:
"""图像/视频生成模型统一入口:按 capabilities 调用 AGNES 等媒体生成 API。
返回可直接在 AI 聊天中渲染的富文本(含 <img>/<video> 标签)。"""
provider = db.query(ModelProvider).filter(ModelProvider.id == ai_model.provider_id).first()
api_key = ai_model.api_key or (provider.api_key if provider else None)
if not api_key:
return "⚠️ 该媒体模型缺少 API Key,请在后台「大模型库」中配置。"
caps = [c.lower() for c in (ai_model.capabilities or [])]
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
timeout = ai_model.timeout or 120
# ============ 图像生成 ============
if "image" in caps:
api_url = ai_model.endpoint
if not api_url:
api_url = f"{provider.api_base_url.rstrip('/')}/v1/images/generations" if provider else ""
if not api_url:
return "⚠️ 该图像模型未配置 Endpoint。"
payload = {
"model": ai_model.name,
"prompt": prompt,
"size": "1024x768",
"extra_body": {"response_format": "url"},
}
try:
async with httpx.AsyncClient(timeout=timeout) as client:
resp = await client.post(api_url, headers=headers, json=payload)
if resp.status_code != 200:
return f"⚠️ 图像生成失败 (HTTP {resp.status_code}): {resp.text[:200]}"
data = resp.json()
items = data.get("data") or []
if not items:
return f"⚠️ 图像生成失败:响应无图片数据 {str(data)[:200]}"
item = items[0]
url = item.get("url") or item.get("b64_json")
if not url:
return f"⚠️ 图像生成失败:未返回图片地址 {str(data)[:200]}"
if not url.startswith("http"):
url = f"data:image/png;base64,{url}"
return (f'<img class="md-img" src="{url}" alt="生成图片"/>\n\n'
f'**提示词:** {_html_escape(prompt)}')
except Exception as e:
return f"⚠️ 图像生成异常:{str(e)[:200]}"
# ============ 视频生成 (异步任务 + 轮询) ============
if "video" in caps:
api_url = ai_model.endpoint
if not api_url:
api_url = f"{provider.api_base_url.rstrip('/')}/v1/videos" if provider else ""
if not api_url:
return "⚠️ 该视频模型未配置 Endpoint。"
base = api_url.split("/v1")[0] if "/v1" in api_url else api_url
payload = {
"model": ai_model.name,
"prompt": prompt,
"height": 768,
"width": 1152,
"num_frames": 121,
"frame_rate": 24,
}
try:
async with httpx.AsyncClient(timeout=timeout) as client:
resp = await client.post(api_url, headers=headers, json=payload)
if resp.status_code != 200:
return f"⚠️ 视频生成失败 (HTTP {resp.status_code}): {resp.text[:200]}"
data = resp.json()
video_id = data.get("video_id") or data.get("task_id") or data.get("id")
if not video_id:
return f"⚠️ 视频生成失败:未返回任务ID {str(data)[:200]}"
# 轮询结果(最多约 5 分钟)
result_url = None
err_msg = None
for _ in range(60):
await asyncio.sleep(5)
r2 = await client.get(f"{base}/agnesapi?video_id={video_id}", headers=headers)
if r2.status_code != 200:
continue
rd = r2.json()
st = rd.get("status")
if st == "completed":
result_url = rd.get("url")
break
if st == "failed":
err_msg = rd.get("error")
break
if result_url:
return (f'<video class="md-video" controls src="{result_url}"></video>\n\n'
f'**提示词:** {_html_escape(prompt)}')
return f"⚠️ 视频生成失败:{err_msg or '生成超时,请稍后在对话中重试'}"
except Exception as e:
return f"⚠️ 视频生成异常:{str(e)[:200]}"
return "⚠️ 未知的媒体模型类型。"
# ============================================================ # ============================================================
# Request Models # Request Models
# ============================================================ # ============================================================
@@ -881,7 +984,14 @@ async def send_message(
if model_id: if model_id:
ai_model = db.query(AIModel).filter(AIModel.id == model_id).first() ai_model = db.query(AIModel).filter(AIModel.id == model_id).first()
if ai_model and ai_model.endpoint: # ====== 图像/视频生成模型分支 ======
caps = [c.lower() for c in (ai_model.capabilities or [])] if ai_model else []
if ai_model and ("image" in caps or "video" in caps):
try:
ai_content = await _generate_media(db, ai_model, req.content, current_user.id)
except Exception as e:
ai_content = f"⚠️ 媒体生成失败:{str(e)[:200]}"
elif ai_model and ai_model.endpoint:
provider = db.query(ModelProvider).filter(ModelProvider.id == ai_model.provider_id).first() provider = db.query(ModelProvider).filter(ModelProvider.id == ai_model.provider_id).first()
api_key = ai_model.api_key or (provider.api_key if provider else None) api_key = ai_model.api_key or (provider.api_key if provider else None)
api_url = ai_model.endpoint api_url = ai_model.endpoint
@@ -1333,6 +1443,7 @@ def get_models(db: Session = Depends(get_db)):
"display_name": getattr(m, "display_name", None) or m.name, "display_name": getattr(m, "display_name", None) or m.name,
"provider_id": m.provider_id, "provider_id": m.provider_id,
"provider_name": provider.name if provider else "", "provider_name": provider.name if provider else "",
"capabilities": m.capabilities or [],
"status": m.status, "status": m.status,
"price_per_million_tokens": float(m.price_per_million_tokens or 0), "price_per_million_tokens": float(m.price_per_million_tokens or 0),
"is_enabled": m.is_enabled, "is_enabled": m.is_enabled,
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff