WorkBuddy 部署器:磁盘 token 被客户端加密之后,脚本怎么自救
WorkBuddy 桌面端 2026-09-23 起把磁盘上的 AT/RT 换成了 AEAD envelope,磁盘上再也读不到明文;当时那份部署脚本把 dict 静默 str() 成字符串写进 Secret,CI 连挂 4 天。本文给出现在两条取凭据的路径、7 轮实测挖到的真问题,以及给上游脚本打 4 处幂等补丁的做法。
一句话
WorkBuddy 桌面端 2026-09-23 凌晨起,把磁盘上 workbuddy-desktop.info 里的 accessToken / refreshToken / phoneNumber 换成了 AEAD envelope({"$wbEncrypted": 1, "envelope": "eyJ..."})。磁盘上再也读不到明文 token。而我那份当时看起来很正常的部署脚本,把 dict 静默 str() 成字符串写进了 GitHub Secret —— 本地一切正常,CI 连着挂 4 天,日志里只有一句 TOKEN_EXPIRED。
现在两条路:加号 / 换号走官方短信登录(不碰客户端、不受加密影响);沿用本机当前账号则拿 60 秒预算去进程内存里认领。
python deploy.py --add-account # 回车后输手机号 + 短信验证码只用标准库,0 第三方依赖;凭据只经 stdin 写进 GitHub Secret,不落盘、不回显。
问题长什么样
deploy.py --dry-run 打出来的是这一行:
✓ 账号 {'$****'} AT 2919 字符 / RT 2923 字符 · AT 有效期未知2919 字符的 AT?正常应该 1300 出头。{'$****'} 是 mask_phone() 在 phone 是 dict 时打印出来的 —— 看到这行就知道:磁盘上那个字段已经不是字符串了。
CI 侧只有一句 TOKEN_EXPIRED,连挂 4 次。因为本地跑 deploy.py 根本不联网发请求,只生成 Secret 字符串就完事了 —— 本地永远看不出问题。
根因:str(dict) 长得太像 token
把 envelope 里的字段解出来是这样:
{ "suite": 1,
"keyId": "9127dea1b44020a7",
"nonce": "CQkzLIQVI5IRgX9m",
"authTag": "SjBa7wvkzJyX8j7wm8R7pg==",
"ciphertext": "p2F0p7H2bmIvUtdey0xAh4Y..." }suite: 1 加 16 字节 authTag → AES-256-GCM 类 AEAD 套件。keyId 只是个标识,真实密钥不落盘,得从客户端进程内存(或操作系统钥匙串)里拿 —— 离线脚本拿不到。
老代码是这样读的:
at = str(auth.get("accessToken") or "").strip()
rt = str(auth.get("refreshToken") or "").strip()
if rt:
return phone, at, rt, pathaccessToken 现在是 dict → str(dict) 得到一个 2919 字符的字符串,if rt 照样判真(非空 dict)。整条逻辑走完、一声不响,还把这一坨塞进了 Secret。
如果客户端是删掉字段而不是加密,str(...)会得到'{}'、if rt判假、直接进告警分支 —— 我至少知道坏了。加密真正麻烦的地方不是「读不到」,而是读不到还长得像读到了。
时间线:客户端什么时候改的
客户端每次写 workbuddy-desktop.info 之前会先按时间戳备份一份。%LOCALAPPDATA%\CodeBuddyExtension\Data\Public\auth\ 里躺着 9 份,从 8-21 一直到 9-25:
| 时间 | 状态 |
|---|---|
| 2026-08-21 08:15 | 明文 AT(1301 字符) |
| 2026-09-22 10:46 ~ 12:41 | 明文(6 份) |
| 2026-09-23 04:27 | 加密 envelope |
| 2026-09-23 04:32 ~ 现在 | 全部加密 |
时间点和 9-23 凌晨那次客户端后台升级完全对得上。
现在怎么加号 / 换号
只有一条路:手机号 + 短信验证码登录。走官方接口,不读本机登录态、不受加密影响、不需要客户端开着。
python deploy.py --add-account # 加一个号;还有号就再跑一次,已有的号不受影响
| 手机号怎么写 | 说明 |
|---|---|
13800000000 | 国内号,直接 11 位 |
+8613800000000 | 带国家码也行 |
+85250001234 | 境外号带国家码 —— 官方接口同样支持 |
138 0000 0000 | 空格 / 连字符 / 全角数字会自动归一 |
号码输错会当场让你重输(3 次机会),不会一路掉到看不懂的兜底文案上。
有意留下的缺口:收不到短信的账号(纯邮箱 / 微信登录)加不进来,只能用 --token "手机号:AT:RT" 手动给。拿覆盖面换确定性。改了什么(7 轮实测挖到的真问题)
1. 加密态静默通过(本次起因)load_credential 里加 _check_envelope_or_die():见到 $wbEncrypted=1 直接 die,并把三件事说清楚 —— 这是客户端 09-23 之后的行为、磁盘上已经没有明文、以及三条绕法。报错要指向「密钥不落盘所以本机读不出来」,别只说「字段无效」。
2. --token 少打一个冒号会静默吞掉 AT
原来 2 段输入会兜底成 phone::rt,CI 必 401。现在直接 die、不再静默兜底;顺手把 README 承诺过的「多行 / @ 分隔多账号」实现了。
3. 令牌库主键会丢数据
原来用 phone or "account" 当主键 —— 两个没有手机号的账号(邮箱 / 微信登录)会共用同一个键,后者把前者顶掉。现在退到 AT 里的 preferred_username / email / sub。
4. 短信登录成功之后立刻崩
报 ValueError: not enough values to unpack (expected 5, got 4)。凭据其实已经拿到了,只是返回值元组少一个字段、写不进仓库,白输一次验证码。现在统一契约,中间加一层形状收口(4 个补齐、5 个放行、其它形状立刻报明确错误)。
5. 境外号被本地正则挡掉
有读者用带 + 国家码的号加号时报「格式不对」,然后跳到一句跟手机号毫无关系的「自动恢复链全失败」。查下来是本地位数正则太窄 —— 官方接口本来就收。现在只挡明显不可能的输入(含字母、位数离谱),其余一律交给服务端判。
6. 加号通道收敛成一条
以前还有第二条路:先在客户端切到那个号、让脚本去内存里认领。它有时 0.1 秒就成、有时一两分钟不动,还认错过号 —— 已去掉。内存扫描保留,但只服务于「沿用本机当前登录的那个账号」。
7. 仓库永远收不到上游更新sync_with_origin() 里的 git reset --hard origin/main 本意是保住仓库里的令牌库,却把刚克隆下来的上游代码整个丢弃。实测仓库脚本比上游少 341 行 —— 上游 09-23/24/25 新增的 7 个小程序任务、Buddy 前置判据全都没同步进来。现在在 reset 之后自动把上游最新代码取回来(只取脚本,令牌库不动),再重新打补丁。
外加几个小的:parse_schedule 不去重导致 yml 报 Duplicated value;--gh-proxy 无 URL 校验(javascript: 能拼进环境变量);show_run_summary 只认 emoji 关键词、run 失败时一条日志都打印不出来。测试从 36 涨到 117 个。
上游脚本的 4 处补丁
部署器克隆上游后会自动打补丁,不改上游仓库、全部幂等(重复跑会报「已是目标形态」且文件哈希不变):
| 补丁 | 改什么 |
|---|---|
| 报告输出 | 把 send_notify_all 规范成「没有推送渠道时把报告打到 stdout」 |
| 接口容错(点) | 4 处关键 .json() 包进 _safe_json(),解析失败降级为空 |
| 接口容错(面) | 全局把 Response.json() 换成「非 JSON 则降级」,失败时打一行带状态码的告警 |
| 单账号故障隔离 | main() 里的 run_account() 包进 try/except,坏账号跳过并在报告里标红 |
一个值得记的坑:容错写在异常的下游,等于没写
上游本来写了 .get("data", {}) 想兜底 —— 但异常在 .json() 那一步就抛出来了,默认值根本没机会生效。
更麻烦的是:只在「已知崩点」上打补丁治不住。上次补完 4 处,第二天崩在另一个 .json() 上。实测上游一共 90 处 .json(),其中 51 处不在 try 里 —— 所以这次改成全局兜底。
为什么敢全局改:先用 AST 把全部 39 处「位于try体内」的调用扫了一遍,确认它们的except分支只做「记日志 / 跳过」、没有一处靠这个异常去降级到另一个请求 —— 所以把「抛异常」换成「返回空」不改变任何控制流。
踩到的第二个坑:第一版守卫把所有非 JSON 都吞成空数据,结果那个已经废掉的号在报告里显示「✅ 全部完成!」 —— 用户完全看不出账号已经不能用。现在 401/403 一律上抛(那是凭据级故障,不是「这一步拿不到数据」),交给逐账号隔离在报告里如实标红:
👤 账号4 19100000000
❌ 该账号处理失败(已跳过,不影响其它账号):
RuntimeError: 凭据被服务端拒绝(HTTP 401 /v2/activity/growth/tasks)
—— 该账号需要重新登录,或从令牌库移除验证矩阵
| 阶段 | 用例数 |
|---|---|
| 初版 | 36 |
| 加密态检测 | 44 |
| 进程内存认领当前账号 | 73 |
| 令牌库主键修复 | 85 |
| 加号收敛成单通道 | 92 |
| 返回值契约 + 上游补丁 | 111 |
| 全局守卫 + 单账号隔离 | 117 |
python -m unittest test_deploy → 117/117,全程只用标准库。
上游补丁单独对真实上游文件验过一次:连打 5 遍,第 1 遍生效、之后每遍都报「已是目标形态」且 sha256 不变,产物 ast.parse 通过。
加号路径是端到端真跑的:短信登录拿凭据 → 写入仓库 → 触发 Actions → 两次实跑均 success,报告里五个账号四个正常、一个如实标红。
分享前的三类审计
给别人之前固定做三件事:
1. 凭据扫描 —— eyJ JWT / ghp_ PAT / 手机号 / 真实邮箱 / 本机路径。踩过:只查「连写形式」,漏了 +852 5000 1234 这种带分隔符的变体 —— 现在按任意分隔符扫。
2. 依赖可达性 —— 只 import 标准库,0 第三方包,对方拿到就能跑,不用 pip install 任何东西。
3. 上游许可 —— 上游是 MIT;部署器只做「部署封装 + 幂等补丁」,所有补丁都带 hardened-by-deployer 注释,便于识别与日后 merge。
还有两条实战经验:wb_refresh_tokens.json 会被 Actions 用 git add -f 提交进仓库历史 —— 把整个目录拷给别人,等于把明文 RT 一起给出去(脚本有清场机制,会先 reset 到干净上游历史);GitHub 提交里的 author / committer 邮箱永远公开,跟个人资料里是否隐藏邮箱无关,所以 push 用的是 noreply@users.noreply.github.com。
下载
永久链接,不限次数:https://pan.ailxw.com/pickup/37209
整包 100425 字节,SHA256 5f8cb5affe48e0096cb1a2d54cd70f78b6ede82462a19a0264a295e28d6b3a36;里面 deploy.py 157597 字节,SHA256 983d39be031836942a8f4bcc61d17b705b8cc1122d61aa6fac4892f74d176f08 —— 下完可以自己核一下。
里面含 deploy.py、README.md、test_deploy.py、deploy.sh、一键部署.bat / .ps1、login-tool/workbuddy_login.py。
已经在用旧版的,重跑一次部署器即可带上全部修复与上游最新代码(令牌库不会被动)。
本文主题:WorkBuddy 部署器:磁盘 token 被客户端加密之后,脚本怎么自救
技术摘要:WorkBuddy 桌面端 2026-09-23 起把磁盘上的 AT/RT 换成了 AEAD envelope,磁盘上再也读不到明文;当时那份部署脚本把 dict 静默 str() 成字符串写进 Secret,CI 连挂 4 天。本文给出现在两条取凭据的路径、7 轮实测挖到的真问题,以及给上游脚本打 4 处幂等补丁的做法。
引用出处:https://isoziyuan.com/p/100169/(作者:Isoziyuan · 发布于爱搜资源网)
登录后参与讨论
注册或登录账户,即可查看并发表文章评论。