SGBH6AXS / notesZhang Shuai's Personal Blog

私人日记系统:现代化架构与实施方案

This post is only available in Chinese for now.

私人日记系统:现代化架构与实施方案 v1

设计日期:2026-10-10
状态:推荐设计稿,供 Phase 0 评审和后续实现使用;尚未实现或通过运行验证。
输入:用户提供的《我的长期私人日记系统项目说明》。

1. 推荐结论与系统边界

保留原方案的核心方向:自托管、内部用户身份与 OIDC 解耦、Markdown 正文、原始媒体不可变、版本历史、私有附件、可导出可导入、独立备份。现代化改造主要落实在离线体验、明确的数据契约、可验证的数据生命周期和维护方式上。

推荐架构:模块化单体 + 本地优先的写作客户端 + PostgreSQL 事务与版本记录 + 不可变媒体存储 + 开放档案格式。

Web、API、Worker、CLI 可以分开运行,但共享领域模块、数据库和发布版本。不拆成多个独立业务服务。系统优先服务一位用户,允许日后接入少量独立用户;V1 不实现共享日记和协作权限。

服务端已接受的日记以不可变 Revision 及其附件关系为内容依据;Entry 当前状态是事务内维护的查询投影。客户端未同步的草稿也是需要保护的数据,只是尚未被服务器接受。不能以服务器记录为理由丢弃本地草稿。

本方案默认:服务器能够读取正文;浏览器首次登录需要联网;离线使用限定已初始化的个人设备;多设备冲突显式保留;原图不被转换覆盖;回收站和已接受的版本默认长期保存。端到端加密、原生 App、外部 AI、共享功能需要独立设计。

需要调整的原设计

原设计 推荐调整 原因
离线能力主要放在 V1.5 V1 具备本地草稿、待同步队列和基本冲突保留 每日写作不应依赖网络响应;避免后续重写保存链路
Revision 主要记录标题和正文 Revision 保存 Entry 完整业务快照及附件关联 恢复旧版本必须同时恢复日期、标签、图片顺序和说明
附件表直接承担文件身份 区分 Blob、Attachment、RevisionAttachment 相同文件字节与不同业务引用是不同对象
同步用自增 seq 每个用户的事务计数器串行分配同步序号 自增序号的分配顺序不能直接代表提交顺序
导出主要是当前 Markdown 和媒体 阅读导出与完整档案分开;完整档案涵盖历史、回收站和冲突 人类可读与完整往返恢复是两个验收目标
备份链式复制 生产、NAS、异地分别保留历史;异地保护与生产账号隔离 同步复制会传播删除,不能替代可恢复的历史副本
将天气等一律视为可重建数据 区分检索派生物与不可重现的外部观测 已取得的天气观测或用户认可的 AI 文本可能具有档案价值

2. 架构与部署

flowchart TD
  Client["Web / PWA"] --> Edge["OpenResty:TLS 与入口"]
  Client --> Local["IndexedDB:草稿与待同步操作"]
  Edge --> API["API / BFF"]
  API --> SSO["ZITADEL:OIDC"]
  API --> DB["PostgreSQL"]
  API --> Objects["私有原始媒体"]
  Worker["Worker / CLI"] --> DB
  Worker --> Objects
  Worker --> Archive["Markdown / JSON 档案"]
  Backup["独立备份任务"] --> DB
  Backup --> Objects
  Backup --> Copies["NAS 与异地加密副本"]

生产 Compose 初始只包含 API、Worker、PostgreSQL;Web 静态资源由现有 OpenResty 提供。ZITADEL 使用既有实例。备份由独立系统任务运行,避免让日记 API 持有所有备份管理凭据。

同一个 HTTPS origin 提供 Web、/api/v1、/auth 和授权媒体接口。数据库不暴露公网。API 和 Worker 分别设资源限制;图片处理任务先按一个并发执行,防止大照片解码挤占写作服务资源。

部署成本主要受媒体体积、预览生成和备份带宽影响,不能只按 Entry 数量估算。文本查询不需要先引入独立搜索服务。视频转码后续单独评估 CPU、临时空间和队列容量。

技术选型

层 推荐方案 选择依据与边界
运行时 Node.js 24 LTS、TypeScript、pnpm 延续原技术方向;按支持周期升级,不固定十年不更新
Web React、Vite、TypeScript 私人应用无主要 SEO 需求,客户端编辑与缓存更直接
页面与数据 TanStack Router;IndexedDB 为本地编辑数据层 不同时维护多套独立 Entry 状态;请求缓存仅用于设置、任务等在线数据
本地数据库 Dexie 封装 IndexedDB 定义本地 schema、事务与升级路径,不将内部存储格式当公开档案格式
富文本编辑 优先验证 Milkdown;Markdown 源码模式作为基础能力 Milkdown 面向 Markdown 写作;仍需验证中文输入和往返转换
源码编辑 CodeMirror 6 候选 保留原始 Markdown 编辑路径;与富文本通过同一正文契约连接
API / BFF Fastify 5 统一请求校验、Session、业务事务及同步入口
请求契约 JSON Schema、TypeBox、OpenAPI 服务端与客户端从同一契约生成类型;数据库实体不直接成为 HTTP DTO
数据库 PostgreSQL 17 当前维护补丁版本 初始选择明确的受支持主版本;升级另做演练,不依赖最新主版本的专有函数
数据访问 Drizzle + PostgreSQL 驱动;关键事务用明确 SQL ORM 辅助类型,不隐藏锁、外键和迁移内容
媒体 FilesystemObjectStore;Sharp 生成图片预览 S3 adapter 按同一契约后续实现;HEIC 解码取决于实际构建能力
异步任务 PostgreSQL jobs 表 租约、幂等、重试、死信;V1 无 Redis 依赖
测试 Vitest、真实 PostgreSQL 集成测试、Playwright 重点验证数据丢失、跨用户访问和恢复场景
备份 PostgreSQL 逻辑备份 + restic 初期接受小时级 RPO;需求提高后增加 WAL / PITR
运维 Docker Compose、OpenResty、结构化日志 先用少量指标与告警,不引入整套服务网格或编排平台

2026-10-10 检索的官方 Node.js 发布页将 24 列为 LTS。PostgreSQL 官方按主版本提供五年支持;项目需要维护升级计划。[S1][S2] Fastify 的支持规则也依赖运行时支持情况。[S3]

Milkdown 官方项目介绍其基于 ProseMirror 和 remark,面向所见即所得 Markdown 编辑。[S4] Tiptap 也可以评估,但检索时其 Markdown 模块仍标为 Beta,并列出转换限制。[S5] 以上选型不表示已经验证移动端输入、附件插件或无损往返;Phase 0 应先做编辑器验证,再锁定实现。

3. 产品体验与界面设计

产品围绕记录、浏览、找回三个动作组织。默认打开“今天”,可直接新建无标题 Entry。标题可选,正文为空但存在照片的 Entry 合法。标签、收藏、所属日记本在编辑界面保持次要位置。

场景 推荐交互
桌面 左侧导航、中间时间线、右侧正文;窄屏可合并为列表与详情
手机 单列正文;底部“今天、时间线、搜索、更多”;新建入口固定可见
时间线 按保存的 local_date 分组;摘要、首张预览、标签、收藏与同步状态
日历 月视图显示 Entry 数量及照片标记;点击后展示当天多条记录
编辑 轻量工具栏;图片插入、拖动排序、说明文字;源码模式可切换
版本 按时间展示版本;文本差异和附件变化;恢复后生成新版本
冲突 展示共同基线、本地提交副本、当前服务器版本;提供合并或保存为另一条
数据管理 导出范围、导入预检、设备撤销、备份状态;与写作界面分开

视觉建议:中性背景、单一强调色、明确的文字层级、适合长文的行宽;提供浅色与深色主题、字体大小和行距调节。图片保留比例,不以缩略图裁切作为原图展示。交互支持键盘、屏幕阅读器和可见焦点;动画只辅助状态变化。

**保存状态需要可核实。**区分“正在保存到此设备”“已保存到此设备”“正在同步”“已同步”“需要重新登录”“存在冲突”“照片等待上传”“本地存储失败”。“已同步”只表示服务器接受了该版本及所需原图,不表示它已经进入异地备份。

不在正常写作流程显示 operation_id、游标、Blob 等实现信息。备份状态可在数据管理页显示最近成功时间与最近恢复演练时间。

4. 领域模型与 PostgreSQL Schema v1 草案

本节给出表、字段和约束的设计边界,不是已经执行验证的 DDL。实现阶段需形成迁移 SQL,并通过真实数据库检查外键、索引和事务行为。

4.1 身份、日记本与条目

实体 / 表 核心字段 约束与职责
User / users id、display_name、status、created_at 内部身份;与 SSO ID 无耦合
Identity / user_identities id、user_id、issuer、subject、provider_label、created_at UNIQUE(issuer, subject);issuer 精确匹配配置,不自行改写
Session / sessions token_hash、user_id、device_id、expires_at、last_seen_at、revoked_at 随机 Session token,数据库只存其哈希;不当作档案内容
Journal / journals id、owner_id、name、description、icon、sort_order、revision、archived_at、deleted_at 一个用户可拥有多个;归档不隐藏历史数据
Entry / entries id、owner_id、journal_id、current_revision_id、revision、title、body_markdown、时间字段、favorite、pinned、deleted_at、created_at、updated_at 当前查询投影;必须与 Revision 在同一事务更新
Tag / tags id、owner_id、name、normalized_name、revision、deleted_at 用户内名称去重规则明确;不自动翻译或合并不同语言
EntryTag / entry_tags owner_id、entry_id、tag_id 当前关系投影;不用于独立恢复历史版本
Device / devices id、owner_id、display_name、created_at、last_seen_at、revoked_at 登记同步设备;device_id 不是认证凭据

对存在跨表归属关系的实体建立 UNIQUE(owner_id, id),并使用包含 owner_id 的复合外键。例如 Entry 的 (owner_id, journal_id) 指向 Journal 的 (owner_id, id)。服务端 owner_id 从 Session 得到,不信任请求体。

V1 不使用 ON DELETE CASCADE 自动清除日记本内容、Entry 历史或媒体;删除进入 Trash,物理清除由专门维护流程执行。数据库 RLS 可增加防护,但不能替代应用授权;V1 先落实明确查询范围、复合外键和越权测试。

4.2 Revision 与冲突

实体 / 表 核心字段 约束与职责
EntryRevision / entry_revisions id、owner_id、entry_id、revision_number、parent_revision_id、operation_id、action、snapshot_schema_version、snapshot_json、body_markdown、body_sha256、accepted_at、device_id UNIQUE(entry_id, revision_number);快照不可原位修改
RevisionAttachment / revision_attachments owner_id、revision_id、attachment_id、position、caption、alt_text、placement 每个版本保存自己的附件引用与顺序;不依赖当前关联
EntryAttachment / entry_attachments owner_id、entry_id、attachment_id、position、caption、alt_text、placement 当前版本的查询投影,可以由 Revision 重建
EntryConflict / entry_conflicts id、owner_id、entry_id、base_revision_id、observed_head_revision_id、candidate_schema_version、candidate_snapshot、candidate_body_markdown、operation_id、created_at、resolved_revision_id 冲突候选单独保留;不伪装成已接受的主线版本
ConflictAttachment / conflict_attachments owner_id、conflict_id、attachment_id、position、caption、alt_text、placement 冲突记录也保护原图,参与导出和 GC 引用检查

快照至少包括:title、body_markdown、Markdown profile、journal_id 与当时名称、完整时间模型、favorite、pinned、标签 ID 与当时名称、deleted_at、附件清单、用户填写的说明和位置。名称快照帮助解释旧版本;恢复时关联实体以 ID 为准,对已删除的标签或日记本提供明确恢复策略。

原文 body_markdown 只保存一份正文;snapshot_json 不重复嵌入正文,包含其哈希和其他业务字段。body_sha256 对正文精确 UTF-8 字节计算。快照字段由 versioned schema 定义,不能靠任意 JSON 维持长期兼容。

从版本 15 修改到版本 16,在一个数据库事务里完成:检查基线、插入不可变快照和附件关系、更新 Entry 当前投影、更新标签关系、写同步变化、记录操作结果。任一步失败则事务回滚。

恢复版本 9 是“以版本 9 为内容创建新版本 17”,不把当前 revision 改回 9,不删除 10–16。移动 Entry、修改时间、删除、恢复、收藏和附件关系变化都属于版本化业务状态。用户可以在 UI 中按编辑时段分组查看;已接受的 Revision 不因界面分组而被合并删除。

编辑中的逐字草稿不逐字上传历史。建议本地约 300–800ms 去抖落盘;服务器提交可按空闲约 3–5 秒、显式完成、离开编辑页和重连触发。这些是可调实现参数,不能承诺浏览器关闭事件一定执行。

4.3 Blob、附件与派生物

实体 / 表 核心字段 约束与职责
MediaBlob / media_blobs id、owner_id、sha256、byte_size、storage_backend、object_key、state、created_at、verified_at UNIQUE(owner_id, sha256);字节数据身份,按用户去重
Attachment / attachments id、owner_id、blob_id、original_filename、detected_mime、source_metadata_json、metadata_schema_version、created_at 上传及业务引用身份;同一 Blob 可有不同附件记录
Upload / uploads id、owner_id、device_id、state、expected_size、received_size、expires_at staging → verifying → ready / failed;不能把未验证字节标为上传成功
Derivative / media_derivatives id、owner_id、blob_id、kind、recipe_version、object_key、mime、dimensions、state 可重建缩略图、预览与转码;保留生成配方版本

去重限定用户范围,不通过“这个哈希已存在”向其他用户暴露上传情况。附件说明、位置和排列属于关系数据,不写入共享 Blob。源 EXIF 是原始数据的提取结果;如需脱敏,创建新附件副本并显式说明,不能覆盖原文件。

物理对象键采用 owner_id 和哈希分片,例如 originals/<owner-id>/sha256/ab/cd/<full-hash>;原始文件名保存为元数据,不成为受信任路径。UUID 和哈希都不能替代授权检查。

4.4 同步与运行表

表 核心字段 职责
sync_heads owner_id、epoch、committed_seq 每用户一行,事务内锁定并更新计数器
sync_changes owner_id、seq、entity_type、entity_id、action、revision_id / after_image、created_at PRIMARY KEY(owner_id, seq);已提交变化,包含删除标记
operation_receipts owner_id、operation_id、request_sha256、status、result_json、created_at UNIQUE(owner_id, operation_id);重复操作返回相同结果
entity_tombstones owner_id、entity_type、entity_id、purged_at 永久清除后保留最小删除标记,防止旧设备复活内容
jobs id、kind、payload_version、payload、state、attempts、next_run_at、lease_until、dedupe_key 崩溃后租约可过期重领;有界重试与失败列表
archive_runs / import_runs id、owner_id、status、snapshot_epoch、snapshot_seq、counts、result_object_key、created_at 导出、导入预检及审计
object_holds blob_id、holder_type、holder_id、expires_at 导出、导入及备份任务对原图的保留约束
audit_events id、actor_user_id、event_type、target_type、target_id、created_at 登录、身份绑定、导出与清除等;不保存正文
instance_state instance_id、recovery_generation、schema_version 恢复流程使用;与 Archive format_version 分开

BIGINT 的同步序号通过 JSON 传输时使用十进制字符串,避免 JavaScript Number 精度问题。数据库 schema 版本、API 版本、正文 profile 版本、snapshot 版本、archive 版本分别维护。

建议初始索引:Entry 的 (owner_id, local_date DESC, occurred_at DESC, id DESC);owner_id + journal_id + local_date;当前未删除 Entry 的收藏索引;EntryRevision 的 entry_id + revision_number;同步变化的 owner_id + seq;附件关系外键索引。时间线采用键集分页,不靠大 OFFSET。

5. ID、正文与时间契约

5.1 ID

Entry、Journal、Attachment、Device、Operation 及 Revision 使用 UUIDv7;客户端需要离线创建的实体由客户端生成,服务器验证并通过唯一约束拒绝碰撞。UUIDv7 属于 RFC 9562。[S6] 不从 UUID 推断日记发生时间,也不将其作为秘密令牌。Session 使用独立随机 token。

5.2 Markdown Profile v1

正文定义为 UTF-8、LF 换行、CommonMark 核心语法,加明确列出的 GFM 功能。V1 支持段落、标题、强调、列表、引用、链接、代码和分隔线;任务列表、删除线可以纳入受测试子集。表格可先保留源码与阅读能力,富文本复杂表格延后。

不支持随意字体颜色、字号、复杂嵌套块、页面布局、任意 HTML 和脚本。客户端遇到自己不认识的语法时保留原文,进入源码模式或只读模式,不以富文本序列化结果静默覆盖。

内部图片引用使用规范定义的 URI,例如 ![照片说明](attachment:<uuid>)。媒体节点只包含永久 Attachment ID,不保存过期的下载 URL。视频未来可通过普通链接与附件清单表达,不为视频创造难以解析的正文块格式。

编辑器缓存 AST 可以存在,但 canonical body 仍是 Markdown。未编辑的源码必须原样保留;富文本编辑后的格式规范化允许改变空格等呈现写法,但不得丢失受支持语义,修改前原文保存在历史里。切换模式前检测不支持的语法,而不是只在保存后告知。

建立 fixture 集合覆盖中文输入法组合、英文、emoji、嵌套列表、软硬换行、代码、空段落、链接、图片、粘贴 HTML 与未知语法。检验语义往返,不要求两种合法 Markdown 写法字节一致;对未编辑原文则要求字节不被改写。

5.3 时间

字段 含义
occurred_at 精确发生时间,UTC timestamp;只知道日期时可为空
timezone 当时使用的 IANA timezone,例如 Asia/Shanghai
utc_offset_minutes 精确时间当时使用的 UTC 偏移,作为解释记录
local_date 用户选择的所属日,DATE;不随查看设备时区重算
local_time 用户当时填写或生成的当地时间;date-only 时为空
time_precision instant 或 date;以后扩展需要版本化
time_source user、capture、import 等来源
created_at / updated_at 服务器接受记录的时间;不是发生时间
client_recorded_at 客户端记录时间,仅作参考,不控制版本顺序

普通“现在写”由客户端提交完整时间字段,服务端检查一致性。补写历史日记允许只有 local_date,不编造午夜时间。夏令时存在重复或缺失的当地时间时,需要明确偏移或让用户选择有效时刻。

排序、年份分组、On This Day 使用保存的 local_date;用户修改所属日会产生新 Revision。EXIF 拍摄时间不能自动替代 Entry 的发生时间。时区库升级后不批量重算既有 local_date。

6. 认证与权限契约

采用 ZITADEL OIDC Authorization Code + PKCE S256,Web 为 confidential client / BFF。ZITADEL 官方支持该流程,OAuth 安全最佳实践推荐 confidential client 也使用 PKCE。[S7][S8]

校验 state、nonce、PKCE、ID token 签名、iss、aud、exp 等,由维护中的 OIDC 库完成;redirect_uri 精确配置。回跳使用 GET code 模式时,Session Cookie 可采用 __Host-journal_session; HttpOnly; Secure; SameSite=Lax; Path=/,不设置 Domain。

Session 状态放 PostgreSQL。初始建议最长有效期 7 天、空闲有效期 24 小时;这是产品默认值,可配置。身份绑定、完整导出和永久清除要求近期重新认证,建议窗口 15 分钟。敏感操作仍要验证当前授权与 CSRF,不能只检查一个前端按钮。

Journal 不调用第三方 API 时,不申请 offline_access 或长期保存 refresh token。登录交换得到的 token 仅按实际需求使用;若以后需要 refresh token,只能服务端加密存储并限制生命周期。

个人部署默认关闭公开注册,仅允许预置的 (issuer, subject);管理员 CLI 可将新身份显式绑定到已有内部 user_id。身份迁移不按 email 自动匹配。SSO 不可用时,已授权的离线设备可继续保存本地草稿;服务器 Session 失效后暂停同步,重新登录后恢复。不增加另一套公网密码登录。

身份绑定 CLI 需要服务器管理权限、审计和操作者对目标身份的确认。该机制不解决服务器失陷,因此不把它宣传为独立防护。

离线身份的边界

首次使用必须联网登录并初始化用户数据区。可信设备可选择保留正文与指定媒体;共享电脑不启用持久离线模式。IndexedDB 数据以内部 user_id 和实例标识隔离;换账号不能读取前一个账号的数据区。

服务器撤销设备只能阻止后续在线访问,不能从断网设备远程抹除已缓存内容。UI 锁屏可以遮挡内容,但不能代替操作系统磁盘加密或浏览器本地数据保护。若未来需要真正加密的离线保险库,应单独设计密钥恢复和多设备流程。

退出登录停止同步并处理本地数据。存在待同步草稿时,产品界面提供重新登录同步、下载应急副本或显式清除选择,不能悄悄删除。关闭标签页与主动登出是不同事件。

7. 本地优先与同步协议 v1

7.1 本地数据组织

IndexedDB 保存服务端基线、当前草稿、待提交操作、待上传媒体和同步 cursor;区分 server head 与本地编辑态。

每次落盘在同一本地事务里更新草稿和待提交意图。只有事务成功后,UI 才显示“已保存到此设备”。QuotaExceededError、IndexedDB 不可用和升级失败均需要显式状态;可继续暂存内存并提供文件下载,但不能宣称已落盘。

一个请求发送后,其 operation_id 和 payload 固定。尚未发送的草稿可合并为较新的提交意图;已发送且未确认的操作先查询或重试原结果,不改变同一 ID 的正文。

同一 Entry 的操作按设备队列顺序提交。A 请求在途中时用户又写了 B,A 的 ACK 只更新服务器基线,不用 A 的正文覆盖 B。必要时等待 A 成功后,以 A 的 revision 为基线创建 B 的新操作。多标签页通过单一队列协调,保留各自草稿,不能假设同一 device_id 只有一个编辑器。

同步主要在打开应用、恢复前台、网络恢复和用户主动点击时执行。Background Sync 可作为增强,但它在部分常用浏览器中不可用,不能承诺关闭应用后自动完成上传。[S9]

浏览器存储默认是 best-effort;请求 navigator.storage.persist() 并检查配额,但用户清除网站数据仍会删除本地记录。[S10] 因此提供“下载此设备待同步内容”:正文、时间与附件列表、尚未上传的原图、基线和操作状态一并导出。未同步照片不能只导出一个 Attachment ID。

7.2 Push 契约

建议所有在线和离线变更共用命令处理器,避免 CRUD 与 sync 两套保存逻辑。批量 push 按操作顺序处理,每个操作单独事务并返回对应结果。

{
  "protocol_version": 1,
  "epoch": "<server-epoch>",
  "device_id": "<uuid>",
  "operations": [
    {
      "operation_id": "<uuid>",
      "entity_type": "entry",
      "entity_id": "<uuid>",
      "action": "update",
      "base_revision": "15",
      "base_revision_id": "<immutable-revision-uuid>",
      "payload_schema_version": 1,
      "payload": {
        "title": "图书馆",
        "body_markdown": "今天读完了一章。",
        "journal_id": "<uuid>",
        "local_date": "2026-10-10",
        "time_precision": "date",
        "timezone": "Asia/Shanghai",
        "occurred_at": null,
        "local_time": null,
        "utc_offset_minutes": null,
        "time_source": "user",
        "favorite": false,
        "pinned": false,
        "tag_ids": [],
        "attachments": []
      }
    }
  ]
}

本例是字段示意;正式 JSON Schema 还要限定长度、大小、枚举及附件清单的结构。创建基线为 null;update、delete、restore 均要求正确的基线。owner_id 不来自请求。

对 (owner_id, operation_id) 保存请求规范化后的 SHA-256 与结果。同一 ID、同一 payload 返回既有结果;同一 ID、不同 payload 返回 OPERATION_ID_REUSED。哈希规范化算法在协议中定义;不能依赖 JS 对象键的偶然顺序。

初始保留操作 receipt,避免长期离线操作重复执行;将来压缩时仍需保留操作识别标记或通过设备生命周期明确拒绝旧操作。设备撤销后拒绝该设备的新同步请求;device_id 必须与认证上下文对应。

7.3 冲突与删除

基线与服务器 head 不一致时,V1 不自动合并正文。保存完整候选和附件关系,返回 conflict_stored 与 conflict_id。对于候选已完整接收的结果,客户端可标记“冲突副本已上传”,不能标记“主版本已同步”。

选择当前服务器版本也不立即删除候选;冲突记录默认保留。合并或采用候选必须以最新 head 再次提交,如期间发生新修改,继续按冲突处理。候选可以另存为新 Entry。

删除也是有基线的命令。删除与离线编辑冲突时,保留两者,不能让旧编辑隐式恢复已删除 Entry。软删除通过 Revision 的 deleted_at 同步;永久清除保留 tombstone,不允许旧设备重新创建同一个永久 ID。

7.4 防止同步序号漏读

不能只用 BIGSERIAL 作为 seq 然后执行 WHERE seq > last_cursor。PostgreSQL sequence 的变更可立即被其他事务看到,而且事务回滚不会撤回其计数变化。[S11] 据此可以推导:若事务 A 先领到 41 但未提交,B 领到 42 并先提交,客户端看到 42 后推进游标,A 后来提交的 41 就可能被跳过。这是设计推导,不是 PostgreSQL 文档直接提供的同步实现建议。

个人系统采用每个用户一行 sync_heads:所有会产生同步变化的写事务先 SELECT ... FOR UPDATE 锁定该行,再读取和验证基线、修改业务、递增普通 BIGINT 字段并写 sync_changes,最后提交。锁保持到提交;事务回滚时普通字段递增一并回滚。同一用户下一次写事务要等前一次结束。

统一锁顺序为用户 head → 具体实体,避免不同命令倒序取锁。导入及后台影响同步的业务修改同样遵守该路径。不得先读取旧基线,再以旧结果覆盖锁后数据。

原图处理、网络 I/O、缩略图和压缩不在持锁事务中执行。操作重复结果可先查询,但事务内仍需重新检查幂等状态。

7.5 Pull、Bootstrap 与恢复

GET /api/v1/sync/changes?cursor=...&limit=... 返回有序变化、next_cursor 和 has_more。cursor 对客户端不透明,绑定 owner_id、epoch、seq 及分页所需 high_watermark。分页只推进到本页已经交付的变化;固定批次 high_watermark,后续新增变化在下一批拉取。

Entry 变化指向不可变 revision_id,其他实体变化保存不可变 after-image 或删除标记。读取不能把旧事件指向一个已被覆盖的可变对象而丢失必要状态。V1 保留变化与 tombstone;若以后裁剪,明确返回 CURSOR_EXPIRED 并走 bootstrap。

首次同步通过数据库一致性快照生成 bootstrap 数据集和对应 committed_seq,再分页交付。不能先读 cursor,再分页读持续变化的当前表。可将该快照序列化到任务临时文件,并保护其引用的媒体;数据交付完毕后从快照 cursor 继续增量。

客户端应用一页变化和更新 cursor 使用同一本地事务;先更新 cursor 后写正文会在崩溃后漏数据。bootstrap 保留已有 outbox 与草稿,不能删除重建整个本地库。

**灾难恢复必须更换 epoch。**数据库回滚到旧备份可能回退序号和 revision_number;新服务器启动前由恢复 CLI 产生新 epoch 并撤销旧 Session。旧客户端收到 SYNC_RESET_REQUIRED,保留本地修改,重新 bootstrap 后逐条比较不可变 base_revision_id,必要时生成冲突。不能拿旧的数字 revision 直接覆盖恢复后的内容。

8. 媒体生命周期与 Storage Contract

8.1 上传

  1. 创建 upload 记录;客户端写作先保留本地原图,不依赖后台上传完成。
  2. 流式上传到 staging;限制大小、超时和剩余空间,不将整张图读入 API 进程内存。
  3. 服务端自行计算 SHA-256 和实际 byte_size,检查文件签名;MIME 不信任客户端 Content-Type。
  4. 原始字节落到不可变对象:Filesystem 使用临时文件、必要 fsync、同文件系统原子发布及不覆盖语义;对象已存在时核实属性,不能盲信路径。
  5. 原图存在且通过验证后,数据库事务将 Blob / Attachment 标为 ready;先文件后数据库允许产生可清理的孤儿,避免正常提交产生缺失原图引用。
  6. Worker 异步生成预览。预览失败不导致已接受原图消失,也不阻止纯文本编辑。

引用未 ready 附件的提交返回 DEPENDENCY_NOT_READY,客户端保留队列并先完成上传。用户写作视图可以显示本地图片,但整条 Entry 状态需表示“照片待上传”。后续若要先提交纯文字,必须作为明确版本策略,不能悄悄丢掉关联。

HEIC、HDR、大图和动图作为 Phase 0 测试样本。浏览器或 Sharp 构建不支持解码时,仍可保存与下载原图,UI 说明预览不可用。色彩转换只生成派生预览,记录配方版本,不改变原始文件。

8.2 存储接口

interface ObjectStore {
  putImmutable(input: {
    key: string;
    stream: AsyncIterable<Uint8Array>;
    sha256: string;
    byteSize: bigint;
  }): Promise<{ key: string; created: boolean }>;
  stat(key: string): Promise<{ byteSize: bigint } | null>;
  openRead(key: string, range?: { start: bigint; end?: bigint }):
    Promise<AsyncIterable<Uint8Array>>;
}

接口为语义草案,具体错误类型、AbortSignal 和 Range 边界在实现契约中补齐。putImmutable 成功表示完整对象已发布,不允许覆盖不同字节;重复相同对象可幂等返回。S3 adapter 不依赖 POSIX rename,通过完成对象上传及条件写入实现同等可见性语义;ETag 不能默认当作 SHA-256。

列举、隔离和物理删除放到独立维护能力,不给普通 API 通用 delete 权限。获取下载方式由媒体交付模块决定,业务领域不返回本机真实路径。

8.3 授权与媒体交付

GET /api/v1/attachments/:id/content?variant=original|preview|thumbnail 验证当前用户所有权,随后通过 OpenResty X-Accel-Redirect 将文件交给内部 location。Nginx 官方说明 internal location 拒绝外部直接访问,并允许上游 X-Accel-Redirect 内部转向。[S12]

OpenResty 容器只读挂载媒体目录;内部 URI 由受控对象键构造,不能从原始文件名拼接。媒体与 Entry API 设置私有缓存策略;V1 媒体默认 Cache-Control: private, no-store,需要离线的图片显式进入用户隔离的本地存储。公用反向代理/CDN 不缓存这些响应。支持图片与视频所需 Range,授权每次请求都执行。

外部图片不自动加载;远程导入默认禁用,避免追踪及 SSRF。上传 SVG、HTML 等可执行内容不作为同源内联页面展示;原文件以下载方式提供,预览生成非活动格式。原文件保留,不意味着浏览器可以执行原文件中的脚本。

8.4 引用与清除

Blob 的保留依据包括当前 Entry、所有 Revision、冲突候选、Trash、导入任务、导出任务和备份保留约束。当前页面看不到某张照片,不代表它可以删除。

V1 不自动永久清除历史或回收站,也不实现常规媒体硬删除。staging 失败文件可以按明确租约清理。将来 GC 需要列出候选、检查所有关系和 hold、隔离等待、再次检查后清除,并与同一 Blob 的新引用创建协调锁定,防止检查后又出现引用。

维护工具区分缺失原图、缺失派生预览、可保留的孤儿和过期 staging。不能把所有“未在当前附件表找到”的文件一律判为异常并删除。

9. Journal Archive Format v1

9.1 两种导出

阅读导出:当前选定 Entry 的 Markdown、可显示预览、原始媒体链接。适合直接阅读和迁移到一般笔记软件。选定导出不能标为“完整备份”。

完整档案:所有 Journal、Entry、已接受 Revision、冲突、Trash、标签、附件元数据、原始媒体,以及必要 tombstone。完整恢复默认按此格式;默认不导出 Session、refresh token、身份管理密钥和备份密码。

默认完整档案只包含服务端已接受数据。发起页面显示此设备待同步内容;用户可先完成同步,或另行下载设备应急副本。其他离线设备上的未提交草稿无法由服务器自动纳入导出。

9.2 布局

路径 内容
README.md 人类可读说明、版本、恢复方法和敏感数据提示
manifest.json 格式、范围、版本、快照、计数、文件清单和 SHA-256
schemas/ 当前 Archive v1 使用的 JSON Schema 与 Markdown profile 说明
data/journals.json 日记本及归档状态
data/tags.json 标签及状态
data/entries.ndjson Entry ID、当前 revision_id、创建时间及业务状态映射
data/tombstones.ndjson 已物理清除实体的最小删除标记
data/attachments.ndjson 附件 ID、Blob 哈希、大小、原文件名、类型和元数据
revisions//.json 不可变版本快照、正文路径、hash 与附件清单
revisions//.md 原始 canonical Markdown 精确内容
conflicts//.json + .md 冲突基线、候选正文及引用
current/YYYY/MM/DD/.md 带 Front Matter 和可阅读相对媒体路径的当前视图
media/originals/sha256/ab/. 原始字节,扩展名仅帮助读取,不作为身份
media/previews/ 可选浏览预览;不是原图替代物
SHA256SUMS 对所有档案文件的校验清单,排除自身

真实恢复以 revision JSON、精确正文和 ID 映射为准;current 是人类阅读投影。如果用户修改 current 文件而权威记录未变化,导入预检报告不一致,不能无提示猜测哪个文件代表用户意图。另设“从编辑过的 Markdown 新建记录”模式处理此类修改。

保留精确内部引用正文,在 current 视图按 Markdown AST 改写 attachment:<uuid> 为相对路径,不用全局字符串替换。链接不得引用私有下载接口或有时效的签名 URL。HEIC 原图可能不能直接预览;可增加 JPEG 预览和明确原图下载链接。

9.3 元数据示例

---
archive_format: journal-archive
archive_format_version: 1
entry_id: "<uuid>"
revision_id: "<uuid>"
revision_number: "17"
journal_id: "<uuid>"
journal_name: "Personal"
local_date: "2026-10-10"
timezone: "Asia/Shanghai"
time_precision: "date"
occurred_at: null
favorite: false
deleted_at: null
tag_ids: []
---

YAML 只采用限定的简单类型;ID 和日期显式引号,不使用自定义 tag、对象构造或无限别名。JSON 作为机器恢复依据,避免日期隐式类型转换造成歧义。

Manifest 包含 format、format_version、generated_at、export_scope、snapshot_epoch、snapshot_seq、counts 和 files。counts 分别列 accepted entries、trashed entries、revisions、conflicts、logical attachments 和 unique blobs;不能把附件数和物理文件数当成相同计数。

files 对每个 payload 文件保存相对路径、字节大小、SHA-256;manifest 不记录自己的哈希或 SHA256SUMS 的哈希,避免循环依赖。生成完 manifest 后创建 SHA256SUMS,覆盖 manifest 和所有 payload,排除 SHA256SUMS 自身。校验文件作为附带清单,而非权威文件身份。

SHA-256 检验一致性,不能单独证明来源可信;攻击者可以同时修改文件和清单。抵御篡改主要依赖隔离的历史副本,未来可给 manifest 增加独立签名。

9.4 一致性导出

在 repeatable-read 数据库快照中固定实体、所有引用关系和对应 sync head;将所需元数据序列化到任务 staging,并对原图建立 hold。元数据快照固定后退出数据库事务,再复制原图和生成归档,避免为复制大量媒体长期持有数据库事务。

导出与永久清除互斥,或由 hold 协调;原图不可变,因此快照之后复制同一个哈希仍能得到对应字节。完成前验证每一个引用原图存在、大小和哈希一致;缺文件时导出失败并给出报告,不交付标为完整的残缺归档。

9.5 导入

流程:校验路径与大小限额 → 防止 ZIP/TAR 路径穿越、符号链接和解压炸弹 → 验证 schema 与 hash → 预检实体图及引用 → 显示计划 → staging 原图 → 事务发布数据 → doctor 检查。

Phase 1 先实现导入到空用户空间并保留 ID。上传归档不信任其中 owner_id;授权操作者指定目标内部用户,其他 ID 在不冲突时保留。身份凭据不随档案自动绑定。

同一 ID、相同内容的重复导入应幂等跳过;同一 ID、不同内容报告冲突,不直接覆盖。合并到已有用户、ID 重映射和从一般 Markdown 导入作为后续显式模式;重映射必须连带改写附件 URI 和历史关系。

格式升级提供离线迁移器,不覆盖原归档;未知 format_version 拒绝导入并解释。保留 v1 fixtures 与独立校验工具,避免当前应用更新后丢失旧格式的测试基础。

10. 搜索、回顾与扩展

V1 使用 PostgreSQL 当前 Entry 的标题、正文、标签和结构化过滤。搜索默认不包含 Trash 与历史,用户可以显式扩展范围。始终先限制 owner_id,不能在全库检索后才在返回阶段过滤。

中文短词采用限定用户与可选日期范围的 ILIKE 子串搜索;将 % 和 _ 按产品语义转义,并使用参数化 SQL、分页和 statement timeout。较长查询可使用 pg_trgm GIN;英文可增加 FTS 排序。pg_trgm 官方指出,无可提取 trigram 的模式会退化为全索引扫描。[S13] 因而不能承诺两个中文字也能普遍高效使用三元索引。

先用包含中文短词、混合语言和时间范围的真实样本测响应,再决定是否需要独立搜索引擎。搜索索引删除后应可重建,无法影响正文和档案读取。

On This Day 以 local_date 的月日匹配;闰日策略明确采用“仅 2 月 29 日回顾”,而不自动转到 2 月 28 日。照片视图使用既有附件关系去重展示,点击显示所有关联 Entry,不复制另一套照片库。

天气、位置、OCR 和 AI 分为三类:用户写入的数据、外部获取并保存的观测、可重建派生结果。前两类保存来源和时间并进入档案;纯索引、未认可的摘要可删除重建。用户将 AI 文本采纳为日记的一部分时,作为普通版本化正文保存,不能随清理 embedding 一起消失。

V1 不把正文发送外部 AI。未来按 Journal 或用户明确选择范围授权,记录模型、来源 Entry / revision_id、生成时间;总结引用原记录,区分事实与模型推断。Embeddings 同样可能泄露私人信息,需要授权、备份和删除策略。

11. Threat Model 与运维防护

威胁 V1 应对 边界
SSO 账号被接管 SSO MFA / Passkey、关闭公开注册、近期认证、会话与设备撤销 不能阻止合法会话已经读取的数据外流
Session 被盗 HTTPS、HttpOnly、随机 token、过期和撤销、减少日志暴露 XSS 仍可能利用当前登录态发送请求
CSRF SameSite + Origin 校验 + CSRF token;变更禁用 GET 同站其他子域也要考虑,不能只依赖 SameSite
XSS / Markdown 注入 禁止活动 HTML、渲染 sanitizer、CSP、自托管 JS 与字体、限制 URL scheme 编辑器 AST 不等于可信 HTML
跨用户访问 请求授权、复合外键、所有权校验、越权测试 UUID 难猜不是权限机制
原图与媒体泄露 私有目录、授权接口、internal location、私有缓存、阻止执行活动格式 获取后的本地副本不可远程回收
恶意媒体 文件签名、尺寸与解码资源限制、隔离 Worker、无外网、更新解码库 原始媒体保留与安全渲染分别处理
数据库泄露 内网、最小权限、独立备份凭据、主机保护 V1 数据库明文可读,不是 E2EE
主机被接管 补丁、最小权限、减少服务、独立受保护备份、监控 磁盘加密不阻止运行中的 root 读取明文
备份泄露 restic 加密、密钥单独保管、导出临时文件生命周期 密钥遗失也可能造成无法恢复
设备遗失 个人设备系统加密、离线模式选择、设备撤销 不假定撤销可以抹除 IndexedDB
误删 / 同步错误 Revision、Trash、冲突保留、独立历史备份 同步副本不算恢复保障
勒索 / 生产凭据失陷 异地保留、离线副本或删除受限凭据、独立管理身份 普通 restic 仓库本身不自动提供不可删除性
位腐烂 / 磁盘故障 SHA-256 巡检、独立副本、恢复演练 checksum 不防止同时篡改文件和清单

应用日志只允许必要字段:request_id、路由模板、状态码、耗时、错误分类及必要内部 ID。禁止请求正文、token、cookie、authorization code、精确位置与完整私有链接。OpenResty 对 OIDC callback 去除敏感 query 日志;错误采集也要做字段白名单。

主要监控:备份最近成功时间、恢复最近验证时间、原图缺失数、hash 错误、磁盘余量、最老失败 job、同步错误率及数据库连接。日志与指标不采集正文、日记标题或 EXIF 位置。

12. Backup & Recovery

12.1 服务目标与副本

初始建议目标:已同步数据 RPO ≤ 1 小时;服务恢复 RTO ≤ 4 小时。二者是设计目标,只有恢复演练测得结果后才能宣称达标。本地未同步草稿不在服务端 RPO 范围内,需要应急导出保护。

内容 建议频率 初始保留策略
数据库逻辑备份及对应原图 每小时 48 个小时点、30 个日点、12 个周点、12 个月点
异地加密副本 每小时备份成功后 与本地独立保留,失败需告警
完整开放档案 每月及重大迁移前 月档至少 12 份、年度档长期;考虑历史图像去重与存储容量
原图与备份数据完整性检查 每周分片,每季度完整循环 记录覆盖范围、失败对象和完成时间
空环境恢复演练 每季度及重要升级前 保存结果和耗时,不仅记录备份命令成功

生产、NAS、异地不采用仅保留最新镜像的链式同步。NAS 使用独立账号和快照;生产 API 不持有 NAS 快照删除权限。异地使用独立身份管理保留策略,或离线副本;是否支持 append-only / object lock 取决于实际备份后端,需要配置验证。

备份密钥、SSH/存储访问凭据、OIDC client secret 分别保管。恢复清单包含运行配置和基础设施说明;不要求把已有浏览器 Session 恢复为可用登录态。ZITADEL 自身备份另行维护;恢复日记档案不依赖复活旧 SSO 数据库。

12.2 数据库与媒体一致性

小型个人部署初期可采用一个简单、明确的备份窗口:

  1. 取得全局备份维护锁,暂停新的业务写入、媒体最终提交、导入和永久清除;等已开始的写事务完成。本地写作继续落盘,在线请求返回可重试维护状态。
  2. 在此窗口生成 PostgreSQL custom-format dump 和精确媒体清单,记录 schema_version、epoch、seq 与 backup_id。
  3. 保护该清单的原图引用,释放业务写入锁;既有原图不可变,后续可以异步读取。
  4. restic 备份 dump、元数据清单、所需原图和必要部署配置。媒体根目录中额外的新原图可接受,但清单内不能少任何一个。
  5. 校验备份内所需原图及数据库文件,成功后才标记本次恢复点可用。失败保留错误状态并重试,不把半成品当成功副本。

上线前测量窗口耗时。数据量增加后,可改为在线 MVCC 快照协调和原图 hold,或者物理备份 + WAL;不能简单移除维护锁而保留“同一恢复点”的宣称。

pg_dump 提供并发环境下的一致数据库导出,但不覆盖 PostgreSQL 外的媒体。[S14] 本设计显式将它限定在小规模、小时级恢复目标;需要更细粒度恢复时使用 PostgreSQL 物理备份和 WAL/PITR,同时继续保护对应原图。

不要直接复制运行中的 PostgreSQL 数据目录作为普通文件备份。缩略图与搜索索引可以不进入核心备份,但恢复后需能重建。

12.3 恢复顺序

创建隔离环境 → 选择已验证恢复点 → 恢复数据库与原图 → 用对应应用版本进行初始检查 → 必要时运行受测试的前向迁移 → 检查内容图及哈希 → 更换 epoch / 撤销 Session → 配置现有身份映射 → 在实际客户端验证 → 切换入口。

doctor 至少检查:外键、Entry 当前指针、Revision 连续性、正文哈希、所有历史和冲突的附件引用、原图存在/大小/hash、当前投影一致性、Trash、tombstone、待处理 job 与可解释孤儿。丢失派生预览报告为可重建,不与丢失原图使用同一严重级别。

restic 默认 check 不会读取所有 pack 字节,完整检查需要 --read-data;可以按 --read-data-subset=n/t 分组完成检查覆盖。[S15] 备份仓库检查与应用级空环境恢复都要做,二者不能互相替代。

13. Repository、ADR 与维护策略

初始 monorepo 采用 apps/web、apps/api、apps/worker、apps/cli;packages/domain、contracts、db、editor、sync、storage、archive。部署文件在 deploy;设计文档在 docs。auth 初期作为 API 内部模块,只有存在第二个调用方时再拆独立 package。避免仅为目录形式拆出十几个空包。

依赖方向:domain 不依赖 Fastify、React、Drizzle;应用负责组合具体数据库、存储和身份适配器;archive 使用明确版本契约,不序列化 ORM 对象;编辑器不能直接写数据库。

推荐 ADR:

ADR 决定
001 模块化单体与单一事务数据库
002 内部用户身份与 OIDC issuer + sub 解耦
003 Markdown Profile v1 为正文格式;保留源码路径
004 不可变 Revision 与完整附件关系快照
005 Blob / Attachment / 引用关系分离,用户内去重
006 PWA 本地保存前置;承认浏览器持久性与后台边界
007 Revision 乐观并发与冲突保留,V1 不用 CRDT
008 事务化用户同步计数器及恢复 epoch
009 阅读导出与完整档案分开;往返恢复作为发布验收
010 原图先发布后提交引用,派生物可重建
011 无自动历史 GC;永久清除单独实施
012 无 V1 E2EE;明确主机与离线数据安全边界
013 数据库与原图同恢复点,独立历史副本与恢复演练

迁移采用版本管理和显式 SQL;禁止生产使用 ORM 自动 push schema。结构修改优先 expand / migrate / contract,多版本客户端兼容需要记录窗口。破坏性变更前进行备份、开放档案导出和恢复演练。

应用镜像固定版本及 digest,保留相应源码 tag、迁移、锁文件和构建说明;前端 Service Worker 更新不得在尚未落盘时强制刷新。IndexedDB 升级需要独立测试旧版未同步草稿;部署后保持旧静态资源一段兼容窗口。

CLI 提供 export、import preflight、doctor、backup verify、identity bind、recovery finalize 等能力。独立档案校验器应只依赖通用文件读取、JSON 和 SHA-256,便于未来离开 Web 项目继续验证资料。

14. 实施阶段与验收

Phase 0:冻结关键契约与验证未知部分

输出:architecture、domain/schema、Markdown profile、archive v1、storage、auth、sync v1、threat model、backup/recovery 与 ADR。将本设计拆成仓库正式文档,并给所有示例补充 schema。

做三个有限验证:

  1. 编辑器:中文输入法、手机键盘、语法往返、未知语法保留、照片插件;不通过时先交付源码编辑,不堵塞数据模型。
  2. 媒体:实际 HEIC、HDR、大图上传、断开重试、哈希、私有下载、预览失败保留原图。
  3. 同步与恢复:两个客户端离线修改、响应丢失、事务提交乱序、恢复 epoch、ACK 不覆盖新草稿。

阶段完成条件不是 UI 图,而是契约 fixtures 能校验、风险有明确处理、后续工程可以据此实现。

Phase 1A:可写、可找回的文字闭环

内部身份映射与 OIDC → Journal / Entry → Revision → 本地草稿与 outbox → 幂等命令与基础 push/pull → 冲突与 Trash → 文字档案导入导出 → 初始备份恢复。

验收:断网写作并刷新后草稿存在;断线重试不重复创建;双设备修改不丢候选;恢复历史形成新版本;空用户导入恢复相同内容;恢复旧数据库后旧设备不会静默覆盖。

Phase 1B:愿意每天使用的 V1

加入照片及历史附件关系、Timeline、Calendar、Tags、Favorites、中文/英文搜索、轻量富文本、完整媒体档案、可信设备 PWA 模式与运维页面。

V1 发布必须同时满足:

  • 日记正文、历史、冲突、Trash、原图的 Archive roundtrip 一致。
  • 删除、日期修改、附件移除、版本恢复后引用与投影一致。
  • 其他用户无法读取 Entry、Revision、冲突、原图或导出任务。
  • 原图提交后进程崩溃、响应丢失、预览失败均不产生正常记录丢图。
  • 本地落盘失败和附件待上传的 UI 状态准确。
  • 用真实备份恢复到空环境;检查所有引用及哈希;记录 RPO/RTO 演练结果。
  • 日志与反向代理配置没有正文、Cookie 或 OIDC code 暴露。

如果容量或媒体兼容验证不通过,缩减照片预览和浏览功能,不降低原图保存与导出要求。

Phase 1.5:改善回顾与日常维护

On This Day、照片墙、模板、离线范围选择、长时间离线重连体验、独立档案查看器、更好的冲突差异展示、PDF 阅读导出。PDF 作为阅读副本,不替代完整档案。

Phase 2:原生与媒体扩展

实际 PWA 使用暴露出后台上传、大视频、相册导入或平台能力不足时再选择原生方案。移动端采用 OIDC public client + PKCE,令牌存平台安全存储,复用 API、同步和 archive 契约;不预设手机一定能复用所有 Node.js 模块。

加入位置、地图、音频、视频、OCR 和转录,先设媒体大小、配额、恢复与转换策略。原生 App 不要求更换现有服务端。

Phase 3:AI 与长期检索

按明确授权范围建立派生检索、带出处摘要和年度回顾;先解决数据选择、隐私、成本与可删除性,再引入向量索引。避免用 AI 推断作为未标注的个人事实。

15. 需在代码开始前记录的决定

以下给出可用默认值,不表示用户已经逐条确认;Phase 0 中将它们写入 ADR。遇到真实使用偏好差异再调整。

决策 推荐默认值
私密性目标 自托管、服务器可读;V1 不做 E2EE
用户与共享 单人优先;可接少量独立用户;不共享日记本
离线范围 可信设备缓存正文和选定媒体;全量原图离线下载不默认开启
版本与回收站 已接受版本长期保存;Trash 无自动清除
自动提交 本地频繁落盘,服务端按空闲和离开触发,不逐字提交
日期语义 local_date 固定;补写可 date-only
正文能力 CommonMark + 受测试 GFM 子集;不支持任意 HTML
媒体保留 原始字节长期保存;预览可删除重建;用户内去重
导出恢复 完整档案含历史、冲突、Trash;空用户导入先实现
备份目标 初始 RPO 1 小时、RTO 4 小时,需演练确认
密钥与恢复 备份密钥独立保管;身份显式绑定;恢复更换 epoch
AI 与远程加载 V1 不向外部 AI 发送日记;不自动加载远程图片

16. 技术依据

下列为设计时核对的官方文档或维护方资料。技术事实与基于事实作出的架构建议已在正文中区分;项目实现仍需验证实际版本及环境。

Comments

No comments yet — be the first.

Comments are moderated. Plain text only.