文章目录
-
- 前言
- 一、故事的起因:一个按钮引发的血案
-
- [1.1 本机签到为什么靠不住](#1.1 本机签到为什么靠不住)
- [1.2 搬到云端的代价](#1.2 搬到云端的代价)
- 二、凭据这东西,比前任还难处理
-
- [2.1 为什么不能把 token 写进代码仓库](#2.1 为什么不能把 token 写进代码仓库)
- [2.2 为什么必须在本机取一次](#2.2 为什么必须在本机取一次)
- [2.3 为什么要合并成一个仓库](#2.3 为什么要合并成一个仓库)
- 三、先立规矩,代码才不会写歪
-
- [3.1 命名约定是强制的](#3.1 命名约定是强制的)
- 四、实操:五步走完
-
- [4.1 第一步,准备密钥仓库](#4.1 第一步,准备密钥仓库)
- [4.2 第二步,一键生成密钥文件](#4.2 第二步,一键生成密钥文件)
- [4.3 第三步,粘贴进密钥仓库](#4.3 第三步,粘贴进密钥仓库)
- [4.4 第四步,跑一次验证](#4.4 第四步,跑一次验证)
- [4.5 第五步,之后就自动跑了](#4.5 第五步,之后就自动跑了)
- [五、WorkBuddy 的隐藏任务:签完到,猫还要去旅游](#五、WorkBuddy 的隐藏任务:签完到,猫还要去旅游)
-
- [5.1 签到只是这条线的一半](#5.1 签到只是这条线的一半)
- [5.2 旅行为什么单独一个文件](#5.2 旅行为什么单独一个文件)
- [5.3 通知怎么配](#5.3 通知怎么配)
- [5.4 启动时打印通知配置(脱敏)](#5.4 启动时打印通知配置(脱敏))
- 六、MiniMax:唯一不用装客户端的
-
- [6.1 token 在哪拿](#6.1 token 在哪拿)
- [6.2 签到口径](#6.2 签到口径)
- [七、skip 和 error 的边界:绿了不等于成功了](#七、skip 和 error 的边界:绿了不等于成功了)
-
- [7.1 为什么口径要定死](#7.1 为什么口径要定死)
- [7.2 注意,skip 不等于签到成功](#7.2 注意,skip 不等于签到成功)
- 八、加一个新工具:四步,不碰流水线配置
- 九、本地自测:不联网也能查装配
- 十、目录结构
- 十一、常见问题速查
P.S. 无意间发现了一个巨牛的人工智能教程,非常通俗易懂,对AI感兴趣的朋友强烈推荐去看看, 传送门https://blog.csdn.net/qq_34419312
前言
作为一个程序员,我最近给自己找了个班上:每天早上打开客户端,点一下签到按钮。就一下。
但你别小看这一下。它承载着我的自律,承载着我的发量,还承载着我对"今天终于没断签"的卑微喜悦。
别人早起叫自律,我早起叫签到。自律能上瘾,签到只会让人想骂人。坚持了两周之后,我做了一个所有程序员面对重复劳动时的经典选择:写脚本,自动化,然后把它丢到云端去。
折腾完发现这套方案还挺完整,中间有几个坑,跳过的人都懂。今天从头捋一遍,代码和命令都给全,可以直接抄。
一、故事的起因:一个按钮引发的血案
1.1 本机签到为什么靠不住
签到这件事,本质上是"每天某个时刻,你得在线"。
本机方案有一个绕不过去的坑:机器得开着。
笔记本合盖、下班关机、客户端没启动,签到就断了。合盖的那一刻,签到任务的命运就已经注定,像极了爱情,你一转身,它就把你忘了。
断了还得手动补,补的时候又常常已经错过活动重置时间。你以为你在补签,其实你在给昨天补窟窿,补完发现今天又漏了。
程序员为了这几个签到,把笔记本当服务器跑,把工位当机房用。就为了每天零点那两下点击,我的笔记本活得像个无人工厂。
1.2 搬到云端的代价
解法就摆在面前:把签到挪到云端定时跑。机器不用开,客户端不用装,躺着也能把积分领了。
代价是什么?凭据得从本机搬到云端。
于是问题来了:凭据怎么搬,才不至于搬出一个安全事故来?
二、凭据这东西,比前任还难处理
2.1 为什么不能把 token 写进代码仓库
凭据不能写进代码仓库,这是底线。
把 token 写进仓库,等于把家门钥匙贴在门外面,还附了一张说明书:钥匙在此,欢迎光临。
所以方案里专门拆了一个"密钥仓库":独立、私有。.cnb.yml 里没有任何 env 硬编码的 token,凭据只能经由 CNB 的 imports 从那个私有仓库注入。
这么干有三个好处:
签到仓库可以放心开源、随手分享,里面一个密钥都没有;换 token 只动密钥仓库一个文件,不用改代码、不用重跑流水线;泄漏面收敛,就算代码仓库被人看光了,也拿不到任何可用凭据。
2.2 为什么必须在本机取一次
更扎心的事实是:几个平台的登录态,云端根本没法解密。因为它们在本机就不是明文躺着的。
| 平台 | 本机登录态形态 | 为什么云端解不了 |
|---|---|---|
| WorkBuddy | AES-256-GCM 信封 | 解密密钥只驻留客户端进程内存,不落盘 |
| TRAE Work | AES-256-CBC 加密块 | 密钥由客户端代码派生,且设备ID只出现在客户端日志里 |
| Qoder CN | DPAPI 保护的 AES 密钥 | DPAPI 与本机 Windows 账户强绑定,不可导出 |
| MiniMax Code | 浏览器 localStorage 的 _token |
就是个 JWT,浏览器里复制一下就行,唯一不需要客户端的 |
看到没?AES-256-GCM 信封、AES-256-CBC 加密块、DPAPI 保护的 AES 密钥,一个比一个能打。
解密密钥只驻留在客户端进程内存里,不落盘。翻译成人话:这加密比你的前任还难搞,人都走了,东西就是不给。
DPAPI 跟本机 Windows 账户强绑定,不可导出。微软说不可导出,那就是真不可导出。Windows 的安全机制有时候像过年时的亲戚,看起来什么都懂,实际上谁也劝不动。
唯一例外的,居然是 MiniMax Code,浏览器 localStorage 里躺着一个现成的 JWT,抠出来就能用。这是程序员一生中唯一一次理直气壮打开 F12 还不用心虚的瞬间。
所以整个架构只能是三段式:本机取一次,密钥仓库中转,云端只读。
# 三段式:本机取 → 密钥仓库存 → 云端跑
本机(Windows,一次性) CNB 密钥仓库(私有) CNB 云端(每天定时)
auth/<平台>_auth.py → <平台>_secrets.yml → runner.py + providers/
解出明文凭据 只读、只被 imports 引用 不碰加密,直接用明文
2.3 为什么要合并成一个仓库
最初每个工具各占一个仓库,同一套"取凭据 → 存密钥 → 定时签到"被抄了三遍。三份代码慢慢长得不一样,命名也各玩各的。
这就是传说中的复制粘贴一时爽,维护起来火葬场。
合并成一个仓库之后:一套调度,几点跑、跑哪些、跑挂了怎么办,只有一个地方要改;一套命名,脚本、产物、变量名全部同构;加工具只加文件,不碰 .cnb.yml。
工具自己的附加动作也收在 provider 里。runner 里不再出现平台判断,它的职责就三件事:逐个跑、逐个兜错、统一收口。
三、先立规矩,代码才不会写歪
3.1 命名约定是强制的
这套约定是强制的,tests/selftest.py 会检查,写歪了自测直接红。
像极了驾考:你觉得自己开得挺好,考官直接给你踩刹车。
| 项 | 约定 | 示例 |
|---|---|---|
| 取凭据脚本 | auth/<平台>_auth.py |
auth/qoder_auth.py |
| 一键导出(汇总器) | export_auth.py |
tools/ai-checkin/export_auth.py |
| 取凭据产物 | <平台>_secrets.yml |
qoder_secrets.yml |
| 密钥仓库内文件 | .cnb/<平台>-secrets.yml |
.cnb/qoder-secrets.yml |
| 云端实现 | providers/<平台>.py |
providers/qoder.py |
| 环境变量前缀 | <平台大写>_ |
QODER_TOKEN、QODER_MACHINE_OS |
| 工具名 | 全小写平台名 | workbuddy / trae / qoder / minimax / baidu |
一句话:平台名是唯一的命名基准,脚本、文件、变量都从它派生。取凭据脚本一律叫 *_auth.py,作用一律是"拿本机登录态,生成密钥文件",产物一律以平台名打头。
汇总器只有一个:export_auth.py 调各平台的 collect(),把它们合成一份成品。它是"汇总"不是"取凭据",所以放在 tools/ai-checkin/ 而不是 auth/。selftest.py 会盯着这条边界,别把平台无关的脚本混进 auth/。
四、实操:五步走完
4.1 第一步,准备密钥仓库
搞一个私有仓库专门放凭据,把 .cnb.yml 里 imports 的地址换成你的。源码可以直接 Fork 一份,Fork 是个好词,它让"抄作业"听起来像开源协作。Fork 完你就有了自己的签到仓库,下面所有操作都在你自己的仓库里进行。
4.2 第二步,一键生成密钥文件
在仓库根目录执行:
python tools/ai-checkin/export_auth.py
注意:必须带上 tools/ai-checkin/ 这个相对路径。在根目录直接敲 python export_auth.py 会报 can't open file '...\export_auth.py': [Errno 2] No such file or directory。也可以先 cd tools/ai-checkin 再跑,脚本自己会处理路径。
它会逐个平台取本机登录态,然后合成一份 ai-checkin-secrets.yml。文件与密钥仓库里那份同名同构,全选复制、整份覆盖粘贴即可。
终于不用手工拼 YAML 了,省下来的脑细胞可以留着写 Bug。
yaml
# ── AI 工具签到 · CNB 密钥仓库文件(一键生成,整份复制即可)────────
# 已包含工具:workbuddy、trae
# 未包含工具(补上凭据后重新跑一次 export_auth.py 即可):
# - qoder:未找到 Qoder CN 登录态文件,请先在客户端登录。
allow_slugs:
- "<你的组织>/<签到仓库>"
allow_events:
- "crontab: 2 0 * * *"
- "crontab: 2 10 * * *"
- web_trigger
# ── workbuddy 凭据 ─────────────────────────────────────
WORKBUDDY_TOKEN: "<明文凭据>"
WORKBUDDY_DOMAIN: "www.codebuddy.cn"
# ── trae 凭据 ──────────────────────────────────────────
TRAE_CLOUDIDE_TOKEN: "<明文凭据>"
TRAE_DEVICE_ID: "<纯数字设备ID>"
# ── minimax 凭据 ───────────────────────────────────────
MINIMAX_TOKEN: "<浏览器 localStorage 的 _token>"
# ── baidu 凭据 ─────────────────────────────────────────
BAIDU_COOKIE: "<客户端登录态 Cookie,含 BDUSS>"
授权范围 allow_slugs / allow_events 只出现一次,从 .cnb.yml 逐字推导。这正是手工合并最容易写坏的地方,三份产物各带一份,同名 key 互相覆盖。你以为你在合并文件,其实你在玩俄罗斯方块,还玩砸了的那种。
前提:对应客户端已启动并登录。各平台数据来源不同,一台机器上通常只能导出装了客户端的那几个。取不到的平台自动跳过,文件头会注明原因,不当失败,runner 对缺凭据的工具本来就是 skip。
常用选项:
python tools/ai-checkin/export_auth.py --stdout # 打到终端,不落盘
python tools/ai-checkin/export_auth.py --copy # 生成后直接进剪贴板
python tools/ai-checkin/export_auth.py --only qoder # 只导出指定平台
python tools/ai-checkin/export_auth.py --slug <组织>/<仓库> # 覆盖 allow_slugs
三个安全约定:授权范围占位符会在粘贴前提醒;覆盖已有产物时若平台变少会拒绝,加 --force 才放行,免得一次误跑把凭据越写越少;一个都没取到就不落盘。
这个"平台变少就拒绝"的功能,我愿称之为防秃头模式。凭据和头发一样,只会越来越少。
只想单独取某一个平台也可以,生成的是单平台文件:
python tools/ai-checkin/auth/workbuddy_auth.py # → workbuddy_secrets.yml
python tools/ai-checkin/auth/trae_auth.py # → trae_secrets.yml
python tools/ai-checkin/auth/qoder_auth.py --full # → qoder_secrets.yml
MINIMAX_TOKEN="eyJ..." python tools/ai-checkin/auth/minimax_auth.py # → minimax_secrets.yml
python tools/ai-checkin/auth/baidu_auth.py # → baidu_secrets.yml
不想落盘就加 --verify(只校验能不能解出来)或 --stdout。这条路要自己把几份产物拼成一份,注意 allow_slugs / allow_events 只能留一份。
4.3 第三步,粘贴进密钥仓库
把 ai-checkin-secrets.yml 整份复制,覆盖密钥仓库里的 .cnb/ai-checkin-secrets.yml,提交。
.cnb.yml 里只有一行 imports,指向的正是这个路径:
yaml
imports:
- https://cnb.cool/<密钥仓库>/-/blob/main/.cnb/ai-checkin-secrets.yml
文件只有一份,变量按工具分几组。加工具只是多一组变量,不用动 imports。
手写也行,tools/ai-checkin/secrets.example.yml 是完整的变量清单模板。但 allow_events 必须与 .cnb.yml 的事件键逐字一致,含 cron 表达式和空格,只写 crontab 会在 Prepare 阶段报 does not conform to allow_events。用脚本生成就没这个问题。
4.4 第四步,跑一次验证
在"代码构建"页点"执行",或者推一次代码触发 push,然后看日志:
>>> [workbuddy] WorkBuddy(签到 + 派遣 Buddy 旅行)
结果: OK · 签到成功(12.3s)
(detail 里多一段 travel:派猫猫旅行:空闲(可派遣)......)
>>> [trae] TRAE Work(每日签到领积分)
结果: SKIP · 今日已签到(3.1s)
>>> [qoder] Qoder CN(每日签到领 Credits)
结果: SKIP · 当前无可领活动(2.4s)
>>> [minimax] MiniMax Code(每日签到领积分)
结果: OK · 签到成功(0.6s)
>>> [baidu] 百度搭子(客户端签到领积分)
结果: OK · 签到成功(1.1s)
汇总:成功 3 / 跳过 2 / 失败 0
日志末尾会打整段 JSON 摘要,含每个工具的 detail,要看全就往下翻。
WorkBuddy 的 detail.travel 就是派遣结果:available: false 说明旅行接口没通,签到本身没坏;auto_log 里是"已派出 Buddy 前往【XX】"之类的原话。
没配的工具显示 SKIP 是正常的,不算失败。就像班级点名,没到的不一定是旷课,也可能是压根没这门课。想启用,把它的凭据补进密钥仓库就行。
用"代码构建"页手动跑时,密钥仓库的 allow_events 要加上 web_trigger,否则 Prepare 阶段失败。脚本生成的产物会自动带上。
4.5 第五步,之后就自动跑了
| 时间(北京) | 跑什么 | 为什么是这个点 |
|---|---|---|
| 每天 00:02 | workbuddy + trae + minimax + baidu | 按自然日重置,零点后 2 分钟领最稳 |
| 每天 10:02 | qoder | Qoder 活动每日 10:00 重置,早跑必是空的 |
Qoder 的活动每天 10:00 重置,早跑必是空的,所以它单独排了条 10:02 的 cron。早了没用,晚了亏,10:02 刚刚好,像极了掐着点赶末班车。
五、WorkBuddy 的隐藏任务:签完到,猫还要去旅游
5.1 签到只是这条线的一半
签到只是 WorkBuddy 的一半。签完还要派遣 Buddy(喵喵)出去旅行。
程序员连签到都要给猫安排行程,属实是把云养猫玩明白了。
| 动作 | 在哪 | 说明 |
|---|---|---|
| 派遣 Buddy 旅行 | providers/travel.py |
签到之后"先领后派"闭环 |
| 多渠道消息通知 | notifier.py |
runner 跑完每个工具发一条,配置从密钥仓库注入 |
5.2 旅行为什么单独一个文件
因为旅行接口和签到接口不是同一个域名,路径规则也不一样:
签到 https://www.codebuddy.cn/v2/billing/meter/... ← 带 /v2
旅行 https://www.workbuddy.cn/activity/growth/buddy/... ← 不带 /v2
写错任一项直接 404。两个接口靠得那么近却死活不互通,像极了同部门但不说话的同事。
把两者塞进一个 provider 里,改签到路由时很容易连累旅行,所以独立成 travel.py,默认域名写死,只有服务端路由变了才用 WORKBUDDY_TRAVEL_DOMAIN 覆盖。
闭环逻辑:已到达就先领积分,空闲且未达每日上限才派出。三条安全约束:
达每日上限绝不发派遣请求,上限外的请求服务端会白记一次;只读接口失败(500 / 返回结构不对)一律降级,一个写请求都不发;派遣任何异常都就地吞掉,不改变签到结论、不影响退出码。
想只签到不派遣?密钥仓库里加 WORKBUDDY_TRAVEL: "off"。
5.3 通知怎么配
通知配置和凭据一样,只能从密钥仓库来。容器里没有本机配置文件。
yaml
# 值本身是一份 JSON,不是文件路径
AI_CHECKIN_PUSH_CONFIG: '{"channels": {"wecom": {"webhook": "https://..."}}}'
AI_CHECKIN_PUSH_CHANNELS: "wecom" # 选填,缺省发配置里全部已就绪渠道
AI_CHECKIN_PUSH_ON: "error" # 选填:error(默认,只报失败)/ ok / always
支持钉钉、飞书、企业微信、PushPlus、邮件、Bark、Slack、Telegram、通用 webhook,渠道实现从之前的 push_message.py 平移过来,字段见 notifier.py 的 _SAMPLE_CONFIG。
默认只在签到失败时发。每天四条"签到成功"迟早被练成免打扰,真该看的失败反而淹在里面。失败必须响,成功静默即可。通知这东西,像公众号的早安问候,一开始还看,后来直接折叠。
不注入就完全不发通知,属预期,不算失败。通知坏了也只吞掉自己,它和签到是同一条流水线里的两件事,通知挂掉不该让签到结果变红。
本机那份 ~/.workbuddy/scripts/notify_config.json 仍然会被读,方便本机调试。但云端容器里没有它,线上只有注入这一条路。
5.4 启动时打印通知配置(脱敏)
每次 runner 起来,先打一遍通知配置,省得"到底配没配、发去哪"只能靠猜:
通知:已配置(来源 AI_CHECKIN_PUSH_CONFIG,模式 error,enabled=True)
· wecom:webhook=https://qyapi.weixin.qq.com/***/***/***?key=***
· 未配置:dingtalk, feishu, wechat, bark, ...
没注入时是"通知:未配置,本次不发通知(属预期,不算失败)"。配置写坏了也会打出原因,而不是把签到带崩。
凭据一律脱敏:URL 的 path 整段遮掉、query 只留键名,token 类字段只留长度。host 足够判断配到哪个平台,凭据一个字符都不落日志。我打码的技术,比某些视频平台还好。
想单独看这份摘要,不跑签到:
cd tools/ai-checkin
python3 notifier.py --report # 与流水线启动时打印的是同一份
python3 notifier.py --ready # 机器可读版(JSON)
六、MiniMax:唯一不用装客户端的
6.1 token 在哪拿
四个平台里唯一一个不用装客户端的。网页版签到,凭据就是浏览器 localStorage 里的 _token,一个 JWT,抠出来即可。
cd tools/ai-checkin
MINIMAX_TOKEN="eyJ..." python auth/minimax_auth.py # 生成 minimax_secrets.yml
MINIMAX_TOKEN="eyJ..." python auth/minimax_auth.py --verify # 先在线校验一次再导出(推荐)
user_id 是必填项,不是可选优化。签到 query 里缺了它、或写成 0,网关一律回 401,看起来和"token 过期"一模一样。
像极了领导问"方案呢",你说"我发了",他说"没收到",两边都觉得自己没错。
好消息是脚本会自动去 user/info 把 user_id 补上,通常你只要给 _token 就够。想显式指定就用 --user-id,或直接把 DevTools 里复制的整段 JSON 丢给 --token。想省事可以 --save,token 存到 ~/.minimax/token.json,之后重跑不用再粘。明文,只在本机,别提交。
6.2 签到口径
接口就两个:GET /signin/status(7 天进度面板)、POST /signin/claim(领取)。
只有 status 明确说"今天可领"才发 claim,不可领时一个写请求都不打,比某些自动续费还克制。
"今日已签到"是 skip 不是失败,面板今天那格已是 Claimed,或 claim 返回 AlreadyClaimed,都按 skip 收口,重跑不会刷红流水线。
401 是 token 失效,按 skip · Token 失效 收口;400 是签名被拒,才是真 error,网页端公共参数变了,看 providers/minimax.py。
七、skip 和 error 的边界:绿了不等于成功了
7.1 为什么口径要定死
四个平台的凭据都会过期,过期是常态,不是事故。所以口径必须定死:
| 结果 | 什么时候 | 流水线 |
|---|---|---|
skip |
今日已签到、当前无可领、凭据失效、未配置 | 绿 |
error |
网络/5xx、响应结构变了、签到被拒但原因不明 | 红 |
凭据失效的判定收在 providers/authfail.py 一处(HTTP 401/403,或业务码 1001),各 provider 只管调它。
为什么?从前的实现里每家自己 return error,任何一家 token 过期就把整条流水线刷红。其它三家明明签得好好的,日报却只有一片红,像极了团队里一个人背锅,全组 KPI 变红。分不清是"签到坏了"还是"该换 token 了"。
现在过期只留一行 skip · Token 失效,请重新...,流水线照常绿,看到 reason 就知道该去密钥仓库换凭据了。
7.2 注意,skip 不等于签到成功
该换的凭据还是得换。这就像朋友圈里天天发健身打卡的人,不一定瘦了。
真出错仍是 error。这个口径只放宽"凭据失效"这一种,不兜底 5xx 和响应异常。
八、加一个新工具:四步,不碰流水线配置
四个步骤,全程不用改 .cnb.yml:
-
providers/下加文件,导出PROVIDER = YourProvider()。 -
providers/__init__.py的_MODULES加一行。 -
auth/下加your_tool_auth.py,从_common拿现成骨架,导出collect(full=False) -> (values, summary),一键导出就是靠它自动带上新工具。 -
密钥仓库加一组
YOUR_TOOL_*变量。
第 4 步的变量名要登记到 tests/selftest.py 的 test_real_required_env_names。写错变量名线上会永远 skip 且没人发现,这条断言就是防这个的。毕竟没人会主动检查一个永远安静的工具。
工具自己的附加动作(比如 WorkBuddy 的派遣)放在它自己的文件或同级模块里,不要往 runner.py 里加平台判断。
九、本地自测:不联网也能查装配
cd tools/ai-checkin && python3 tests/selftest.py
手动跑一次真实签到(会消耗当天签到机会,注意幂等):
cd tools/ai-checkin
python3 runner.py --list # 看哪些工具已配置
python3 runner.py workbuddy # 只跑一个
python3 runner.py # 跑全部
不联网,验证装配是否正确,含命名约定、变量对齐、AES 后端对拍。程序员最爱的测试类型:不用求人,不用等接口,不会 404。
十、目录结构
.
├── .cnb.yml # 流水线:00:02 主跑 / 10:02 Qoder / 手动 / 知识库
├── README.md # 本文档
└── tools/ai-checkin/
├── runner.py # 汇总调度器:逐个跑、逐个兜错、统一摘要、发通知
├── notifier.py # 多渠道通知(标准库,配置由环境变量注入)
├── export_auth.py # 一键导出:各平台凭据合成一份密钥文件
├── secrets.example.yml # 密钥仓库文件模板(手写时的变量清单)
├── auth/ # 取凭据:本机跑,生成密钥文件
│ ├── _common.py # 公共骨架:授权推导 + YAML 生成/合并 + 脱敏输出
│ ├── _workbuddy_atrest.py # WorkBuddy 登录态解密器
│ ├── workbuddy_auth.py
│ ├── trae_auth.py
│ ├── qoder_auth.py
│ ├── minimax_auth.py # 网页凭据:--from-browser 直读 / --renew 续期
│ └── baidu_auth.py # 百度搭子:读客户端 auth.json + .cookie-key 解密(L1/L2/L3 分层)
├── providers/ # 云端签到:零第三方依赖
│ ├── __init__.py # 工具注册表
│ ├── base.py # Provider 基类与环境变量取值
│ ├── authfail.py # 凭据失效判定:401/403、业务码 1001/10001 → skip
│ ├── workbuddy.py # 签到,签完调用 travel
│ ├── travel.py # 派遣 Buddy 旅行(域名与签到不同,故独立)
│ ├── trae.py
│ ├── qoder.py
│ ├── minimax.py # MiniMax Code 网页签到(网关签名 + user_id 自愈 + 401 自动续期)
│ └── baidu.py # 百度搭子(DuMate)签到领积分(Cookie 鉴权)
└── tests/selftest.py # 离线回归自检
结构不大,五脏俱全,比某些大型项目好懂多了。
十一、常见问题速查
| 现象 | 原因与处理 |
|---|---|
skip · 缺少凭据 X |
该工具未配置,正常。补密钥仓库变量即可 |
skip · Token 失效 |
凭据过期(401/403 或业务码 1001),不刷红流水线,但也没签到。重跑 export_auth.py 更新密钥仓库 |
error + 9074(TRAE) |
设备ID 不对,必须是客户端 AHA SDK 的纯数字 did,不是 telemetry.devDeviceId |
Prepare 阶段 allow_events 报错 |
密钥里的 allow_events 与 .cnb.yml 事件键不一致,用脚本重新生成 |
| Qoder 永远"无可领活动" | 活动每日 10:00 重置,确认跑在 10:02 那条 cron |
| 构建整体失败但只有一家挂了 | runner 口径是"有 error 就非 0",看摘要定位具体工具;若是 skip · Token 失效 则不会失败,属提醒 |
| 取凭据报"未找到登录态" | 对应客户端没登录,或没和脚本在同一用户下运行 |
export_auth.py 只导出了一部分 |
正常:没装客户端的平台取不到。文件头会写明缺席的是谁 |
| 覆盖产物时被拒(平台变少) | 防"越跑越秃",确认要删就加 --force |
| WorkBuddy 有签到但没派遣 | 看 detail.travel.available:false 是旅行接口没通(签到没坏);WORKBUDDY_TRAVEL=off 则是自己关掉的 |
| 一次通知都没收到 | 先看启动那几行"通知:":显示"未配置"= 没注入(属预期);已配置但模式是 error 且这次全成功,那本来就不发 |
通知里出现 notify_error |
通知模块自己抛了(签名/配置/网络)。签到结论不受影响,看这行定位 |
| 派遣多打了一个请求 / 404 | 旅行路径不带 /v2、域名默认 www.workbuddy.cn,两者都别跟签到混 |
skip · 鉴权失败(MiniMax) |
token 与 user_id 有一个不对(401 只看"token 过期"会误判)。用 auth/minimax_auth.py --from-browser 重新导出 |
error + 400 签名无效(MiniMax) |
网页端公共参数/签名算法变了,看 providers/minimax.py 的签名段 |
error + 401 缺少凭据(MiniMax) |
token 压根没注入,不是"过期" |
| MiniMax 签到总在 401 但 token 刚抄 | 多半是 user_id 缺失/为 0,不是 token 过期。换 --from-browser |
百度搭子 skip · 登录态失效 |
Cookie 过期(常态):开一次 DuMate 客户端重登,再跑 auth/baidu_auth.py 重导。客户端不重登,云端换不了这份 cookie |
| 百度搭子导出时报"读不出 Cookie" | 先跑 python auth/baidu_auth.py --check:它会列全探测过的候选路径。多数是跨了 Windows 账户,或客户端没真正登录过。--cookie 可手给兜底 |
| 百度搭子"未找到登录态"(新版已修) | 早期按 Electron safeStorage 解、而客户端其实是 .cookie-key + AES-256-GCM,密文永远解不开。现按实测契约解 |
有几个问题值得单独展开。
凭据多久换一次?各家各自打自己登录态里真实的到期时间,几家的天数本来就不一样,这正常。
| 平台 | 到期信号 | 日志里能提前看到吗 |
|---|---|---|
| WorkBuddy | HTTP 401/403 或业务码 1001 | 能:WORKBUDDY_EXPIRES_AT(客户端 expiresAt)→ JWT exp |
| TRAE | 返回 code=1001 |
能:TRAE_EXPIRES_AT(客户端 expiredAt)→ JWT exp |
| Qoder | 活动列表返回 401 | 能:QODER_EXPIRES_AT(登录态 expiresAt)→ JWT exp |
| MiniMax Code | 签到接口 401(user_id 缺失也会 401;脚本先补 user_id、再自动续期) | 不能:_token 不是 JWT,也没有到期字段,不打 |
| 百度搭子 | HTTP 401/403 或业务码 10001/10006(按 skip 收口) | Cookie 里真带 expires 才打(BAIDU_EXPIRES_AT),会话 Cookie 不打 |
providers/expiry.py 只做一件事:把凭据里真实存在的那个时刻打出来。取值顺序是"客户端自己写的最先,其次才是解 token",因为客户端那份更贴真实失效点。
只打真实数据,不推算。为什么要打"签发于,已过 X 天"?只有"剩余 X 天"时,刚换完凭据却看到剩 27 天,会让人怀疑是不是没换上。有签发时间就一眼分明:刚换是"签发于今天",没换则签发时间不动。
像极了给手机充电,明明充满了,还是忍不住拔了再插一次确认一下。
早期版本为了算"已过",拿一个假设的寿命(已过 = 55 - 剩余)去反推。那是编的:寿命是猜的、剩余是算的,两个不确定量一相减,得到的数只对"寿命恰好 55 天"成立。现在改成认 iat,JWT 里没有 iat 就省掉这半句,只报真实剩余天数,绝不拿假设凑数。LIFETIME_DAYS 这个口子已删除,selftest 盯着。
不足 7 天时追加一行 ::warning::,日志页会高亮:
::warning::<平台> 登录态剩余不足 7 天(5.2 天),请在电脑上打开客户端刷新后重新导出凭据更新密钥仓库
毫秒戳(13 位)会先归一化成秒,否则会被当成公元 5 万年。你的程序不仅穿越了,还穿越过头了。
这个提示纯属旁路:字段畸形、缺失都只是不打印,绝不影响签到结果与退出码。
换完凭据想确认"到底换没换",看日志那行"签发于"是不是变成了今天。只重传了密钥文件却忘了客户端重新登录,token 不变,签发时间也不会变。
客户端大版本升级后建议重新取一次,设备指纹和登录态格式都可能变。毕竟你换了个新发型,门禁可能就不认识你了。
整套东西跑起来之后,我终于不用每天早起按那个按钮了。
现在每天早上,我打开日志看一眼:三绿两 skip,一片祥和。
然后安心地继续睡。签到交给云端,睡眠还给自己。这大概就是程序员的浪漫。
P.S. 无意间发现了一个巨牛的人工智能教程,非常通俗易懂,对AI感兴趣的朋友强烈推荐去看看,传送门https://blog.csdn.net/qq_34419312