原创不易,觉得有用的话点个赞再走~
适用人群:想给 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"
  }
}

我们后面要用到:

字段来源是否必须
accessTokenauth.accessToken必须
uidaccount.uid建议带上
domainauth.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

说明两点:

  1. GitHub Actions 的 cron 用的是 UTC
    北京时间 09:05 → 5 1 * * *
  2. workflow_dispatch 方便你在网页上点「手动执行」调试

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

建议配置:

Secret 名称必填值从哪来
WORKBUDDY_ACCESS_TOKENauth.accessToken
WORKBUDDY_UID建议account.uid
WORKBUDDY_ACCOUNT_NAME昵称,仅日志展示
WORKBUDDY_DOMAIN默认 www.codebuddy.cn
WORKBUDDY_ENTERPRISE_ID企业账号才需要

5)手动跑一次验证

  1. 打开仓库 Actions
  2. 左侧选 WorkBuddy Daily Checkin
  3. Run workflow
  4. 等跑完看日志

成功标志:

  • 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-statustoday_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 会过期。过期后:

  1. 重新打开本地 auth 文件
  2. 复制新的 accessToken
  3. 更新 GitHub Secret:WORKBUDDY_ACCESS_TOKEN

不用改代码,不用重新部署。


七、维护成本总结

跑通以后,日常几乎不用管:

  • 每天自动签到
  • 想看结果就去 Actions 日志
  • Token 过期改一个 Secret

适合那种「功能很小、但忘了就烦」的自动化需求。同思路也可以迁移到别的「每日打卡 / 每日领取」类接口脚本上。


有问题欢迎评论区留言,我看到会回。


更多推荐