package qq_obv11 // 本文件承载 OneBot v11 的**入站附件字节内联**(切片 IM-1 / IM-IMG):文件类消息段 // (数组形态的 `{type:"file",data:{...}}` 与 CQ 码形态的 `[CQ:file,...]`)与**图片段** // (`{type:"image",data:{url:...}}`)里,实现方通常会给出一个 `url`(下载地址), // 字节就照它取回 —— 文件与图片走**同一条路、同一份来源白名单**(图片多出来的只是 // 「字节必须是白名单光栅图」与 5 MiB 的图片上限)。 // // 三条边界: // - **只认平台系资产主机**:白名单 = 腾讯系 CDN(`*.qq.com`、`*.qq.com.cn`、 // `*.qpic.cn` 的 http/https 两种协议,覆盖 `multimedia.nt.qq.com.cn` 与 // `gchat.qpic.cn`)+ 部署自己声明的 OneBot API 端点(本地实现常自己下发资产 URL)。 // 消息段里的 `url` 是对端上报的内容,白名单是硬门:不在名单里一个字节都不发出去; // - **没有真 url 就不下载**:段里只有文件 id/文件名时(`url` 缺失,`file` 被当作 // 引用写进了 URL 字段),它连绝对 http(s) 地址都不是 ⇒ 白名单直接否决,绝不 // 拼凑地址去猜; // - 尺寸/超时/预筛/日志口径全在 `internal/channel/mediafetch`(四个适配器共用)。 import ( "context" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/mediafetch" ) // 腾讯系资产主机白名单:OneBot 实现上报的文件与**图片**地址都落在这些子树里(`.qq.com` // 家族覆盖 `multimedia.nt.qq.com.cn` 之外的多媒体域名,`.qpic.cn` 覆盖图片/文件 CDN)。 // 通配条目**不含**裸的 `qq.com` 本身,因此不会顺带放行 `qq.com` 主页这类无关主机。 // // ⚠️ **两种协议都在名单里,这不是第三份名单,而是同一份名单补上 OneBot 的真实形状**: // `http://` 变体来自经典实现下发图片地址的实际形状(`http://gchat.qpic.cn/gchatpic_new/…`), // 只放行 `https://` 会让那些图片**永远取不到**(白名单拒绝 = 一个字节都不发,模型只能收到 // 「读不到」)。放行面仍是**平台自己的 CDN 主机**(子域由腾讯控制,发送方无法把请求引到 // 别的主机),且取回的字节仍要过 `mediafetch` 的图片白名单(5 MiB + 字节嗅探),因此 // 明文协议只影响「与腾讯 CDN 之间是否 TLS」,不扩大可达主机面。 const ( qqAssetOriginQQ = "https://*.qq.com" qqAssetOriginQQCN = "https://*.qq.com.cn" qqAssetOriginQpic = "https://*.qpic.cn" qqAssetOriginQQHTTP = "http://*.qq.com" qqAssetOriginQQCNHTTP = "http://*.qq.com.cn" qqAssetOriginQpicHTTP = "http://*.qpic.cn" ) // inlineInboundMedia 把受支持格式的附件(**文件类与图片类**)取回并内联到消息上。失败不抛出: // 该附件保持无字节,由 channelbridge 的入站摄取照既有口径给模型一行诚实提示。 func (a *Adapter) inlineInboundMedia(ctx context.Context, msg *adapter.UnifiedMessage) { mediafetch.Inline(ctx, a.log, msg, qqFetcher{a: a}) } // qqFetcher 实现 mediafetch.Fetcher:OneBot 上报的 `url` 就是资产地址。 type qqFetcher struct{ a *Adapter } // Fetch 按来源白名单取回一份平台资产(**文件与图片同一条路**)。下载**不复用**出站 // HTTP API 的客户端:那个客户端带着 OneBot access_token 头,绝不能把它发到 CDN 主机上, // 因此这里交给 mediafetch 自建一个干净客户端。 func (f qqFetcher) Fetch(ctx context.Context, m adapter.Media) ([]byte, error) { return mediafetch.Fetch(ctx, mediafetch.Request{ URL: m.URL, Allow: f.a.assetAllowlist(), Timeout: mediafetch.DefaultTimeout, }) } // assetAllowlist 给出 OneBot 平台资产的来源白名单:腾讯系 CDN(文件与**图片**共用同一份, // 见上面两种协议的说明)+ 已配置的 OneBot API 端点(`api_base_url`,本地部署常是 // `http://127.0.0.1:5700` 这类地址 —— 它由部署显式声明,可信度与其余 API 调用一致)。 // `api_base_url` 为空时只用腾讯系条目。图片不另立名单:它落的是同一批主机, // 多一份名单只会多一处漂移。 func (a *Adapter) assetAllowlist() mediafetch.Allowlist { origins := []string{ qqAssetOriginQQ, qqAssetOriginQQCN, qqAssetOriginQpic, qqAssetOriginQQHTTP, qqAssetOriginQQCNHTTP, qqAssetOriginQpicHTTP, } if origin, err := mediafetch.OriginOf(a.cfg.APIBaseURL); err == nil { origins = append(origins, origin) } return mediafetch.NewAllowlist(origins...) }