一次代码考古实录

举报
yd_282816122 发表于 2026/10/26 11:42:36 2026/10/26
【摘要】 一、先搞清楚项目在干什么打开仓库,README 写得很清楚:两个 AI Skill 串起来做小红书运营——Skill ① hot_topic_search:搜热点话题、下载配图Skill ② xiaohongshu_workflow:通过 Chrome 远程调试端口(CDP)自动发布图文笔记、管理评论快速开始文档给了两步命令:# 第 6 步:搜索热点并下载配图(默认存到 ~/image/)...

一、先搞清楚项目在干什么

打开仓库,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 正文我按这个结构写:

  1. 现象:贴文档原命令 + 实际报错
  2. 根因分析:贴出 expand_photos 和 validate_inputs 源码,解释 glob / os.path.isfile 为何不展开 ~
  3. 影响范围:点明端到端流程断裂 + 报错信息误导排查
  4. 期望行为:列出 --photos 应支持的四种输入
  5. 建议修复:直接给 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
  1. 建议补充测试:附 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。/认真做下来也是一次完整的代码考古 + 调试记录。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。