Files

80 lines
4.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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...)
}