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

276 lines
12 KiB
Markdown
Raw Permalink 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.
# Developer Manual
开发笔记本快照扩展的工程手册。面向**接手开发**的人:架构、构建、调试、打包全流程。
> 用户视角(安装 / 配置 / 排错)见 [README.md](README.md)。设计目标见 [design.md](design.md)。AI 编码规范见 [AGENTS.md](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](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. 开发环境搭建
一次性:
```bash
# 后端
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
```
每日:
```bash
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` 里):
```python
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**(不通过前端):
```bash
# 启动后
.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
```bash
.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
```bash
.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.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 分钟清单
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)。