从「成功日志」里识别假成功:SUCCESS 不等于服务端落库
一次真实故障复盘:无头浏览器登录态迁移。文中所有数字都来自 2026-11-14 凌晨在本机(macOS + Chrome 157 + Node 22.22.2)真实跑出的结果,脚本与判据均可复现。文中不出现任何真实口令、密钥或 cookie 值,只讨论机制与统计量。
0. 结论先行
自动化脚本打印 SUCCESS,只说明这段代码认为自己走完了,不说明目标系统把结果写进去了。把日志当成事实,是这轮故障里代价最大的一个错误:一度把「票据没注入」误判成「票据已过期」,据此整轮停手,当天积分 0 分。
可用的判据只有一条链:动作 → 服务端状态 → 积分明细。中间任何一跳缺失,都要把结论降级为「未确认」,而不是「失败」或「成功」。
1. 事故现场:六个"看起来没问题"的信号
| 时刻(北京时间) | 脚本/页面给出的信号 | 实际状态 | 误判类型 |
|---|---|---|---|
| 01:43 | LOGIN_GATE FAIL → 推断「CAS 票据 24h 过期」 |
票据完好地躺在另一个 Chrome profile 里,只是没被注入 | 归因错误:把「缺数据」当成「数据失效」 |
| 01:47 | 注入 48 条导出 cookie,INJECT_OK 48 / FAIL 0 |
/grow 仍判未登录:导出文件只含 developer.huaweicloud.cn 与 .huaweicloud.cn 两域,缺 auth.huaweicloud.cn |
局部成功 ≠ 全局成功 |
| 01:48 | 解密 168 条华为域 cookie,DECRYPTED 0 / BAD 168 |
不是数据损坏,是 PBKDF2 迭代次数用错 | 参数错误伪装成数据损坏 |
| 01:49 | 换成正确迭代次数,163/168 解密成功并注入 |
LOGIN_GATE OK nick=true numericPoints=true loginWall=false |
正确 |
| 02:0x | Star 脚本 STAR_SUCCESS,计数 267→268 |
服务端 today_status=todo, evaluator=external_check |
动作成功 ≠ 判定完成(外部轮询延迟) |
| 02:0x | DevSpace ECHO_DOM hit=false |
OCR 从 xterm canvas 截图中读到命令回显 | DOM 读不到 ≠ 没执行 |
六个信号里有五个如果不加核验就会导向错误动作:重跑灌水、整轮跳过、反复改脚本。
2. 机制:登录态由三层状态决定,日志不属于任何一层
定义三个可观测集合:
- L1 本地票据:磁盘 cookie 库里的记录,和运行时
context.cookies()的集合。记为 、。 - L2 页面会话:访问
/grow后服务端渲染出的昵称与积分数字。它是 经过服务端校验后的投影,不是 本身。 - L3 业务判定:任务卡的
is_finish/today_status,以及积分明细里的入账行。这是唯一与收益直接相关的状态。
设脚本输出 SUCCESS 为事件 ,服务端真正落库为事件 。我们要控制的量是假成功率
工程上无法把 压到 0,因为 是本地事件、 是远端事件。能做的是引入一个独立于写入路径的观测 (列表页、状态 API、积分明细),并要求 与 一致后才结算。本轮六个信号里,INJECT_OK 属于 ,nick=true 属于 ,积分明细 +10 属于 ;只用 就会翻车。
2.1 Chrome v10 cookie 的加密定义
macOS 上 Chrome 把 cookie 值以 v10 前缀存进 SQLite,解密链路是:
其中 来自系统钥匙串条目 Chrome Safe Storage, 是迭代次数,macOS 为 1003,Linux/Windows 为 1。存储字节为 "v10" || C。
这里有个很好用的自检性质:用错误的 解密时,PKCS#7 填充校验通过的概率约为 (最后一字节必须是 0x01,或 0x02 02、……近似 )。168 条样本的期望"偶然通过"数约 条,实测 0 条。所以「全量报 bad decrypt」几乎必然是密钥/参数错,而不是 168 条数据同时坏了——这条先验能直接把排查方向从「数据」扳回「参数」。
2.2 Session cookie 的语义陷阱
expires_utc = 0 表示会话级 cookie,Chrome 启动时清除。经 CDP 注入时,正确做法是不传 expires 字段;某些封装会把 expires=-1 当成 1970 年前的绝对时间直接丢弃,表现为「注入返回成功、运行时却查不到」,和本次 INJECT_OK 48 但缺域是同一类假成功。另外 domain 带前导点(如 .huaweicloud.cn)表示包含子域,导出/注入时必须原样保留,否则同名的 host-only cookie 与域 cookie 会互相覆盖。
3. 可运行代码(脱敏版)
下面这段就是本机跑通的解密核心,密钥从钥匙串读取、不落盘、不打印:
const crypto = require('crypto');
const { readFileSync } = require('fs');
// macOS: 1003 次;Linux/Windows: 1 次。两个都试,靠 PKCS#7 自动筛掉错的
function candidateKeys(keyFile) {
const pass = readFileSync(keyFile, 'utf8').trim();
return [1003, 1].map(c => crypto.pbkdf2Sync(pass, 'saltysalt', c, 16, 'sha1'));
}
function decrypt(encryptedValue, keys) {
const pre = encryptedValue.subarray(0, 3).toString('latin1');
const body = /^v\d{2}$/.test(pre) ? encryptedValue.subarray(3) : encryptedValue;
const iv = Buffer.alloc(16, 0x20);
for (const key of keys) {
try {
const d = crypto.createDecipheriv('aes-128-cbc', key, iv);
let out = Buffer.concat([d.update(body), d.final()]); // 错 key 会抛 bad decrypt
const head = out.subarray(0, 32);
let bad = 0;
for (const b of head) if (b < 0x20 || b > 0x7e) bad++;
if (bad > 3 && out.length > 32) out = out.subarray(32); // 部分版本前置 domain hash
const s = out.toString('utf8');
if (s && !s.includes('\uFFFD')) return s;
} catch (_) { /* 填充不合法 = 这个 key 不对 */ }
}
return null;
}
注入侧的关键两行——只给真正有到期时间的项传 expires:
const o = { name, value, domain, path: path || '/', secure: !!secure, httpOnly: !!httpOnly };
if (Number.isFinite(exp) && exp > 0 && exp > Date.now() / 1000) o.expires = Math.floor(exp);
// exp <= 0(session)时什么都不传,千万不要写 expires: -1
await ctx.addCookies(rows);
三跳核验(写完后必做,不依赖脚本的自我报告):
// 跳 1:运行时确实有票据
const live = (await ctx.cookies()).filter(c => /SSOTGC|SSOJTC/.test(c.name));
// 跳 2:页面会话确实认这个票据(昵称 + 积分数字 + 无登录墙)
const t = await growPage.evaluate(() => document.body.innerText);
const pageOk = /昵称|账号名/.test(t) && /我的积分\s*\d+/.test(t) && !/登录\/注册查看您的积分/.test(t);
// 跳 3:积分明细里出现今天的行;这一步才是收益的判据
4. 本机实测数据
实验 A:PBKDF2 迭代次数对解密成功率的影响(样本:同一份只读备份里 168 条 %huaweicloud% 记录,两轮除了 之外完全相同)
| 迭代次数 | 样本数 | 解密成功 | 失败 | 失败表现 |
|---|---|---|---|---|
| 1 | 168 | 0 | 168 | bad decrypt(PKCS#7 填充校验) |
| 1003 | 168 | 163 | 5 | 空值 / 非 UTF-8 明文,主动丢弃 |
163 条里会话级(无到期时间)135 条、持久 28 条;过期项 0 条。这个 0 很关键:没有任何一条是因为"过期"被跳过的,与「票据 24 小时过期」的初始推断直接矛盾。
实验 B:注入域覆盖与登录判定的关系
| 阶段 | 注入条数 | 覆盖域 | SSOTGC/SSOJTC 运行时条数 |
判闸结果 |
|---|---|---|---|---|
| 仅用户导出的浏览器 cookie 文件 | 48 | developer.huaweicloud.cn、.huaweicloud.cn |
0 / 0 | FAIL |
| 追加 auth 域解密票据 | 163 | 含 auth.huaweicloud.cn、.auth.huaweicloud.cn |
1 / 1 | OK |
运行时 cookie 总数变化:273 → 284 → 306。注意第一阶段 INJECT_OK 48 是真实的,48 条确实都在 context.cookies() 里,但缺的域恰恰是判定所依赖的那个。
实验 C:三跳状态并不总是同步
| 动作 | 跳 1 本地 | 跳 2 服务端状态 | 跳 3 积分明细 |
|---|---|---|---|
| 签到 | 脚本无回显 | — | +10 @ 01:46 已入账 |
| 浏览鸿蒙案例 | 停留 35s | status=done |
待复检 |
| Star 鸿蒙案例仓 | 计数 267→268 | todo / external_check |
待复检(已知 30~40 分钟轮询延迟) |
| AI DevSpace | OCR 读到命令回显 | is_finish 待复检 |
待复检 |
5. 参数对照表
| 参数 | 取值 | 后果 | 建议 |
|---|---|---|---|
| PBKDF2 迭代 | 1003 / 1 | macOS 用 1 → 0/168 成功 | 按平台试两个候选,靠填充校验自动判定 |
expires 字段 |
不传 / -1 / 未来秒 |
-1 会被静默丢弃 |
仅 exp>0 且未过期 时传 |
domain 前导点 |
.huaweicloud.cn vs huaweicloud.com |
丢失子域覆盖 | 原样保留导出值 |
| 导出范围 | 单站 48 条 / 全域 163 条 | 单站缺 auth 域 → 判闸 FAIL | 按判定依赖的域导出,不按"当前站点" |
| 判定观测 | 脚本日志 / 页面 / 明细 | 日志单独使用 → 假成功 | 三跳齐备才结算 |
external_check 延迟 |
30~40 分钟 | 立刻重跑会重复动作 | 只等,不改脚本 |
6. 边界条件
- 票据有效期 ≠ cookie 到期时间。会话 cookie 的
expires=0只说明"浏览器关掉就清",服务端 session 是否还认要由页面/接口回答,不能从 SQLite 推断。 - 两个 Chrome profile 共用系统钥匙串密钥,所以本地解密可行;换机器、换用户、换钥匙串条目后不成立,不要把这个方案当通用凭据同步手段。
- 只读备份优先:用
sqlite3 .backup拿一致性快照再读,别在运行中的库上直接查,-wal未落盘时会读到不完整状态。 - 注入是覆盖语义:同名同域同 path 会被覆盖。恢复登录时这是想要的;做对照实验时要先记基线。
SUCCESS之外还要看分母:INJECT_OK 48缺了「应该有多少条」这个分母,所以看不出少了整个 auth 域。- 外部轮询型判定(
external_check)天然滞后,动作已完成而状态仍todo是正常态,不构成立即重试的理由。
7. 排查清单(下次遇到"登录失效/0 分"照这个走)
- [ ] 先把结论降级为「未确认」,不要直接写「票据过期」「方法无效」。
- [ ] 分别列出 、,确认关键票据在运行时里,而不是只在文件里。
- [ ] 核对域覆盖:把导出文件的 domain 去重打印,和判定依赖的域做差集。
- [ ] 解密类操作先看加密前缀(
v10/v20),再按平台试参数;全量失败优先怀疑参数,不是数据。 - [ ] session cookie 注入不传
expires;注入后用context.cookies()反查条数。 - [ ] 页面判闸用「昵称 + 积分数字 + 无登录墙」三条件,别用"有没有密码框"这种单条判据。
- [ ] 动作完成后查服务端
is_finish/today_status,最后查积分明细;三者不同步时以明细为准。 - [ ] 状态滞后但服务端已
done时只等;服务端长期todo且历史明细从无该条目时,才补正确入口动作。 - [ ] 任何"重跑"前,先到列表页确认写入是否已落地,避免重复灌水。
8. 收尾
这次从「今天 0 分、票据过期」到 LOGIN_GATE OK,中间没有重新登录、没有扫码、没有改账号,只是把已经存在但没被放进正确位置的票据补齐,并且不再相信脚本自己打印的 SUCCESS。
对自动化系统来说,最贵的不是失败,是把失败归因错之后的反复重做。日志只能证明代码走到哪一行;要证明事情办成了,永远需要一个独立于写入路径的观测点,以及一个愿意等状态同步的窗口。
- 点赞
- 收藏
- 关注作者
评论(0)