dsh-qqchat 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en.md +276 -0
- package/README.md +211 -0
- package/client-src/plugin.cts +15 -0
- package/client-src/settings.cts +216 -0
- package/client-src/shared.cts +104 -0
- package/client-src/workspace.cts +164 -0
- package/cordis.patch.yml +7 -0
- package/docs/ARCHITECTURE.en.md +346 -0
- package/docs/ARCHITECTURE.md +411 -0
- package/docs/DEVELOPMENT.md +69 -0
- package/docs/MEDIA_AND_QUOTES.md +644 -0
- package/docs/MEMORY.md +99 -0
- package/docs/MEMORY_CONTEXT.md +254 -0
- package/docs/QUOTES.md +355 -0
- package/docs/README.md +22 -0
- package/docs/SECURITY.md +62 -0
- package/docs/STREAMING.md +347 -0
- package/docs/assets//346/211/253/347/240/201/351/223/276/346/216/245.jpg +0 -0
- package/docs/assets//347/276/244/350/201/212/347/225/214/351/235/242.jpg +0 -0
- package/docs/assets//350/256/260/345/277/206/347/263/273/347/273/237.jpg +0 -0
- package/docs/assets//350/256/276/347/275/256/347/225/214/351/235/242.jpg +0 -0
- package/docs/devlog.md +24 -0
- package/lib/client.js +433 -0
- package/lib/commands/dispatch.d.ts +16 -0
- package/lib/commands/dispatch.js +48 -0
- package/lib/config.d.ts +3 -0
- package/lib/config.js +63 -0
- package/lib/gateway/api.d.ts +30 -0
- package/lib/gateway/api.js +202 -0
- package/lib/gateway/auth.d.ts +35 -0
- package/lib/gateway/auth.js +80 -0
- package/lib/gateway/crypto.d.ts +2 -0
- package/lib/gateway/crypto.js +17 -0
- package/lib/gateway/gateway.d.ts +32 -0
- package/lib/gateway/gateway.js +219 -0
- package/lib/gateway/normalize.d.ts +6 -0
- package/lib/gateway/normalize.js +248 -0
- package/lib/index.d.ts +14 -0
- package/lib/index.js +35 -0
- package/lib/media/image-tool.d.ts +4 -0
- package/lib/media/image-tool.js +62 -0
- package/lib/media/media-tools.d.ts +3 -0
- package/lib/media/media-tools.js +68 -0
- package/lib/media/store.d.ts +14 -0
- package/lib/media/store.js +176 -0
- package/lib/session/agent-bridge.d.ts +56 -0
- package/lib/session/agent-bridge.js +613 -0
- package/lib/session/memory-snapshot.d.ts +23 -0
- package/lib/session/memory-snapshot.js +27 -0
- package/lib/session/runtime.d.ts +41 -0
- package/lib/session/runtime.js +395 -0
- package/lib/shared/augmentations.d.ts +40 -0
- package/lib/shared/augmentations.js +1 -0
- package/lib/shared/logging.d.ts +14 -0
- package/lib/shared/logging.js +41 -0
- package/lib/storage/db.d.ts +82 -0
- package/lib/storage/db.js +669 -0
- package/lib/storage/memory.d.ts +50 -0
- package/lib/storage/memory.js +566 -0
- package/lib/transport/rpc.d.ts +18 -0
- package/lib/transport/rpc.js +318 -0
- package/lib/types.d.ts +416 -0
- package/lib/types.js +1 -0
- package/package.json +115 -0
- package/scripts/wrap-client.mjs +20 -0
- package/src/commands/dispatch.ts +75 -0
- package/src/config.ts +70 -0
- package/src/gateway/api.ts +260 -0
- package/src/gateway/auth.ts +99 -0
- package/src/gateway/crypto.ts +16 -0
- package/src/gateway/gateway.ts +207 -0
- package/src/gateway/normalize.ts +236 -0
- package/src/index.ts +46 -0
- package/src/media/image-tool.ts +62 -0
- package/src/media/media-tools.ts +66 -0
- package/src/media/store.ts +157 -0
- package/src/session/agent-bridge.ts +624 -0
- package/src/session/memory-snapshot.ts +41 -0
- package/src/session/runtime.ts +368 -0
- package/src/shared/augmentations.ts +45 -0
- package/src/shared/logging.ts +36 -0
- package/src/storage/db.ts +760 -0
- package/src/storage/memory.ts +581 -0
- package/src/transport/rpc.ts +316 -0
- package/src/types.ts +456 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.client.json +14 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
# 媒体与引用消息方案
|
|
2
|
+
|
|
3
|
+
## 易读概述
|
|
4
|
+
|
|
5
|
+
本文审查当前 `dsh-qqchat`、官方 `dsh-qqbot` 与咕咕的媒体和引用消息处理方式,并给出 `dsh-qqchat` 后续实现方案。
|
|
6
|
+
|
|
7
|
+
目标是让 QQ 中的图片、语音、视频、文件和引用消息能够:
|
|
8
|
+
|
|
9
|
+
- 被稳定接收、保存和清理;
|
|
10
|
+
- 正确进入 DSH Agent 上下文;
|
|
11
|
+
- 在模型支持时真正被理解,而不是只显示一个 URL;
|
|
12
|
+
- 在 DSH Web 和 QQ 侧保留清晰的消息关系;
|
|
13
|
+
- 不破坏现有 Session、记忆、权限和消息兼容格式。
|
|
14
|
+
|
|
15
|
+
本文是设计与审查文档,不代表所有功能已经实现。
|
|
16
|
+
|
|
17
|
+
当前实现进度:Phase 0–2 的基础链路已完成,Phase 3–4 已完成媒体接收、元数据工具、引用关系和附件复用的 Host 侧基础能力。已包含标准化媒体/引用类型、QQ 附件解析、SQLite 附件关系、安全下载、媒体清理、图片附件接入、`qqchat_describe_image`、`qqchat_read_file`、`qqchat_media_info` 和 JSON 可序列化的 Web transcript 摘要。DSH Web 中针对自定义 transcript 的原生媒体 renderer、文件/音频/视频播放能力、短 TTL 媒体 URL 和真实环境验收仍未完成,不能视为完整的 Phase 3–4 交付。
|
|
18
|
+
|
|
19
|
+
Web 渲染后续采用“DSH 原生优先”的方案:触发 Agent 的消息使用 DSH 原生消息内容和 `ImageBlock`;静默记录或未触发 Agent 的 QQ transcript 只增加最小的附件适配,不另建一套气泡、图片预览、文件卡片或媒体主题。这样不会为了显示媒体而改变群聊接收模式或额外唤醒 Agent。
|
|
20
|
+
|
|
21
|
+
## 结论
|
|
22
|
+
|
|
23
|
+
官方 `dsh-qqbot` 已经支持媒体和引用,但主链路采用“安全下载到本地文件,再把路径和元数据注入文本上下文”的方案。图片理解通过独立的视觉工具完成,视频和文件则交给 `ffmpeg`、文件工具等能力处理。
|
|
24
|
+
|
|
25
|
+
咕咕的实现更完整:它有持久化附件 ID、消息级附件关系、图片原生多模态输入、语音模型、视频压缩和模型能力路由。不过咕咕也依赖更多供应商扩展,复杂度和维护成本更高。
|
|
26
|
+
|
|
27
|
+
`dsh-qqchat` 建议采用分阶段方案:
|
|
28
|
+
|
|
29
|
+
1. 先建立统一的媒体与引用数据模型和安全下载层;
|
|
30
|
+
2. 默认采用官方兼容模式,媒体路径和元数据进入 Agent 上下文;
|
|
31
|
+
3. 图片在视觉模型可用时通过 DSH 官方 attachment seam 转成 `ImageBlock`;
|
|
32
|
+
4. 后续按模型能力逐步增加原生视频、音频输入;
|
|
33
|
+
5. 引用消息与普通消息共享同一套附件和生命周期管理。
|
|
34
|
+
|
|
35
|
+
## 现状审查
|
|
36
|
+
|
|
37
|
+
### dsh-qqchat 当前状态
|
|
38
|
+
|
|
39
|
+
当前插件已经支持基础媒体和引用处理:
|
|
40
|
+
|
|
41
|
+
- 从 `message_reference`、`reference`、`quote` 和 QQ 原生 `message_scene.ext` 中提取引用文本;
|
|
42
|
+
- 将引用文本保存到 `messages.quoted_text`;
|
|
43
|
+
- 将引用正文和附件保存为独立结构;
|
|
44
|
+
- 使用 SQLite 引用索引按账号、聊天范围和 `msg_idx` 回填历史引用;
|
|
45
|
+
- 对已保存的引用附件复用现有附件 ID 和物理文件;
|
|
46
|
+
- 给 Agent 和 DSH Web 组装独立引用块;
|
|
47
|
+
- 发送回复时使用当前 QQ 消息 ID 作为回复目标。
|
|
48
|
+
|
|
49
|
+
当前仍未完成的媒体/引用能力:
|
|
50
|
+
|
|
51
|
+
- 真实 QQ C2C 图片引用和网关重启恢复验收;
|
|
52
|
+
- 机器人出站消息的完整引用索引;
|
|
53
|
+
- 多次引用、过期引用和下载失败的真实集成测试;
|
|
54
|
+
- DSH Web 自定义 transcript 的完整原生媒体 renderer。
|
|
55
|
+
|
|
56
|
+
引用索引的专项实施方案见 [QUOTES.md](QUOTES.md)。
|
|
57
|
+
|
|
58
|
+
### 官方 dsh-qqbot
|
|
59
|
+
|
|
60
|
+
官方仓库的入站链路会将消息拆成文本、引用、发送人标签、媒体元数据和历史上下文。媒体附件会被安全下载到本地目录,并在 Agent 文本上下文中标记类型、名称和本地路径。
|
|
61
|
+
|
|
62
|
+
图片分析由 `qqbot_describe_image` 完成:工具读取本地图片,保存为 DSH 图片附件引用,然后发起包含图片块的视觉请求。也就是说,官方支持 DSH 的原生图片输入,但不是把每一条 QQ 图片消息都直接作为主 Agent turn 的图片块。
|
|
63
|
+
|
|
64
|
+
官方还包含以下安全和生命周期措施:
|
|
65
|
+
|
|
66
|
+
- 仅允许 HTTPS;
|
|
67
|
+
- 下载前后检查域名解析结果,阻止内网和本机地址;
|
|
68
|
+
- 单文件和单消息大小限制;
|
|
69
|
+
- 超时和重定向限制;
|
|
70
|
+
- 文件名清理和随机文件名;
|
|
71
|
+
- 定期删除过期媒体文件。
|
|
72
|
+
|
|
73
|
+
参考:
|
|
74
|
+
|
|
75
|
+
- [官方仓库](https://github.com/tencent-connect/dsh-qqbot/tree/main)
|
|
76
|
+
- [官方媒体目录](https://github.com/tencent-connect/dsh-qqbot/tree/main/src/media)
|
|
77
|
+
- [官方图片视觉工具](https://github.com/tencent-connect/dsh-qqbot/blob/main/src/media/vision-tool.ts)
|
|
78
|
+
- [官方附件下载](https://github.com/tencent-connect/dsh-qqbot/blob/main/src/transport/attachment.ts)
|
|
79
|
+
- [官方入站上下文组装](https://github.com/tencent-connect/dsh-qqbot/blob/main/src/transport/inbound.ts)
|
|
80
|
+
|
|
81
|
+
### 咕咕
|
|
82
|
+
|
|
83
|
+
咕咕当前的媒体链路比官方方案更丰富:
|
|
84
|
+
|
|
85
|
+
- QQ 附件在 worker 侧统一下载和暂存;
|
|
86
|
+
- 使用 `attach_id` 和 `chat_attachments` 管理附件;
|
|
87
|
+
- 图片可根据当前模型能力构造成图片输入;
|
|
88
|
+
- 支持图片缩放、压缩、数量限制和格式转换;
|
|
89
|
+
- 语音可使用独立语音识别模型;
|
|
90
|
+
- 视频支持时长、大小、码率和分辨率限制,并可按供应商要求压缩或转为 `mm_file`;
|
|
91
|
+
- 文件可作为附件卡片显示,也可由 Agent 读取或保存到文件库;
|
|
92
|
+
- 引用附件会被识别并标记为 `quoted`;
|
|
93
|
+
- 会话删除、孤儿附件、视频转码缓存都有清理路径。
|
|
94
|
+
|
|
95
|
+
咕咕的“引用附件复用”方案还需要区分设计和完整落地状态:它已经有消息级引用附件接入和持久化附件基础,但通过稳定消息 ID 复用历史附件、引用计数和并发删除保护主要记录在其设计文档中,不能默认视为已经全部完成。
|
|
96
|
+
|
|
97
|
+
## 设计原则
|
|
98
|
+
|
|
99
|
+
### 身份和引用分离
|
|
100
|
+
|
|
101
|
+
引用关系必须使用 QQ 稳定消息 ID 和发送人稳定 ID,不能用昵称或引用文本作为唯一键。
|
|
102
|
+
|
|
103
|
+
昵称只用于显示;消息 ID 用于关联附件、查找历史消息和避免重复下载。
|
|
104
|
+
|
|
105
|
+
### 附件和消息分离
|
|
106
|
+
|
|
107
|
+
消息正文不直接保存二进制数据。消息只保存附件引用和必要的展示元数据,物理文件由附件存储层管理。
|
|
108
|
+
|
|
109
|
+
### 主 Agent 与媒体工具分层
|
|
110
|
+
|
|
111
|
+
默认不要因为收到图片就强制当前模型切换成视觉模型。媒体首先进入统一附件层,再根据当前模型能力决定:
|
|
112
|
+
|
|
113
|
+
- 直接构造原生图片/音视频输入;
|
|
114
|
+
- 调用图片描述工具;
|
|
115
|
+
- 交给文件工具或 `ffmpeg`;
|
|
116
|
+
- 只注入“当前模型无法处理”的清晰文本提示。
|
|
117
|
+
|
|
118
|
+
### 引用消息复用普通消息能力
|
|
119
|
+
|
|
120
|
+
引用消息不是另一种孤立的消息类型。它应当复用普通消息的文本、附件、记忆、权限和展示逻辑,仅额外携带引用关系。
|
|
121
|
+
|
|
122
|
+
### 可恢复优先
|
|
123
|
+
|
|
124
|
+
附件下载失败、模型不支持媒体、引用消息过期或附件被清理时,都应降级为可理解的文本提示,不能让整轮 Agent 因附件异常崩溃。
|
|
125
|
+
|
|
126
|
+
## 推荐数据模型
|
|
127
|
+
|
|
128
|
+
### 标准化入站结构
|
|
129
|
+
|
|
130
|
+
建议在 QQ Gateway 和 Agent 之间使用如下结构:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
interface QQAttachmentInput {
|
|
134
|
+
sourceUrl?: string
|
|
135
|
+
filename?: string
|
|
136
|
+
contentType?: string
|
|
137
|
+
size?: number
|
|
138
|
+
width?: number
|
|
139
|
+
height?: number
|
|
140
|
+
durationMs?: number
|
|
141
|
+
platformFileId?: string
|
|
142
|
+
quoted?: boolean
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface QQQuoteInput {
|
|
146
|
+
messageId?: string
|
|
147
|
+
senderId?: string
|
|
148
|
+
senderName?: string
|
|
149
|
+
text: string
|
|
150
|
+
attachments: QQAttachmentInput[]
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface QQNormalizedMessage {
|
|
154
|
+
// 现有字段省略
|
|
155
|
+
text: string
|
|
156
|
+
attachments: QQAttachmentInput[]
|
|
157
|
+
quote?: QQQuoteInput
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`raw` 仍可保留用于诊断,但 Agent、存储和 UI 不应直接依赖 QQ 原始 payload。
|
|
162
|
+
|
|
163
|
+
### 附件持久化结构
|
|
164
|
+
|
|
165
|
+
第一版可以使用 SQLite 管理元数据,物理文件存放在插件数据目录或 DSH 官方附件存储中:
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
attachments
|
|
169
|
+
id
|
|
170
|
+
account_id
|
|
171
|
+
owner_scope
|
|
172
|
+
source_message_id
|
|
173
|
+
source_file_id
|
|
174
|
+
sha256
|
|
175
|
+
kind image | audio | video | voice | file
|
|
176
|
+
filename
|
|
177
|
+
content_type
|
|
178
|
+
size_bytes
|
|
179
|
+
width
|
|
180
|
+
height
|
|
181
|
+
duration_ms
|
|
182
|
+
storage_key
|
|
183
|
+
status staged | attached | expired | deleted | failed
|
|
184
|
+
created_at
|
|
185
|
+
expires_at
|
|
186
|
+
|
|
187
|
+
message_attachments
|
|
188
|
+
message_id
|
|
189
|
+
attachment_id
|
|
190
|
+
role own | quoted
|
|
191
|
+
ordinal
|
|
192
|
+
|
|
193
|
+
message_quotes
|
|
194
|
+
message_id
|
|
195
|
+
quoted_platform_message_id
|
|
196
|
+
quoted_sender_id
|
|
197
|
+
quoted_sender_name
|
|
198
|
+
quoted_text
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
如果后续完全使用 DSH 的 durable attachment API,可以将 `storage_key` 替换为 DSH attachment ref,但消息级关系仍应由插件保存,因为 QQ 的消息 ID、引用角色和群范围属于插件领域数据。
|
|
202
|
+
|
|
203
|
+
## 入站媒体处理流程
|
|
204
|
+
|
|
205
|
+
```text
|
|
206
|
+
QQ dispatch
|
|
207
|
+
↓
|
|
208
|
+
校验 account / chat / sender stable ID
|
|
209
|
+
↓
|
|
210
|
+
解析正文、@、引用和原始附件
|
|
211
|
+
↓
|
|
212
|
+
标准化 QQAttachmentInput
|
|
213
|
+
↓
|
|
214
|
+
校验 URL、大小、类型、重定向和 DNS
|
|
215
|
+
↓
|
|
216
|
+
下载并写入 staged attachment
|
|
217
|
+
↓
|
|
218
|
+
写入消息与 message_attachments 关系
|
|
219
|
+
↓
|
|
220
|
+
根据接收模式决定:记录 / 回复 / 丢弃
|
|
221
|
+
↓
|
|
222
|
+
构造 Agent 上下文
|
|
223
|
+
↓
|
|
224
|
+
按模型能力选择媒体输入方式
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
下载必须在 worker 或 Host 的受控边界完成,不能让 Agent 自己根据 QQ URL 随意发起网络请求。
|
|
228
|
+
|
|
229
|
+
推荐的失败降级文本:
|
|
230
|
+
|
|
231
|
+
```text
|
|
232
|
+
用户发送了一张图片:文件名 xxx.png,附件 ID xxx。
|
|
233
|
+
当前模型不支持直接查看图片;如果用户要求保存,可使用附件保存能力。
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
不应把带签名的原始 QQ 下载 URL 长期写入 Session、日志或前端响应。
|
|
237
|
+
|
|
238
|
+
## 引用消息处理流程
|
|
239
|
+
|
|
240
|
+
### 解析顺序
|
|
241
|
+
|
|
242
|
+
引用解析建议按以下优先级执行:
|
|
243
|
+
|
|
244
|
+
1. 读取协议提供的结构化 `message_reference` / `reference` / `quote`;
|
|
245
|
+
2. 读取 QQ 原生 `message_scene.ext` 中的引用消息索引;
|
|
246
|
+
3. 在 `msg_elements` 中按引用索引定位被引用消息;
|
|
247
|
+
4. 从引用节点提取发送人、正文和附件;
|
|
248
|
+
5. 如果只有引用文本,仍然创建 quote 结构;
|
|
249
|
+
6. 如果无法确认引用关系,不要猜测,保留当前消息正文并记录受限诊断信息。
|
|
250
|
+
|
|
251
|
+
### 引用附件处理
|
|
252
|
+
|
|
253
|
+
引用附件应使用和当前消息附件完全相同的下载与安全校验流程,并在关系表中标记 `role=quoted`。
|
|
254
|
+
|
|
255
|
+
当引用消息已经处理过并且附件仍然有效时,可以按以下条件复用物理文件:
|
|
256
|
+
|
|
257
|
+
- 命中稳定的源消息 ID;
|
|
258
|
+
- 附件类型、文件 ID 或内容指纹一致;
|
|
259
|
+
- 源附件未过期、未删除;
|
|
260
|
+
- 新的消息附件关系提交成功前,源附件不能被并发清理。
|
|
261
|
+
|
|
262
|
+
复用时应创建新的消息附件关系或新的附件元数据行,共享物理 `storage_key`,不要让两条消息直接共用一个可独立删除的记录。
|
|
263
|
+
|
|
264
|
+
### Agent 上下文格式
|
|
265
|
+
|
|
266
|
+
建议让引用内容和当前正文明确分块:
|
|
267
|
+
|
|
268
|
+
```text
|
|
269
|
+
[引用消息]
|
|
270
|
+
发送人:Alice
|
|
271
|
+
消息 ID:...
|
|
272
|
+
内容:请看看这张图
|
|
273
|
+
附件:图片 xxx.png(attachment_id=...)
|
|
274
|
+
|
|
275
|
+
[当前消息]
|
|
276
|
+
用户:这个是什么意思?
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
不要把引用正文直接拼接到用户正文中作为无标记文本,否则模型和网页展示都会难以区分消息边界。
|
|
280
|
+
|
|
281
|
+
## 模型输入策略
|
|
282
|
+
|
|
283
|
+
### 第一阶段:官方兼容模式
|
|
284
|
+
|
|
285
|
+
默认采用官方 `dsh-qqbot` 的思路:
|
|
286
|
+
|
|
287
|
+
- 图片、视频、文件先保存为附件;
|
|
288
|
+
- Agent 文本上下文注入媒体类型、名称和附件 ID/受控本地路径;
|
|
289
|
+
- 图片通过 `qqchat_describe_image` 或等价工具分析;
|
|
290
|
+
- 视频交给 `ffmpeg` 和文件工具;
|
|
291
|
+
- 文本文件可以直接读取内容;
|
|
292
|
+
- 无能力处理的媒体只提供结构化提示。
|
|
293
|
+
|
|
294
|
+
优点是对模型要求低,不会改变所有 QQ turn 的模型路由,也不会把大量二进制内容直接塞进上下文缓存。
|
|
295
|
+
|
|
296
|
+
### 第二阶段:图片原生输入
|
|
297
|
+
|
|
298
|
+
当当前模型明确声明支持 image modality 时,可以将图片附件转换为 DSH 官方 `ImageAttachmentRef`,构造 `ImageBlock` 发送给视觉模型。
|
|
299
|
+
|
|
300
|
+
必须满足:
|
|
301
|
+
|
|
302
|
+
- 模型路由声明支持图片;
|
|
303
|
+
- 图片格式为 PNG、JPEG、WebP 或 GIF 等受支持格式;
|
|
304
|
+
- 图片大小、尺寸和数量经过限制;
|
|
305
|
+
- 使用 durable attachment ref,不在 Session event 中放入 Buffer、Blob 或其他不可 JSON 序列化对象;
|
|
306
|
+
- 失败时退回图片工具或文本提示。
|
|
307
|
+
|
|
308
|
+
### 第三阶段:音频和视频
|
|
309
|
+
|
|
310
|
+
音频和视频暂不应伪装成 DSH 通用 `ContentBlock`,因为 DSH 的核心内容块主要覆盖文本、推理、图片和工具结果。应按实际模型路由能力使用供应商适配层:
|
|
311
|
+
|
|
312
|
+
- 语音优先转写成文本;
|
|
313
|
+
- 音频多模态由明确支持的 provider 处理;
|
|
314
|
+
- 视频先做时长、大小和格式检查,必要时压缩;
|
|
315
|
+
- 不支持原生视频的模型使用抽帧、转写或 `ffmpeg` 分析降级。
|
|
316
|
+
|
|
317
|
+
## 存储、清理和安全
|
|
318
|
+
|
|
319
|
+
### 下载安全
|
|
320
|
+
|
|
321
|
+
必须复用统一下载器,至少包含:
|
|
322
|
+
|
|
323
|
+
- 只允许 HTTPS;
|
|
324
|
+
- 连接前和重定向后都检查 URL;
|
|
325
|
+
- DNS 解析结果阻止回环、私网、链路本地和保留地址;
|
|
326
|
+
- 设置连接、读取和总超时;
|
|
327
|
+
- 限制单文件和单消息总大小;
|
|
328
|
+
- 不信任远端文件名和 Content-Type;
|
|
329
|
+
- 使用随机安全文件名;
|
|
330
|
+
- 写入临时文件后原子移动;
|
|
331
|
+
- 下载失败只记录脱敏错误。
|
|
332
|
+
|
|
333
|
+
### 生命周期
|
|
334
|
+
|
|
335
|
+
建议附件状态遵循:
|
|
336
|
+
|
|
337
|
+
```text
|
|
338
|
+
staged → attached → expired → deleted
|
|
339
|
+
↘ failed
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
- `staged`:已下载但尚未与消息提交关联;
|
|
343
|
+
- `attached`:已被消息或引用关系使用;
|
|
344
|
+
- `expired`:超过保留时间但仍保留元数据;
|
|
345
|
+
- `deleted`:物理文件已经删除;
|
|
346
|
+
- `failed`:下载或转换失败。
|
|
347
|
+
|
|
348
|
+
删除物理文件前必须按 `storage_key` 检查是否仍有其他存活引用,避免引用附件复用后误删共享文件。
|
|
349
|
+
|
|
350
|
+
### 日志和隐私
|
|
351
|
+
|
|
352
|
+
日志只记录附件类型、大小、失败原因和脱敏 ID,不记录:
|
|
353
|
+
|
|
354
|
+
- 原始 QQ 签名 URL;
|
|
355
|
+
- 图片、音频或文件内容;
|
|
356
|
+
- AppSecret、Token 和临时密钥;
|
|
357
|
+
- 用户发送的完整文件名(如其中可能包含敏感信息)。
|
|
358
|
+
|
|
359
|
+
## DSH Session 与 Web 展示
|
|
360
|
+
|
|
361
|
+
### Agent turn
|
|
362
|
+
|
|
363
|
+
触发 Agent 的消息应该继续使用 DSH 原生 `user/message`。媒体作为该消息的附件引用或结构化上下文注入,不要额外创建一套并行 AgentLoop。
|
|
364
|
+
|
|
365
|
+
不触发 Agent 的群消息仍可以使用现有的 `qqchat/message` log-only event,但 event payload 必须是 JSON 可序列化的摘要:
|
|
366
|
+
|
|
367
|
+
```json
|
|
368
|
+
{
|
|
369
|
+
"text": "看看这个",
|
|
370
|
+
"attachments": [{
|
|
371
|
+
"id": "att_...",
|
|
372
|
+
"kind": "image",
|
|
373
|
+
"name": "image.png",
|
|
374
|
+
"quoted": false
|
|
375
|
+
}]
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
不能把 `Buffer`、文件句柄、SDK 原始对象或包含循环引用的 payload 直接写入 Session event。这正是此前 `agent/inbox/spliced` 非 JSON 数据错误的同类风险。
|
|
380
|
+
|
|
381
|
+
### DSH Web
|
|
382
|
+
|
|
383
|
+
官方 `dsh-qqbot` 主要负责媒体接收和 Agent 处理,没有单独实现 QQ 媒体 Client 渲染层。`dsh-qqchat` 的 Web 展示需要区分 Agent 消息和 log-only QQ transcript。
|
|
384
|
+
|
|
385
|
+
#### Agent 消息:DSH 原生 ImageBlock
|
|
386
|
+
|
|
387
|
+
当当前模型明确支持图片时,入站图片可以在安全下载并通过 `ctx.attachments.saveImage()` 后,作为同一条 DSH 用户消息的图片块:
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
createUserMessage({
|
|
391
|
+
content: [
|
|
392
|
+
{ type: 'text', text: '用户发来一张图片' },
|
|
393
|
+
{ type: 'image', attachment: imageRef },
|
|
394
|
+
],
|
|
395
|
+
})
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
这样图片由 DSH Web 原生消息渲染,同时可以进入视觉模型。文本模型或附件服务不可用时,必须退回当前的文本提示和 `qqchat_describe_image` 工具路径,不能让整轮消息失败。
|
|
399
|
+
|
|
400
|
+
#### 静默消息:最小适配的附件读取 RPC
|
|
401
|
+
|
|
402
|
+
静默记录、未 @ 的群消息和其他不触发 Agent 的消息继续使用 `qqchat/message`,不能为了显示图片改成普通 `user/message`。自定义事件只携带可序列化的附件摘要:
|
|
403
|
+
|
|
404
|
+
```json
|
|
405
|
+
{
|
|
406
|
+
"messageId": "db:123",
|
|
407
|
+
"content": "看看这个",
|
|
408
|
+
"attachments": [
|
|
409
|
+
{
|
|
410
|
+
"id": "qqatt-xxx",
|
|
411
|
+
"kind": "image",
|
|
412
|
+
"filename": "image.png",
|
|
413
|
+
"sizeBytes": 123456
|
|
414
|
+
}
|
|
415
|
+
]
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
如果 DSH 原生 Conversation renderer 无法直接消费 `qqchat/message` 中的附件引用,Client 才请求受保护的 `attachment/read` RPC。该 RPC 只负责把插件附件转换为 DSH 原生渲染器可接受的输入,不负责定义新的视觉风格。Host 必须在返回内容前校验:
|
|
420
|
+
|
|
421
|
+
- 当前 Session 是否属于对应 QQ 群或私聊;
|
|
422
|
+
- 附件是否属于当前 account、消息和会话范围;
|
|
423
|
+
- 附件是否过期或已删除;
|
|
424
|
+
- 媒体类型和返回大小是否允许;
|
|
425
|
+
- 不向 Client 返回 QQ 原始签名 URL。
|
|
426
|
+
|
|
427
|
+
返回值可以是短期 data URL 或一次性、短 TTL 的本地受保护 URL:
|
|
428
|
+
|
|
429
|
+
```json
|
|
430
|
+
{
|
|
431
|
+
"mime": "image/png",
|
|
432
|
+
"dataUrl": "data:image/png;base64,...",
|
|
433
|
+
"expiresAt": 1750000000000
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
当前 Host 已提供受权限保护的附件读取 RPC,但仓库中尚未完成自定义 transcript event 到 DSH 原生附件 renderer 的 Client 接入。因此这里的“原生显示”和“薄适配层”仍属于待实现方案,不应视为已经交付。读取失败、附件过期或超出限制时,Host 不返回原始 URL。
|
|
438
|
+
|
|
439
|
+
#### 文件、音频和视频
|
|
440
|
+
|
|
441
|
+
这些媒体后续应优先映射到 DSH 已有的附件或文件展示能力,不强行伪装成图片:
|
|
442
|
+
|
|
443
|
+
- 文件:文件名、大小、类型和后续下载/保存动作;
|
|
444
|
+
- 语音:时长、播放按钮和转写状态;
|
|
445
|
+
- 视频:时长、尺寸、缩略图或视频播放入口;
|
|
446
|
+
- 不支持浏览器播放的格式:显示媒体信息,并提示可使用 Agent 工具处理。
|
|
447
|
+
|
|
448
|
+
目前已完成元数据和 Agent 工具侧处理,但尚未完成 DSH Web 的文件、音频、视频 renderer 接入。播放和下载也必须通过有权限、短期有效的附件读取 RPC,不能把 QQ 原始 URL直接暴露给浏览器。
|
|
449
|
+
|
|
450
|
+
#### 引用消息
|
|
451
|
+
|
|
452
|
+
引用消息复用同一套附件组件,但视觉上保持独立:
|
|
453
|
+
|
|
454
|
+
```text
|
|
455
|
+
引用块
|
|
456
|
+
├─ 引用发送人
|
|
457
|
+
├─ 引用文本
|
|
458
|
+
└─ 引用附件预览
|
|
459
|
+
|
|
460
|
+
当前消息气泡
|
|
461
|
+
├─ 当前正文
|
|
462
|
+
└─ 当前附件预览
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
引用附件和当前消息附件都使用 DSH 原生附件引用查询;`quoted` 只控制消息关系,不改变 DSH 的附件展示样式或权限校验逻辑。
|
|
466
|
+
|
|
467
|
+
网页显示必须复用 DSH 原生消息和附件样式:
|
|
468
|
+
|
|
469
|
+
- 图片显示缩略图和可选的打开动作;
|
|
470
|
+
- 语音显示播放条;
|
|
471
|
+
- 视频显示文件卡片或预览入口;
|
|
472
|
+
- 文件显示名称、大小和下载/保存入口;
|
|
473
|
+
- 引用消息显示独立的引用块,正文和引用附件分开;
|
|
474
|
+
- 不在插件里重新实现 DSH 的气泡、输入框、Modal、图片预览、文件卡片或附件基础样式;
|
|
475
|
+
- 插件 Client 只负责把 QQ 消息、引用和附件转换成 DSH renderer 能消费的结构。
|
|
476
|
+
|
|
477
|
+
## 媒体显示实施 TODO
|
|
478
|
+
|
|
479
|
+
### A. DSH 原生能力确认
|
|
480
|
+
|
|
481
|
+
- [x] 确认当前 DSH 核心对 `ImageBlock`、`ImageAttachmentRef` 和附件引用的输入边界;
|
|
482
|
+
- [ ] DSH 当前未向插件公开完整的图片预览、放大、加载失败和空状态组件接口,QQChat 不复制这些基础组件;(待与实际 Web renderer 接口确认)
|
|
483
|
+
- [ ] 确认 DSH 原生文件、音频、视频展示组件是否可以由插件复用;
|
|
484
|
+
- [ ] 确认自定义 `qqchat/message` event 是否能直接承载 DSH 原生附件引用;
|
|
485
|
+
- [x] 自定义 event 不能直接变成原生 `ImageBlock` 时,记录并采用最小适配边界,不复制 DSH 基础组件。
|
|
486
|
+
|
|
487
|
+
### B. Host 附件读取和权限
|
|
488
|
+
|
|
489
|
+
- [x] 增加 `attachment/read` RPC,仅接受 `attachmentId` 和当前 Session 上下文;
|
|
490
|
+
- [x] 校验 Session 是否映射到当前 QQ account、群或私聊;
|
|
491
|
+
- [x] 校验附件是否已经关联到当前消息、引用消息和会话范围;
|
|
492
|
+
- [x] 校验附件状态、过期时间和图片读取大小;媒体类型由入站媒体归一化和 DSH 图片附件服务共同校验;
|
|
493
|
+
- [x] 图片响应优先带回 DSH 原生 `ImageAttachmentRef`;仅为当前自定义 transcript 的图片预览提供受限 data URL;
|
|
494
|
+
- [ ] 当前实现尚未使用短 TTL URL,而是使用受大小限制的 RPC 响应 data URL;后续接入 DSH renderer 后应移除该 fallback;
|
|
495
|
+
- [x] 不把 QQ 原始签名 URL、AppSecret、Token 或本地文件路径返回给 Client;
|
|
496
|
+
- [x] 无权限、过期、删除和超限附件会被拒绝;损坏文件由 RPC 失败处理,不泄露存储路径。
|
|
497
|
+
|
|
498
|
+
### C. Agent 消息原生渲染
|
|
499
|
+
|
|
500
|
+
- [x] 当前模型支持图片时,将 QQ 图片保存为 DSH `ImageAttachmentRef`;
|
|
501
|
+
- [x] 在同一条 DSH `user/message` 中加入文本块和 `ImageBlock`;
|
|
502
|
+
- [x] 让 DSH Web 原生显示 Agent 图片,同时让视觉模型直接接收图片;
|
|
503
|
+
- [x] 当前模型不支持图片时,退回 `qqchat_describe_image` 工具;
|
|
504
|
+
- [x] 附件服务不可用、图片损坏或图片超限时,保留文本提示/工具 fallback,不让整轮媒体失败;
|
|
505
|
+
- [x] 保证所有 `ImageBlock` 在 Session event 发布前已经持久提交;
|
|
506
|
+
- [x] 保证 Session event 中不存在 Buffer、Blob、本地路径、SDK 原始对象或循环引用。
|
|
507
|
+
|
|
508
|
+
### D. 静默 QQ transcript 最小适配
|
|
509
|
+
|
|
510
|
+
- [x] 静默消息继续使用 `qqchat/message`,不为了显示媒体改成 `user/message`;
|
|
511
|
+
- [x] event 只携带 JSON 可序列化的附件摘要:ID、类型、文件名、大小、引用标记;
|
|
512
|
+
- [ ] 优先寻找 DSH 原生 renderer 消费自定义 event 附件的方式;
|
|
513
|
+
- [ ] 当前没有自定义 event 的原生入口,尚未完成无主题的薄适配层;
|
|
514
|
+
- [ ] 薄适配层只负责附件引用转换和加载状态,不复制气泡、Modal、按钮或预览组件;
|
|
515
|
+
- [x] 图片加载通过受保护的附件读取 RPC,保持与 DSH 原生附件引用相同的权限边界;
|
|
516
|
+
- [ ] 文件、语音、视频先使用统一的 DSH 风格媒体卡片承载元数据;原生播放器待 DSH 暴露公开入口后接入;(当前只有 Host 元数据,不代表已有 Client 媒体卡片)
|
|
517
|
+
- [x] 加载失败、过期和无权限状态不显示原始 URL 或路径。
|
|
518
|
+
|
|
519
|
+
### E. 引用消息显示
|
|
520
|
+
|
|
521
|
+
- [x] 引用正文、引用发送人和当前正文保持独立数据结构;
|
|
522
|
+
- [x] 引用附件和当前消息附件复用同一套附件摘要、读取权限和显示组件;
|
|
523
|
+
- [x] `quoted` 只表示消息关系,不改变附件权限和渲染主题;
|
|
524
|
+
- [ ] 引用图片使用与普通图片相同的图片预览路径;原生放大能力待 DSH renderer 公开接口后接入;
|
|
525
|
+
- [ ] 引用文件、语音和视频使用与普通媒体相同的媒体卡片;原生播放器待公开接口后接入;
|
|
526
|
+
- [x] 引用附件复用时延长物理附件生命周期,避免引用关系指向已删除文件;
|
|
527
|
+
- [x] 多附件、无文本引用和引用失效均有稳定的摘要/空状态;嵌套引用按当前 QQ 引用结构扁平化处理。
|
|
528
|
+
|
|
529
|
+
### F. 媒体类型显示
|
|
530
|
+
|
|
531
|
+
- [ ] 图片:受保护的缩略图读取、文件名和失败状态;Agent 消息走 DSH 原生图片块;(Host 已完成,Web transcript 仍待接入)
|
|
532
|
+
- [ ] 文件:文件名、大小和类型元数据;下载/保存动作待 DSH 文件 renderer 公开后接入;(Host 已完成,Web renderer 未完成)
|
|
533
|
+
- [ ] 语音:统一媒体卡片承载类型、文件名和大小;播放/转写待 DSH 原生能力公开后接入;(Host 已完成,Web renderer 未完成)
|
|
534
|
+
- [ ] 视频:统一媒体卡片承载类型、文件名和大小;播放/尺寸入口待 DSH 原生能力公开后接入;(Host 已完成,Web renderer 未完成)
|
|
535
|
+
- [x] 不支持的格式:显示媒体元数据,并可引导使用 Agent 工具处理;
|
|
536
|
+
- [x] 不把媒体 URL 直接拼进消息正文;
|
|
537
|
+
- [x] 不让浏览器直接访问 QQ 临时签名 URL。
|
|
538
|
+
|
|
539
|
+
### G. 测试和验收
|
|
540
|
+
|
|
541
|
+
- [ ] Agent 图片消息在视觉模型下能同时被 Web 显示和模型理解;
|
|
542
|
+
- [ ] 文本模型收到图片时不会导致 Agent turn 失败;
|
|
543
|
+
- [ ] 静默群图片不唤醒 Agent,但能在 Web 中按原生风格显示;
|
|
544
|
+
- [ ] 普通附件和引用附件都能显示;
|
|
545
|
+
- [ ] Session、account、群、成员跨范围读取附件会被拒绝;
|
|
546
|
+
- [ ] 过期、删除、损坏和超限附件不会泄露 URL 或本地路径;
|
|
547
|
+
- [ ] 图片、文件、语音和视频的 UI 不引入独立插件主题;
|
|
548
|
+
- [ ] 覆盖 Session 归档、消息去重、附件复用和并发清理;
|
|
549
|
+
- [ ] 使用真实 DSH Web 构建产物验证,而不是只通过 TypeScript 检查。
|
|
550
|
+
|
|
551
|
+
推荐先完成 A–D,形成最小可用版本;再完成 E–G,补齐引用媒体和真实环境验收。
|
|
552
|
+
|
|
553
|
+
## 权限边界
|
|
554
|
+
|
|
555
|
+
媒体访问必须继承当前 QQ 会话的权限:
|
|
556
|
+
|
|
557
|
+
- 群成员是否能使用工具仍由现有工具权限控制;
|
|
558
|
+
- 媒体附件只能在所属 account、群、成员和 Session 范围内查询;
|
|
559
|
+
- 引用附件不能因为可被 Agent 读取而绕过群成员权限;
|
|
560
|
+
- 保存到文件库需要单独的保存动作,不应默认把所有 QQ 附件永久保存;
|
|
561
|
+
- Owner、普通群成员和私聊用户沿用现有 stable sender ID 判断。
|
|
562
|
+
|
|
563
|
+
## 分阶段实施计划
|
|
564
|
+
|
|
565
|
+
### Phase 0:审计和契约
|
|
566
|
+
|
|
567
|
+
- 增加附件、引用和媒体类型的 TypeScript 类型;
|
|
568
|
+
- 明确 QQ payload 的图片、文件、语音、视频和引用样本;
|
|
569
|
+
- 增加 normalize 单测;
|
|
570
|
+
- 确定附件保留时间、大小限制和模型能力读取方式。
|
|
571
|
+
|
|
572
|
+
### Phase 1:统一入站和持久化
|
|
573
|
+
|
|
574
|
+
- 增加 `attachments` 和 `message_attachments` 表;
|
|
575
|
+
- 标准化当前消息和引用消息附件;
|
|
576
|
+
- 实现安全下载和 staged 状态;
|
|
577
|
+
- 把附件摘要写入消息记录;
|
|
578
|
+
- 保留当前纯文本引用行为作为降级路径。
|
|
579
|
+
|
|
580
|
+
### Phase 2:图片处理
|
|
581
|
+
|
|
582
|
+
- 增加图片附件展示数据;
|
|
583
|
+
- 增加 `qqchat_describe_image`;
|
|
584
|
+
- 复用 DSH attachment 服务构造图片输入;
|
|
585
|
+
- 增加模型不支持图片、图片过大和下载失败的测试。
|
|
586
|
+
|
|
587
|
+
### Phase 3:文件、语音和视频
|
|
588
|
+
|
|
589
|
+
- 文本文件读取;
|
|
590
|
+
- 语音转写适配;
|
|
591
|
+
- `ffmpeg` 探测、抽帧和转码;
|
|
592
|
+
- 供应商能力路由;
|
|
593
|
+
- 媒体缓存和转码缓存清理。
|
|
594
|
+
|
|
595
|
+
### Phase 4:引用附件复用和完整 Web 展示
|
|
596
|
+
|
|
597
|
+
- 按稳定 QQ 消息 ID 查找历史附件;
|
|
598
|
+
- 处理共享 `storage_key` 的引用计数;
|
|
599
|
+
- 增加引用附件预览和关系展示;
|
|
600
|
+
- 覆盖多附件、嵌套引用、过期附件和并发清理场景。
|
|
601
|
+
|
|
602
|
+
## 验收标准
|
|
603
|
+
|
|
604
|
+
### 解析
|
|
605
|
+
|
|
606
|
+
- [ ] 普通文本、纯图片、图片加文本、文件、语音、视频都能生成标准化消息;
|
|
607
|
+
- [ ] `message_reference` 和 QQ 原生 `message_scene.ext` 都能解析;
|
|
608
|
+
- [ ] 引用正文、引用发送人和引用消息 ID 不会混入当前正文;
|
|
609
|
+
- [ ] 引用附件会被标记为 `quoted`。
|
|
610
|
+
|
|
611
|
+
### Agent
|
|
612
|
+
|
|
613
|
+
- [ ] 图片模型可用时图片能被真正分析;
|
|
614
|
+
- [ ] 图片模型不可用时不会导致 Agent turn 失败;
|
|
615
|
+
- [ ] 文件能被识别为附件而不是裸 URL;
|
|
616
|
+
- [ ] 语音可以转写或明确提示不支持;
|
|
617
|
+
- [ ] 媒体摘要和附件引用均为 JSON 可序列化数据;
|
|
618
|
+
- [ ] 不会再次出现 `session event ... non-JSON-serializable data`。
|
|
619
|
+
|
|
620
|
+
### 存储和安全
|
|
621
|
+
|
|
622
|
+
- [x] 下载 URL、重定向和 DNS 都经过安全检查;
|
|
623
|
+
- [ ] 单文件、单消息和视频时长限制生效;(文件和单消息大小已限制,视频时长尚未限制)
|
|
624
|
+
- [ ] 附件失败不会留下无限增长的临时文件;
|
|
625
|
+
- [ ] 删除消息或 Session 后,附件关系和物理文件按引用计数清理;
|
|
626
|
+
- [x] 日志和前端不泄露签名 URL、Token 或文件原文。
|
|
627
|
+
|
|
628
|
+
### UI 和 QQ
|
|
629
|
+
|
|
630
|
+
- [ ] DSH Web 使用原生附件和消息样式;
|
|
631
|
+
- [ ] 引用块和当前正文视觉上分离;
|
|
632
|
+
- [x] QQ 回复仍然能正确引用触发消息;
|
|
633
|
+
- [x] 私聊、群聊、Owner 和普通成员沿用现有权限与 Session 隔离。
|
|
634
|
+
|
|
635
|
+
## 不建议的实现方式
|
|
636
|
+
|
|
637
|
+
- 不要把 QQ 原始 payload 直接作为 Session event 写入;
|
|
638
|
+
- 不要把 Buffer、Blob、文件句柄或 SDK 实例放进 Session;
|
|
639
|
+
- 不要只把媒体 URL 拼进正文然后期待模型自动下载;
|
|
640
|
+
- 不要用昵称判断引用人或附件归属;
|
|
641
|
+
- 不要为引用消息另建一套 AgentLoop;
|
|
642
|
+
- 不要让每条图片消息无条件切换视觉模型;
|
|
643
|
+
- 不要在没有引用计数的情况下直接删除共享附件文件;
|
|
644
|
+
- 不要在插件 Client 里重新实现 DSH 的基础附件和气泡组件。
|