尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Odysseus 附件引用与上传存储机制:稳定引用、安全持久化与 Fail-Closed 清理
Odysseus 附件引用与上传存储机制稳定引用、安全持久化与 Fail-Closed 清理【免费下载链接】odysseusSelf-hosted AI workspace.项目地址: https://gitcode.com/gh_mirrors/ody/odysseus本文围绕 Odysseus 的 docs/attachments.md 展开系统讲解该项目如何用attachment_ref稳定引用替代 base64 内联媒体字节覆盖引用结构、chat_messages持久化策略、SQLite FTS 索引的媒体剥离迁移、工具侧的odysseus://attachment/id访问契约以及上传目录的 fail-closed 保留与删除流程。读完后可理解 Odysseus 从上传落盘、去重、引用发现到清理回收的完整生命周期实现。核心设计字节只存一份历史只传引用Odysseus 的设计目标很直接上传的字节只在配置的上传目录中存储一次随后通过聊天历史、工具和未来的 artifact 工作流传递稳定的引用stable reference。这样做是为了避免在chat_messages.content或 SQLite FTS 索引中重复大体积的内联媒体 payload——一张几 MB 的截图如果被 base64 后写进消息内容列不仅会膨胀数据库还会污染全文搜索索引。需要区分两个层面实时的模型调用仍可能收到 provider 特定的多模态块data URL、image_url block 等因为当前回合的 LLM 确实需要看到图片内容而持久化则完全不同——入库和索引的只是可读文本加紧凑的附件引用行。这个“实时富媒体 / 持久瘦引用”的分离是整个模块的第一性原则。附件引用的最小结构Reference Shape附件引用采用如下最小 JSON 结构这也是对外暴露的稳定契约{ type: attachment_ref, attachment_id: 32hex-or-32hex.ext, name: original-filename.png, mime: image/png, size: 12345, checksum_sha256: hex-digest, created_at: 2026-07-09T12:00:00 }字段说明attachment_id上传 ID格式为 32 位十六进制 UUID可带一个字母数字扩展名32hex或32hex.ext。这一约束来自 src/upload_handler.py 中的UPLOAD_ID_RE正则以及 _build_upload_idID 由uuid.uuid4().hex拼上净化后的扩展名生成。源码注释解释了为什么扩展名必须是纯字母数字——secure_filename会保留_和-像.jpg-1浏览器重复下载追加的后缀这样的扩展名会让 ID 永远通不过校验导致落盘文件再也无法被解析下载因此构建 ID 时会用re.sub(r[^A-Za-z0-9], , ext)净化扩展名checksum_sha256文件内容的 SHA-256 摘要用于去重与清理时的引用匹配name原始文件名未净化的original_name。可选字段width、height、vision、vision_model、gallery_id只在上传方或预处理路径知道它们时才出现。比如save_upload在检测到image/*类型时会用 PIL 读取 EXIF 旋转后的真实宽高src/upload_handler.py供聊天缩略图骨架提前按正确宽高比占位。结构归一化由 attachment_ref() 完成它做了三件文档没有明说但很重要的事ID 兼容取值依次接受id或attachment_id字段别名归一checksum_sha256可来自checksum_sha256、sha256或hash任一别名created_at可来自created_at或uploaded_atname可来自name或original_name大小容错size转int失败时回退为 0保证任何非结构化输入都不会让引用构造抛异常。测试 tests/test_attachment_refs.py 中的test_attachment_ref_normalizes_hash_aliases直接验证了第 2 点传入hash: abc和uploaded_at的旧格式行输出里出现的是规范字段checksum_sha256和created_at。持久化content 列、metadata 与 FTS 索引的三层分工持久化分三层各司其职1.chat_messages.content可读文本 紧凑引用行入库前消息内容经过 persistable_message_content() 折叠若 content 是消息块列表multimodal blocks_text_from_blocks()逐块处理text块保留文本attachment_ref块渲染为引用行image/image_url/input_image/audio/input_audio/file等媒体块见MEDIA_BLOCK_TYPES统一计数为省略项输出[N inline media payload(s) omitted]内联的data:*;base64,...URL 被strip_inline_data_urls()用正则替换为[inline media omitted from persisted history]最后从metadata.attachments提取结构化引用追加为可读引用行。引用行的渲染格式由_ref_line()决定形如[Attachment: diagram.png | idabc123.png | mimeimage/png | size42 bytes | sha256digest] [Attachment description: A small architecture diagram.]这个格式不是随意的——它正是清理阶段引用发现的正则目标见下文ATTACHMENT_REFERENCE_LINE_RE即人类可读格式与机器可发现格式是同一份数据。测试 tests/test_attachment_refs.py 断言了完整行为一个包含 5000 字符 base64 的image_url块入库后结果中不含base64、不含原始 payload但保留了用户文本、Attachment: diagram.png、idabc123.png、sha256摘要以及 vision 描述。2.chat_messages.metadata.attachments结构化引用消息 metadata 的attachments列表保存完整的attachment_ref对象供 UI 重新加载和历史处理使用。提取入口是 attachment_refs_from_metadata()它过滤掉非 dict 项和缺 ID 的项保证 metadata 被手工篡改或上游异常时不会污染引用集合。3. SQLite FTS 迁移新行不索引媒体、旧行洗白FTS 侧的实现在 core/database.py 的_migrate_chat_messages_fts()先探测 FTS5 是否可用不可用则整体跳过创建chat_messages_fts虚表和AFTER INSERT/DELETE/UPDATE三个触发器。插入与更新触发器中的内容表达式是一个CASE WHEN只要 content 里出现;base64,、data:image/或data:audio/就写入[inline media omitted from search index]而不是原文core/database.py存量行通过INSERT INTO chat_messages_fts ... SELECT补建索引然后调用_scrub_legacy_chat_message_fts_media()清洗历史数据找出所有含媒体特征的旧行删除其 FTS 记录改用 search_index_text() 重算可搜索文本后重新插入。search_index_text会尝试把旧格式JSON 序列化的消息块列表解析回块结构再折叠解析失败则退化为纯正则剥离——所以即使库里躺着无法解析的脏数据索引侧也不会泄漏 base64。工具访问URI 契约与四道 path 门Agent/工具上下文收到的是带 URI 的attachment_refmanifest形如{ type: attachment_ref, attachment_id: ..., uri: odysseus://attachment/id, read_policy: owner_checked_upload, path: /…/uploads/2026/07/09/id }manifest 由 routes/chat_helpers.py 的build_uploaded_file_manifest()构建。它对每个当前回合的上传 ID 调用upload_handler.resolve_upload(att_id, ownerowner)只有当以下检查全部通过时才在 manifest 中附带本地path字段这是为兼容存量内置工具保留的过渡字段上传 ID 能通过UploadHandler.resolve_upload解析本质是reserve_upload见 src/upload_handler.py 的 docstring内部处理器在读取字节前必须做 owner 感知的查找请求方 owner 被允许读取该上传reserve_upload内逐行比对owner字段owner 不匹配直接返回None文件仍位于配置的上传目录内_inside_upload_dir基于realpathcommonpath判定防符号链接逃逸文件路径位于工具可读根内通过src.tool_execution._resolve_tool_path校验 realpath 未被解析到别处。任一检查失败path被置为None但 manifest 仍包含uri和attachment_id——文档明确要求的契约外部 MCP/自定义工具应当把 URI 和 attachment ID 当作稳定契约通过 owner 校验的服务端路径请求字节而不是假设宿主文件系统布局。上传落盘与去重uploads.json 索引引用机制的地基是上传落盘流程核心在 UploadHandler.save_upload()步骤实现要点速率限制每 IP 每分钟最多 60 个文件upload_rate_limit 60按文件计数并发上传上限 3。源码注释解释了为何是 60聊天输入框单批最多挂 10 个文件阈值必须显著大于 10否则单批第 6 个文件起就会被自己拒绝对应 issue #1346大小/空文件空文件 400超过get_chat_upload_max_bytes()见 src/upload_limits.py400文件名净化secure_filename做 NFKD 归一、去路径分隔符、去危险字符、禁 dotfileMIME 检测优先python-magic嗅探内容回退扩展名猜测is_safe_file_type拒绝可执行类 MIME.exe/.dll/.ps1等与危险 content-type去重计算 SHA-256在_index_lock内按hash owner查uploads.json命中则刷新last_accessed并返回is_duplicate: True不重复落盘。查重与写入同锁防止并发插入覆盖索引落盘文件 ID 由_build_upload_id生成存入按日期分层目录uploads/YYYY/MM/DD/idget_upload_dir索引行写入uploads.json键为{owner}:{file_hash}无 owner 时仅为 hash字段含id、path、mime、size、name、hash、checksum_sha256、original_name、uploaded_at、created_at、last_accessed、client_ip、owneruploads.json的读写本身也做了工程加固_atomic_write_json() 先写同目录临时文件、fsync后os.replace原子覆盖并维护.bak上一快照_load_upload_index()用 dev/inode/size/mtime_ns/ctime_ns 组合签名做缓存一致性校验避免同时间戳的损坏写入骗过缓存destructive 调用方如清理以fail_on_errorTrue加载索引缺失或损坏时宁可抛错也不当作空库处理。保留与删除fail-closed 的清理流水线文档描述的清理策略在 UploadHandler.cleanup_old_uploads() 中完整落地默认保留窗口cleanup_days 301. 引用快照缺失即整体跳过fail closed。参数referenced_upload_ids或referenced_upload_hashes为None表示引用发现未完整完成函数直接返回 0 并打警告——底层清理 API 在没有完整引用快照时不删除任何东西。2. 引用发现扫描哪些地方。管理员清理入口先扫描持久化的聊天 metadata/content、文档版本、PDF 源标记、gallery hash、notes 和 calendar 记录收集“活引用”。提取逻辑在 src/upload_handler.pyextract_upload_ids宽松匹配任意 32-hex token用于清理时的保守引用收集extract_internal_upload_ids严格匹配三类显式内部引用——odysseus://attachment/id与/api/upload/idURLINTERNAL_UPLOAD_URL_RE、PDF 源标记注释!-- pdf_source ... upload_id... --PDF_SOURCE_UPLOAD_RE、以及持久化引用行[Attachment: ... | idid | ...]ATTACHMENT_REFERENCE_LINE_RE与_ref_line的输出格式严格对应。严格版用于写入侧预留避免把笔记/日历文本里恰好 32 位的 hex 校验和误判为上传引用。3. 索引行一致性门禁。每个候选文件经_upload_index_keys_for_file()校验所有相关索引行必须在 ID、规范路径、owner、唯一非空校验和上完全一致且具备完整生命周期时间戳出现 owner 冲突、校验和冲突或缺字段时记警告并放弃该候选返回空列表宁可不删。4. 索引行先删、字节后删失败回滚。通过全部门禁的文件先以sync_backupTrue原子地把移除后的索引写回uploads.json同步备份防止恢复时复活已删字节的元数据再os.remove文件字节删除失败则原子恢复原索引并抛UploadCleanupSafetyError。文件恰好已不在FileNotFoundError时按“对账缺失文件”处理保留精简索引。5. 写入侧预留与删除串行化。文档要求“带附件的写入方必须先拿到 owner 校验的预留”。实现是 reserve_upload()在清理共用的_index_lock内完成索引查找、owner/路径校验和last_accessed刷新写新引用前用 reserve_message_upload_references() 从消息 content 与 metadata 中提取 ID 并逐个预留任一 ID 无法预留返回None就应中止持久化从而堵住“刚写完引用就被清理删掉字节”的竞态。6. 删除聊天不等于删除字节。删除一个聊天只删聊天行共享的上传字节不立即回收——同一上传可能同时被 gallery 项、文档、去重上传行或未来的 artifact 记录引用。当前 schema 没有独立的 artifact 表聊天或文档文本中持久化的 artifact 类引用由规范的 attachment-ID 扫描覆盖未来若新增 artifact 存储必须先把它接入引用发现清理才允许认为那些上传“无引用”。7. 单 worker 前提。清理与写入预留共享的是进程内threading.Lock_index_lock这关闭的是文档所述单 worker 部署下的扫描/写入/删除竞态。文档与源码注释src/upload_handler.py一致明确多进程部署必须引入跨进程锁如 flock或把生命周期状态迁入数据库才允许在多个 worker 中启用破坏性清理。相关行为由 tests/test_attachment_refs.py引用构造、持久化折叠、旧格式 FTS 文本与 tests/test_upload_handler_cleanup.py清理门禁等用例覆盖。小结一张生命周期链路图把各部分串起来一次带图消息的完整链路是上传save_upload限流→净化→嗅探→哈希去重→按日期目录落盘→写uploads.json索引行含 owner、SHA-256、生命周期时间戳当前回合模型收到 provider 多模态块工具侧收到odysseus://attachment/idmanifestpath仅在四道检查通过后存在持久化persistable_message_content把 content 折叠为可读文本 [Attachment: ...]引用行metadata.attachments存结构化attachment_refFTS 触发器/清洗器确保索引中永不出现 base64引用发现清理扫描聊天、文档、PDF 标记、gallery、notes、calendar用宽松/严格两套正则收集活引用与活 hash清理fail-closed 快照→索引行一致性门禁→原子删索引→删字节→失败回滚写入侧reserve_upload与清理共享锁串行化。涉及的核心文件docs/attachments.md设计文档、src/attachment_refs.py引用与持久化折叠、src/upload_handler.py落盘、索引、预留、清理、routes/chat_helpers.py工具 manifest 构建、core/database.pyFTS 迁移与媒体清洗。【免费下载链接】odysseusSelf-hosted AI workspace.项目地址: https://gitcode.com/gh_mirrors/ody/odysseus创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED

相关推荐

Vision Transformer (ViT) 原理与 PyTorch 实现:从 Patch Embedding 到分类头的完整解析

Vision Transformer (ViT) 原理与 PyTorch 实现:从 Patch Embedding 到分类头的完整解析

Vision Transformer (ViT) 原理与 PyTorch 实现:从 Patch Embedding 到分类头的完整解析 【免费下载链接】annotated_deep_learning_paper_implementations 🧑‍🏫 60 Implementations/tutorials of deep learning papers with side-by-side …

📅 2026/10/8 13:55:00
AI数据中心的电力瓶颈:从PUE到冷却技术的能效优化指南

AI数据中心的电力瓶颈:从PUE到冷却技术的能效优化指南

在实际 Java Web 项目中,权限控制很少只是登录成功这么简单。真正麻烦的是认证流程、授权规则、异常处理和会话管理如何配合起来。但当项目规模扩展到 AI 训练或推理场景时,权限问题会换一种面貌出现——不再是登录和角色,而是数据中心能不能…

📅 2026/10/6 0:32:27
Unity 3D坦克大战源码深度解析:C#工程化实践指南

Unity 3D坦克大战源码深度解析:C#工程化实践指南

简介:本资源是一套完整可用的C#期末大作业项目——基于Unity引擎开发的3D坦克大战游戏源码,专为计算机类专业本科生课程设计与期末大作业打造,切实解决学生缺乏实战项目经验、难以独立完成高质量UnityC#综合实践任务的痛点。压缩包共136个文件…

📅 2026/10/16 14:21:39
MORE NEWS

更多资讯

📰

SpringBoot微信小程序养老服务平台功能介绍

商品编号:000040【一、项目简介】 面向养老服务预约的微信小程序平台,配套网页运营后台。提供生活照料、医疗陪护、康复护理、心理慰藉、文化娱乐和家政服务分类,包含服务浏览、预约订单、评价反馈与行为分析模块。展示服务分类、热门服务与推…

📰

ROS2 Web端地图显示与导航控制实战指南

简介:本资源是一套基于ROS 2 Web前端的TurtleBot3地图可视化与交互导航实现方案,面向ROS初学者及Web前端开发者,解决机器人SLAM建图结果在浏览器端实时显示、目标点导航与位姿校正等核心需求。项目纯前端实现,不依赖后端服务或键盘…

📰

短信PDU编码解析与生成:嵌入式设备短信通信实战指南

简介:本资源是一套面向嵌入式开发与通信协议学习者的短信PDU编码解码C语言实现,适用于理解GSM网络中SMS消息底层传输机制的初学者与进阶开发者。代码完整覆盖电话号码国际格式转换、短信中心地址编码、SMS-SUBMIT/SMS-DELIVER类型识别、GSM 7-bit与UCS-2…

📰

特斯拉Optimus3量产优化:执行器、灵巧手与热管理如何突破可靠性瓶颈

1. 从"周周谈"里挖出的真问题:Optimus3这次到底改了什么特斯拉的AI团队最近放出的消息里,Optimus3的优化方向比很多人预想的要务实得多。不是继续堆叠自由度数字,也不是搞什么炫技的跑酷演示,而是把力气花在了"怎么…

📰

人工智能课程资源模块化复习:从神经网络到AI伦理

简介:这是一套面向零基础学习者与自学者的微软人工智能课程中文版资料,对应为期12周、共24课的系统大纲,围绕符号人工智能、神经网络与深度学习、计算机视觉、自然语言处理、遗传算法及多智能体系统等主题展开,并结合TensorFlow与…

📰

电动汽车VCU开发实战:从原理图到控制策略的完整拆解

简介:这份资源面向电动汽车整车控制器(VCU)的学习者与开发者,提供一套从软件到硬件的完整开发参考。包内包含VCU控制源码、硬件PCB原理图、控制策略说明书与硬件说明书,源码覆盖底层驱动、中间件与应用层控制逻辑&…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬