forming / System Design Archive

Visual System

Visual System 不把一张图当一个首页卡片,而是把无限来源收敛成有限 VisualCollection;Immich 承担媒体 ingest、AI tagging、embedding 和语义搜索,MyBlog 承担策展、关系、Graph 和阅读流入口。

Pinterest Visual ShellImmich Media RuntimeVisual Collection SystemVisual Bookmark SyncVisual Material SystemPaletteSealSticker
Inspiration

借鉴对象

  • Immich
  • Google Photos
  • Are.na Channels
  • Cosmos
  • Eagle
  • Milanote
  • Pinterest Board
  • museum catalog
Rejected

拒绝的方案

  • 通用 SaaS 卡片网格,因为它会让视觉素材变成后台列表。
  • 一张图一个首页卡片,因为 Pinterest / Pixiv 级素材源是无限的,首页 Feed 必须有限。
  • 把 Pinterest 整个账号搬运到本地,因为用户不是 Pinterest 的 owner,Saved Pins、排序、反爬和平台会话都不该被 MyBlog 伪装成自己能控制的实时源。
  • 前端手写爬虫读取 Pinterest / Pixiv,因为外部账号、网络和原图体积都不应进入公开页面冷启动链路。
  • 把 Immich 当 Astro 组件或 /immich 子路径,因为它是独立媒体服务,必须部署在自己的域名根路径。
  • 自己训练图片识别模型,因为 Immich / CLIP / RAM++ 这类成熟系统已经覆盖基础识别和向量能力。
  • 默认开启 hover preview,因为用户已明确预览不是核心体验,应放到设置开关且默认关闭。
  • 大面积渐变和装饰圆点,因为它们不能解释素材本身。
Runtime

运行方式

  • 当前 P0 真源是 apps/web/src/data/visuals.ts 内的 VisualCollection[];visualItems 只作为兼容派生,不再是首页主模型。
  • 2026-05-07 之后的浏览层 canonical 方向是 Pinterest Visual Shell:全站导航和首页 Feed tabs 都在 OpenList 旁边提供 Pinterest 站内嵌入入口,由 BaseLayout 持有 embed layer;/visuals/ 只是视觉索引和策展页面,不再承担唯一入口。
  • Pinterest full page 不能被完整 iframe:实测 Pinterest 返回 CSP frame-ancestors self,官方 embedUser 只是 marketing widget,会强插 Follow CTA、限制无限滚动和交互。全局 Pinterest 入口不得再使用 embedUser 冒充完整 Pinterest。
  • Pinterest 入口必须是站内 Browser-session mirror Shell,不是外跳 profile / saved pins 链接,也不是新窗口。触发器是 [data-pinterest-embed-open],关闭器是 [data-pinterest-close]。
  • BaseLayout 的 Pinterest Shell 读取 /api/runtime/visuals/snapshot 的 Pinterest collections,在 MyBlog 自己的 Masonry board 里渲染;runtime 不可用时回退 public-data/visual-sources/visual-manifest.json 构建期镜像。
  • MyBlog 不复制 Pinterest cookie、sessionStorage、localStorage auth blob 或 OAuth token;登录态只留在浏览器 profile 和 Pinterest 平台。官方 pinit.js 只允许用于 indexed pin 的 Save Button,不再用于全局 profile embed。
  • Pinterest / Pixiv 账号接入由 tools/import-visual-sources.mjs 执行 bookmark sync:使用 .runtime/visual-import-browser-profile 的本机浏览器 profile,登录态只留在浏览器,不写入 JSON、README 或源码。
  • public-data/visual-sources/sources.json 只记录非敏感 URL、标签、limit 和 collection metadata;public-data/visual-sources/visual-manifest.json 只记录收藏来源链接、平台预览图 URL 和公开 metadata。
  • 2026-05-07 之后的 backend mirror 方向是准实时索引:cron/manual trigger -> Pinterest API / Apify provider -> MySQL upsert visual_pins -> deleted_at diff -> deterministic partition -> /api/runtime/visuals/snapshot -> /visuals runtime hydrate。静态 visual-manifest.json 只作为构建期 fallback。
  • MySQL runtime mirror 是 local visual index / cache,不是 Pinterest 的替代真相;它服务于搜索、Graph、贴纸、注释、断链兜底和 deterministic partition,实时浏览体验优先由 Pinterest Shell 承担。
  • Runtime 表由 apps/admin-next/lib/runtime-db.js 管理:visual_sources、visual_pins、visual_sync_runs。visual_pins 使用 source_id + pin_id upsert,记录 first_seen_at、last_seen_at、deleted_at、position_index 和 downloaded=false。
  • 官方 Pinterest API provider 需要 PINTEREST_ACCESS_TOKEN 与 PINTEREST_BOARD_ID;未配置时同步器必须返回配置错误,不能回退成首屏 Playwright 抓取并声称全量。Pinterest 没有稳定 webhook,成熟形态是 1-10 分钟轮询 + diff。
  • Apify provider 是 saved pins / profile / board 的快速镜像通道:Apify scheduled scraper 负责登录态和分页采集,MyBlog 只读取 dataset/task 最近一次成功输出,完整分页 upsert 到 visual_pins。provider_config_json 只存 datasetId/taskId 这类非敏感配置,APIFY_TOKEN 只放服务器环境变量。
  • 每次 sync 的完成条件不是“取到一屏”,而是 provider 当前分页结果读取完毕;本轮未出现的旧 pin 标记 deleted_at,前端 snapshot 只渲染 active pins。
  • 2026-05-07 当前运行状态:Pinterest / Saved Pins 已同步真实 pin 收藏,当前 manifest 为 25 条并拆成 4 个 partition collection;Pixiv 在专用 browser profile 中仍返回未登录/404,sync report 记录 syncedItems: 0,前端不展示登录页推荐图或 404 占位图。
  • 外部同步源必须进入 Visual Collection Partition:不要把 100 张图塞进一个大卡,也不要一图一卡。P0 按 partitionPattern 确定性拆成 [6, 4, 9, 12] 循环的 mini moodboard collection,后续 P2 才接 AI clustering / CLIP embedding。
  • Collection card 顶部是非对称 mini moodboard 拼贴,通常用 3-4 张代表图表达这一组的视觉主题;board 内显示这一组的完整图片。导入器必须累积滚动过程中的可见条目,因为 Pinterest 会虚拟滚动并卸载旧 DOM。
  • Immich 是首选 AI 媒体库运行时:负责图片 / 视频 ingest、时间线、人脸、物体识别、CLIP embedding、语义搜索、缩略图、EXIF 和视频关键帧;MyBlog 不重复实现这些底层能力。
  • Immich 公共入口固定为 https://photos.blog.tengokukk.com/;当前站内只提供外部入口和架构合同,待 DNS、独立存储卷、Docker Compose 与 Nginx vhost 就绪后再启用服务。
  • Immich 结果进入 MyBlog 的长期链路是 Immich API -> admin-next import -> MySQL / visual snapshot -> VisualCollection / Knowledge Object;前台不在访客请求里触发模型推理。
  • 首页视觉 Feed 按 collection 渲染代表图堆叠;/visuals/ 展示 collection card,点击后展开内部 board。
  • 当前阶段不下载原图、不生成本地图片副本;离线 thumbnail mirror 只能作为用户明确批准后的后续管线。
  • Hover Preview 作为全站交互层保留,但由设置开关控制。
  • Collection 必须携带 mood、sourceLabel、palette、coverImages、images 和 curationNote,而不是只展示图片。
Tradeoff

取舍

  • Collection 会牺牲单图曝光量,但换来首页密度、策展感和长期维护秩序。
  • 自动配色未来有价值,但必须基于真实图片分析,不使用固定 AI 味配色。
  • 前端只读 manifest 会让同步多一步,但避免每个访客承担外部抓取和账号会话成本。
  • 官方 Pinterest embed 牺牲了完全可控的 DOM 和排序,但换来合规、稳定和真实登录态;MyBlog 的增值点转向视觉壳、注释、聚类和 Graph。
  • 把 Immich 独立部署会增加运维成本,但避免 MyBlog 复刻媒体库、缩略图、AI 推理和向量索引。
  • 浮层统一 portal 能解决 overflow 裁剪,但默认关闭降低干扰。
Future Direction

后续方向

  • 在独立存储卷上部署 Immich,并把 photos.blog.tengokukk.com 反代到 Immich 根路径。
  • 通过 admin-next 消费 Immich API,把 album、asset、tag、embedding 结果写入 VisualCollection runtime snapshot。
  • 在平台预览图断链率不可接受时,再建立 OpenList + thumbnail mirror 的可选离线化任务。
  • 做 dual-save:Save to Pinterest 同时 Save to Visual Graph,Pinterest 负责公共收藏,MyBlog 负责审美记忆和知识关系。
  • 用 CLIP embedding 或同级图像特征做自动聚类,生成候选 collection。
  • 让图片主色生成 metadata 线、印章色和弱边界色。
  • 把视觉素材纳入 Unified Knowledge Object。
  • 让 Visual Collection 可以被文章、书籍高亮、项目 Room 和 Graph 引用。