
Sim 上传会话架构全解析PostgreSQL 控制面与直传对象存储的数据面分离设计【免费下载链接】simSim is the collaborative workspace to build, deploy, and monitor AI agents and workflows. Used by 100,000 builders.项目地址: https://gitcode.com/GitHub_Trending/sim16/simUpload Sessions 是 Simapps/sim中承载文件直传对象存储这一能力的核心模块它把上传的控制面状态会话、Token、最终 Key、进度持久化在 PostgreSQL而把字节流的数据面直接指向 S3、Azure Blob、GCS 或本地磁盘的最终存储 Key从而让任意大小的文件都无需经过应用进程内存缓冲。读完本文你将掌握该模块的两种传输方式单次签名 PUT 与 Provider Multipart及其阈值选择、完整状态机、完成阶段的多重校验流程、安全 Token 与不可变凭据绑定机制以及本地/云存储两套过期清理方案在多副本自托管部署下的正确落地方式。一、为什么需要上传会话控制面与数据面分离传统上传把整个文件通过应用服务器代理到对象存储文件越大应用进程的内存与带宽开销越大。Sim 的做法是把一次上传拆成两个独立的平面控制面Control Plane上传会话本身状态存放在 PostgreSQL 的upload_session表中schema 定义见 packages/db/schema.ts负责记录会话身份、存储上下文、最终 Key、进度与状态机数据面Data Plane客户端拿到的签名 URL直接向存储提供方写字节完全不经过 Sim 进程。从 service.ts 的实现看一次上传会直接落到最终存储 KeyfinalKey而不是先写临时 Key 再搬运——例如 workspace 文件使用generateWorkspaceFileKey(workspaceId, fileName)知识库文档使用generateKnowledgeBaseFileKey(fileName)执行附件则使用包含 workspaceId/workflowId/executionId 的唯一 Key见 service.ts。数据库表通过upload_session_final_key_unique唯一索引见 packages/db/schema.ts保证同一个最终 Key 只会被一个会话使用从结构上杜绝了覆盖冲突。二、两种传输方式与阈值选择会话创建时createUploadSession根据文件大小决定传输方式见 service.ts 中的常量定义常量值含义UPLOAD_SESSION_PUT_MAX_BYTES50 MiB50 * 1024 * 1024云存储单次 PUT 的上限UPLOAD_SESSION_PART_SIZE8 MiB8 * 1024 * 1024multipart 每个分片的大小UPLOAD_SESSION_LOCAL_PUT_MAX_BYTES8 MiB PART_SIZElocal 提供方单次 PUT 的上限UPLOAD_SESSION_MAX_PART_URLS100单次请求最多签发的分片 URL 数UPLOAD_SESSION_TTL_MS24 小时会话从创建到过期的时长UPLOAD_SESSION_ASSET_MAX_BYTES5 MiB头像/工作区 Logo 等小资产的硬上限选择逻辑见 service.tsconst provider uploadStorageProvider() const putMaxBytes provider local ? UPLOAD_SESSION_LOCAL_PUT_MAX_BYTES : UPLOAD_SESSION_PUT_MAX_BYTES const method: UploadTransferMethod params.fileSize putMaxBytes ? put : multipart const partSize method multipart ? UPLOAD_SESSION_PART_SIZE : null const partCount method multipart ? Math.ceil(params.fileSize / UPLOAD_SESSION_PART_SIZE) : null1. 单次签名 PUT≤ 50 MiB小文件只签发一个 create-only 的签名PUTURL客户端一次写完。关键约束是create-only 前置条件签名时携带的元数据uploadId、userId、originalName、purpose、workspaceId等见uploadSessionObjectMetadata加上 create-only 语义保证 URL 只能创建新对象而不能覆盖已有对象。测试 service.test.ts 明确断言persists a hashed token and signs a create-only PUT at the final key。2. Provider Multipart 50 MiB大文件走存储提供方原生 multipart会话创建时直接initiateMultipartProviderUpload对 local 是创建.multipart/uploadId/目录随后客户端通过createUploadPartUrls按需申请分片签名 URL单次最多UPLOAD_SESSION_MAX_PART_URLS 100个且要求分片号是1..partCount之间的不重复整数见 service.ts。分片 URL 的签名有效期由UPLOAD_URL_TTL_MS 60 * 60 * 10001 小时控制见 provider.ts。multipart 会话的生命周期远长于单个分片 URL——分片 URL 过期后客户端重新申请即可这是与一次性 PUT 的显著差异PUT URL 过期等价于本次上传中断需要重新创建会话。3. Local 提供方的特殊阈值local的 PUT 不是发给对象存储而是通过应用路由代理/api/v2/uploads/uploadId见 provider.ts因此受路由 body 限制约束。代码特意把UPLOAD_SESSION_LOCAL_PUT_MAX_BYTES与UPLOAD_SESSION_PART_SIZE保持相等但分开命名service.ts 注释说明目的是未来为云存储吞吐调大分片大小时不会悄悄移动 local 代理的阈值——超过该阈值 local 也会转 multipart而分片本身恰好已按路由限制设计尺寸。三、会话状态机与数据库模型upload_session表使用 PostgreSQL 枚举列约束状态见 packages/db/schema.ts状态uploading → completing → finalizing → completed另有aborting → aborted、failed、expired传输方式put | multipart存储提供方local | s3 | blob | gcs用途purposeworkspace_file | table_import | knowledge_document | profile_picture | workspace_logo | mothership_attachment | execution_attachment每种用途决定了存储上下文、最终 Key 生成方式和大小上限见 service.ts用途大小上限存储上下文workspace_fileMAX_WORKSPACE_FILE_SIZE 5 GiBshared/types.tsworkspacetable_import同上table-importknowledge_documentMAX_KNOWLEDGE_DOCUMENT_FILE_SIZE 100 MiBknowledge-baseprofile_picture/workspace_logoUPLOAD_SESSION_ASSET_MAX_BYTES 5 MiBprofile-pictures/workspace-logosmothership_attachment同 workspace_filemothershipexecution_attachmentMAX_WORKSPACE_FORMDATA_FILE_SIZE 100 MiBexecution此外workspace_file与knowledge_document还会在创建时执行存储配额检查checkStorageQuotaForBillingContext见 service.ts。会话处理使用**租约lease**机制防并发claimSession通过processingLeaseIdprocessingLeaseExpiresAt默认 5 分钟PROCESSING_LEASE_MS原子认领会话认领时校验租约为空或已过期service.ts。任何完成/中止/清理操作都必须持有有效租约失败时通过租约丢失错误中止避免两个进程同时处理同一会话。四、完成Complete流程服务端全量校验completeUploadSession的设计原则是客户端无需提交分片清单manifest服务端从存储提供方重新列出并校验分片。完整流程见 service.ts校验会话状态仅uploading、completing、finalizing可继续已completed且存在completedFileId时直接通过loadCompleted返回持久化结果幂等重试不重复运行 finalizer认领会话claimSession对uploading状态额外校验未过期headProviderObject探测最终对象若已存在校验对象身份后直接进入 finalizing覆盖PUT 已写入但 complete 请求丢失的场景对 multipartlistMultipartProviderParts从提供方列出分片经validatedSortedProviderParts严格校验service.ts分片数必须恰好等于partCount分片号必须是连续的1..partCount各一次每个分片字节数必须等于期望值前partCount-1个为partSize最后一个为余数见expectedUploadPartSizeS3/GCS 分片必须携带 ETag调用completeMultipartProviderUpload完成 provider 上传成功后再次head并执行assertObjectIdentityservice.ts——校验最终对象的 uploadId、字节大小、contentType 三项全部与会话一致任何一项不匹配都抛conflict状态置为finalizing并持久化providerObjectVersion运行领域 finalizerfinalize回调对 workspace 文件是注册workspace_files记录见 application.ts 的completeWorkspaceUploadSessionmarkUploadSessionCompleted写入completed状态与completedFileId。对 local 提供方multipart 的完成是assembleLocalParts按顺序把各分片 append 到 staging 临时文件写元数据再通过linkLocalArtifact原子发布到最终路径link失败返回EEXIST即拒绝覆盖跨设备EXDEV时先复制到目标设备再 link保住 create-or-fail 语义见 provider.ts。中止Abort与删除保护abortUploadSessionservice.ts对已completed会话直接拒绝对finalizing且已有completedFileId领域资源已注册的会话也拒绝中止。中止时通过discardIncompleteProviderState若最终对象已存在则校验身份后按版本删除否则中止 provider 的 multipart 状态。这一保护正是 README 强调的清理不得在领域资源创建后删除对象——markUploadSessionCompleted只有在 finalizer 报告completedFileId时才写入该值若 finalizer 在自己的注册事务内记录markUploadSessionFileRegistered该值被保留abort guard 与过期清理就不会把已存在持久资源的会话当作可丢弃对象。五、安全模型Token 与不可变凭据绑定每个会话创建时生成 32 字节安全随机 TokengenerateSecureToken(32)数据库中只存 SHA-256 哈希tokenHash见 packages/db/schema.ts校验使用常量时间比较safeCompareservice.ts。Token 是字节面能力凭证它只能证明持有者可以写字节本身不授予任何工作区访问权。对于workspace_file、knowledge_document、table_import这三种用途会话还会在metadata.authBinding中持久化创建时授权的不可变凭据绑定UploadSessionAuthBinding见 service.ts支持四类主体session用户会话绑定userId sessionIdpersonal_api_key绑定userId keyIdworkspace_api_key绑定workspaceId keyIddelegated执行器委派绑定serviceIdexecutor、subjectUserId、audience、workflowId、可选executionId且仅允许明确的委派受众如table_import的sim:tables。控制面操作GET 状态、申请分片 URL、complete、abort在每次调用时都会用当前的 Principal 重新核对绑定assertUploadSessionAuthBinding即使 Token 泄露没有匹配的凭据也无法操作会话测试 service.test.ts rejects workspace control access without the matching immutable credential binding 验证了这一行为。对旧版本创建的、无绑定字段的会话仅保留有限的兼容路径assertLegacyUploadSessionOwner且新会话一律写入绑定service.ts。六、过期清理云 Provider 生命周期规则与本地有界清扫1. 云存储交给 Provider 生命周期规则README 明确要求S3 / GCS生产桶必须配置生命周期规则两天后自动中止未完成的 multipart uploadAzure Blob自动对未提交的 block 进行垃圾回收默认七天后清理。云部署下数据库层的过期会话uploading/completing/aborting以及无completedFileId的finalizing且expiresAt已过、租约空闲由 cron 路由 apps/sim/app/api/cron/cleanup-tasks/route.ts 调用的cleanupExpiredUploadSessions处理service.ts每批最多CLEANUP_BATCH_SIZE 100条按expiresAt升序每条先claimSession拿租约再discardIncompleteProviderState中止 provider multipart 或按版本删除对象最后置为expired对超过TERMINAL_RETENTION_MS 7 天的终态会话completed/aborted/expired做二次清扫aborted/expired会话删除其拥有的最终对象deleteOwnedFinalObject先校验对象身份再按版本删然后物理删除数据库行completed会话仅删除行、不删对象。2. 本地存储.multipart/下的有界清扫local 提供方的 multipart 分片存放在上传目录的.multipart/uploadId/下staging 临时文件在.staging/下见 storage-key.ts由 cleanup.ts 负责清理参数如下常量值含义LOCAL_UPLOAD_CLEANUP_INTERVAL_MS15 分钟两次清扫的最小间隔限流LOCAL_UPLOAD_ARTIFACT_TTL_MS25 小时产物存活上限与云 Provider 的两天规则等效的本地实现LOCAL_UPLOAD_CLEANUP_MAX_ENTRIES200单次清扫的最大扫描条目数maybeCleanupLocalUploadArtifacts是单飞single-flight且限流的并发调用会合并为一次15 分钟内的重复调用直接返回{ scanned: 0, removed: 0 }cleanup.ts。有界清扫的关键设计是进程内目录游标cleanupRootStates为每个清理根.multipart、.staging保留一个opendir句柄nextCleanupRootIndex在根之间轮转cleanup.ts。每次清扫从游标处继续而不是从头扫描因此大量新条目无法无限期饿死目录中靠后的过期条目——这正是 README 强调的 retains its process-local directory cursor between bounded runs。测试 cleanup.test.ts continues from its directory cursor so old entries cannot starve behind fresh ones 专门验证了这一行为。清扫对超过 TTL 的条目按 mtime 判定并递归删除对目录用rm(path, { recursive: true })staging 中崩溃请求遗留的临时文件同样被回收。3. 多副本自托管部署的运维注意本地清扫目前是机会性的只有当同一个 Sim 进程创建新的上传会话时才会触发createUploadSession中 local provider 分支调用maybeCleanupLocalUploadArtifacts见 service.ts。仓库没有能安全到达每个副本进程本地文件系统的调度器HTTP cron 请求只会命中一个副本Trigger workers 不拥有 Web 副本的磁盘。因此对于使用非共享本地磁盘的多副本自托管部署运维必须二选一确保上传流量持续打到每个副本使机会性清扫在每个副本上都能被触发在各自的副本级维护钩子中直接调用导出的sweepLocalUploadArtifacts该函数也被设计为供副本级维护钩子与确定性测试使用的纯有界清扫入口接受{ now, maxEntries }参数见 cleanup.ts。云部署则直接使用上文所述的 Provider 生命周期规则无需依赖应用进程内的清扫。七、本地数据面的工程细节local 提供方为了在单机部署下复现对象存储语义做了几处值得关注的工程处理均有对应测试佐证对象身份元数据每个对象旁挂.upload-metadata.json侧车文件LOCAL_UPLOAD_METADATA_SUFFIX记录uploadId、contentType与完整元数据headProviderObject据此返回与云一致的对象身份provider.ts版本号local 版本由dev:ino:size:mtimeMs组成localVersiondeleteProviderObjectVersion先比对版本再删除避免误删被替换的对象字节数精确校验写入 PUT 对象与分片时流式Transform计数器实时监控超限即抛LocalUploadBodyError最终字节数不符也拒绝provider.tsNAME_MAX 防护staging 临时文件命名只依赖固定宽度 uploadId 而非目标文件名避免长文件名触发ENAMETOOLONGKey 末段通过buildStorageKeySegment在 255 字节组件上限内预留侧车后缀空间并尽量保留扩展名storage-key.ts。八、源码阅读索引状态机与核心服务apps/sim/lib/uploads/upload-session/service.ts提供方适配与本地数据面apps/sim/lib/uploads/upload-session/provider.ts本地有界清扫apps/sim/lib/uploads/upload-session/cleanup.ts领域用例workspace 文件上传编排apps/sim/lib/uploads/upload-session/application.ts类型定义apps/sim/lib/uploads/upload-session/types.ts数据库表与枚举packages/db/schema.tsCron 清理入口apps/sim/app/api/cron/cleanup-tasks/route.ts存储提供方配置S3/Azure/GCS 环境变量apps/sim/lib/uploads/config.ts测试服务状态机与安全绑定 service.test.ts、本地清扫 cleanup.test.ts、本地数据面 provider.test.ts、领域用例 application.test.ts九、总结Sim 的上传会话模块用PostgreSQL 控制面 直传存储数据面的架构把上传字节流彻底移出应用进程50 MiB 以内走 create-only 签名 PUT更大文件走 Provider Multipart完成阶段服务端从存储提供方重新列出并校验分片数量、字节数与最终对象身份后才运行领域 finalizerToken 只作为字节面凭证控制面操作额外核验不可变凭据绑定过期清理在云上依赖 Provider 生命周期规则S3/GCS 两天、Azure 七天在本地则由带目录游标的有界清扫25 小时 TTL兜底——多副本自托管部署需要自行确保每个副本的清扫入口都能被执行。【免费下载链接】simSim is the collaborative workspace to build, deploy, and monitor AI agents and workflows. Used by 100,000 builders.项目地址: https://gitcode.com/GitHub_Trending/sim16/sim创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考