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

85 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
### `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 + S3boto3),哑接口 `put/get/list_keys/delete`
* **内容清洗 (Sanitizer)**:剥离全部 `outputs``execution_count`,基于清洗后 JSON 计算 MD5 并记录;**新内容 hash 与最近版本相同则跳过保存**(弹 warningappend-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() 落盘
```