OpenProgram Web 聊天 · 综合 Claude Code / opencode / openclaw 三家做法 + 自身约束
(文件类型 × 模型声明的模态) 重算,逐级降级。每个文件 prompt 成本 O(1),与大小无关;同一次上传 codex 现在能用、换 Claude/Gemini 自动升级,前端零改动。
@提及/打路径=有 → 原地引用,零复制。document 时,才把 PDF 升级成原生 document block(P1)。DELIVER 基于默认 codex/gpt-5.5(model.input=["text","image"],无 document)。某行只有当模型声明对应模态时才翻转。
| 来源 | 文件类型 | 落盘 | DELIVER(现在, codex/gpt-5.5) | READ 路径 |
|---|---|---|---|---|
| upload | image | 否 | ImageContent block(像素) | 模型 vision 原生 |
| upload | text/code | 是 | [attachment:..@/abs] + ≤4KB 首部预览 | read 2000行/200KB 分页 |
| upload | 是 | [attachment:..(P页)@/abs] + 第1页+大纲 | pdf 80KB/页窗口 | |
| upload | 其它二进制 | 是 | [attachment:..@/abs] 仅提及 | bash file/strings/xxd |
| @-mention | image | 否 | ImageContent block | 模型 vision 原生 |
| @-mention | text/pdf | 否 | [attachment:..@/abs] + 首部(file-resolve) | read/pdf 分页 |
| 打路径 | 任意 | =@ | file-resolve 把裸路径按对应类型同等处理 | |
| 远程渠道 | image | 是 | ImageContent(从落盘字节重读) | 模型 vision 原生 |
| 远程渠道 | text/pdf | 是 | [attachment:..@/abs] + 首部预览 | read/pdf 分页 |
轴的纪律:来源轴只决定字节落在哪;(文件类型 × 能力) 是唯一决定 DELIVER 的东西。
一个纯函数 choose_delivery(类型, 大小, 模型) 就是全部决策;每一级降级都保持同一个稳定身份——落盘的绝对路径。
误判永远是安全的:落到工具抽取,不会丢数据。"document" 暂当 “pdf-capable” 处理,docx/xlsx 一律 path_preview/path_only。
十个 30MB PDF 一起拖:那一轮约 10×(90B + 4KB) ≈ 41KB,之后为零——与大小无关。500 页 PDF 在 codex 上:落盘一次,提及带 “500 pages”,预览=第1页 + 每页首行大纲(截到 ~50 条后 “…(450 more pages)”),8MB 本体永不进上下文;agent 靠大纲 pdf(offset=N, limit=20) 直接跳页。
实测上限(源码核验):pdf 80KB 字符/次按页分页;read 2000 行/次、结果上限 200KB;file_search 的 256KB 只喂预览、永不喂交付。
-N 循环无字节比较 → 多存一份。修法:sha256 会话内去重(.opdedup.json,复用前校验)。validate_input_modalities 在 HTTP 前会 raise,不是静默。属优雅降级增强(存盘 + image_analyze 提示),不咬默认模型。.resolve() 已解析符号链接、is_relative_to 已拒。现有代码无此洞。workdir/attachments/——就是 agent 的 cwd、每轮 git 提交、可重放。比 openclaw 全局 + TTL 更适合 agentic。只有无路径来源(上传/远程)落盘;@/打路径原地引用零复制。_safe_attach_name:basename + 非安全字符替 _ + 120 上限,人类可读。sha256 仅作会话内去重索引,不做文件名。不同字节同名 → -N。@/打路径走 file-resolve 的 resolve()+is_relative_to → 越界 400。GC 会话级懒回收(删会话 = rm -rf workdir)。[attachment: name (type, KB[, P pages|L lines]) @ /abs],徽章显示 “500 pages” / “200K lines”;@/abs 显示时剥掉。<attachment-preview> 片段是给模型的,气泡里整块剥掉——用户看到 chip,不是 4KB 首部。在扫提及之前剥,防止预览里的文件内容生成假 chip。_title_from_text 在 50 字截断前剥提及 + 预览块,防止截断的长路径泄漏。_persist:32MB 上限 + “too large”;sha256 去重;页/行数注入括号组;一次性 <attachment-preview>(≤4KB 首部,二进制无预览)。_title_from_text 扩剥预览。
NATIVE_DOC_INLINE_CAP 守卫;chat.py:307 的“进 dispatcher 前剥文档”按能力条件化;types.py + validate_modalities 加 "document"。接缝在 P0 就位,到时零前端改动。media:// + discord/wechat 适配器,把入站字节接进同一 _persist。保存+提及+预览的表示已能容纳“只有字节”的来源。两个可调常量,都有默认、都是单一旋钮:PREVIEW_CAP=4KB、MAX_ATTACH_BYTES=32MB。唯一长期产品问题:大二进制永久累积在 per-session git 历史(“workdir = 自包含已提交状态”不变式的代价)是否可接受——现在有意保留这个不变式,不是 P0 待决项。
完整 markdown 版:docs/design/attachment-handling.md · 报错超时设计:docs/design/error-and-timeout-mechanism.html