45 min · 精读 web/app.py 的骨架(用 grep 速览,不通读)
🎒 预备知识:完成上一课「L08」(零基础入口:第 0 课)
FastAPI 是 Python 的 Web 框架:你用装饰器声明"哪个 URL 路径 + 哪个 HTTP 方法" 交给哪个函数处理,框架负责解析请求、调用函数、返回响应。web/app.py 就是 Easel Web 工作台的全部后端:前端(React)发的每个请求都在这里被处理。
先回答一个该问的问题:为什么 4000 行塞一个文件?这是取舍不是事故—— 单文件让"全局状态、中间件、端点"的可见性最高(都在一起),对单人/小团队快速迭代友好; 代价是必须靠纪律分区(文件内注释分区清晰)+ 外部校验(tests/ 里的 test_web_security.py)。 单文件不是原罪,无结构的单文件才是——这门课教的就是怎么在有结构的单文件里导航。
cd ~/Code/Projects/easel-repo
grep -n '@app\.\(get\|post\|put\|delete\)' web/app.py | head -60
这一条命令就是你的地图生成器:每个 @app.get(...) 装饰器是一个路由声明。
顺着行号扫一遍,你会发现端点天然聚簇——聚簇就是分区。
| 分组 | 代表端点 | 读时问自己 |
|---|---|---|
| 画像 | GET /api/personas | 六维文件怎么在线编辑? |
| 技能 | POST /api/skill | 怎么同步执行一个技能? |
| 配置 | POST /api/env(白名单) | 为什么有 _ENV_ALLOWLIST? |
| 对话 | POST /api/chat/stream | 下一课主角 |
| 素材/产物 | GET /api/outputs | 为什么忽略 _ 前缀目录? |
| 账号登录 | POST /api/login/{platform} | LOGIN_RUNNERS 注册表?(L12) |
| 归因 | GET /api/analytics/{p} | headless 浏览器取数? |
| 发布 | POST /api/publish/{p} | 分发到各平台脚本?(L07 的双调用方) |
| 运营 | /api/trends /api/schedule | 状态存哪? |
这张地图同时回答了 L07 的悬案:POST /api/publish/{platform} 直接 Popen
skills/shared/scripts/ 里的 xhs_publish.py 等——Web 后端和 agent 是同一份发布脚本的
两个调用方,这就是脚本住 shared 的原因。
① 双层锁:每会话 asyncio.Lock(进程内)+ fcntl.flock 跨进程锁。 为什么需要两把?asyncio.Lock 只管本进程内的并发(同一事件循环里的协程); 但 Easel 的会话可能被两个进程同时推进——Web 后端推进中,用户又开了 CLI chat。 flock 是操作系统级的文件锁,跨进程生效。锁的粒度 = 会话:不同会话互不阻塞, 同一会话严格串行(A9 的 lane 思想在 Web 侧的镜像)。
② local_write_guard + TrustedHost 中间件:防浏览器跨站写本地文件。 Web UI 在浏览器里跑,恶意网页可以让你的浏览器向 localhost:7860 发写请求(CSRF 思路)—— 守卫确保写操作只接受合法来源。有专门测试(test_web_security.py)护着。
③ 监听 0.0.0.0 但有注释辩护(:4060):"监听 0.0.0.0(非仅回环)是为了兼容 Docker/远程容器端口转发访问 Easel 的场景"。写清"为什么"的注释本身就是架构文档—— 没有这行注释,下个人看到 0.0.0.0 会当成安全隐患"修掉",Docker 用户全线失联。 对比 gateway 的 --bind loopback(L01):一个对外一个对内,各自有注释说理由。 安全相关的绑定选择必须留注释,这是给未来审计者的交代。
坑 1:通读 4000 行。第 3 天你还在第 800 行;grep 路由 30 秒有全图。
坑 2:把端点当孤立函数读。端点之间共享大量辅助函数(拼消息/取画像/锁会话),
按组读才能看到共享面。坑 3:忽视轮询类端点。GET /api/login/{p}/status 这类
轮询端点是"文件即协议"(L12)的 Web 消费侧——单独看它没意义,连着状态文件看才懂。
不看资料,画出 9 组端点地图 + 每组一个代表端点。追问准备:为什么对话端点要单独一整层流式机制?(下一课答案)
一手源:web/app.py 的分区注释 + grep -n '^# ---\|^# ===' 看作者的分区方式。