Files
tao.chenandClaude 3476517479 docs: add DEVELOPER.md (architecture, build, debug, packaging)
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>
2026-07-21 18:26:11 +08:00

12 KiB
Raw Permalink Blame History

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 ABCput/get/list/delete │
│  ├─ local.py      LocalFileStorage(原子写 tmp+rename     │
│  └─ s3.py         S3Storageboto3,懒加载)                 │
└──────────────────────────────────────────────────────────────┘

关键设计决策(实现时不得违背,见 AGENTS.md):

  1. 存储适配器是哑字节桶——不知道 notebook / 版本 / manifest 这些概念
  2. MD5 在清洗后 JSON 上计算;hash 与最近版本相同则跳过保存
  3. 快照文件是 envelope{id, timestamp, name, description, hash, size, notebook}),name/description 内嵌以便 manifest 损坏时从版本文件重建
  4. manifest 是可重建的索引,原真相在 envelope 里
  5. 手动提交,不绑定 Ctrl+S自动 commit
  6. Cell diff 按 id 优先匹配,LCS 兜底

2. 仓库布局

.
├── src/                    前端 TypeScript / React
│   ├── index.ts            插件入口(注册命令、挂面板、装工具栏)
│   ├── commands.ts         命令实现(snapshot:commit / restore
│   ├── api.ts              类型化 HTTP 包装
│   ├── model.ts            SnapshotModelpath + versions + 信号)
│   ├── diff.ts             Cell 级 diffid 优先 + 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/                  CSSbase.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 前端) 改了 TypeScriptjlpm watch 自动重编 → 浏览器硬刷新Cmd+Shift+R)拿新 bundle 改了 CSSjlpm 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.ymlnodeLinkernode-modules,再重装

6. 打包与发布

6.1 版本管理

  • 单一事实源package.jsonversion 字段
  • 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

  1. package.json 的 version(不要改 pyproject.toml
  2. 更新 CHANGELOG.md
  3. git tag v0.1.0
  4. 跑全套验证:pytest + jlpm build:prod + jlpm lint:check
  5. python -m build 出 wheel
  6. (可选) twine upload dist/* 发 PyPI
  7. git push --tags

CI 流程见 .github/workflows/build.ymlpublish-release.yml

7. 已知坑

7.1 Jest 在 pnpm 链接器下跑不起来

jlpm testCannot find module 'jest-util'stack 指向 ts-jest/dist/legacy/config/config-set.js

根因yarnLinker: pnpm.yarnrc.yml)的虚拟存储把 ts-jestjest-util 隔开,pnpm 严格隔离破坏了 require 路径。

绕过

  • 暂时不修——前端用 jlpm buildtsc 严格模式 + rspack bundle)验证类型和模块等价
  • 真要跑单测,改 .yarnrc.ymlnodeLinker: 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.jsonyarn.lock

8. 给新 contributor 的 5 分钟清单

  1. source .venv/bin/activate && pip install -e ".[dev,test,s3]"
  2. .venv/bin/jlpm install && .venv/bin/jlpm build
  3. jupyter server extension enable snapshot && jupyter labextension develop . --overwrite
  4. jupyter lab → 打开任意 .ipynb → 看工具栏「保存快照」按钮
  5. 点按钮 → 填 name → 看左侧栏时间轴;改 cell 再保存 → 时间轴更新

如果第 4 步没看到按钮:硬刷新浏览器(Cmd+Shift+R)。