修复:企微群聊不发送告警截图

根因:_get_presigned_url 对完整 COS 永久 URL 直接原样返回,
未重新生成预签名 URL,导致私有桶的图片下载失败(403)。

修复:
1. _get_presigned_url 增加 COS URL 识别,提取 object key 重新签名
2. 新增 _extract_cos_object_key 解析两种 COS URL 格式
3. send-card 增加截图诊断日志(追踪 IoT/DB 来源和最终 URL)
4. upload_media_from_url 增加下载/上传诊断日志
This commit is contained in:
2026-03-30 14:30:01 +08:00
parent 342fbd87b5
commit 9513951b1b
3 changed files with 49 additions and 1 deletions

View File

@@ -128,6 +128,11 @@ async def send_card(request: Request):
if alarm_snapshot_key:
presigned_url = _get_presigned_url(alarm_snapshot_key)
logger.info(f"群聊截图诊断: alarm={req.alarmId}, "
f"iot_snapshot={req.snapshotUrl!r}, "
f"db_snapshot={alarm_snapshot_key!r}, "
f"presigned_url={presigned_url[:80] if presigned_url else '(空)'}...")
# alarm_type: 用 alarm_type_code 避免"告警告警"
actual_alarm_type = alarm_type_code or req.title

View File

@@ -144,10 +144,19 @@ async def process_alarm_notification(alarm_data: Dict):
def _get_presigned_url(snapshot_url: str) -> str:
"""将 COS object key 转为预签名 URL"""
"""将 COS object key 或永久 URL 转为预签名 URL"""
if not snapshot_url:
return ""
# 如果是完整 COS 永久 URL提取 object key 后重新生成预签名 URL
if snapshot_url.startswith("http"):
object_key = _extract_cos_object_key(snapshot_url)
if object_key:
try:
from app.services.oss_storage import get_oss_storage
return get_oss_storage().get_presigned_url(object_key)
except Exception as e:
logger.warning(f"从COS URL生成预签名失败: {e}")
# 无法提取 key返回原 URL可能是外部 URL
return snapshot_url
try:
from app.services.oss_storage import get_oss_storage
@@ -157,6 +166,37 @@ def _get_presigned_url(snapshot_url: str) -> str:
return snapshot_url
def _extract_cos_object_key(url: str) -> str:
"""从 COS 永久 URL 中提取 object key
支持格式:
- https://{bucket}.cos.{region}.myqcloud.com/{key}
- https://cos.{region}.myqcloud.com/{bucket}/{key}
"""
try:
from urllib.parse import urlparse
parsed = urlparse(url)
host = parsed.hostname or ""
path = parsed.path.lstrip("/")
if not path or "myqcloud.com" not in host:
return ""
# 格式1: {bucket}.cos.{region}.myqcloud.com/{key}
if ".cos." in host and host.endswith(".myqcloud.com"):
return path
# 格式2: cos.{region}.myqcloud.com/{bucket}/{key}
if host.startswith("cos.") and host.endswith(".myqcloud.com"):
parts = path.split("/", 1)
if len(parts) == 2:
return parts[1]
return ""
except Exception:
return ""
def _save_vlm_result(alarm_id: str, vlm_result: Dict):
"""将 VLM 复核结果写入 alarm_llm_analysis 表"""
db = get_session()

View File

@@ -218,9 +218,12 @@ class WeChatService:
async def upload_media_from_url(self, image_url: str) -> Optional[str]:
"""从 URL 下载图片后上传到企微,返回 media_id"""
logger.info(f"企微截图上传: 开始下载 url={image_url[:100]}...")
image_data = await self._download_image(image_url)
if not image_data:
logger.warning(f"企微截图上传: 下载失败,无法上传 url={image_url[:100]}")
return None
logger.info(f"企微截图上传: 下载成功size={len(image_data)} bytes开始上传到企微")
return await self.upload_media(image_data)
# ==================== 个人消息:按钮交互型模板卡片 ====================