Skip to content

免费 AI Chat / Tools / Fun — Cloudflare 边缘网关

免登录 · 流式聊天 · 工具集合 · 趣味页面(含图片瀑布流)

观者终端
Go back

公开仓库也能安全分发 Token:GitHub Actions + age 加密 Release + VPS 自动体检清理

发布于:

很多人做 token 自动化时,最容易踩的坑不是脚本写得不够花,而是“分发链路”不安全:

这篇文章记录一条在 public repo 也能跑的端到端方案:Token 工厂 → 加密发布 → VPS 自动拉取 → 全量探测 → 删除失效 → TG 汇报。文中不包含任何可复用的真实密钥/账号信息,全部用占位符。

把“token 分发”变成“发布密文制品”

核心策略只有一句话:

GitHub Release 上永远只放密文(tokens.zip.age),明文 token 从不离开 runner 与目标 VPS。

因此,即便仓库与 Release 都是公开的,别人下载到的也只是一坨密文。

我们用的是 age(一个很轻量的公钥加密工具):

产出侧:GitHub Actions 生成 token → zip → age 加密 → 上传 Release

假设你的任务脚本会输出:codex/*.json

Actions 做三件事:

  1. codex/*.json 打包成 tokens.zip(明文只存在于 runner 临时环境)
  2. 用 age 公钥加密成 tokens.zip.age
  3. 生成一个 manifest.json(包含 sha256、数量、时间戳),并把这两个文件上传到 Release

配置要点(不敏感)

在仓库的 Actions Secrets(或 Variables)里准备:

并确保 workflow 有:

permissions:
  contents: write

这样就能用 GitHub 自动注入的 ${{ github.token }} 完成 release 创建与 asset 上传,无需 PAT。

一个重要细节:Release tag 必须每次运行唯一

如果你一天跑几十次 release,tag 不能按“日期”命名,否则会冲突。

推荐 tag:

tokens-YYYYMMDD-HHMMSS-r<RUN_NUMBER>-a<RUN_ATTEMPT>

下面是一个可复用的工作流(示例文件名:.github/workflows/regi+release.yml):

name: Daily Task Job

on:
  schedule:
    - cron: "*/40 * * * *"
  workflow_dispatch:
    inputs:
      count:
        description: "本次注册数量"
        required: false
        default: "40"

permissions:
  contents: write
  actions: read

jobs:
  register:
    runs-on: ubuntu-latest
    timeout-minutes: 30

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"
          cache: "pip"

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Run registration
        env:
          REG_COUNT: ${{ github.event.inputs.count || '40' }}
        run: |
          mkdir -p codex
          for i in $(seq 1 $REG_COUNT); do
            python task_runner.py --once || true
            sleep $((RANDOM % 20 + 10))
          done

      - name: Install age
        if: always()
        run: |
          sudo apt-get update
          sudo apt-get install -y age

      - name: Package + Encrypt + Upload Release asset
        if: always()
        env:
          AGE_RECIPIENT: ${{ secrets.AGE_RECIPIENT }}
          GH_TOKEN: ${{ github.token }}
        run: |
          set -euo pipefail

          COUNT=$(( $(find codex -name '*.json' 2>/dev/null | wc -l) + 0 ))
          [ "$COUNT" -gt 0 ] || exit 0

          ZIP_NAME="tokens.zip"
          ENC_NAME="tokens.zip.age"
          MANIFEST="manifest.json"

          rm -f "$ZIP_NAME" "$ENC_NAME" "$MANIFEST"
          zip -r "$ZIP_NAME" codex/*.json

          age -r "$AGE_RECIPIENT" -o "$ENC_NAME" "$ZIP_NAME"

          SHA256=$(sha256sum "$ENC_NAME" | awk '{print $1}')
          DATE_TAG=$(date +%Y%m%d-%H%M%S)
          TAG="tokens-${DATE_TAG}-r${GITHUB_RUN_NUMBER}-a${GITHUB_RUN_ATTEMPT}"

          cat > "$MANIFEST" <<EOF
          {
            "tag": "${TAG}",
            "sha256": "${SHA256}",
            "count": ${COUNT},
            "generated_at": "$(date -Is)"
          }
          EOF

          gh release create "$TAG" \
            --title "Tokens ${DATE_TAG}" \
            --notes "Encrypted token bundle (assets are age-encrypted)." \
            --latest=false || true

          gh release upload "$TAG" "$ENC_NAME" "$MANIFEST" --clobber

Release 清理:只保留最近 6 小时(否则会爆炸)

高频发布时,Release 会迅速膨胀。下面的 workflow 每 15 分钟清一次,删除 6 小时前的 tokens-* release:

name: Cleanup old token releases (keep 6 hours)

on:
  schedule:
    - cron: "*/15 * * * *"
  workflow_dispatch:

permissions:
  contents: write

jobs:
  cleanup:
    runs-on: ubuntu-latest
    steps:
      - name: Delete token releases older than 6 hours
        env:
          GH_TOKEN: ${{ github.token }}
          REPO: ${{ github.repository }}
          KEEP_HOURS: 6
        run: |
          set -euo pipefail

          cutoff_epoch=$(date -u -d "${KEEP_HOURS} hours ago" +%s)

          gh api "repos/${REPO}/releases?per_page=100" --paginate |
            jq -r '.[] | [.tag_name, .created_at] | @tsv' |
          while IFS=$'\t' read -r tag created_at; do
            case "$tag" in
              tokens-*)
                created_epoch=$(date -u -d "$created_at" +%s)
                if [ "$created_epoch" -lt "$cutoff_epoch" ]; then
                  gh release delete "$tag" -y
                fi
                ;;
            esac
          done

消费侧:VPS 拉取 Release → 校验 → 解密 → 追加落盘

VPS 上的同步器建议做到三点:

一个实用的小经验:增量判断优先用 published_at(不存在再回退 created_at),并在本地写 state,避免重复处理。

下面是一份“可运行的骨架版”同步脚本(关键变量用 env 占位):

#!/usr/bin/env bash
set -euo pipefail

# /etc/token-sync.env
# REPO="owner/repo"
# AGE_IDENTITY="/root/.config/age/your-private.key"
# MAX_PER_RUN="20"

source /etc/token-sync.env

BASE="/opt/cli-proxy-plus/token-sync"
INBOX="$BASE/inbox"
WORK="$BASE/work"
STATE="$BASE/state.json"
DEST="/opt/cli-proxy-plus/auths"

mkdir -p "$INBOX" "$WORK" "$DEST"

LAST=""
[ -f "$STATE" ] && LAST=$(jq -r '.last_published_at // ""' "$STATE" 2>/dev/null || true)

RELEASES=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases?per_page=50")
CANDIDATES=$(echo "$RELEASES" | jq -c '[.[] | select(.tag_name|startswith("tokens-")) | {tag:.tag_name, t:(.published_at // .created_at)}] | sort_by(.t)')

TOTAL=$(echo "$CANDIDATES" | jq 'length')
PROCESSED=0

for i in $(seq 0 $((TOTAL-1))); do
  TAG=$(echo "$CANDIDATES" | jq -r ".[$i].tag")
  T=$(echo "$CANDIDATES" | jq -r ".[$i].t")

  [ -z "$LAST" ] || [[ "$T" > "$LAST" ]] || continue

  MANIFEST_URL="https://github.com/${REPO}/releases/download/${TAG}/manifest.json"
  ASSET_URL="https://github.com/${REPO}/releases/download/${TAG}/tokens.zip.age"

  m="$INBOX/manifest-${TAG}.json"
  a="$INBOX/tokens-${TAG}.zip.age"
  curl -fsSL "$MANIFEST_URL" -o "$m"
  curl -fL "$ASSET_URL" -o "$a"

  EXPECTED=$(jq -r '.sha256' "$m")
  GOT=$(sha256sum "$a" | awk '{print $1}')
  [ "$EXPECTED" = "$GOT" ]

  zip="$WORK/tokens-${TAG}.zip"
  age -d -i "$AGE_IDENTITY" -o "$zip" "$a"

  out="$WORK/unzipped-${TAG}"
  rm -rf "$out" && mkdir -p "$out"
  unzip -o "$zip" -d "$out" >/dev/null

  cp -n "$out/codex"/*.json "$DEST"/ || true
  chmod 600 "$DEST"/*.json 2>/dev/null || true

  LAST="$T"
  PROCESSED=$((PROCESSED+1))
  [ "$PROCESSED" -ge "${MAX_PER_RUN:-20}" ] && break

done

jq -n --arg last "$LAST" '{last_published_at:$last}' > "$STATE"

全量检测:用真实请求探测 token 是否失效

同步只是“把文件拉下来”,维护的核心其实是“判断可用性”。最稳的方法是:对每个 token 做一次真实请求探测

一个常用的做法是请求 usage/quota 接口,例如:

GET https://chatgpt.com/backend-api/wham/usage

然后按 HTTP 状态码分类:

检测脚本要注意节流与超时:

#!/usr/bin/env bash
set -euo pipefail

AUTH_DIR="/opt/cli-proxy-plus/auths"
SLEEP_SEC="${SLEEP_SEC:-0.2}"
TIMEOUT_SEC="${TIMEOUT_SEC:-12}"

total=0; ok=0; invalid_401=0; no_quota=0; other=0

for f in "$AUTH_DIR"/*.json; do
  [ -f "$f" ] || continue
  total=$((total+1))

  # 这里的 token 读取方式按你 auth json 的字段来(示例用占位)
  token=$(jq -r '.access_token // empty' "$f" 2>/dev/null || true)
  [ -n "$token" ] || { other=$((other+1)); continue; }

  code=$(curl -sS -m "$TIMEOUT_SEC" -o /tmp/usage.$$ -w '%{http_code}' \
    -H "Authorization: Bearer $token" \
    "https://chatgpt.com/backend-api/wham/usage" || echo 000)

  case "$code" in
    200) ok=$((ok+1)) ;;
    401) invalid_401=$((invalid_401+1)); rm -f "$f" ;;
    402) no_quota=$((no_quota+1)) ;;
    *) other=$((other+1)) ;;
  esac

  sleep "$SLEEP_SEC"
done

echo "total=$total ok=$ok invalid_401=$invalid_401 no_quota=$no_quota other=$other"

最后:通过 Telegram 汇报(多行不截断)

TG 汇报建议只发统计,不传任何 token 内容。多行消息不截断的关键是:

MSG=$(cat <<EOF
[token-maintain hourly]
新增: ${NEW}
同步后总数: ${TOTAL}
检查结果: total=${TOTAL} ok=${OK} invalid_401=${BAD401} no_quota=${NOQ} other=${OTHER}
已删除401: ${BAD401} 剩余: ${REMAIN}
EOF
)

curl -s -X POST "https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage" \
  --data-urlencode "chat_id=${TG_CHAT_ID}" \
  --data-urlencode "text=${MSG}" \
  --data-urlencode "disable_web_page_preview=true" >/dev/null || true

这套流程跑通之后,你得到的是一种很“工程化”的稳定感:

如果你要把它进一步产品化,下一步通常是把“检测/删除策略”从硬编码变成配置,把异常(other/timeout)引入重试与隔离机制。但在大多数场景里,上面这个版本已经足够稳定、足够可维护。

#GitHubActions #age #Systemd #DevOps #Telegram #Security #Tokens


Share this post on:

Previous Post
Oracle ARM 上不死磕 PVE:我用 Incus routed 切出了两只能长期运维的小鸡
Next Post
OpenClaw 记忆归档、压缩、Reset、复健流程
🎵