UcnacDx2's Blog
← 返回首页

AI 参与真实开源 PR:以 OpenList 139 驱动为例,我怎么验证它不是在写 Demo

用一个已经合并的 OpenList PR 拆解 AI 辅助开发:具体改了哪些文件、测试怎么跑、哪些结果由真实账号验证,以及为什么最后责任仍然在人。

· AI / Open Source / OpenList / Engineering

我确实会用 AI 写代码,但如果文章只写:

AI 帮我读代码、生成代码、补测试,我再人工检查。

那几乎没有信息量。

所以这篇直接拿一个真实 PR 做证据:

https://github.com/OpenListTeam/OpenList/pull/2067

PR 标题是:

fix(drivers/139): optimize login flow with cookie reuse and robust fallback

它已经合并到 OpenList,上游 merge commit 是:

376cada3d4917f9ac9136eb2ca313c8384557dcf

我在 PR 里也明确做了 AI Disclosure:使用了 ChatGPT 和 Codex,范围包括代码生成、重构、文档、测试和 review assistance。

重点不是“AI 用得多”,而是:这些 AI 产物最后怎么被验证。

这个任务不是从 prompt 开始,而是从一个真实失败链路开始

139 云盘驱动的问题不是新建一个函数,而是修改已有鉴权状态机。

它涉及:

Authorization
MailCookies
Username / Password
SSO Cookie
HTTP Redirect
Token Refresh
邮箱风控

这类任务里,AI 最容易犯的错误不是语法错,而是生成一套“看起来合理、实际上不符合现有协议状态”的代码。

所以我的第一层约束是:尽量让改动落在现有结构里,而不是让模型重新设计一套驱动。

最后 #2067 实际只改了四个文件:

drivers/139/driver.go
drivers/139/meta.go
drivers/139/util.go
drivers/139/util_test.go

这个 diff 边界本身就是一种约束。

AI 可以帮我快速提出方案,但我需要先确定“状态”

原始逻辑在 driver.go 里对凭证的判断比较隐式。

改造后,初始化入口收敛成:

if !d.isShare() {
    if err := d.validateAndInitCredentials(); err != nil {
        return err
    }
}

并在 util.go 里明确区分:

const (
    credentialStateAuthorization credentialState = iota
    credentialStateFullLogin
    credentialStateCookiesOnly
)

这类改造 AI 很适合协助做,因为它需要把散落的 if/else 归并成一套明确状态。

但真正需要人确认的是:

哪些状态真的被上游服务支持?
优先级应该是什么?
某个 Cookie 有值是否真的代表可复用?

例如 MailCookies fast login 并不是“Cookie 非空就走”,而是要检查:

Os_SSo_Sid
RMKEY

两个关键 Cookie 是否同时存在。

这个判断最终必须来自实际协议行为和测试,不是语言模型对 Cookie 名字的猜测。

我要求 AI 生成测试,而不是只生成实现

#2067 新增 / 修改了 util_test.go,覆盖的不是“函数能调用”这种弱测试,而是几个关键规则:

  • credential state 选择;
  • Cookie sanitize;
  • Cookie merge;
  • redirect status 处理。

实际跑的命令是:

go test ./drivers/139

这一步对 AI 辅助开发特别重要。

因为如果只让 AI 改 implementation,很容易形成一个自洽故事:它先假设某个行为正确,再按照这个假设写代码。

测试至少逼迫改动变成可以被机械检查的条件。

单元测试通过还不够:我又跑了真实账号 smoke test

这个驱动最关键的问题是第三方真实认证链路,所以本地 test 不能证明全部事情。

我用临时环境变量提供真实凭证,没把真实 Cookie、token、用户名密码放进代码或 PR 描述。

当时有一个非常具体的输入:

现有 MailCookies 缺少 RMKEY

预期行为应该是:

不能走 fast login
→ 自动 fallback 到密码登录

实际验证结果是:

fast path 被跳过
→ Step 1
→ Step 2
→ Step 3
→ 生成 Authorization

这个测试把一个“AI 觉得逻辑合理”的 patch,变成了一个有真实服务端响应支撑的结果。

我还专门检查了敏感日志

鉴权代码非常容易在 debug 时顺手输出:

Cookie
Authorization
token
password hash
服务端解密后的响应

AI 在排错时也很喜欢建议“多打印一点”。

但在真正准备 PR 时,我做的方向恰好相反:把敏感登录值从日志中移掉。

例如 fallback 后只记录:

log.Debugf(
  "139yun: password fallback generated authorization: %t",
  newAuth != "",
)

只留下“有没有生成”的布尔状态,不留下真正凭证。

这也是我认为“AI 能写”和“代码能合并”之间很现实的一道边界。

PR 描述本身也是验证清单

#2067 的 Testing 部分不是一句:

Tested locally.

而是写清:

  • 跑了哪个 test command;
  • 哪个真实输入缺少 RMKEY;
  • fast path 为什么被跳过;
  • 密码链路实际跑到 Step 1 → 2 → 3;
  • 敏感信息没有被提交。

这其实也是我现在越来越喜欢的 AI 使用方式:

让 AI 帮我把“我声称做对了”变成一份可检查的 checklist。

后续 PR 更能说明为什么不能把 AI patch 当最终答案

后来我继续做了 PR #3029:

https://github.com/OpenListTeam/OpenList/pull/3029

它进一步尝试解决:第一次用户名密码登录不应该要求预先抓 MailCookies、短信风控如何续上、刷新后的 Cookie 如何复用等问题。

这版又新增了非常具体的约束:

SetRetryCount(0)

用于登录和短信 POST,避免 Resty 自动重试非幂等请求。

同时验证了:

stale MailCookies
→ fast login 失败
→ password fallback
→ 刷新 MailCookies
→ 后续 fast login 可复用

但截至我整理这篇文章时,#3029 仍然是 open,尚未合并。

所以即使代码已经跑过真实账号回归,我也不能把它描述成“OpenList 当前正式实现”。

这就是另一个很重要的置信度边界:

本地验证成功 ≠ upstream 已接受

我现在会怎么定义“AI 参与了一个真实工程任务”

至少需要四层证据:

1. Diff
   AI 最后到底改了哪些文件、哪些函数

2. Automated test
   哪条命令能重复跑

3. Real behavior
   真实输入是否跑过真实外部系统

4. Review / upstream state
   已合并、仍 open,还是只是我自己的 branch

如果只有第一层,那更像代码生成。

如果四层都能说清,我才愿意把它写成“AI 辅助工程实践”。

AI 在这里最有价值的确实是缩短探索和修改循环;但置信度不是模型给的,而是 diff、测试、真实运行结果和 upstream 状态共同给的。