feat(runtime): document 内容类型与持久化(design/24 S2)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- model 新增 ContentTypeDocument + Content 的四字段 + DocumentMessage 构造器 +
  ValidateDocumentParts(文档部件只允许 RoleUser,非 user ⇒ ErrDocumentOnNonUser,fail-loud)。
- 两个编码器入口(buildMessages / buildAnthropicMessages)对文档部件报 ErrBadRequest:
  本期 document 永不抵达编码器,且绝不静默丢块。
- history:拆出 types.go(Message 增 Document 列),新增 document.go(DocumentRef 同时是
  落库 JSON 与 DTO 形状,6 字段含 status ⇒ §4.4 的「读不到要可见」;坏列回 unreadable 不静默)。
- chat_messages 加 document 列(迁移 0015 + NewStore 幂等前滚);historyMessage.Document DTO;
  session_persist 的 messageTextImage → messageTextImageDocument(保留窄视图以免牵连 steer 配对)。
- 只落「引用 + 统计」,绝不落抽取正文(TestPersistedDocumentHoldsNoBody 把全部列拼起来扫正文)。

验收(主会话独立复跑,非自报):隔离 worktree(HEAD + 本切片)go build/vet 绿、
全树 58 包 0 FAIL、linux/amd64 + darwin/arm64 交叉编译绿、18 条 Document* 测试 PASS。
变异独立复现:ValidateDocumentParts 开头 return nil ⇒ 非 user 负例红。
This commit is contained in:
AskaEth committed 2026-10-04 17:07:24 +08:00
1 parent 407c934a05
commit 6b2b07cbef
15 files changed
+895 -101

No files matched your search

+22 -3
View File
@@ -61,9 +61,14 @@ type historyMessage struct {
Mode string `json:"mode,omitempty"`
Mood string `json:"mood,omitempty"`
Image string `json:"image,omitempty"`
Thinking string `json:"thinking,omitempty"`
Platform string `json:"platform,omitempty"`
DeviceID string `json:"deviceId,omitempty"`
// Document 是这条消息携带的文档引用与抽取统计(design/24 §2.4、§6.1),
// 形状为 {name,mime,size,chars,truncated,status}。它**只带引用与统计,
// 绝不带抽取正文** —— 正文若从这里回传,每翻一页历史都要背着它(G3 的
// 教训)。没有附件的消息省略该字段。
Document *history.DocumentRef `json:"document,omitempty"`
Thinking string `json:"thinking,omitempty"`
Platform string `json:"platform,omitempty"`
DeviceID string `json:"deviceId,omitempty"`
// SenderName 是第三方消息(群聊/陌生人,design/16 §4.6)
// 给人看的发送者显示名。对主会话中主人的私聊它为
// 空,因此空意味着「标记为主人」。
@@ -402,6 +407,7 @@ func toHistoryMessage(m history.Message) historyMessage {
Mode: m.Mode,
Mood: m.Mood,
Image: m.Image,
Document: decodeHistoryDocument(m.Document),
Thinking: m.Thinking,
Platform: m.Platform,
DeviceID: m.DeviceID,
@@ -418,3 +424,16 @@ func toHistoryMessage(m history.Message) historyMessage {
// historyDefaultLimit 是客户端省略 limit 查询参数时使用的页大小。
const historyDefaultLimit = 50
// decodeHistoryDocument 解码已存消息的 document 列(JSON 小对象)。空列表示
// 这条消息没有附件,返回 nil 以省略该字段。列被写坏时(本服务端只会写合法
// JSON,因此那只能是外部改动或未来格式变更)**绝不静默当成没有附件** ——
// 「一方以为附件传上去了,另一方一无所知」正是 F2 要消灭的误解;这时回一个
// 标着 status=unreadable 的最小对象,让客户端看得见这条引用读不出来。
func decodeHistoryDocument(raw string) *history.DocumentRef {
ref, err := history.DecodeDocument(raw)
if err != nil {
return &history.DocumentRef{Status: history.DocumentStatusUnreadable}
}
return ref
}