Snaply 最容易被记住的是外观:Windows 98/2000 式的窗口、直角按钮、像素化边框,还有一个把上传过程写成终端日志的页面。重新打开仓库后,我的注意力很快从外观移到了上传按钮后面。图片出现于画廊,只是一次操作在页面上的结果;它写到了哪里,元数据是否完整,派生标签有没有失败,刷新以后还能不能找回来,才是接下来需要核对的事。
公开仓库建于 2026 年 1 月 19 日,第一个提交一次带入 41 个文件、约 6997 行内容。公开历史没有更早的开发过程,无法据此还原未被提交记录覆盖的开发过程或某次上传故障,也不能把两天内的五个提交写成一段从零开始的完整开发日记。这篇复盘只对固定提交 0cfe595 负责:从页面点下上传开始,代码实际留下了什么,还有哪些环节没有接上。

Snaply 上传后的状态链路:对象、JSON 元数据与 AI 标签分开处理,下方列出一致性窗口和当前构建失败。
图:根据公开提交 0cfe595 与 2026-08-09 隔离复测整理的自制图。红色项是当前缺口,绿色“建议/未实现”只表示后续修复方向。
一张图片会变成两份状态
Snaply 的服务端使用 Hono。收到 multipart/form-data 后,路由为文件生成随机 ID,先调用存储适配器保存对象,再用 Sharp 读取宽高,最后把名称、URL、大小、日期、尺寸和标签写入 JSON。一次上传由此留下两份事实:存储后端里的对象,以及画廊用来展示它的元数据。
分开保存让原型保持轻量,也带来了两个相反的失败窗口。对象写入成功后,如果 JSON 落盘失败,存储里会留下画廊不知道的孤立文件。删除时,路由调用 storage.delete() 后没有检查它返回的布尔值,便继续删除 JSON 记录并返回成功;对象删除失败时,画廊仍可能把它当成已经删掉。
文件方案可以继续使用,但双写需要有失败后的去处。上传时可以先生成稳定的操作 ID,对象写入后再提交元数据;第二步失败,就按同一 ID 删除刚写入的对象,或者把它记为待对账。删除时也可以先留下墓碑记录,等存储端确认成功再清理元数据。定期比较对象列表与元数据,还能把两类孤立状态从日志之外找回来。这些都只是待实现的修复方向,固定提交里还没有相应逻辑。
当前 Database 会把整个图片数组读进内存,每次新增、改标签或删除都用 writeFile 覆盖整份 JSON。对个人、低频、单进程的原型,这是容易理解和排查的取舍:不用部署数据库,打开文件就能看懂数据。但它没有锁、事务、版本检查和原子替换。多个请求或实例同时修改时,后一次整体写回可能覆盖前一次结果。
文件只在进程第一次读取时加载,后续始终使用内存数组。另一个实例或人工脚本改了磁盘上的 JSON,已运行进程不会自动感知。即使继续沿用文件方案,也要把“临时文件写入、刷盘、原子重命名”和“外部修改如何合并”分成两个问题处理。
S3 兼容仍有具体前提
仓库抽出了 StorageAdapter,让本地目录、S3 和 MinIO 共用上传路由。职责分开以后,存储服务之间的差异仍然存在。S3 实现固定使用 path-style,上传时也固定发送 ACL: public-read。有的后端不支持对象 ACL,有的部署希望用桶策略、CDN 或签名 URL 控制访问;相同的 API 形状,并不意味着公开策略也相同。
元数据保存一条 URL,列表路由会取它的最后一段作为文件名,再按当前配置组装展示地址。这能应对一部分域名或前缀变化,也默认了“URL 最后一段始终是对象键”。更稳妥的数据模型可以直接保存存储类型、bucket 与 object key 等稳定标识,把展示 URL 当成可重新计算的结果。以后更换 CDN 或访问策略时,就不必从旧 URL 反推存储事实。
配置页还有一个更直观的断点。POST /api/config/test 上方留着“实际测试 S3/MinIO 连接”的 TODO,代码随后直接返回 Connection OK。这段逻辑没有触发 DNS、认证、桶读写或最小权限探测。按钮显示成功,只代表这条分支返回了成功字符串。
批量重新打标则暴露了读取契约的缺口。它调用 storage.get(filename) 读回图片,但 StorageAdapter 没有声明 get,S3 实现也没有这个方法,只有本地存储额外实现了读取。参数被写成 any 后,TypeScript 没能在构建阶段指出问题。因此,配置里可以选择多种存储,不代表每项功能都已经覆盖这些存储。
配置项还没有进入上传链路
旧文曾把 WebP 转换、缩略图和文件大小限制写成已经发生的上传步骤。顺着固定提交的源码重新走一遍,convertToWebp、webpQuality、generateThumbnail、thumbnailSize 和 maxFileSize 只存在于配置类型、默认值和界面控件里,上传路由没有读取它们。
Sharp 当前做的事情更窄:先为元数据读取原图尺寸;当图片超过 2 MB 且要发送给 AI 时,再生成最大 1024×1024、质量 80 的 JPEG 输入。后一份 buffer 只进入模型请求,存储里仍然是原文件。配置页上的开关表示字段可以被保存,还不能说明数据路径已经执行了对应功能。
AI 标签是上传后的派生任务
上传路由在对象和 JSON 都保存以后,才异步调用 AI。这个顺序让模型超时或返回异常时,原图仍能成为一条有效记录,标签可以稍后重试或手工修正。标签是上传后的派生数据,不应控制原图是否保存成功。
现在的异步处理主要依赖日志。标签成功时更新数组,失败时写一条错误;元数据本身没有 pending、failed、尝试次数和最后错误。页面重新打开后,一张无标签图片究竟还在等待、已经失败,还是根本不需要 AI,现有数据回答不了。给派生任务增加独立状态和有上限的重试,才能同时保留上传主链路和失败记录。
代码里有 Ollama、Gemini、通义、智谱和硅基流动五个分支,这只能证明五种请求路径被写过。服务端在文件顶部静态导入 @google/generative-ai,server/package.json 却没有声明这个包,导致服务端即使不启用 Gemini 也无法完成打包。缺少 provider 的真实连通记录、超时策略、失败状态和回归测试时,“支持五种 AI”更接近一份待验证的适配清单。
选择云端 provider 后,图片还会离开本地存储范围。配置管理也要分别处理认证、返回脱敏、允许写入的字段和凭据存储。固定提交没有提供可公开部署的验收记录,所以这篇复盘只保留原则层面的风险,不推断是否存在公网实例或真实配置。
页面成功之前先读回数据
前端为后端不可达准备了一个“离线模式”:用 FileReader 把图片变成 Data URL,再放进 Pinia 的内存数组。画廊可以在当前页面显示图片,但源码没有写入 localStorage、IndexedDB 或文件系统,刷新后也就没有恢复来源。界面把它标成 LocalStorage Fallback,实际能力更接近“本页临时预览”。
API 封装也把几种状态混在了一起。图片列表和上传函数没有先检查 HTTP 状态与 success;响应 JSON 可以解析却缺少 data 时,data || [] 会返回空数组,让正常空画廊和部分服务端拒绝难以区分。JSON 本身解析失败时,res.json() 会直接 reject:列表加载捕获异常后进入离线状态,上传路径则继续抛错,不会得到空数组。这两条路径需要分开记录。
我给上传成功留了一条较窄的验收标准:接口返回成功后,立即用服务端返回的 ID 重读元数据,再对 URL 做一次对象存在性检查。它不能提供事务保证,只是在页面显示成功之前,确认对象和元数据都能被读回。读回失败时,页面应保留操作 ID 和失败阶段,而不是自动落到一个看起来正常的空画廊。
这次干净构建没有通过
2026 年 8 月 9 日,我从远端 HEAD 重新导出隔离副本,使用 Node 22.22.3 和 pnpm 10.6.4 复测。根目录的冻结锁文件安装能够完成,随后执行全量构建,服务端步骤找不到 tsup,并提示自己的 node_modules 缺失。仓库没有 pnpm-workspace.yaml,根安装没有把服务端纳入完整工作区。
改到 server 目录单独冻结安装,锁文件又因为 pkg 声明没有同步而被 pnpm 拒绝。放宽锁文件安装后,服务端打包停在缺少 @google/generative-ai。前端独立构建也没有通过:imageStore.ts 有两处可能取到 undefined 的 TypeScript 错误。
远端树里没有项目自有测试和 GitHub Actions,HEAD 也没有 check run、成功状态或部署记录。v0.0.1 虽然是正式 Release,却没有构建附件,tag 还停在最后一个部署文档提交之前。这些记录无法证明项目从未在别的机器上运行;它们只说明固定 HEAD 在本次干净环境里无法复现一次完整构建。
可复现的 Release 至少要指向确切提交,用冻结锁文件完成前后端构建,并留下命令结果。若再提供二进制或容器镜像,还要有附件哈希和最小启动验收。功能列表写的是计划交付什么,构建产物和验收记录才对应当时实际留下的版本。
下次先修什么
下一次打开仓库,先统一工作区与安装路径,同步服务端锁文件,声明或移除 Gemini 依赖,再修掉前端两处类型错误。冻结锁文件的全量构建能够连续通过以后,新增功能才有稳定的起点。
随后再补存储契约:让连接测试真实执行最小权限探测,为批量重标定义统一读取方法,给对象与元数据的删除失败补上处理,并为 JSON 并发和上传后 AI 失败增加测试。若要提供公网访问,认证、配置脱敏和访问策略也应先于部署文档完成。
WebP、缩略图、更多 AI 和新的 Release 可以再往后排。对这个项目而言,下一轮维护更需要先回答几个朴素的问题:对象写到了哪里,元数据是否提交,派生任务有没有完成,失败后从哪一层恢复。答案能够从代码和运行记录中读出来以后,页面上的“上传完成”才算有了对应的状态。