Files
notebook-snapshot-extension/design.md
T
tao.chenandClaude cd1aba6698 dev: initial implementation of notebook-snapshot extension
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>
2026-07-21 17:33:55 +08:00

6.7 KiB
Raw Blame History

notebook-snapshot 架构总结

本方案摒弃了对宿主机 Git CLI 的依赖,采用轻量级本地文件 / 对象存储快照模式。通过后端存储网关实现版本的压缩持久化,前端专注监听与 Cell 级的 UI 比对,整体具有零环境依赖、轻量极速、天然兼容云存储的特点。

1. 前端职责 (TypeScript + React)

  • 手动快照提交:注册命令 snapshot:commitnotebook 工具栏按钮 + 命令面板入口),弹出对话框填写 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 + S3boto3),哑接口 put/get/list_keys/delete
  • 内容清洗 (Sanitizer):剥离全部 outputsexecution_count,基于清洗后 JSON 计算 MD5 并记录;新内容 hash 与最近版本相同则跳过保存(弹 warningappend-only 历史不变)。
  • 压缩持久化:快照文件为 gzip 压缩的 JSON envelope{id, timestamp, name, description, hash, size, notebook}),文件名 v_<ts>_<hash8>.ipynb.gzKB 级。
  • 索引维护 (Manifest)manifest.json 是纯索引(原子写)。真相在快照文件的 envelope 内,manifest 损坏 / 丢失时可扫描版本文件重建。

3. 可配置项

  • 存储类型local(默认;路径 c.SnapshotConfig.local_root,缺省 <root_dir>/.notebook_versions / s3boto3;六件套 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() 落盘