ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Friend 后端 Frame-Request 像素留存架构:双桶分层、幂等清理与部署契约校验指南

2026/9/15 18:04:55 拓冰建站 浏览量
Friend 后端 Frame-Request 像素留存架构:双桶分层、幂等清理与部署契约校验指南 Friend 后端 Frame-Request 像素留存架构双桶分层、幂等清理与部署契约校验指南【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/FriendFrame-request即时截帧请求是 Friend 项目中看屏幕、给建议能力的核心数据通道本指南围绕 backend/docs/frame-request-retention.md 展开系统讲解后端如何用临时桶 永久桶两级存储管理截帧像素如何通过调度型 retention worker 幂等收敛清理以及部署时必须通过的桶契约与独立健康门禁。读完本文你将掌握这套留存策略的完整生命周期、每一条删除/晋升路径的实现依据以及可复制的离线校验命令与部署验收清单。一、为什么需要两套物理存储桶临时性与永久性并存Frame-request 队列本身只存元数据见 backend/database/frame_requests.py 的模块说明The queue carries metadata only像素字节通过两个物理隔离的 GCS 桶承载环境变量分别由BUCKET_FRAME_REQUESTS_TEMPORARY与BUCKET_FRAME_REQUESTS绑定。两级设计的核心差异如下维度临时桶BUCKET_FRAME_REQUESTS_TEMPORARY永久桶BUCKET_FRAME_REQUESTS生命周期规则6 天Delete规则delete_age_days: 6无对象过期规则expires_objects: false软删除关闭soft_delete_retention_seconds: 0常规策略承载对象requested / claimed / uploaded 状态的上传像素conversation-attached已挂接会话的对象保存期限未挂接对象最多 6 天跟随会话生命周期永久保留源码中对这种分桶的实现非常直白backend/utils/retrieval/frame_request_storage.py 中_bucket(permanent...)按布尔值选择环境变量并注释The temporary bucket is lifecycle-backed; the permanent bucket must have no object-expiration rule.存储 ID 以temporary-/permanent-前缀区分层级TEMPORARY_STORAGE_PREFIX/PERMANENT_STORAGE_PREFIX_is_permanent()对拆分前遗留的无前缀对象回退为永久桶frame_request_storage.py保证历史数据兼容。为什么调度器停机不能依赖桶规则兜底文档明确临时桶虽然带 6 天删除规则且关闭软删除但调度器停机无法让未挂接像素留存 7 天。也就是说桶生命周期规则只是最后一层物理兜底真正的留存纪律由上层 worker 按 6 天红线主动收敛二者不能互相替代。从源码看FRAME_REQUEST_MAX_TTL_SECONDS 6 * 24 * 60 * 60见 backend/utils/retrieval/frame_request_policy.py请求最大 TTL 同样以 6 天为上限与临时桶删除规则保持一致。二、挂接晋升Promotion的原子流程先留收据再复制后换引用当一个上传的帧请求被挂接到会话conversation时像素必须从临时桶晋升到永久桶且整个过程要满足任何时刻崩溃都可恢复。文档给出的顺序是先持久化一份永久清理收据cleanup receipt——对象尚未被任何会话引用时收据确保它可被发现、可被清理把临时对象复制到永久桶原子地将会话照片conversation photo与请求元数据的 storage ID 切换为永久 ID晋升成功后用可重试的收据移除被替换的临时副本。源码完整印证了这四步reserve_frame_promotion_copy()database/frame_requests.py在复制之前写入frame_upload_orphans集合下的收据cleanup_next_attempt_at设为当前时间 1 小时在临时保留期限内留足复制与事务重试时间copy_frame_request_pixels_to_permanent()frame_request_storage.py执行幂等复制目标 storage ID 必须带permanent-前缀否则直接抛错attach_frame_request_to_conversation()database/frame_requests.py在单个 Firestore 事务内完成写入/校验照片子集合文档、更新会话has_content/has_photos标记、把请求行置为attachedcleanup_state permanentexpires_at created_at表示不再按时间过期、并在同一事务中删除晋升收据——注释明确说明A failed/ambiguous promotion therefore leaves either a referenced permanent object or a discoverable object due for cleanup要么留下已被引用的永久对象要么留下可被发现、待清理的对象二选一被替换的临时副本由reserve_frame_storage_cleanup()写入kind: displaced_temporary收据database/frame_requests.py随后由清理 worker 通过收据独立重试删除。会话内关键约束在模型中也有体现backend/models/frame_request.py 校验attached状态必须携带conversation_id、expires_at必须等于created_at即不允许基于时间的过期、且不能携带 terminal reason而FRAME_REQUEST_MAX_ATTACHED_PER_CONVERSATION 1frame_request_policy.py保证每个会话最多一张永久关键帧。三、调度型 retention worker元数据终态驱动的收敛清理requested / claimed / uploaded 的对象除了依赖桶生命周期外还会被调度型 retention worker 主动删除触发条件是其 Firestore 行到达终态清理状态cleanup_state为pending或failed且cleanup_next_attempt_at已到期。这意味着即使没有任何设备再调用 pending 轮询接口外部像素删除依然会收敛——这是文档强调的与队列投递解耦。worker 的入口与执行链为独立 Cloud Run Jobbackend/modal/frame_request_retention_job.py日志与入口均独立不做内存态假设核心维护逻辑run_frame_request_retention_maintenance()backend/services/frame_request_retention.py每一账号按rows_per_user默认 32即FRAME_REQUEST_MAX_BATCH分页执行六类操作prune_expired_conversation_keyframe_jobs——收敛会话关键帧 outboxprune_expired_frame_requests——把过期未挂接行置为prunedterminal_reason: retention_expireddatabase/frame_requests.pycleanup_frame_request_pixels——对终态且cleanup_state为pending/failed的行重试 GCS 删除失败时按min(86400, 2 ** min(attempts, 16))指数退避database/frame_requests.pydelete_expired_frame_request_metadata——仅在像素清理收敛后cleanup_state为not_required/deleted才删除过期元数据行database/frame_requests.pycleanup_expired_frame_vision_outputs——剥离过期的派生描述文本但保留无内容、不过期的收据防止崩溃后把清理误判为再次调用视觉提供商的许可database/frame_requests.pycleanup_ambiguous_frame_upload_pixels与cleanup_conversation_frame_deletion_outbox——收敛歧义上传收据与会话删除 outbox。worker 的幂等与并发设计同样值得关注_acquire_lease/_release_lease/_fenced_storeservices/frame_request_retention.py使用 Firestore 事务维护代际化generation-fenced租约默认租期 20 分钟防止重叠 worker 回退任一游标租约过期即可安全恢复崩溃实例。每账号重试队列retry_accounts子集合与全局人口扫描各自持有独立游标与有限页预算每轮user_limit默认 1000其中约 1/4 预留给重试账号文档特别点明poison account 无法钉死全局人口扫描——一个永久失败的 UID 只占用重试预算不会阻塞新账号扫描。3.1 有界分页与整页完成后才推进游标_load_user_page()services/frame_request_retention.py只在整页处理完成后才推进持久游标崩溃后的重复工作是安全的且advancing only after the page finishes prevents a skipped account避免跳号账号。_drain_due_pages()第 179-190 行把每个操作限制在max_pages8页内防止单账号无限消费调度时间。四、JIT 视觉的只读临时端点与不重复付费收据临时端点读取永不晋升、永不续期一个未挂接会话的已上传请求uploaded保持临时性质仅通过**属主 账号代际owner- and generation-fenced**的临时图像端点对已认证的 JIT 视觉开放读取它既不会把对象提升到永久桶也不会延长其过期时间。这保证了看过一眼不改变留存语义。look_at_frame消费端的两次预留后端 Agent 的look_at_frameconsumer 只接受该请求中准入的屏幕证据引用screen evidence reference并在调用付费视觉之前同时预留请求作用域的单帧预算one-frame budget持久、稳定的单轮调用收据durable stable-turn invocation receipt。收据先于提供商出网写入见reserve_frame_vision_invocation()database/frame_requests.py收据 ID 由authority_key的 SHA-256 派生事务内校验request_id与account_generation必须匹配否则抛出PermissionError(vision receipt authority mismatch)。预留后若崩溃返回诚实的结果不确定indeterminate响应绝不重复付费——complete_frame_vision_invocation()第 144-175 行只写入截断到 4000 字符的有界描述且output_expires_at遵循FRAME_REQUEST_MAX_TTL_SECONDS6 天。遥测内容纪律该路径的 telemetry 只包含封闭结果字段与是否调用视觉布尔字段绝不携带帧 ID、OCR 文本、像素或描述内容。源码侧同样贯彻这一点——cleanup_expired_frame_vision_outputs更新时使用firestore.DELETE_FIELD直接删除description/completed_at/output_expires_at而非置空字符串删除失败重试元数据也只写last_error_text截断 2000 字符这类有界错误信息。五、桌面端Mac关键帧元数据 outbox 与恢复循环已完成桌面会话独立于发布可用性持久化一条仅元数据的关键帧 outbox 意图metadata-only keyframe outbox intent。其后由三路机制收敛会话 finalization、后续屏幕同步later screen sync、以及每小时的恢复 workerhourly recovery worker。文档规定了选区selection的严格条件源码与配置侧均可验证查询限定精确设备、账号代际、会话时间窗口且最新优先newest-first要求 Mac 提供fail-closed 的 Rewind 排除认证Rewind-exclusion attestation——认证失败即拒绝而非放行使用权威本地截图 IDauthoritative local screenshot ID。上传解码纪律上传解码会剥离元数据EXIF 等并将 JPEG / PNG / WebP 统一规范化canonicalize为有界 JPEG再进行存储或送视觉。这既压缩体积、规避隐私泄漏也保证视觉输入格式一致。恢复循环与降级既有的桌面恢复循环负责上传并晋升promote胜出截图本地捕获缺失或老化时会话终态化为 pruned但不阻塞纯文本会话继续。也就是说关键帧是增强项缺失时对话照常可用只是少了截帧证据。六、删除边界outbox 先行、枚举兜底、永久对象永不被临时清理选中文档在删除路径上给出了三条强保证均有源码佐证会话删除persist_conversation_frame_deletion_outbox()database/frame_requests.py在删除会话元数据之前先为每个对象持久化一条删除 outbox收据 ID sha256(conversation_id \0 storage_id)随后cleanup_conversation_frame_deletion_outbox()独立重试删除——即使外部存储删除失败也不会丢失对象引用始终可恢复。delete_frame_requests_for_conversation()第 1233-1271 行以 450/页有界分页删除队列行与关键帧任务并带硬页栅栏静默截断会直接抛RuntimeError而非给出已删除的假象。账号删除delete_all_frame_request_pixels_for_user()frame_request_storage.py枚举两个存储层临时 永久删除后再次列出前缀校验remaining为空残留即抛错配套的list_all_frame_request_storage_ids/list_all_frame_upload_orphan_storage_ids/list_all_frame_deletion_outbox_storage_idsdatabase/frame_requests.py对队列页、歧义上传收据、删除 outbox、照片子集合做穷举式分页枚举默认 500/页、上限 1000 页超限抛错确保不透明对象在删号时无一遗漏。临时清理永不选中永久对象cleanup_frame_request_pixels()的查询显式排除attached状态TERMINAL_FRAME_REQUEST_STATES - {attached}delete_expired_frame_request_metadata()的索引查询同样只覆盖非 attached 终态prune_expired_frame_requests事务内重读行并显式跳过attached杜绝并发晋升被过期清理覆盖。delete_frame_request_pixels()则通过_is_permanent(storage_id)选择正确的桶并把NotFound视为安全幂等结果frame_request_storage.py。对象路径的不可寻址性存储路径为frame-requests/{uid}/{sha256(storage_id)}frame_request_storage.py由哈希派生的不透明路径杜绝调用方传入任意路径读写他人对象写入时还套用owner_storage_write_gate属主写闸门。七、部署验收桶契约离线校验 线上桶门禁 独立健康门禁文档把部署验收拆成三层任何一层不过都不应宣称留存配置正确。7.1 契约文件与离线校验桶契约定义在 backend/deploy/frame-request-bucket-contract.json关键字段{ permanent_bucket_env_var: BUCKET_FRAME_REQUESTS, temporary_bucket_env_var: BUCKET_FRAME_REQUESTS_TEMPORARY, conversation_attachment_policy: conversation_lifetime, allowed_locations: [US, US-CENTRAL1], uniform_bucket_level_access: true, public_access_prevention: enforced, encryption_at_rest: google_managed_or_cmek, lifecycle: { expires_objects: false }, temporary_lifecycle: { delete_age_days: 6, soft_delete_retention_seconds: 0 } }离线source-only校验命令文档原样给出脚本位于 backend/scripts/validate_frame_request_bucket_contract.pypython backend/scripts/validate_frame_request_bucket_contract.py \ --source-only \ --runtime-env backend/deploy/runtime_env.yaml \ --contract backend/deploy/frame-request-bucket-contract.json校验器是刻意离线的模块 docstringThis is intentionally an offline validator... It never creates or mutates a bucket它做四件事validate_contract_document()校验契约本身——必须命名两个环境变量、conversation_attachment_policy必须为conversation_lifetime、永久桶expires_objects必须显式为false、位置限定、强制 UBLA、public_access_prevention必须为enforced、临时桶删除年龄在[1,7)天内且软删除关闭validate_runtime_binding()递归扫描 runtime env 清单backend/deploy/runtime_env.yaml要求两个桶变量均被绑定且保留env_var名称绑定必须携带 default/value--source-only模式下若同时传入--bucket/--lifecycle-json等实参会报错--source-only cannot claim live bucket validation——源码模式不能声称验证了线上桶非 source-only 模式配合gcloud storage buckets describe --formatjson导出的 lifecycle JSON会逐项比对真实桶名、location、lifecycle 规则存在age/createdBefore等过期条件即报expires objects、UBLA、公共访问防护、加密Google 托管或指定默认 KMS 键。7.2 线上桶门禁部署必须额外通过工作流的在线gcloud storage buckets describe验证对两个桶逐一确认精确桶身份bucket name 与绑定一致、location、分层的 lifecycle、软删除、uniform bucket-level access、public-access prevention 与加密姿态。文档明确仅 runtime-env 校验不能声称通过线上桶门禁。7.3 独立健康门禁frame-request-retention-hourly独立的frame-request-retention-hourlyCloud Scheduler 目标同样是部署门禁且不共享canonical 的 memory-maintenance 调度。关键回滚安全机制在操作者观察到该 job 健康并设置FRAME_REQUEST_RETENTION_INDEPENDENT_HEALTHYtrue之前memory maintenance 会保留旧版清理调用作为回滚安全桥。源码印证backend/modal/memory_maintenance_job.py 中当FRAME_REQUEST_RETENTION_INDEPENDENT_HEALTHY非true时仍以user_limit250调用run_frame_request_retention_maintenance()作为 legacy 安全兜底且其异常只记日志、不影响 canonical maintenance 继续。runtime env 中该变量的绑定见 backend/deploy/runtime_env.yaml与BUCKET_FRAME_REQUESTS*绑定同时出现在保留 job 的服务定义中。7.4 设备身份边界文档特别澄清桌面端device_id是属主作用域的路由与恢复标识符而非独立凭据Firebase 认证、账号代际与精确设备匹配仍是权威判定当前产品不主张加密级设备认证加强该边界属于独立的后续安全决策。八、用户导出显式的bytes_available语义用户导出包含三类内容frame-request 元数据持久化的视觉 / 关键帧收据durable vision/keyframe receipts显式的会话照片清单explicit conversation-photo manifests。清单的可用性语义是显式的当专属对象可读时清单携带bytes_base64字段当对象不可用时以bytes_available: false显式表达该图不可导出而不是静默省略图片。这避免了客户端把缺失误读为导出成功但无图保证隐私与数据完整性语义对用户透明。九、全生命周期速查与关键文件索引关注点关键实现双桶选择与对象路径backend/utils/retrieval/frame_request_storage.py队列状态机与清理状态backend/models/frame_request.py挂接晋升、outbox、各清理操作backend/database/frame_requests.py调度 worker 主循环与租约backend/services/frame_request_retention.py独立 job 入口backend/modal/frame_request_retention_job.py策略常量TTL/配额/批大小backend/utils/retrieval/frame_request_policy.py桶契约与离线校验器backend/deploy/frame-request-bucket-contract.json、backend/scripts/validate_frame_request_bucket_contract.pyruntime env 桶绑定backend/deploy/runtime_env.yaml双桶行为测试backend/tests/unit/test_frame_request_storage_tiers.pyworker 分页与独立 job 测试backend/tests/unit/test_frame_request_retention_pagination.py、backend/tests/unit/test_frame_request_retention_job.py从整体设计看这套留存体系的核心可概括为三句话临时与永久物理隔离晋升靠先收据后复制再原子换引用保证任何时刻可恢复删除靠outbox/收据先行 调度 worker 指数退避重试保证外部存储失败不丢引用部署靠契约文件 离线校验 线上桶门禁 独立健康门禁四层验收保证留存语义不漂移。对需要落地类似会话级永久图片 未挂接对象限期回收留存策略的团队这份文档与其配套源码是一份可直接复用的参考实现。【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考