综合项目:Notebook Service(10/2 – 10/6,≈50h)¶
一个"个人知识库"HTTP 服务 + 命令行客户端。它只用本仓库学到的 Python 知识,不含任何 LLM 调用;但它的每一块(FastAPI + SSE、SQLite 仓储、Pydantic 模型、httpx 客户端、日志、测试、分层)都是
learn-agent里 StudyBuddy 要直接复用的积木。做完它,agent 项目里剩下的就只是"LLM 与工具"本身。位置:
projects/notebook/(uv workspace 成员,uv run notebook --help、uv run notebook-server)。骨架不预先给出——这是结业项目,从cd projects && uv init --package notebook开始自己搭,然后在仓库根pyproject.toml的dependencies里加"notebook"、在[tool.uv.sources]里加notebook = { workspace = true },uv sync后入口点即可用;projects/kb与projects/aggregator是你的参考实现。运行时依赖(fastapi[standard]、pydantic-settings、httpx、tenacity、typer、rich)在根环境里已经装好,但projects/notebook/pyproject.toml仍要如实声明自己的dependencies。
1. 功能清单(MVP 全部必做)¶
| # | 功能 | 接口 / 命令 | 涉及知识 |
|---|---|---|---|
| F1 | 笔记 CRUD | POST /notes GET /notes/{id} PATCH /notes/{id} DELETE /notes/{id} |
FastAPI、Pydantic、SQLite 仓储 |
| F2 | 列表与过滤 | GET /notes?tag=&q=&limit=&offset= |
分页、查询校验 |
| F3 | 搜索 | GET /notes/search?pattern=®ex=true |
正则、服务层 |
| F4 | 批量导入 Markdown 目录 | POST /import {"path": "..."} → 返回 job_id,后台执行 |
BackgroundTasks/asyncio.Task、pathlib、第 1 周管线 |
| F5 | 导入进度推送 | GET /jobs/{job_id}/events(SSE:progress/done/error 事件) |
fastapi.sse、异步生成器、队列 |
| F6 | 外部数据拉取 | POST /notes/from-url {"url": "..."}:用 httpx 抓取 JSON/文本生成笔记(演示服务调外部服务,带超时与重试) |
httpx、tenacity、错误分类 |
| F7 | 健康与统计 | GET /health、GET /stats(笔记数、标签分布、导入任务数) |
聚合、Counter |
| F8 | 鉴权 | 写操作需 X-API-Key(来自 settings) |
依赖注入、pydantic-settings |
| F9 | CLI 客户端 | notebook add/list/search/import/watch-job(通过 httpx 调服务) |
typer、rich、httpx |
| F10 | 可观测 | 请求日志中间件(请求 id、耗时、状态码);文件 JSON 日志轮转;/stats 里含错误计数 |
logging、中间件 |
可选扩展(做 1 个):导出 Markdown/JSON 备份;标签重命名事务;简单的 ETag/If-None-Match;WebSocket 版进度;用 uvx ty check 全绿。
2. 架构要求¶
projects/notebook/
├── pyproject.toml # name="notebook"; scripts: notebook = "notebook.cli:app", notebook-server = "notebook.main:run"
├── README.md .env.example CHANGELOG.md
├── src/notebook/
│ ├── main.py # create_app()、lifespan(建库、启动任务表)、异常处理器、中间件、include_router
│ ├── settings.py # Settings(BaseSettings):db_path、api_key、log_level、import_root、http_timeout
│ ├── logging_config.py
│ ├── domain/ # dataclass 领域模型:Note、Tag、ImportJob;领域异常 NotebookError/NotFound/Conflict/Invalid
│ ├── schemas/ # Pydantic 传输模型:NoteIn/NoteOut/NotePatch/ImportRequest/JobOut/StatsOut
│ ├── repositories/ # NoteRepository(Protocol)、JobRepository(Protocol);sqlite 实现 + memory 实现
│ ├── services/ # NoteService、ImportService(生成器管线 + 进度回调)、FetchService(httpx)
│ ├── api/ # routers:notes.py jobs.py stats.py health.py;deps.py(get_settings/get_repo/verify_api_key)
│ ├── sse.py # 任务进度 → EventSourceResponse 的异步生成器
│ └── cli.py # typer 客户端
└── tests/
├── conftest.py # 内存仓储、TestClient、AsyncClient(ASGITransport)、tmp_path 数据
├── unit/ # domain、services(用内存仓储与假 http transport)
└── api/ # 路由测试、SSE 测试(读前 N 条事件)、鉴权测试
硬性规则:
1. services/ 与 domain/ 不 import FastAPI、sqlite3、httpx 的具体类型——只依赖 Protocol;所有 I/O 实现放 repositories/ 与 FetchService 的注入客户端。
2. 领域异常在 main.py 统一映射为 HTTP(NotFound→404、Conflict→409、Invalid→422)。
3. 所有文件读写 encoding="utf-8";SQLite 一律参数化查询、with conn: 事务。
4. 异步端点内不出现阻塞调用(SQLite 用 to_thread 或同步端点)。
5. 每个公开函数有一句话 docstring;类型注解完整;ruff check(E F W I UP B N D SIM)零警告;Pylance 零错误。
6. 测试:单元 + API + 至少 1 个 SSE 测试 + 1 个鉴权失败测试 + 1 个 httpx MockTransport 测试;pytest --cov=notebook ≥80%。
7. 配置只来自环境变量/.env,仓库里只有 .env.example。
3. 里程碑与时间¶
| 里程碑 | 日期 | 内容 | 结束时必须有 |
|---|---|---|---|
| M1 领域与仓储 | 10/2(10h) | uv init --package 建包;领域模型与异常;NoteRepository 协议 + 内存 + SQLite 实现;NoteService CRUD/搜索;单元测试 |
pytest tests/unit 全绿;uv run python -m notebook --help 可用 |
| M2 服务与 API | 10/3(10h) | settings、logging、create_app、routers(F1/F2/F3/F7/F8)、异常映射、中间件、API 测试 |
fastapi dev 起服务,/docs 能走通 CRUD;API 测试全绿 |
| M3 导入、SSE、外部拉取、CLI | 10/4(10h) | ImportService(第 1 周管线 + 进度回调)、任务表、/import + /jobs/{id}/events SSE、FetchService(httpx + tenacity)、typer CLI |
CLI 能 import 一个目录并 watch-job 看到进度条走完 |
| M4 收口 | 10/5(10h) | 覆盖率 ≥80%、ruff/Pylance 零错误、README、.env.example、CHANGELOG、可选部署到 H20 |
验收清单前 12 条全勾 |
| M5 评审与发布 | 10/6(10h) | AI 评审(模板 4)→ 重构 ≤5 条 → 回归;uv build --package notebook;git tag v1.0.0;复盘文档 |
推送 cnb.cool;复盘写进 checklist.md |
风险控制:10/3 结束时若 API 没跑通,10/4 砍掉 F6 与 CLI 的 watch-job,先保 F4/F5;10/5 结束时若覆盖率不足,优先补服务层测试而不是路由测试。
4. 验收清单¶
-
uv sync后uv run notebook-server起服务,/docs可用,/health返回 200 - F1–F10 全部可用(用 CLI 走一遍:add → list → search → import → watch-job → stats)
- 写操作无
X-API-Key返回 401,错误 key 返回 403 - SSE:导入 50 个文件时能看到
progress事件逐条到达,最后done - 导入目录中有一个损坏/非 UTF-8 文件时任务不崩溃,事件里出现
error且其余文件导入成功 -
from-url对不可达地址返回 502/504 类错误而不是 500,日志有分类 - 删掉数据库文件重启服务能自动建库;
.env缺失时用默认值并警告 -
pytest --cov=notebook --cov-report=term-missing≥80%,全部通过 -
ruff check projects/notebook零警告;Pylance 零错误 -
services/、domain/中grep不到fastapi、sqlite3、httpx - 请求日志包含请求 id 与耗时;文件日志为 JSON、UTF-8、可轮转
- README 含:一句话介绍、架构图(文字版)、运行、配置项表、接口表、测试、已知限制、下一步
- Git 历史 ≥15 次 Conventional Commits;
v1.0.0tag;已推送 cnb.cool - AI 评审后改动 ≤5 条并回归通过
- (可选)在 H20 上运行并通过
ssh -L在本机访问/docs
5. 结业复盘模板(写进 checklist.md)¶
- 三个最有价值的知识点(各一句话,说明它在 Notebook Service 的哪一行起作用)
- 三个最难的 bug(现象 → 根因 → 用哪个工具找到的)
- 如果重做一次,架构上会改什么
- 对照
learn-agent计划:哪些 Python 前置已就位(打勾),哪些还要补 - 下一个 30 天:只写三件事