80 lines
4.6 KiB
Go
80 lines
4.6 KiB
Go
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...)
|
||
}
|