一次代码考古实录
一、先搞清楚项目在干什么
打开仓库,README 写得很清楚:两个 AI Skill 串起来做小红书运营——
- Skill ①
hot_topic_search:搜热点话题、下载配图 - Skill ②
xiaohongshu_workflow:通过 Chrome 远程调试端口(CDP)自动发布图文笔记、管理评论
快速开始文档给了两步命令:
# 第 6 步:搜索热点并下载配图(默认存到 ~/image/)
python3 hot_topic_search/scripts/search_images_fast.py "热点话题" --count 4
# 第 7 步:发布图文笔记
python3 xiaohongshu_workflow/scripts/run.py publish \
--photos "~/image/*.jpg" \
--title "标题" --body "正文" --topics "话题1"
第 6 步把图存进 ~/image/,第 7 步再从 ~/image/*.jpg 读出来发布。看起来天衣无缝。
但我是来挑 bug 的,不是来夸文档的。于是把两个核心脚本拉下来逐行读。
二、读代码:一个 ~ 引起的血案
看到的第一段可疑代码
xiaohongshu_workflow/scripts/run.py 里处理 --photos 路径的函数:
def expand_photos(photo_args):
photos = []
for item in photo_args:
matched = sorted(glob.glob(item)) # ← glob.glob 不展开 ~
photos.extend(matched if matched else [item])
return photos
紧接着的校验:
def validate_inputs(photos, title, body):
...
missing_files = [p for p in photos if not os.path.isfile(p)] # ← os.path.isfile 也不展开 ~
两个标准库函数,叠在一起,都不展开 ~。
为什么 ~ 展不开?
这是个容易被忽略的细节:~ 是 shell 的展开行为,不是 Python 标准库的行为。
- 你在终端敲
ls ~/image/*.jpg,是 bash 先把~替换成/home/yourname,再交给ls。 - Python 里
glob.glob("~/image/*.jpg")拿到的是字面量字符串,glob原样拿去匹配文件系统——文件系统里没有一个叫~的目录,所以返回[]。 os.path.isfile("~/image/p1.jpg")同理,返回False。- 唯一会展开
~的是os.path.expanduser()。
于是链路是这样的:
--photos "~/image/*.jpg"
→ glob.glob("~/image/*.jpg") 返回 [] # ~ 没展开,匹配不到
→ 走 else 分支,把字面量 "~/image/*.jpg" 塞进 photos
→ os.path.isfile("~/image/*.jpg") 返回 False # ~ 还是没展开
→ 报错:"图片文件不存在: ~/image/*.jpg"
按官方文档逐字操作,发布流程必然跑不通。
更有意思的是:端到端流程是断的
光看 run.py 这边坏了还不够。我去看了 search_images_fast.py 那边怎么存图:
DEFAULT_IMG_DIR = Path(os.environ.get("XHS_IMG_DIR", str(Path.home() / "image")))
Path.home() 正确展开了家目录,图片确实落到了 ~/image/。
这就形成了一个讽刺的局面:
搜索 Skill:用 Path.home() ✓ 正确存到 ~/image/
发布 Skill:用 glob.glob(~) ✗ 读不回来 ~/image/*.jpg
两个 Skill 串不起来。 文档第 6 步成功、第 7 步失败,而且报错信息 图片文件不存在 会误导人去查"图是不是没下载下来",实际图好好的躺在那儿,是路径展开的锅。
这种"单看每个模块都没大问题、串起来才暴露"的 bug,比单点故障更值得提 issue。
三、写 Issue:把发现变成可执行的修复
找到 bug 不算完,得让别人能复现、能修、能验证。Issue 正文我按这个结构写:
- 现象:贴文档原命令 + 实际报错
- 根因分析:贴出
expand_photos和validate_inputs源码,解释glob/os.path.isfile为何不展开~ - 影响范围:点明端到端流程断裂 + 报错信息误导排查
- 期望行为:列出
--photos应支持的四种输入 - 建议修复:直接给 patch
修复其实就一行的事——glob 之前先 expanduser:
def expand_photos(photo_args):
photos = []
for item in photo_args:
expanded = os.path.expanduser(item) # ← 先展开 ~
matched = sorted(glob.glob(expanded))
photos.extend(matched if matched else [expanded])
return photos
- 建议补充测试:附 pytest 用例,用
monkeypatch.setenv("HOME", ...)+tmp_path隔离环境,覆盖~+glob 和~+字面量两种输入:
def test_expand_photos_tilde(monkeypatch, tmp_path):
monkeypatch.setenv("HOME", str(tmp_path))
(tmp_path / "image").mkdir()
for n in (1, 2):
(tmp_path / "image" / f"p{n}.jpg").write_bytes(b"x")
assert len(expand_photos(["~/image/*.jpg"])) == 2
assert expand_photos(["~/image/p1.jpg"]) == [str(tmp_path / "image" / "p1.jpg")]
一个小原则:顺带发现
--photos文档写"逗号分隔"但代码是nargs="+"(空格分隔),这是另一个独立问题,单独提,不混进这个 issue 把 scope 搅大。
四、提交之路:踩了两个坑
写完 issue 才走了一半——得有 GitCode 写权限才能提。这中间踩了两个坑,值得记一笔。
坑 1:官方安装脚本的 latest 链接 404
文档给的安装命令:
curl -sfL //
直接 404。latest 这个 tag 根本不存在。解决办法是调 release API 拿到真实版本号:
curl /*
返回 v0.0.1 ~ v0.0.6,install 里 get_latest_version 用 grep tag_name | tail -1 取到最新的 v0.0.6,再按 linux/arm64 拼下载地址。文档里的 latest 是个坏链,但 install.s自己的版本探测逻辑是对的——绕过坏链直接跑 install 就行。
坑 2:GitCode 的认证头是 Bearer,不是 token
拿到 OAuth token 后,我按 Gitea 惯例用 Authorization: token <TOKEN> 发 POST 创建 issue,结果:
{"error_code":401, "error_message":"401, token not found"}
但同一个 token 用官方 gitcode-oauth get 子命令发 GET 却好好的。说明 token 没问题,是认证头格式的问题。
写个脚本把几种格式都试一遍:
Authorization: token <TOKEN> → 401 ← Gitea 惯例,GitCode 不认
Authorization: Bearer <TOKEN> → 200 ← 正解
Authorization: <TOKEN> → 200
PRIVATE-TOKEN: <TOKEN> → 200
GitCode(AtomGit)走的是 Bearer,不是 Gitea 的 token 前缀。 从 Gitea 生态迁过来的代码这里会静默失败。换成 Bearer 之后,issue #170 一次提交成功。
五、结果
Issue #170://
- 标题:
Bug:run.py publish --photos 不展开 ~,导致 README 文档示例必然失败(端到端流程断裂) - 包含:复现步骤、根因分析、影响范围、修复 patch、测试用例
- 状态:open
六、几点心得
1. "有技术含量"的 issue 是读出来的,不是编出来的。
泛泛的"建议增加错误处理""建议优化文档"谁都会写。真正的价值在于把代码读到位,找到一个别人没注意到、但按文档操作就会踩到的具体 bug。
2. 端到端视角比单点视角值钱。
单看 run.py,路径展开失败是个小 bug;把搜索和发布两个 Skill 串起来看,才发现"存得进去、读不出来"——这是流程级故障。提 issue 时点明这一点,分量完全不同。
3. ~ 是 shell 的事,不是 Python 的事。
这个细节很多从 shell 转过来的人会想当然。glob.glob、os.path.isfile、os.path.exists 都不展开 ~,只有 os.path.expanduser 会。凡是要把用户输入的路径喂给这些函数,先 expanduser 一道准没错。
4. 认证头别假设,试一遍最快。
Gitea 用 token、GitCode 用 Bearer、GitLab 用 PRIVATE-TOKEN——同源生态都能不一样。与其查文档猜,不如写个循环把几种格式都打一发,30 秒见分晓。
5. 顺手发现的问题别塞进当前 issue。
--photos 逗号分隔 vs 空格分隔的文档不一致,是顺手发现的,但它和 ~ 展开是两个独立问题。混在一起提,修复者要么改一半、要么 scope 膨胀。分开提,各自干净。
写于 2026-10-26。/认真做下来也是一次完整的代码考古 + 调试记录。
- 点赞
- 收藏
- 关注作者
评论(0)