WorkBuddy 每天自动领积分!教你用云函数做个签到机器人

0 阅读9分钟

workbuddy_cover_16_9.png

这是个什么东西

一个部署在阿里云函数计算上的 Python 脚本,每天早上 9 点自动执行一次,干三件事:

  1. 调用客户端的签到接口,替你完成当天的签到
  2. 查询签到结果(连续天数、累计积分、活动信息)
  3. 把结果推送到你的微信上

跑完之后的日常体验是这样的——每天早上微信收到一条消息:

活动: 每日签到 · 第 7 期
活动截止: 2026-08-24 11:00
获得积分: +100
连续签到: 3 天
累计积分: 300

到此为止。你不需要开电脑、不需要登录客户端、不需要记得任何事。

它解决什么问题

手动签到这件事,单次成本只有三秒,但它是个每天都必须执行的动作,于是真实成本变成了:

  • 得记得:出差、休假、忙起来就会忘,而断一天就丢连续记录;
  • 得有条件:得打开电脑或手机装着客户端,设备不在身边就没办法;
  • 得自己确认:签了之后还得看一眼有没有真的成功。

这个脚本把这三件事全部自动化了:触发不依赖你的设备,执行不需要你的操作,结果主动推给你。而且整套方案的月成本是 0 元——每天 1 次调用,只占函数计算免费额度的 0.003%。

技术选型上为什么用函数计算而不是本地脚本或服务器?一句话:每天只跑 10 秒的任务,不值得为它维护一台机器。函数计算不跑不收费、自带定时触发器(秒级准点)、失败可以接云监控告警,正好卡住所有需求。

使用前你需要准备

三样东西,都是免费获取的:

准备项获取方式用途
阿里云账号fcnext.console.aliyun.com 注册并实名承载函数计算
Server酱 SendKeysct.ftqq.com 微信扫码登录微信推送通道
签到接口的 accessToken见下文「获取 accessToken」接口鉴权

关于合规先说清楚:这个方案只对你自己的账号生效,用的是你自己客户端的凭据,本质是"让云函数替你点一下签到按钮"。请不要用它做多账号、代刷或其他超出自己账号范围的事,也请遵守你所使用平台的服务条款。

完整代码

整个项目就两个文件。第一个是核心脚本 checkin.py,纯 Python 标准库、零第三方依赖(这一点很重要:不用 pip install,不用 requirements.txt,部署包几 KB,不会有环境不一致的坑):

#!/usr/bin/env python3
"""每日自动签到:查询状态 → 执行签到 → 复查 → 微信通知"""
import json
import os
import sys
import base64
import urllib.request
import urllib.error
import urllib.parse
from pathlib import Path
from datetime import datetime

API_BASE = "https://copilot.tencent.com"
STATUS_URL = f"{API_BASE}/v2/billing/meter/checkin-activity-status"   # 查询接口
CLAIM_URL = f"{API_BASE}/v2/billing/meter/daily-checkin"              # 签到接口
REQUEST_TIMEOUT = 15

CONFIG_FILE = Path(__file__).resolve().parent / "config.json"


def log(msg):
    # 函数计算会自动采集 stdout,print 就是日志
    print(f"[{datetime.now():%Y-%m-%d %H:%M:%S}] {msg}", flush=True)


def load_token():
    """读凭证:环境变量(云端推荐)优先,其次 config.json(本地调试用)"""
    token = os.environ.get("ACCESS_TOKEN", "").strip()
    if token:
        return token
    if CONFIG_FILE.exists():
        cfg = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
        token = cfg.get("access_token", "").strip()
    return token


def token_remaining_days(token):
    """解析 JWT 的 exp 字段,返回剩余天数(用于到期预警)"""
    try:
        payload = token.split(".")[1]
        payload += "=" * (4 - len(payload) % 4)
        exp = json.loads(base64.urlsafe_b64decode(payload)).get("exp", 0)
        return int((exp - datetime.now().timestamp()) / 86400) if exp else -1
    except Exception:
        return -1


def api_post(url, token, body=None):
    """POST JSON。注意:4xx 时业务码在响应体里,必须读出来,否则只剩 Bad Request"""
    data = json.dumps(body or {}).encode("utf-8")
    req = urllib.request.Request(url, data=data, method="POST", headers={
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
    })
    try:
        with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) as resp:
            return json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as e:
        try:
            return json.loads(e.read().decode("utf-8"))   # 错误体里往往有业务码
        except Exception:
            log(f"❌ HTTP {e.code}: {e.reason}")
            return None
    except Exception as e:
        log(f"❌ 请求异常: {e}")
        return None


def notify(title, desp):
    """Server酱 微信推送。desp 按 markdown 渲染,字段间必须用空行分隔"""
    key = os.environ.get("SERVERCHAN_SENDKEY", "").strip()
    if CONFIG_FILE.exists() and not key:
        cfg = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
        key = cfg.get("serverchan_sendkey", "").strip()
    if not key:
        log("ℹ️  未配置 SendKey,跳过通知")
        return
    url = f"https://sctapi.ftqq.com/{key}.send"
    data = urllib.parse.urlencode({"title": title, "desp": desp}).encode("utf-8")
    try:
        with urllib.request.urlopen(urllib.request.Request(url, data=data),
                                     timeout=10) as resp:
            ok = json.loads(resp.read().decode("utf-8")).get("code") == 0
            log("📨 微信通知已发送" if ok else "⚠️  微信通知失败")
    except Exception as e:
        log(f"⚠️  微信通知异常: {e}")


def checkin():
    log("=" * 50)
    token = load_token()
    if not token:
        notify("❌ 自动签到失败", "无法获取 accessToken,请检查环境变量或 config.json")
        return False
    remaining = token_remaining_days(token)

    # ① 查状态:今天已签过 → 静默退出(幂等,绝不重复通知)
    status = api_post(STATUS_URL, token)
    data = status.get("data", {}) if status else {}
    if data.get("today_checked_in"):
        log("今日已签到,静默退出")
        return True
    if not data.get("active"):
        log("⚠️  当前无进行中的活动(可能处于活动空窗期)")

    # ② 执行签到
    result = api_post(CLAIM_URL, token)
    if not result or result.get("code") != 0:
        code = (result or {}).get("code", "network")
        msg = (result or {}).get("msg", "")
        log(f"❌ 签到失败: code={code} {msg}")
        notify("❌ 自动签到失败", f"错误码: {code}\n\n{msg}\n\n请到 FC 控制台查看日志")
        return False

    # ③ 复查:签到接口只返回本次结果,累计数据要重新查
    credit = result.get("data", {}).get("credit", 0)
    streak = result.get("data", {}).get("streak_days", 0)
    latest = api_post(STATUS_URL, token)
    total = latest.get("data", {}).get("total_credits", 0) if latest else 0

    log(f"✅ 签到成功 +{credit},连续 {streak} 天,累计 {total}")

    # ④ 通知(字段间用空行分隔,markdown 才会分行)
    lines = []
    if data.get("theme_name"):
        lines.append(f"**活动:** {data['theme_name']} · 第{data.get('season', '')}期")
    if data.get("end_time"):
        lines.append(f"**活动截止:** {str(data['end_time'])[:16]}")
    lines.append(f"**获得积分:** +{credit}")
    lines.append(f"**连续签到:** {streak} 天")
    lines.append(f"**累计积分:** {total}")
    notify("✅ 自动签到成功", "\n\n".join(lines))

    # ⑤ 凭证到期预警(提前 7 天)
    if 0 < remaining <= 7:
        notify("⚠️ 签到凭证即将过期",
               f"凭证仅剩 **{remaining} 天**,请到 FC 控制台更新 ACCESS_TOKEN 环境变量")
    return True


if __name__ == "__main__":
    sys.exit(0 if checkin() else 1)

第二个文件 index.py 是函数计算的入口,只有几行——这样设计的好处是业务脚本在本地可以直接 python3 checkin.py 调试,不用每次部署到云上试错:

# -*- coding: utf-8 -*-
import json
from checkin import checkin


def handler(event, context):
    return json.dumps({"success": bool(checkin())}, ensure_ascii=False)

代码里有三处设计值得注意,都是实测踩出来的:

  • 幂等三段式(查→做→查):先查状态,签过就静默退出。这样就算定时器重复触发(或者你手动跑了一次又赶上定时触发),微信也只收到一条通知;
  • HTTP 4xx 时读响应体:urllib 遇到 4xx 会抛异常,而服务端的业务错误码就在异常携带的响应体里,不读出来你永远只看到 "Bad Request";
  • 通知字段用空行分隔:推送详情页按 markdown 渲染,单个 \n 会被当成空格,字段会挤成一行。

获取 accessToken

脚本需要客户端自己的 accessToken(一个 JWT,有效期约 60 天)。

macOS 客户端的登录态存在本地文件里,路径是:

~/Library/Application Support/CodeBuddyExtension/Data/Public/auth/workbuddy-desktop.info

打开这个 JSON,取 auth.accessToken 字段的值即可。Windows 用户在对应的 %APPDATA% 目录下找同名文件。

提示:这个文件是客户端的登录凭据,不要提交到任何仓库、不要发给别人。脚本里也做了防护——云端运行时 token 通过函数计算的环境变量注入,不进代码包。

部署到阿里云函数计算(约 10 分钟)

第 1 步:打 ZIP 包

把两个文件打成 ZIP:

zip fc-checkin.zip index.py checkin.py

不需要 requirements.txt——纯标准库,这是这套方案部署简单的原因。

第 2 步:创建函数

进入 FC 控制台 → 左侧「函数」→「函数列表」→「创建函数」:

配置项填写值
函数类型事件函数(⚠️ 不是 Web 函数)
函数名称auto-checkin
运行时Python 3.10
代码上传ZIP 包 → 选 fc-checkin.zip
处理程序index.handler
内存512 MB
执行超时120 秒(默认 60s 偏紧)

第 3 步:配置环境变量

函数详情 →「配置」→「环境变量」:

变量值
ACCESS_TOKEN上一步拿到的 accessToken
SERVERCHAN_SENDKEYServer酱的 SendKey
TZAsia/Shanghai(让日志时间和北京时间一致)

环境变量的好处是改完保存立即生效——之后 token 续期时,只需要在控制台粘贴新值,不用重新部署。

第 4 步:配定时触发器

函数详情 →「触发器」→「创建触发器」:

配置项填写值
类型定时触发器
Cron0 0 9 * * *
时区Asia/Shanghai ⚠️ 务必手动改,默认 UTC 会差 8 小时

两个新手坑提前打预防针:FC 的 cron 是六位式(秒 分 时 日 月 周,秒在最前面),别按 Linux 的五位习惯写;代码包本身不包含触发器,只上传代码函数不会自己跑。

第 5 步:测试

函数详情 →「测试函数」→ 事件模板选空事件 {} → 点「测试」。日志里看到签到结果、微信收到推送,就是部署成功了。

日常维护:只有一件事

token 有效期约 60 天,到期需要续期。整个流程已经被优化到最短:

  1. 脚本会自动解析 JWT 有效期,剩 7 天时微信会收到预警——你不会忘;
  2. 打开一次客户端(它会刷新本地登录态文件),取新的 accessToken;
  3. FC 控制台 → 环境变量 → 粘贴新 token → 保存。

10 秒,不用重新部署,不用改任何代码。

除此之外没有其他维护项。函数计算每月免费额度对这个任务来说约等于无限,不会产生账单。

一些可选的增强

加一层兜底:如果担心函数计算偶发故障,可以在 GitHub Actions 上留一个错开时间的兜底任务(比如 09:47)。因为脚本有幂等设计,兜底触发时发现"今日已签"会静默退出,不会造成重复通知——两套调度可以安全共存。

通知失败告警:如果希望"推送失败"也能被发现,可以让函数在推送失败时返回 500,然后配置 FC 的云监控告警(函数执行失败 → 通知)。这样即使推送通道本身挂了,也还有一条不依赖它的告警路径。

怎么确认它真的在跑:不用天天翻日志,看通知就行——如果通知连续多天在固定时刻到达,那就是定时器在正常工作。人手点不出这种整齐度,只有定时器能。

最后

这套东西的本质是:把一件"每天都要做但毫无技术含量"的事,交给一个不睡觉、不忘记、不要钱的系统,并让它主动向你汇报。

签到只是这个模式最简单的应用。把 checkin.py 里的两个接口换掉,同样的骨架可以直接套到其他"每日一做"的场景:定时同步数据、每日巡检、定时备份、日报推送。三件套不变——定时触发、幂等执行、结果通知——这三件事想清楚了,任何无人值守任务都能这么搭。

完整代码就在上文,拿去就能用。有问题欢迎评论区交流。