移动端按上游契约发送,服务端已正常工作(本次复核确认):
POST /api/v1/partners/{partner_id}/chat/execute-stream
{
"content": "…",
"session_key": "app-xxx",
"attachments": [
{ "type": "image", "base64": "…", "filename": "a.png", "mime_type": "image/png", "url": "" }
]
}
media/web/{uuid12}_{filename} ✓新增端点(与建议形态一致,id 采用哈希反查而非路径拼接,结构性免疫路径穿越):
GET /api/v1/partners/{partner_id}/attachments/{attachment_id}
鉴权: router 级 admin 依赖(与全部伙伴 API 一致)
attachment_id: 12 位十六进制 = sha1(文件绝对路径)[:12](与 history 中的 id 同源)
实测:
| 用例 | 结果 |
|---|---|
| 带 Bearer 下载 4MB PDF | 200 · md5 与服务端文件一致 |
Content-Disposition | attachment · 前缀已剥 · RFC5987 UTF-8(中文「勺嘴鹬.png」正常) |
| 无 token | 401 |
| 伪造 / 他人 partner 的 id | 404(admin-gated,无越权面) |
| 不存在 | 404 |
../ 穿越尝试 | 404(id 仅做哈希匹配,不参与路径构造;非 12hex 直接 400) |
history 响应中的附件记录现在长这样(实测,与建议字段完全一致):
{
"id": "f46731c5dce4",
"type": "pdf",
"filename": "2000_Core_Words.pdf", ← uuid12_ 前缀已剥离
"mime_type": "application/pdf",
"size": 4083819,
"download_url": "/api/v1/partners/dina/attachments/f46731c5dce4"
}
path / base64 / extracted_text / extracted_chars 全部不再序列化(实测 0 残留;JSONL 存储保留全量,仅 API 出口裁剪)events 工具轨迹(find/grep 完整输出,单条消息可达 6MB)。新增 ?include_events=false 瘦身参数(默认 true 保持 Web 端活动面板兼容):实测 41.5MB → 55KB(约 750×)。移动端拉历史时请带上该参数。event: ping(data 带 ts),客户端忽略内容但重置空闲计时即可session 帧本就早于推理发出(物料化在首帧前、耗时毫秒级),维持原语义以保证 413/422 仍以 HTTP 状态返回(移动端已依赖)session_key + 整会话删除管理测试污染media/ 目前 5 个测试 PNG + 2 个真实文件,量级可忽略;达到 GB 级再做按 partner 配额GET .../attachments/{id} 返回 200,文件字节与上传一致(md5 比对)attachment_id 含 ../ 等穿越尝试 → 400/404(哈希反查,绝不落到 media 目录外)/app/data/... 内部路径(附件字段已清洗;events 内工具命令文本除外——那是行为轨迹非结构泄露,且 slim 模式不含)Content-Disposition: attachment 且原始文件名可读(中文 UTF-8 实测正常)| 变更 | 对接方式 |
|---|---|
| 附件下载 | GET /api/v1/partners/{pid}/attachments/{id}(id 从 history 的 download_url 直取) |
| 瘦身历史 | GET .../history?include_events=false |
| 心跳 | 解析 SSE 时忽略 event: ping 但重置空闲计时器 |