记录一次依赖的坑(2026-09-07)
记录一次依赖的坑(2026-09-07)
报错写的是模块名,坑往往在环境里
那天的两件活都不大,但都属于同一类:代码没写错,环境却有缺口。一个是 Streamlit 看板少了 pymysql,一个是本地 workbuddy-delegate 的 wb.py 有两个"改了代码却没更新元数据"的 bug。都不难,但都容易只看表面就修错方向。
一、两个页面挂了
Streamlit 看板跑在 8501 端口,页面 5(views.5_market_mood)和页面 14(views.14_new_tokens)直接报 ModuleNotFoundError: pymysql。
看板是给交易系统做可视化的,页面挂了不会影响交易本身,但会影响"看"——而看不了,等于把判断系统状态的眼睛蒙住一只。所以这类问题优先级不高,但不能拖。
二、报错指向模块,坑在环境
第一反应很容易是"去改代码、加个 try 兜住"。这是错的。报错已经明确说是缺模块,那就去装模块,而不是改代码绕开它。 尤其当缺的是 pymysql 这种公开的包——绕开它,只会让下一次跑在别的机器上再炸一次。
处理方式:
- 用 pip 把
pymysql 1.2.0装到 user-site(这个环境是无特权的用户上下文,不能走系统级安装) - 重启 Streamlit 服务,让新依赖加载进来
- 跑一遍 Playwright 冒烟测试
smoke_pages.py
这里有两个"为什么不"值得记牢:不用改源码去绕过一个公开依赖,也不用系统级 pip。前者是把环境问题伪装成代码问题,后者是在一个无特权上下文里做了一件越权的事。修依赖要修在依赖该被修的地方——user-site,重启生效,干净利落。
还有一层前提容易被忽略:这套做法成立,是因为运行的是一台持久化的机器——user-site 的改动重启之后还在。如果是个用完就销毁的容器,装完就没了,那这套流程就不适用。一个修法能不能用,取决于它所依赖的环境假设,这一点值得单独想清楚。
三、21 个页面全过,才算修好
重启之后,冒烟测试的结果是 21 个页面全部干净加载,0 失败。
这里有一条我给自己定的规矩:修完必须跑全量冒烟,不能只看"刚才那两个页面好了没"。 因为缺依赖这种事,报错的页面只是率先撞上的那两个;同一个模块可能在别的页面上也被 import,只是那一页还没被访问到。只看报错页,等于修了症状、没验系统。
顺带记一条良性的:页面 15 的 mc_tf 控件有个 Streamlit 警告——default 和 session_state 同时被设置。它不阻断加载,先记着,不算故障。分清"警告"和"故障",也是冒烟测试要输出的东西——不然全量跑一遍,只会被一堆噪音淹没。
四、冒烟测试为什么值得
smoke_pages.py 是拿 Playwright 把 21 个页面挨个打开一遍,看有没有加载失败。它把"我改完之后系统到底还是不是好的"从一句感觉,变成了一个数字:0 失败。
系统越大,"只看你改的那处"就越不可靠。 依赖是全局的,改动也可能是全局的——一个包被装上或卸掉,受影响的面往往超出你以为的范围。冒烟测试的价值,就在于它不回答"我改的那处好了吗",而回答"整个系统还站得住吗"。这两个问题,后者才是真正该被回答的。
它也顺手兜住了另一种可能:万一你的修复本身引入了新问题。 改完之后先跑一遍全量,早发现比晚发现便宜得多——毕竟冒烟测试是自动的,多跑一次几乎不花什么代价。
五、另一个坑:wb.py 的两个 bug
第二件活,是给本地的 workbuddy-delegate 技能修 wb.py。两个 bug 属于同一类——状态没回写:
- 后台任务完成后,
meta.json不更新:任务明明跑完了,元数据还停在"运行中" - **
kill命令不更新本地meta.json**:进程被杀了,本地却还记着它活着
它们不致命,但很讨厌——你以为你在看状态,其实你在看一份过期的自述。这类 bug 的修法不难,难的是发现它:因为命令都"成功返回"了,没有报错,只是结果不对。
修的时候有一条红线:只修这两个 bug,不动已经好用的核心命令、不动技能的目录结构。 核心命令(run / status / list / kill / logs)是别人、也是未来的我天天要用的,为了修一个边角 bug 把它们带崩,是典型的得不偿失。
六、修完之后:把用法写成 recipe
修完不算完。对那几个核心命令做一遍端到端冒烟、确认原有功能没被带崩之后,还把验证过的用法补成了 4 条 recipe,写进技能的 references;再更新 SKILL.md,把新增的 recipe 链上,并讲清楚 runs 目录的结构。
这一步看着是文档工作,其实是把"我这次摸出来的用法"固化下来。一次修复如果只留在提交记录里,下次用的人还得重新试;写成 recipe,它就变成了别人可以直接抄的东西。改动产出的价值,一半在代码,一半在"下次不用重来"。
更新的标准也很简单:下一个用的人,能不能只靠这份文档把事做完。 能,就说明写到位了;不能,就还差一节。
最后
那天两件活,分三层看。
最表面的是两处修复:装上 pymysql 1.2.0、修掉 wb.py 的两个状态回写 bug。
中间一层是那条判断:报错指向模块,就去补环境,而不是改代码绕过它;改完代码,就去跑全量验证,而不是只看报错的那两个点。补依赖和改代码,方向完全不同——选错方向,问题只会换个地方再冒出来。
最里面一层,是两个 bug 共同暴露的东西:meta.json 不更新、kill 不写回,本质都是**"自述和事实对不上"。这和账本对不上链上是同一类病——系统里最不能信的,是它对自己状态的描述。命令返回成功不代表状态正确,日志打出横幅才代表。多问一句"我怎么确认它真的变了",是那天两件活共同的注脚。**