diff --git a/DEVELOPER.md b/DEVELOPER.md new file mode 100644 index 0000000..af5ebb4 --- /dev/null +++ b/DEVELOPER.md @@ -0,0 +1,275 @@ +# 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 ABC(put/get/list/delete) │ +│ ├─ local.py LocalFileStorage(原子写 tmp+rename) │ +│ └─ s3.py S3Storage(boto3,懒加载) │ +└──────────────────────────────────────────────────────────────┘ +``` + +**关键设计决策**(实现时不得违背,见 [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 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. 开发环境搭建 + +一次性: + +```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)。