原创不易,觉得有用的话点个赞再走~
适用人群:想给 WorkBuddy 做每日签到、又不想每天开电脑的同学
技术栈:Python3 标准库 + GitHub Actions(免费额度足够)
前言
用 WorkBuddy 一段时间了,每天最稳的积分来源就是「每日签到」。问题也很真实:
- 必须手动点客户端领取
- 电脑没开机 / 人一忙就忘
- 断签直接心疼
网上有人用 Gitee Go、云函数、甚至模拟鼠标点击。我这边最终落地的是:
Python 调官方签到接口 + GitHub Actions 每天定时跑
优点很直接:
- 不依赖本地电脑是否开机
- 纯标准库,零第三方依赖
- GitHub Free 私有仓大约 每月 2000 分钟,这个任务每天 1~2 分钟,完全够用
- Token 放 Secrets,不写进代码仓库
下面按「能直接复现」的方式写完整流程,包含我踩过的几个坑。
一、整体思路
流程可以概括成三步:
本地取出 accessToken
↓
Python 脚本调用签到接口
↓
GitHub Actions 每天定时执行脚本
核心接口(POST):
- 查询状态:https://www.codebuddy.cn/v2/billing/meter/checkin-status
- 领取签到:https://www.codebuddy.cn/v2/billing/meter/daily-checkin
注意:网上有文章写 copilot.tencent.com/billing/meter/…,实测会 404。
当前可用域名是 www.codebuddy.cn,路径带 /v2/。
二、准备工作:拿到 accessToken
WorkBuddy 登录后,本地会保存登录态文件。
Windows
路径:
%LOCALAPPDATA%\\CodeBuddyExtension\\Data\\Public\\auth\\workbuddy-desktop.info
也就是类似:
C:\\Users\\你的用户名\\AppData\\Local\\CodeBuddyExtension\\Data\\Public\\auth\\workbuddy-desktop.info
macOS
路径:
~/Library/Application Support/CodeBuddyExtension/Data/Public/auth/workbuddy-desktop.info
用记事本 / VS Code 打开,结构大概是:
{
"account": {
"uid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"nickname": "你的昵称"
},
"auth": {
"accessToken": "eyJhbGc…",
"domain": "www.codebuddy.cn"
}
}
我们后面要用到:
| accessToken | auth.accessToken | 必须 |
| uid | account.uid | 建议带上 |
| domain | auth.domain | 建议,默认 www.codebuddy.cn |
⚠️ 安全提醒:accessToken 等同登录态,绝对不要提交到公开仓库,也不要发到群里/博客正文。
三、本地先跑通签到脚本
先保证本地能签到成功,再谈云端。项目结构建议这样:
workbuddy-checkin/
├── checkin.py
├── config.json # 本地用,不要提交
├── config.example.json
├── .gitignore
├── README.md
└── .github/workflows/checkin.yml
1).gitignore
# 含登录态,禁止提交
config.json
__pycache__/
*.py[cod]
.venv/
venv/
.idea/
.vscode/
.DS_Store
Thumbs.db
*.log
2)config.example.json
{
"access_token": "把你的_accessToken_粘贴到这里",
"account_name": "你的昵称",
"uid": "可选,本地 auth 里的 account.uid",
"domain": "www.codebuddy.cn",
"enterprise_id": ""
}
复制一份改名为 config.json,填上真实值。
3)完整脚本 checkin.py
纯 Python 标准库,复制即可用:
#!/usr/bin/env python3
"""
WorkBuddy 每日签到脚本
支持本地运行和 GitHub Actions 云端定时运行
"""
from __future__ import annotations
import json
import os
import sys
import urllib.error
import urllib.request
from datetime import datetime
from pathlib import Path
API_BASE = "https://www.codebuddy.cn"
CHECKIN_STATUS_URL = f"{API_BASE}/v2/billing/meter/checkin-status"
DAILY_CHECKIN_URL = f"{API_BASE}/v2/billing/meter/daily-checkin"
REQUEST_TIMEOUT = 20
_WIN_AUTH_FILE = (
Path(os.environ.get("LOCALAPPDATA", ""))
/ "CodeBuddyExtension"
/ "Data/Public/auth/workbuddy-desktop.info"
)
_MAC_AUTH_FILE = (
Path.home()
/ "Library/Application Support/CodeBuddyExtension"
/ "Data/Public/auth/workbuddy-desktop.info"
)
_CLOUD_CONFIG_FILE = Path(__file__).resolve().parent / "config.json"
def log(msg: str) –> None:
line = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {msg}"
try:
print(line, flush=True)
except UnicodeEncodeError:
print(line.encode("utf-8", errors="replace").decode("utf-8", errors="replace"), flush=True)
def _read_local_auth(auth_file: Path) –> dict | None:
if not auth_file.exists():
return None
try:
return json.loads(auth_file.read_text(encoding="utf-8"))
except Exception as e:
log(f"[!] 读取 auth 文件失败 ({auth_file}): {e}")
return None
def _creds_from_env() –> dict | None:
token = os.environ.get("WORKBUDDY_ACCESS_TOKEN", "").strip()
if not token:
return None
return {
"access_token": token,
"account_name": os.environ.get("WORKBUDDY_ACCOUNT_NAME", "环境变量账号"),
"uid": os.environ.get("WORKBUDDY_UID", "").strip() or None,
"domain": os.environ.get("WORKBUDDY_DOMAIN", "www.codebuddy.cn").strip(),
"enterprise_id": os.environ.get("WORKBUDDY_ENTERPRISE_ID", "").strip() or None,
}
def _creds_from_config() –> dict | None:
if not _CLOUD_CONFIG_FILE.exists():
return None
try:
data = json.loads(_CLOUD_CONFIG_FILE.read_text(encoding="utf-8"))
token = (data.get("access_token") or "").strip()
if not token:
return None
return {
"access_token": token,
"account_name": data.get("account_name") or "云端账号",
"uid": (data.get("uid") or "").strip() or None,
"domain": (data.get("domain") or "www.codebuddy.cn").strip(),
"enterprise_id": (data.get("enterprise_id") or "").strip() or None,
}
except Exception as e:
log(f"[!] 读取 config.json 失败: {e}")
return None
def load_credentials() –> dict | None:
# CI 优先读 Secrets 注入的环境变量
if os.environ.get("GITHUB_ACTIONS") or os.environ.get("CI"):
creds = _creds_from_env()
if creds:
log(f" 使用环境变量 (账号: {creds['account_name']})")
return creds
creds = _creds_from_config()
if creds:
log(f" 使用 config.json (账号: {creds['account_name']})")
return creds
creds = _creds_from_env()
if creds:
log(f" 使用环境变量 (账号: {creds['account_name']})")
return creds
for auth_file, label in ((_WIN_AUTH_FILE, "Windows"), (_MAC_AUTH_FILE, "macOS")):
data = _read_local_auth(auth_file)
if not data:
continue
token = (data.get("auth", {}).get("accessToken") or "").strip()
if not token:
log(f"[x] {label} auth 中未找到 accessToken")
continue
account = data.get("account") or {}
creds = {
"access_token": token,
"account_name": account.get("nickname") or f"{label}账号",
"uid": (account.get("uid") or "").strip() or None,
"domain": (data.get("auth", {}).get("domain") or "www.codebuddy.cn").strip(),
"enterprise_id": (
(account.get("enterpriseId") or account.get("enterprise_id") or "").strip() or None
),
}
log(f" 使用 {label} 本地 auth (账号: {creds['account_name']})")
return creds
log("[x] 未找到任何 token 来源,请配置 config.json 或环境变量")
return None
def _build_headers(creds: dict) –> dict[str, str]:
headers = {
"Authorization": f"Bearer {creds['access_token']}",
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": "WorkBuddy-Checkin/1.1",
}
if creds.get("uid"):
headers["X-User-Id"] = creds["uid"]
if creds.get("domain"):
headers["X-Domain"] = creds["domain"]
eid = creds.get("enterprise_id")
if eid:
headers["X-Enterprise-Id"] = eid
headers["X-Tenant-Id"] = eid
return headers
def _request_json(url: str, creds: dict, method: str = "POST") –> dict | None:
req = urllib.request.Request(
url,
data=b"{}",
method=method.upper(),
headers=_build_headers(creds),
)
try:
with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) as resp:
raw = resp.read().decode("utf-8")
return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
err_body = ""
try:
err_body = e.read().decode("utf-8", errors="replace")
except Exception:
pass
log(f" HTTP {e.code}: {e.reason} {err_body[:300]}")
try:
return json.loads(err_body) if err_body else None
except Exception:
return None
except Exception as e:
log(f" 请求失败: {e}")
return None
def _msg_of(payload: dict | None) –> str:
if not isinstance(payload, dict):
return ""
return str(payload.get("message") or payload.get("msg") or "")
def already_checked_in(payload: dict | None) –> bool:
if not isinstance(payload, dict):
return False
code = payload.get("code")
msg = _msg_of(payload)
if code == 10001 or "已签到" in msg or "已经签到" in msg:
return True
data = payload.get("data")
if isinstance(data, dict) and (data.get("today_checked_in") or data.get("checked_in")):
return True
return bool(payload.get("today_checked_in") or payload.get("checked_in"))
def unwrap_data(payload: dict | None) –> dict | None:
if not isinstance(payload, dict):
return None
code = payload.get("code")
if code is not None and code not in (0, 200):
msg = _msg_of(payload) or "unknown"
log(f" 业务错误 code={code}: {msg}")
return None
data = payload.get("data")
return data if isinstance(data, dict) else payload
def checkin() –> bool:
log("=" * 56)
log(f" WorkBuddy 每日签到 — {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
log("=" * 56)
log("步骤 1/3: 获取凭证…")
creds = load_credentials()
if not creds:
return False
log("步骤 2/3: 查询签到状态…")
status_raw = _request_json(CHECKIN_STATUS_URL, creds, method="POST")
if already_checked_in(status_raw):
log("[ok] 今日已签到,无需重复领取。")
return True
status = unwrap_data(status_raw)
if status is None:
log("[!] 签到状态查询失败,继续尝试领取…")
else:
log(
f" active={status.get('active')}, "
f"today_checked_in={status.get('today_checked_in')}, "
f"streak_days={status.get('streak_days')}"
)
log("步骤 3/3: 领取签到积分…")
result_raw = _request_json(DAILY_CHECKIN_URL, creds, method="POST")
if already_checked_in(result_raw):
log("[ok] 今日已签到。")
return True
result = unwrap_data(result_raw)
if result is None:
log("[x] 签到失败,请检查 token 是否过期。")
return False
success = result.get("success", True)
credit = result.get("credit", result.get("today_credit", result.get("points")))
streak = result.get("streak_days")
message = result.get("message") or ""
if success is False:
log(f"[!] 领取未成功: {message or result}")
return False
log(f"[ok] 签到成功! credit={credit}, streak_days={streak} {message}")
return True
if __name__ == "__main__":
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
sys.exit(0 if checkin() else 1)
4)本地测试
python checkin.py
成功时大致会看到:
步骤 1/3: 获取凭证…
步骤 2/3: 查询签到状态…
步骤 3/3: 领取签到积分…
[ok] 签到成功! credit=xxx …
如果今天已经签过,接口可能返回类似:
HTTP 400 … {"code":10001,"msg":"今天已签到,请明天再来"}
[ok] 今日已签到。
脚本会把「已签到」也当成成功,避免 Actions 误报失败。
四、部署到 GitHub Actions(重点)
1)创建私有仓库
登录 GitHub,新建仓库:
- 名称例如:workbuddy-checkin
- 建议选 Private
- 不要勾选自动生成 README(避免首次推送冲突)
2)写 Actions 工作流
创建文件:.github/workflows/checkin.yml
name: WorkBuddy Daily Checkin
on:
schedule:
# 每天北京时间 09:05 = UTC 01:05
– cron: "5 1 * * *"
workflow_dispatch:
jobs:
checkin:
runs-on: ubuntu–latest
timeout-minutes: 5
steps:
– name: Checkout
uses: actions/checkout@v4
– name: Setup Python
uses: actions/setup–python@v5
with:
python-version: "3.11"
– name: Run checkin
env:
WORKBUDDY_ACCESS_TOKEN: ${{ secrets.WORKBUDDY_ACCESS_TOKEN }}
WORKBUDDY_ACCOUNT_NAME: ${{ secrets.WORKBUDDY_ACCOUNT_NAME }}
WORKBUDDY_UID: ${{ secrets.WORKBUDDY_UID }}
WORKBUDDY_DOMAIN: ${{ secrets.WORKBUDDY_DOMAIN }}
WORKBUDDY_ENTERPRISE_ID: ${{ secrets.WORKBUDDY_ENTERPRISE_ID }}
run: python checkin.py
说明两点:
北京时间 09:05 → 5 1 * * *
3)推送代码(不要推 config.json)
git init
git add .gitignore README.md checkin.py config.example.json .github/workflows/checkin.yml
git commit -m "init: WorkBuddy GitHub Actions 每日签到"
git branch -M main
git remote add origin https://github.com/你的用户名/workbuddy-checkin.git
git push -u origin main
推送包含 .github/workflows/*.yml 时,Classic PAT 需要同时勾选:
- repo
- workflow
少勾 workflow 会报:refusing to allow a Personal Access Token to create or update workflow
4)配置 Secrets(别把 token 写进代码)
仓库页面路径:
Settings → Secrets and variables → Actions → New repository secret
建议配置:
| WORKBUDDY_ACCESS_TOKEN | 是 | auth.accessToken |
| WORKBUDDY_UID | 建议 | account.uid |
| WORKBUDDY_ACCOUNT_NAME | 否 | 昵称,仅日志展示 |
| WORKBUDDY_DOMAIN | 否 | 默认 www.codebuddy.cn |
| WORKBUDDY_ENTERPRISE_ID | 否 | 企业账号才需要 |
5)手动跑一次验证
成功标志:
- Job 显示绿色 success
- 日志里有 [ok] 签到成功 或 [ok] 今日已签到
之后就会每天北京时间 09:05 自动跑,电脑关着也没关系。
五、方案对比(为什么不选 Gitee Go)
| 本地计划任务 / 模拟点击 | 配置快 | 依赖电脑开机,分辨率/窗口还容易翻车 |
| Gitee Go | 国内访问稳 | 免费时长偏紧,体验额度不够长期用 |
| 云函数 | 定时准 | 配置相对重,有的还有费用门槛 |
| GitHub Actions | 免费额度够、文档全、Secrets 好用 | 偶发网络波动,私有仓吃月度分钟数 |
按「每天跑一次短脚本」这个场景,GitHub Actions 性价比最高。
六、踩坑记录(建议收藏)
坑 1:接口地址写错
旧文常见地址:
https://copilot.tencent.com/billing/meter/daily-checkin
实测 404。请用:
https://www.codebuddy.cn/v2/billing/meter/daily-checkin
并且是 POST,不是 GET。
坑 2:状态字段和领取结果不一致
有时 checkin-status 里 today_checked_in=false,但领取接口返回:
{"code":10001,"msg":"今天已签到,请明天再来"}
所以脚本里要把 code=10001 / 文案含「已签到」也判定为成功,否则 Actions 会红叉。
坑 3:Token 权限不够推 workflow
Fine-grained token 很容易漏权限。个人自动化更建议用 Classic PAT:
- 勾选 repo
- 勾选 workflow
坑 4:把 config.json 推到仓库
私有仓也别养成这个习惯。正确做法是:
- 本地用 config.json
- 云端用 GitHub Secrets
坑 5:Token 过期
accessToken 会过期。过期后:
不用改代码,不用重新部署。
七、维护成本总结
跑通以后,日常几乎不用管:
- 每天自动签到
- 想看结果就去 Actions 日志
- Token 过期改一个 Secret
适合那种「功能很小、但忘了就烦」的自动化需求。同思路也可以迁移到别的「每日打卡 / 每日领取」类接口脚本上。
有问题欢迎评论区留言,我看到会回。



