diff --git a/backend/python/app/api/chat.py b/backend/python/app/api/chat.py
index a0e5647..238725e 100644
--- a/backend/python/app/api/chat.py
+++ b/backend/python/app/api/chat.py
@@ -3,7 +3,7 @@
包含: Skill ID统一(MarketApp映射), 真实Skill执行, Agent工作流注入,
多轮上下文保持, BM25 KB检索, Function Calling循环增强
"""
-from fastapi import APIRouter, Depends, HTTPException, Query
+from fastapi import APIRouter, Depends, HTTPException, Query, Response
from sqlalchemy.orm import Session
from sqlalchemy import and_, desc, asc, func
from pydantic import BaseModel, Field
@@ -173,6 +173,40 @@ def _proxy_for_url(url: str) -> str | None:
return _outbound_proxy()
+# ============================================================
+# 媒体中转(relay)端点:让微信/iLink 能内联渲染 AGNES 生成的图片/视频
+# 背景:AGNES 媒体 URL 位于境外(agnes-ai.com / agnes-ai.space),
+# iLink 服务器(中国大陆)直连常被墙/超时,导致微信里只看到"链接"无法内联播放。
+# 此端点由平台服务器(经香港代理可达 AGNES)代取媒体并以平台域名回传,
+# 微信即可内联显示。仅允许白名单域名,杜绝 SSRF。
+# ============================================================
+_MEDIA_RELAY_ALLOWED = ("agnes-ai.com", "agnes-ai.space", "qihuyun.com")
+
+
+@router.get("/media/relay")
+async def media_relay(url: str = Query(...)):
+ """中转 AGNES 媒体(图片/视频)到平台域名,供微信/iLink 内联渲染。
+
+ 仅允许白名单域名;否则 400。返回的媒体流保持原 content-type。
+ """
+ import urllib.parse as _up
+ host = (_up.urlparse(url).hostname or "").lower()
+ if not any(allowed in host for allowed in _MEDIA_RELAY_ALLOWED):
+ raise HTTPException(status_code=400, detail="unsupported media host")
+ try:
+ async with httpx.AsyncClient(timeout=180, proxy=_proxy_for_url(url), trust_env=False) as client:
+ r = await client.get(url)
+ if r.status_code != 200:
+ raise HTTPException(status_code=r.status_code, detail="upstream error")
+ content_type = r.headers.get("content-type", "application/octet-stream")
+ data = await r.aread()
+ return Response(content=data, media_type=content_type)
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=502, detail=f"relay failed: {e}")
+
+
async def _optimize_media_prompt(raw_prompt: str, media_type: str, db=None) -> str:
"""使用平台文本模型对用户原始输入做「媒体提示词增强优化」。
diff --git a/backend/python/app/api/im_bot_user.py b/backend/python/app/api/im_bot_user.py
index 0af85fc..2559f97 100644
--- a/backend/python/app/api/im_bot_user.py
+++ b/backend/python/app/api/im_bot_user.py
@@ -14,11 +14,13 @@ im_bot_user.py
"""
import base64
import json
+import os
import random
import threading
import time
from datetime import datetime, timedelta, timezone
from typing import Optional
+from urllib.parse import quote as _quote
import requests
from fastapi import APIRouter, Depends, HTTPException, Query
@@ -225,21 +227,31 @@ def _wechat_send_message(baseurl: str, bot_token: str, to_user_id: str, context_
[{"type": 1, "text_item": {"text": text}}])
+def _platform_relay_url(raw_url: str) -> str:
+ """把 AGNES 等境外媒体 URL 改写为平台中转地址,使微信/iLink 可内联渲染。
+
+ iLink 服务器(中国大陆)直连境外 AGNES 常被墙/超时,微信里只能看到"链接"。
+ 改写为平台中转地址(平台服务器经香港代理可达 AGNES),微信即可内联显示图片/视频。
+ """
+ access = os.environ.get("APP_ACCESS_URL", "http://210.16.161.218:12888").rstrip("/")
+ return f"{access}/app/api/chat/media/relay?url={_quote(raw_url, safe='')}"
+
+
def _wechat_send_media(baseurl: str, bot_token: str, to_user_id: str, context_token: str,
media_type: str, url: str):
- """发送媒体(图片/视频)并附带文本链接兜底,确保用户可在微信内直接查看。
+ """发送媒体(图片/视频)并在微信内联渲染。
- iLink 消息 item_list:
- - type=2 图片:image_item: {url}
- - type=5 视频:video_item: {url}
- 同时附带一条文本项(含直链),防止微信/iLink 不渲染外链时用户仍能复制查看。
+ 问题:AGNES 媒体 URL 位于境外,iLink(中国大陆服务器)直连常被墙/超时,
+ 微信里只能看到"链接"而无法内联播放。
+ 解决:改写为平台中转地址(经香港代理可达 AGNES),微信即可内联显示图片/视频。
"""
+ relay_url = _platform_relay_url(url)
if media_type == "image":
- media_item = {"type": 2, "image_item": {"url": url}}
- caption = "🖼️ 图片已生成(若未直接显示,请点链接查看):" + url
+ media_item = {"type": 2, "image_item": {"url": relay_url}}
+ caption = "🖼️ 图片已生成(若未直接显示,请点此查看):" + relay_url
else:
- media_item = {"type": 5, "video_item": {"url": url}}
- caption = "🎬 视频已生成(若未直接播放,请点链接查看):" + url
+ media_item = {"type": 5, "video_item": {"url": relay_url}}
+ caption = "🎬 视频已生成(若未直接播放,请点此查看):" + relay_url
return _wechat_post_msg(baseurl, bot_token, to_user_id, context_token,
[media_item, {"type": 1, "text_item": {"text": caption}}])
@@ -420,11 +432,12 @@ def _wechat_receiver_loop(account_id: int):
media_type = reply.get("media_type") or "image"
media_url = reply.get("url") or ""
ok, ret, _ = _wechat_send_media(baseurl, bot_token, sender, ctx, media_type, media_url)
- # 对话历史里存可渲染的媒体标签(AI 聊天 WeXin Bot 窗与历史页均可显示)
+ # 对话历史里存可渲染的媒体标签(经平台中转,AI 聊天 WeXin Bot 窗与历史页均可内联显示)
+ relay_url = _platform_relay_url(media_url)
reply_for_log = (
- f''
+ f'
'
if media_type == "image"
- else f''
+ else f''
)
else:
reply_text = reply if isinstance(reply, str) else str(reply)
diff --git a/frontend/src/pages/front/ImBotHub.tsx b/frontend/src/pages/front/ImBotHub.tsx
index 85e85ed..fcea410 100644
--- a/frontend/src/pages/front/ImBotHub.tsx
+++ b/frontend/src/pages/front/ImBotHub.tsx
@@ -6,6 +6,16 @@ import { imUserAPI } from '@/api';
const PRIMARY = '#5B5FED';
const PRIMARY_ACTIVE = '#4A4ECC';
+// 后端按北京时间存储(无时区标记的 ISO),此处显式按 Asia/Shanghai 渲染,
+// 避免浏览器本地时区导致对话时间错乱(与个人微信绑定页一致)。
+function formatBjTime(t?: string): string {
+ if (!t) return '';
+ const iso = /[Z+]/.test(t) ? t : t + '+08:00';
+ const d = new Date(iso);
+ if (isNaN(d.getTime())) return t;
+ return d.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false });
+}
+
const ImBotHub: React.FC = () => {
const [searchParams, setSearchParams] = useSearchParams();
const highlight = searchParams.get('platform');
@@ -269,7 +279,7 @@ const HistoryModal: React.FC<{ schema: any; data: any; loading: boolean; onClose