Manual-commit notebook snapshot extension (JupyterLab 4):
Backend
- Storage adapter layer (Local + S3 via boto3, pip extra)
- Dumb put/get/list/delete contract; key validation
- S3Storage: lazy boto3 import, S3ConnectionError with friendly
messages + connectivity check at startup
- SnapshotStore
- Strip code outputs/execution_count, MD5 on cleaned JSON
- Gzipped envelope (id/timestamp/name/description/hash/size/notebook)
- Append-only manifest.json (rebuildable from version files)
- SnapshotUnchangedError when new hash matches most recent version
- REST API (commit / list / content) under /snapshot/ namespace
- traitlets config (storage_type, local_root, s3_* + S3_* env fallback)
Frontend
- snapshot:commit command (toolbar button + command palette)
- Dialog for name + description
- SnapshotPanel (left sidebar timeline) + DiffWidget (main area)
- Cell diff: id-match first, LCS fallback; jsdiff for line diff
- Restore via model.fromJSON() + context.save() (no refresh)
- All AGENTS.md hard conventions enforced
Tests: 33 backend pytest passing (storage + store + routes)
Docs: AGENTS.md, design.md, README.md synced with implementation.
Co-Authored-By: Claude <noreply@anthropic.com>
6.7 KiB
6.7 KiB
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_<ts>_<hash8>.ipynb.gz,KB 级。 - 索引维护 (Manifest):
manifest.json是纯索引(原子写)。真相在快照文件的 envelope 内,manifest 损坏 / 丢失时可扫描版本文件重建。
3. 可配置项
- 存储类型:
local(默认;路径c.SnapshotConfig.local_root,缺省<root_dir>/.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_<ts>_<hash8>.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() 落盘