ADR-010 Segment vs Attachment 双模型设计
约 575 字大约 2 分钟
2026-03-19
为什么 NcatBot 同时存在
DownloadableSegment和Attachment两套"可下载媒体"模型?
背景
NcatBot 需要处理两类场景下的可下载资源:
- 消息内嵌媒体 — QQ 消息中的图片、语音、视频、文件,作为消息的组成部分出现。
- 独立附件 — GitHub Release 资产、文件系统文件等,与消息流无关,是独立的可下载对象。
两者都可"下载",但语义和生命周期完全不同。
决策
保留两套模型,用明确的桥接方法互转。
DownloadableSegment
- 定位:消息的子段(MessageArray 中的一项)
- 来源:QQ/OneBot 协议的消息结构
- 字段:
file(路径/URL/CQ 码)、url(HTTP URL,可能为空)、file_id、file_size、file_name - 特点:
file字段的值不一定是 HTTP URL,可能是 QQ 内部路径(file://)、base64 编码(base64://)或本地路径 - 子类:
Image、Video、Record、File
Attachment
- 定位:独立的跨平台可下载对象
- 来源:GitHub Release 资产、DownloadableSegment 转换、或其他平台原生数据
- 字段:
name、url(必须是有效 HTTP/HTTPS URL)、size、content_type、kind、extra - 特点:
url保证可直接用于aiohttp.get()下载 - 子类:
ImageAttachment、VideoAttachment、AudioAttachment、FileAttachment
桥接
| 方向 | 方法 | 约束 |
|---|---|---|
| Segment → Attachment | seg.to_attachment() | 仅当 seg.url 存在且为 HTTP URL 时设 url;否则 url 为空串,download() 不可用 |
| Attachment → Segment | att.to_segment() | 使用 att.url |
| Attachment → Segment(安全) | att.to_local_segment(cache_dir) | 先下载到本地再转 |
| 批量提取 | MessageArray.get_attachments() | 遍历所有 DownloadableSegment,调用 to_attachment() |
关键约束
- Attachment.url 必须是 HTTP(S) URL —
download()和as_bytes()依赖aiohttp。 - to_attachment() 不用 seg.file 做 URL 回退 —
seg.file可能是本地路径或 QQ 内部路径,不是合法 URL。 - Attachment 不持有对原始 Segment 的引用 — 保持纯数据模型,避免循环依赖。
替代方案(已否决)
| 方案 | 否决原因 |
|---|---|
| 只保留 Segment | Segment 与 OneBot 消息结构耦合,无法表示 GitHub Release 等非消息附件 |
| 只保留 Attachment | 会丢失 Segment 的消息上下文(位置、类型标识),且现有 MessageArray 生态依赖 Segment |
| Attachment 继承 Segment | 两者所属层级不同(Types 层 vs Event/API 桥接),继承会引入循环依赖 |
影响
DownloadableSegment.to_attachment()的url字段只取seg.url,不 fallback 到seg.file- GitHub
get_attachments()使用FileAttachment而非基类Attachment - 插件端统一使用 Attachment API 处理跨平台文件:
download()/as_bytes()/upload_attachment()
版权所有
版权归属:huan-yp
