### `notebook-snapshot` 架构总结 本方案摒弃了对宿主机 Git CLI 的依赖,采用**轻量级本地文件 / 对象存储快照模式**。通过后端存储网关实现版本的压缩持久化,前端专注监听与 Cell 级的 UI 比对,整体具有**零环境依赖、轻量极速、天然兼容云存储**的特点。 #### 1. 前端职责 (TypeScript + React) * **手动快照提交**:注册命令 `snapshot:commit`(notebook 工具栏按钮 + 命令面板入口),弹出对话框填写 **name(必填)+ description(可选)** 后 POST 提交。**不做** contentChanged 防抖自动提交,**不绑定** Ctrl+S(避免与 `docmanager:save` 冲突)。 * **可视化侧边栏**:左侧栏渲染历史版本时间轴(名称 / 描述 / 时间戳 / 大小,来源于 manifest 查询 API),提供「对比 (Diff)」与「恢复 (Restore)」入口。 * **Cell 级 Diff 引擎**:先按 nbformat 4.5 cell id 匹配(modified / unchanged / added / deleted),LCS 仅作无 id 时的对齐兜底;行级 diff 用 jsdiff 渲染红绿行。**禁用 Monaco**。 * **无刷新还原**:确认对话框(含「未保存修改将丢失」警告)→ GET 旧版本 JSON → `model.fromJSON()` 内存覆盖 → `context.save()` 落盘。 #### 2. 后端职责 (Jupyter Server Extension / Python) * **统一存储适配器 (Storage Adapter)**:抹平本地隐藏目录(如 `.notebook_versions/`)与云端对象存储(S3 / MinIO / 阿里云 OSS 等 S3-API 兼容服务)的接口差异。**已实现** Local File + S3(boto3),哑接口 `put/get/list_keys/delete`。 * **内容清洗 (Sanitizer)**:剥离全部 `outputs` 与 `execution_count`,基于清洗后 JSON 计算 MD5 并记录;**新内容 hash 与最近版本相同则跳过保存**(弹 warning,append-only 历史不变)。 * **压缩持久化**:快照文件为 gzip 压缩的 JSON **envelope**(`{id, timestamp, name, description, hash, size, notebook}`),文件名 `v__.ipynb.gz`,KB 级。 * **索引维护 (Manifest)**:`manifest.json` 是纯索引(原子写)。真相在快照文件的 envelope 内,manifest 损坏 / 丢失时可扫描版本文件重建。 #### 3. 可配置项 * **存储类型**:`local`(默认;路径 `c.SnapshotConfig.local_root`,缺省 `/.notebook_versions`) / `s3`(boto3;六件套 traitlet + `S3_ENDPOINT` / `S3_ACCESS_KEY` / `S3_SECRET_KEY` 环境变量兜底;同一配置可指向 AWS S3 或自建 MinIO) #### 4. API 契约 * `POST /snapshot/notebook-version/commit` — body `{path, content, name, description}`;`content` 为前端显式提交的 notebook JSON 本体(后端**不**从磁盘读)。若新内容 hash 与最近版本相同,响应 `{"skipped": true, "reason": "unchanged", "message": "..."}`(HTTP 200,前端弹 warning)。 * `GET /snapshot/notebook-version/list?path=...` — 返回 manifest 版本列表(name / description / 时间戳 / hash / 大小)。 * `GET /snapshot/notebook-version/content?path=...&id=...` — 解压并返回旧版本 notebook JSON(已剥离 outputs)。 * `POST /snapshot/notebook-version/restore` — **暂缓**。前端 restore 走 `fromJSON()` + `context.save()` 闭环,不依赖此端点。 --- ### 系统交互流程图 ``` ┌──────────────────────────────────────────────────────────────────────────────────┐ │ JupyterLab FrontEnd │ └──────────────────────────────────────────────────────────────────────────────────┘ │ ├─► [ 1. 用户点击工具栏「保存快照」按钮 / 命令面板执行 snapshot:commit ] │ │ │ ▼ ├─► [ 2. 弹出对话框:填 name (必填) + description (可选) ] │ │ │ ▼ ├─► [ 3. (POST /snapshot/notebook-version/commit) body={path, content, name, description} ] │ │ ┌──┴─────────────────────────────────────────────────────────────────────────────────┴──┐ │ Jupyter Server Extension │ └──┴────────────────────────────────────────────────────────────────────────────────────┘ │ ├─► [ 4. Outputs / execution_count 清洗 & MD5 哈希计算 (基于清洗后 JSON) ] │ │ │ ▼ ├─► [ 5. 构建 envelope {id, timestamp, name, description, hash, size, notebook} ] │ │ │ ▼ ├─► [ 6. Gzip 压缩 envelope → 写入 v__.ipynb.gz (Storage Adapter) ] │ └─► [ 7. append manifest.json (Storage.put 原子写) ] │ │ (200 entry 响应) ▼ ┌──────────────────────────────────────────────────────────────────────────────────┐ │ JupyterLab FrontEnd │ └──────────────────────────────────────────────────────────────────────────────────┘ │ ├─► [ 8. 侧边栏 refresh (GET /notebook-version/list?path=...) → 渲染带 name 的条目 ] ├──► [ 9. 用户点击某条 "Diff" ] │ │ │ ├──► (GET /notebook-version/content?path=&id=) ──► 后端返回清洗后旧 JSON │ │ │ └──► 前端按 cell id 匹配 + LCS 兜底 + jsdiff 行级 ──► 主区 DiffWidget 红绿高亮 │ │ └───► [ 10. 用户点击 "Restore" ] │ ├──► 确认对话框(警告未保存修改将丢失,提示先建快照) │ ├──► (GET /notebook-version/content?path=&id=) ──► 旧 JSON │ └──► model.fromJSON() 内存覆盖 + context.save() 落盘 ```