feat(gateway): 文档存储端点 POST/GET /api/v1/documents(design/24 S1)

- 字节嗅探唯一裁决 + 扩展名白名单 deny-by-default(23 个文本/代码扩展名);
  客户端 Content-Type 与文件名只作参考、可被否决。
- 内容寻址落盘名 sha256[:32]+ext ⇒ 同字节同扩展名天然去重;首次 201/deduped:false,
  命中 200/同一 name/deduped:true。
- 独立限额 CHAT_DOCUMENT_MAX_BYTES(默认 20 MiB,与图片 5 MiB 刻意解耦)+ MaxBytesReader。
- 读侧 Bearer owner|admin + Content-Disposition: attachment + nosniff(与图片读侧刻意不同);
  写侧审计 document.upload;错误码 unsupported_media_type/document_too_large/document_not_found。
- 元数据表 documents(0014,含 idx_documents_name 唯一、idx_documents_sha256);
  guardDocumentPaths 防路径穿越(否则 ServeMux 会 307 泄漏路径)。
- 既有 /api/v1/upload 与 /api/v1/files/{name} 零改动(upload.go 零 diff)。

验收(主会话独立复跑,非自报):隔离 worktree(HEAD + 本切片)go build/vet 绿、
全树 58 包 0 FAIL、linux/amd64 + darwin/arm64 交叉编译绿、9 条新单测 PASS。
变异独立复现 3 条:白名单加 ".js" ⇒ 415 用例红;嗅探裁决一律放行 ⇒ 谎报类型用例红;
摘掉 guardDocumentPaths ⇒ 穿越用例红(307 重定向,正是那道防护存在的理由)。
This commit is contained in:
AskaEth committed 2026-10-04 17:07:20 +08:00
1 parent 52f08dc343
commit 407c934a05
7 files changed
+1061

No files matched your search

+7
View File
@@ -74,6 +74,13 @@ type ServerConfig struct {
// 「用默认」**,不做「禁用上限」的解释——一张图能有多大只能有一个答案。
// 网关只在入口层用换算出的字符上限拦截,绝不把图片内容写进日志。
ChatImageMaxBytes int `env:"CHAT_IMAGE_MAX_BYTES" default:"5242880"`
// ChatDocumentMaxBytes 是 POST /api/v1/documents 允许的**单文件字节**上限,
// 默认 20971520(20 MiB)。它与图片上限(CHAT_IMAGE_MAX_BYTES)刻意解耦:
// 图片上限被 vision 解码层钳住,文档上限由本项目的解析与上下文预算决定,
// 共用同一个数会让「改文档上限」静默改掉图片行为(design/24 §3.3)。
// **≤0(含未配置的零值)视为「用默认」**,不是「禁用上限」。
ChatDocumentMaxBytes int `env:"CHAT_DOCUMENT_MAX_BYTES" default:"20971520"`
}
// AdminPathPrefix 返回管理面板前端路由的绝对路径前缀(默认 `/admin`)。