接上篇 《把 WorkBuddy 每日签到搬上云端》 (2026-08-28)
把多个工具汇总到一起
仓库地址
源码(建议直接 Fork):cnb.cool/holzwege.co...
Fork 之后就得到了你自己的「签到仓库」,下面所有操作都在你自己的仓库里进行。
一、为什么需要这个
1. 为什么不能只在本机跑
签到要挂在「每天某个时刻」上,本机方案有个绕不过去的坑:机器得开着。 笔记本合盖、下班关机、客户端没启动,签到就断了。断了还得手动补, 补的时候又常常已经错过活动重置时间。
搬到云端定时跑,这些都不再是问题。代价是凭据得从本机挪到云端。
2. 为什么必须有「密钥仓库」这个独立仓库
因为凭据不能写在代码仓库里。
.cnb.yml 里没有任何 env 硬编码的 token ------ 凭据只能经由 CNB 的 imports 从另一个私有仓库注入。这样带来三个好处:
- 签到仓库可以放心开源、随手分享,里面一个密钥都没有
- 换 token 只动密钥仓库一个文件,不用改代码、不用重跑流水线
- 泄漏面收敛:即使代码仓库被看到,也拿不到任何可用凭据
3. 为什么凭据只能在本机取一次
三个平台的登录态都没法在云端解密:
| 平台 | 本机登录态形态 | 为什么云端解不了 |
|---|---|---|
| WorkBuddy | AES-256-GCM 信封 | 解密密钥只驻留客户端进程内存,不落盘 |
| TRAE Work | AES-256-CBC 加密块 | 密钥由客户端代码派生,且设备ID只出现在客户端日志里 |
| Qoder CN | DPAPI 保护的 AES 密钥 | DPAPI 与本机 Windows 账户强绑定,不可导出 |
所以架构只能是三段式:
objectivec
本机(Windows,一次性) CNB 密钥仓库(私有) CNB 云端(每天定时)
auth/<平台>_auth.py → <平台>_secrets.yml → runner.py + providers/
解出明文凭据 只读、只被 imports 引用 不碰加密,直接用明文
4. 为什么只要一个仓库
最初每个工具各占一个仓库,结果是同一套「取凭据 → 存密钥 → 定时签到」 被抄了三遍,三份代码慢慢长得不一样,命名也各不相同。合并成一个仓库之后:
- 一套调度:几点跑、跑哪些、跑挂了怎么办,只有一个地方要改
- 一套命名:见下面「命名约定」,脚本、产物、变量名全部同构
- 加工具只加文件 :不碰
.cnb.yml - 工具自己的「附加动作」也收在 provider 里 :比如 WorkBuddy 签完要派遣 Buddy(喵喵)旅行,就放在
providers/travel.py,由 workbuddy provider 调用, 不往 runner 里塞平台判断(见「五、WorkBuddy 的附加动作」)
二、命名约定
这套约定是强制 的 ------ 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 |
一句话:平台名是唯一的命名基准 ,脚本、文件、变量都从它派生。 取凭据脚本一律叫 *_auth.py,作用一律是「拿本机登录态,生成密钥文件」; 产物一律以平台名打头。
汇总器只有一个:export_auth.py 调各平台的 collect(),把它们合成一份成品。 它是「汇总」不是「取凭据」,所以放在 tools/ai-checkin/ 而不是 auth/ ------ selftest.py 会盯着这条边界,别把平台无关的脚本混进 auth/。
三、操作步骤
① 准备密钥仓库
有一个私有 仓库专门放凭据(本仓库默认指向 holzwege.coz/check-in/secrets)。 没有就新建一个,把 .cnb.yml 里 imports 的地址换成你的。
② 在本机一键生成密钥文件(推荐:一条命令)
在仓库根目录执行:
bash
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再python export_auth.py,脚本自己会处理路径。
它会逐个平台取本机登录态,然后合成一份 ai-checkin-secrets.yml ------ 文件与密钥仓库里那份同名同构,全选复制、整份覆盖粘贴即可,不用再手工拼。
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>"
授权范围(allow_slugs / allow_events)只出现一次 ,从 .cnb.yml 逐字推导 ------ 这正是手工合并最容易写坏的地方(三份产物各带一份,同名 key 互相覆盖)。
前提:对应客户端已启动并登录 。各平台的数据来源不同,所以一台机器上通常 只能导出装了客户端的那几个;取不到的平台自动跳过(文件头会注明原因), 不当失败 ------ runner 对缺凭据的工具本来就是 skip。
常用选项:
bash
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 才放行),免得一次误跑把凭据越写越少;一个都没取到 就不落盘。
只想单独取某一个平台也可以,生成的是单平台文件:
bashpython 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不想落盘就加
--verify(只校验能不能解出来)或--stdout。 这条路要自己把几份产物拼成一份,注意allow_slugs/allow_events只能留一份。
③ 粘贴进密钥仓库
把 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。用脚本生成就没这个问题。
④ 跑一次验证
在「代码构建」页点「执行」(或推一次代码触发 push),看日志:
ini
>>> [workbuddy] WorkBuddy(签到 + 派遣 Buddy 旅行)
结果: OK · 签到成功(12.3s)
(detail 里多一段 travel:派猫猫旅行:空闲(可派遣)......)
>>> [trae] TRAE Work(每日签到领积分)
结果: SKIP · 今日已签到(3.1s)
>>> [qoder] Qoder CN(每日签到领 Credits)
结果: SKIP · 当前无可领活动(2.4s)
汇总:成功 1 / 跳过 2 / 失败 0
日志末尾会打整段 JSON 摘要(含每个工具的 detail),要看全就往下翻。 WorkBuddy 的 detail.travel 就是派遣结果:available: false 说明旅行接口 没通(签到本身没坏),auto_log 里是「已派出 Buddy 前往【XX】」之类的原话。
没配的工具显示 SKIP 是正常的,不算失败 ------ 想启用就把它的凭据补进密钥仓库。
用「代码构建」页手动跑时,密钥仓库的
allow_events要加上web_trigger, 否则 Prepare 阶段失败。脚本生成的产物会自动带上(如果.cnb.yml里有这个入口)。
⑤ 之后就自动跑了
| 时间(北京) | 跑什么 | 为什么是这个点 |
|---|---|---|
| 每天 00:02 | workbuddy + trae | 按自然日重置,零点后 2 分钟领最稳 |
| 每天 10:02 | qoder | Qoder 活动每日 10:00 重置,早跑必是空的 |
四、WorkBuddy 的附加动作:派遣 Buddy(喵喵)
「签到」只是 WorkBuddy 这条线的一半。旧仓库 holzwege.coz/workbuddy-checkin 里还有两件本仓库合并时漏掉的事,这次一并接回来了:
| 动作 | 在哪 | 说明 |
|---|---|---|
| 派遣 Buddy 旅行 | providers/travel.py |
签到之后「先领后派」闭环 |
| 多渠道消息通知 | notifier.py |
runner 跑完每个工具发一条,配置从密钥仓库注入 |
旅行为什么单独一个文件
因为旅行接口和签到接口不是同一个域名,路径规则也不一样:
bash
签到 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"。
通知怎么配
通知配置和凭据一样只能从密钥仓库来(容器里没有本机配置文件):
yaml
# 值本身是一份 JSON(与旧仓库本机的 notify_config.json 同构),不是文件路径
AI_CHECKIN_PUSH_CONFIG: '{"channels": {"wecom": {"webhook": "https://..."}}}'
AI_CHECKIN_PUSH_CHANNELS: "wecom" # 选填,缺省发配置里全部已就绪渠道
AI_CHECKIN_PUSH_ON: "always" # 选填:always(默认)/ ok / error
支持钉钉、飞书、企业微信、PushPlus、邮件、Bark、Slack、Telegram、通用 webhook... (渠道实现从旧仓库 push_message.py 逐字平移,字段见 notifier.py 的 _SAMPLE_CONFIG)。
不注入就完全不发通知,这属预期,不算失败。通知本身坏了也只吞掉自己 ------ 它和签到是同一条流水线里的两件事,通知挂掉不该让签到结果变红。
本机那份
~/.workbuddy/scripts/notify_config.json仍然会被读(方便本机调试), 但云端容器里没有它,所以线上只有注入这一条路。
五、加一个新工具
四步,不用改 .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 里加平台判断 ------ runner 的职责只有「逐个跑、逐个兜错、统一收口」。
六、本地自测
不联网,验证装配是否正确(含命名约定、变量对齐、AES 后端对拍):
bash
cd tools/ai-checkin && python3 tests/selftest.py
手动跑一次真实签到(会消耗当天签到机会,注意幂等):
bash
cd tools/ai-checkin
python3 runner.py --list # 看哪些工具已配置
python3 runner.py workbuddy # 只跑一个
python3 runner.py # 跑全部
七、目录结构
bash
.
├── .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
├── providers/ # 云端签到:零第三方依赖
│ ├── __init__.py # 工具注册表
│ ├── base.py # Provider 基类与公共契约
│ ├── workbuddy.py # 签到,签完调用 travel
│ ├── travel.py # 派遣 Buddy 旅行(域名与签到不同,故独立)
│ ├── trae.py
│ └── qoder.py
└── tests/selftest.py # 离线回归自检
八、常见问题
| 现象 | 原因与处理 |
|---|---|
skip · 缺少凭据 X |
该工具未配置,正常。补密钥仓库变量即可 |
error + 401 / 1001 |
Token 失效,重跑 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 |
| 构建整体失败但只有 Qoder 挂了 | runner 口径是「有 error 就非 0」,看摘要定位具体工具 |
| 取凭据报「未找到登录态」 | 对应客户端没登录,或没和脚本在同一用户下运行 |
export_auth.py 只导出了一部分 |
正常:没装客户端的平台取不到。文件头会写明缺席的是谁 |
| 覆盖产物时被拒(平台变少) | 防「越跑越秃」:确认要删就加 --force |
| WorkBuddy 有签到但没派遣 | 看 detail.travel.available:false 是旅行接口没通(签到没坏);WORKBUDDY_TRAVEL=off 则是自己关掉的 |
| 一次通知都没收到 | 密钥仓库没注入 AI_CHECKIN_PUSH_CONFIG,属预期;注入了就看 detail.notified |
| 派遣多打了一个请求 / 404 | 旅行路径不带 /v2、域名默认 www.workbuddy.cn,两者都别跟签到混 |
凭据多久换一次
| 平台 | 有效期 | 到期信号 |
|---|---|---|
| WorkBuddy | 约 55 天 | 脚本与流水线都会打印剩余天数 |
| TRAE | 随客户端会话 | 返回 code=1001 |
| Qoder | 随客户端登录态 | 活动列表返回 401 |
客户端大版本升级后建议重新取一次 ------ 设备指纹和登录态格式都可能变。