Developer-facing manual covering: 1. Architecture overview with the four-layer data flow diagram 2. Repository layout (src/, snapshot/, storage/, etc.) 3. Dev environment setup (one-time + daily) and the 'what to restart after what type of change' table 4. Common commands quick reference 5. Debugging: backend (server logs, curl), frontend (browser DevTools, console.log diagnostic points, React/Lumino inspection), and end-to-end symptom -> cause table 6. Packaging and release (PyPI wheel via hatch + jupyter-builder, version sourced from package.json, git tag flow, CI) 7. Known pitfalls: jest+pnpm linker incompatibility, S3 strong consistency variance, conda/npm gotchas 8. 5-minute quickstart for new contributors User-facing concerns (install/config/troubleshoot) stay in README.md; design goals stay in design.md; AI coding rules stay in AGENTS.md. DEVELOPER.md slots in for engineer handoff / onboarding. Co-Authored-By: Claude <noreply@anthropic.com>
12 KiB
Developer Manual
开发笔记本快照扩展的工程手册。面向接手开发的人:架构、构建、调试、打包全流程。
用户视角(安装 / 配置 / 排错)见 README.md。设计目标见 design.md。AI 编码规范见 AGENTS.md。
1. 架构概览
四层结构,数据自下而上:
┌──────────────────────────────────────────────────────────────┐
│ JupyterLab 前端 (src/) │
│ ├─ commands / api / model / diff / widgets │
│ └─ 与后端走 HTTP (POST commit / GET list / GET content) │
└──────────────────┬───────────────────────────────────────────┘
│ JSON over HTTP
┌──────────────────┴───────────────────────────────────────────┐
│ Server Extension (snapshot/) │
│ ├─ routes.py REST handlers │
│ ├─ store.py SnapshotStore(清洗 + hash + envelope + │
│ │ manifest 原子写 + 重建) │
│ ├─ config.py traitlets 配置 │
│ └─ __init__.py _load_jupyter_server_extension 入口 │
└──────────────────┬───────────────────────────────────────────┘
│
┌──────────────────┴───────────────────────────────────────────┐
│ Storage Adapter (snapshot/storage/) │
│ ├─ base.py StorageAdapter ABC(put/get/list/delete) │
│ ├─ local.py LocalFileStorage(原子写 tmp+rename) │
│ └─ s3.py S3Storage(boto3,懒加载) │
└──────────────────────────────────────────────────────────────┘
关键设计决策(实现时不得违背,见 AGENTS.md):
- 存储适配器是哑字节桶——不知道 notebook / 版本 / manifest 这些概念
- MD5 在清洗后 JSON 上计算;hash 与最近版本相同则跳过保存
- 快照文件是 envelope(
{id, timestamp, name, description, hash, size, notebook}),name/description 内嵌以便 manifest 损坏时从版本文件重建 - manifest 是可重建的索引,原真相在 envelope 里
- 手动提交,不绑定 Ctrl+S,不自动 commit
- Cell diff 按 id 优先匹配,LCS 兜底
2. 仓库布局
.
├── src/ 前端 TypeScript / React
│ ├── index.ts 插件入口(注册命令、挂面板、装工具栏)
│ ├── commands.ts 命令实现(snapshot:commit / restore)
│ ├── api.ts 类型化 HTTP 包装
│ ├── model.ts SnapshotModel(path + versions + 信号)
│ ├── diff.ts Cell 级 diff(id 优先 + LCS 兜底)
│ ├── request.ts requestAPI(带 /snapshot 前缀)
│ └── widgets/ ReactWidget 组件
│ ├── CommitDialog.ts 名称/描述输入
│ ├── SnapshotPanel.tsx 左侧栏时间轴
│ └── DiffWidget.tsx 主区 diff 视图
├── snapshot/ 后端 Python
│ ├── __init__.py _load_jupyter_server_extension
│ ├── routes.py 三个 REST 处理器
│ ├── store.py SnapshotStore(核心业务)
│ ├── config.py traitlets 配置
│ ├── storage/ 存储适配器
│ │ ├── base.py ABC
│ │ ├── local.py
│ │ └── s3.py
│ └── tests/ pytest
├── style/ CSS(base.css 命名空间 jp-snapshot-*)
├── schema/plugin.json 前端 settings schema(目前空)
├── ui-tests/ Playwright + Galata 集成测试
├── design.md 架构设计文档(单一事实源)
├── AGENTS.md / CLAUDE.md AI 编码规范(CLAUDE.md 是符号链接)
├── README.md 用户文档
└── DEVELOPER.md 本文件
3. 开发环境搭建
一次性:
# 后端
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev,test,s3]"
# 前端
.venv/bin/jlpm install
# 注册扩展到 JupyterLab
jupyter server extension enable snapshot
jupyter labextension develop . --overwrite
每日:
source .venv/bin/activate
.venv/bin/jlpm watch # 终端 1:自动重编
jupyter lab # 终端 2:启动 JupyterLab
改了 Python:重启 jupyter lab(不需要 rebuild 前端)
改了 TypeScript:jlpm watch 自动重编 → 浏览器硬刷新(Cmd+Shift+R)拿新 bundle
改了 CSS:jlpm watch 不会自动 rebuild——手动 jlpm build 后刷新
4. 常用命令
| 操作 | 命令 |
|---|---|
| 后端测试 | .venv/bin/pytest snapshot/tests/ -vv |
| 后端单测 | .venv/bin/pytest snapshot/tests/test_store.py::test_xxx -vv |
| 前端构建 | .venv/bin/jlpm build |
| 前端打包 | .venv/bin/jlpm build:prod |
| Lint | .venv/bin/jlpm lint:check |
| 看日志 | 启动 jupyter lab 的那个终端 |
| 跑 ui-tests | cd ui-tests && yarn install && yarn test |
| 清理 | .venv/bin/jlpm clean:all |
5. 调试
5.1 后端(Python)
jupyter lab 的终端会打印所有 server extension 的日志,包括本扩展。
结构化日志(在 _load_jupyter_server_extension 里):
server_app.log.info(f"Activated snapshot storage backend: {config.storage_type}")
# S3 模式还会打:
# server_app.log.info(f"Snapshot S3 storage ready: endpoint=..., bucket=...")
S3 连接失败:会抛 S3ConnectionError(带 endpoint / bucket / 修复建议),JupyterLab 启动失败。看 traceback。
手动看 API(不通过前端):
# 启动后
.venv/bin/jupyter server --port 8888 &
# 拿到 token from jupyter_server 的输出
TOK=...
# 提交
curl -X POST -H "Authorization: token $TOK" -H "Content-Type: application/json" \
-d '{"path":"test.ipynb","content":{"cells":[],"metadata":{},"nbformat":4,"nbformat_minor":5},"name":"v1","description":""}' \
http://localhost:8888/snapshot/notebook-version/commit
# 列表
curl -H "Authorization: token $TOK" \
"http://localhost:8888/snapshot/notebook-version/list?path=test.ipynb"
5.2 前端(TypeScript / React)
1. 浏览器 DevTools Console
打开 DevTools (F12 或 Cmd+Option+I) → Console。
代码里有 console.log 诊断日志:
'JupyterLab extension snapshot is activated!'— 插件加载'snapshot settings loaded:'— settings 加载'[snapshot] commit response:'— commit 响应(最近调试加的)'Failed to list snapshots'— list 失败'Diff failed'— diff 失败
2. 看 React 组件树:装 React DevTools 扩展
3. 看 Lumino Widget 树:在 Console 跑 JupyterLab.shell.widgets() 看主区 widget,左区 widget 列表
4. 改前端后必须硬刷新:JupyterLab 缓存 labextension,软刷新可能拿到旧 bundle。Cmd+Shift+R 清缓存。
5.3 端到端问题排查
| 症状 | 排查 |
|---|---|
| 工具栏没「保存快照」按钮 | (1) 硬刷新浏览器;(2) jupyter labextension list 看 snapshot 状态 |
| 提交没反应 | 看 Console 有无 [snapshot] commit response: 日志,看 jupyter 终端有无 traceback |
| 「内容未变更」warning 不弹 | Console 里看 commit response 实际是什么 |
| 保存后历史列表没新条目 | 点侧边栏「刷新」按钮(手动 list);检查 S3 manifest 写入(用 aws s3 ls 或 MinIO 控制台) |
| Diff 视图空白 | 检查 model.versions 是否有目标 id;后端 /content?path=&id= 单独 curl 一次 |
| Restore 失败 | Console 报具体错;检查 notebook.model.fromJSON 的入参结构 |
5.4 测试覆盖现状
backend pytest: 33 passed (13 storage + 9 store + 6 routes_api + 5 misc)
frontend jest: ❌ pnpm 链接器下跑不起来(见下方"已知坑")
ui-tests: 跑通需先 `cd ui-tests && yarn install`,未在 CI 中
新增后端测试:snapshot/tests/test_*.py,命名 test_<模块>_<场景>.py
新增前端测试:jest 暂时不可用。如需单测 src/diff.ts 的纯逻辑,可:
- 临时方案:把 diff 函数复制到
__tests__/diff.spec.ts跑(在已知坑解决前) - 正确方案:改
.yarnrc.yml的nodeLinker为node-modules,再重装
6. 打包与发布
6.1 版本管理
- 单一事实源:
package.json的version字段 pyproject.toml通过hatch-nodejs-version自动从package.json同步- 手动修改
pyproject.toml的版本会被覆盖
6.2 后端 wheel
.venv/bin/pip install build hatch hatch-jupyter-builder hatch-nodejs-version
.venv/bin/python -m build
# 产物:dist/snapshot-0.1.0-py3-none-any.whl + dist/snapshot-0.1.0.tar.gz
pyproject.toml 的 [tool.hatch.build.hooks.jupyter-builder] 会自动 build 前端并打进 wheel 的 snapshot/labextension/。
发布到 PyPI:
.venv/bin/python -m twine upload dist/*
6.3 前端 labextension
前端不单独发布。build 产物(snapshot/labextension/)通过 hatch hook 嵌进 wheel。
本地 dev 模式:jupyter labextension develop . --overwrite 建立符号链接,改前端代码即时生效。
6.4 发布 checklist
- 改
package.json的 version(不要改pyproject.toml) - 更新
CHANGELOG.md git tag v0.1.0- 跑全套验证:
pytest+jlpm build:prod+jlpm lint:check python -m build出 wheel- (可选)
twine upload dist/*发 PyPI git push --tags
CI 流程见 .github/workflows/build.yml 和 publish-release.yml。
7. 已知坑
7.1 Jest 在 pnpm 链接器下跑不起来
jlpm test 报 Cannot find module 'jest-util',stack 指向 ts-jest/dist/legacy/config/config-set.js。
根因:yarnLinker: pnpm(.yarnrc.yml)的虚拟存储把 ts-jest 和 jest-util 隔开,pnpm 严格隔离破坏了 require 路径。
绕过:
- 暂时不修——前端用
jlpm build(tsc 严格模式 + rspack bundle)验证类型和模块等价 - 真要跑单测,改
.yarnrc.yml切nodeLinker: node-modules(影响整个 monorepo 解析) - 或完全重写
jest.config.js不依赖@jupyterlab/testing的 baseConfig
7.2 S3 强一致性
AWS S3 自 2020 年起强一致(读写单对象强一致)。但 MinIO / Ceph RGW / 阿里云 OSS 等 S3 兼容服务实现各异。
表现:commit 写入 manifest 成功,但紧随其后的 list 偶尔返回旧 manifest。
临时缓解:侧边栏有「刷新」按钮,用户手动拉一次。
彻底修复(未做):commit 响应里直接返回最新 list,省掉独立 list 调用。
7.3 .yarnrc.yml / .venv / lockfile
- 仓库用 uv/venv(不是 conda):
source .venv/bin/activate - 前端用 Yarn Berry(不是 npm / yarn classic):
jlpm,不要npm install - 不要混
package-lock.json和yarn.lock
8. 给新 contributor 的 5 分钟清单
source .venv/bin/activate && pip install -e ".[dev,test,s3]".venv/bin/jlpm install && .venv/bin/jlpm buildjupyter server extension enable snapshot && jupyter labextension develop . --overwritejupyter lab→ 打开任意.ipynb→ 看工具栏「保存快照」按钮- 点按钮 → 填 name → 看左侧栏时间轴;改 cell 再保存 → 时间轴更新
如果第 4 步没看到按钮:硬刷新浏览器(Cmd+Shift+R)。