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

+10
View File
@@ -128,7 +128,13 @@ func (a *OpenAIChatAdapter) buildRequest(req Request, stream bool) ([]byte, erro
return buf, nil
}
// buildMessages 把归一化消息列表逐条渲染成 OpenAI 线格式,并在入口拒绝
// 挂在非用户回合上的文档部件(design/24 §2.3-③):文档本期永不抵达编码器,
// 漏网即编程错误,绝不静默丢块。
func buildMessages(msgs []Message) ([]openAIMessage, error) {
if err := ValidateDocumentParts(msgs); err != nil {
return nil, err
}
out := make([]openAIMessage, 0, len(msgs))
for _, m := range msgs {
wire, err := encodeOpenAIMessage(m)
@@ -196,6 +202,10 @@ func encodeToolCalls(parts []Content) []openAIToolCall {
return calls
}
// encodeContent 把归一化内容片段转成 OpenAI 的内容字段(纯文本、或
// 文本/图片片段数组)。文档片段不走这里:本期不向提供商发任何文档块
// (design/24 §2.3-②:形状不对就是 400),因此它落到 default 分支被
// 拒绝 —— fail-loud,绝不静默丢块。
func encodeContent(parts []Content) (any, error) {
if len(parts) == 0 {
return "", nil