hqharnesstools · 内网 mediaUrl 换 COS 临时外链

chatbot 上行给 hqharness 的富媒体使用内网 HMAC URL/api/internal/...?sig=...), 供同网段 agent 拉取字节。当 hqharnesstools 需将图片 / 视频 URL 直接交给外部大模型时, 可调用本接口将 WS 帧中的 messages[].mediaUrl 兑换为 COS 预签名公网临时链接。

文档类 file 仍建议内网 GET 下载后在 hqharnesstools 内处理。 详细协议见仓库文档 docs/hqh-inbound-rich-media-对接说明.md

接口基址:/api/internal/media/presign

鉴权与安全

机制说明
源 IP CIDR 请求须来自内网(默认允许 10/8172.16/12192.168/16127/8)。 可通过 WECOM_MSGAUDIT_AGENT_MEDIA_ALLOWED_CIDRS 调整。
URL 签名 请求体中的 mediaUrl 须含有效 sig(HMAC-SHA256), 与上行 WS 帧中的内网链接一致;无效 sig 返回 403。
无需登录 接口在 /api/internal/** 下,Spring Security 放行,不校验登录态与 CSRF; 依赖 IP 白名单 + URL sig。

兑换 COS 预签名 URL

POST /api/internal/media/presign

请求头

Header必填说明
Content-Type application/json

请求体

字段类型说明
mediaUrl 必填 string 上行 WS 帧中的内网 messages[].mediaUrl,可为完整 URL 或仅 path+query。 示例:http://10.0.0.5:8080/api/internal/wecom/msgaudit/media/MSGID?sig=.../api/internal/sim/media/{id}?sig=...

请求示例

curl -sS -X POST 'http://<chatbot-internal-host>/api/internal/media/presign' \
  -H 'Content-Type: application/json' \
  -d '{"mediaUrl":"http://10.0.0.5:8080/api/internal/wecom/msgaudit/media/MSGID?sig=abc123..."}'

响应字段(200)

字段类型说明
urlstringCOS 预签名 GET URL,可公网访问,有效期内可直接交给外部 LLM
expiresAtstring (ISO-8601)预签名过期时间(UTC)
contentTypestring | null媒体 MIME,如 image/jpeg

响应示例

{
  "url": "https://your-bucket.cos.ap-guangzhou.myqcloud.com/wecom-msgaudit/...?sign=...",
  "expiresAt": "2026-07-12T16:04:00Z",
  "contentType": "image/jpeg"
}

支持的 mediaUrl 路径

通道Path 模式说明
企微存档 /api/internal/wecom/msgaudit/media/{msgid} WecomBridgeService 上行 agentMediaUrl 一致
QiWe /api/internal/qiwe/media/{msgUid} 需启用 QIWE_ENABLED=true
模拟器 /api/internal/sim/media/{mediaId} 附件须已存 COS;本地存储返回 422

注意:管理后台浏览器路径 /api/wecom/msgaudit/media/{msgid}(无 /internal/、无 sig不能用于本接口;须使用 WS 上行帧中的内网 URL。

错误码

HTTP说明
400mediaUrl 缺失,或 URL 格式无法识别 / 缺少 sig
403源 IP 不在 CIDR 白名单,或 sig 无效
404媒体不存在、尚未上传 COS,或 QiWe 通道未启用
422模拟器附件为本地存储,无法生成 COS 外链
502生成 COS 预签名 URL 失败
503COS 未配置(COS_ENABLED=true 及 bucket/密钥等)

前置条件与配置

TypeScript 调用示例

if (item.contentType === 'image' || item.contentType === 'video') {
  const resp = await fetch(`${CHATBOT_BASE}/api/internal/media/presign`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ mediaUrl: item.mediaUrl }),
  });
  if (!resp.ok) throw new Error(`presign failed: ${resp.status}`);
  const { url } = await resp.json();
  // 将 url 传给外部大模型
}