#!/usr/bin/env bash # login.sh — WorkBuddy CN OAuth 登录 → 落盘 auth 文件 # # 用法: # ./login.sh # # 流程: # 1. POST /v2/plugin/auth/state 拿授权 URL(无 PKCE,state 由服务端签发) # 2. 你在浏览器打开 URL 完成登录 # 3. 回到这里按 y → poll 拿 token+uid+nickname → 签到 → 落盘 auths/workbuddy-.json # 4. 重启 workbuddy2api 容器加载新账号 set -euo pipefail cd "$(dirname "$0")" AUTH_DIR="./auths" CONTAINER="workbuddy2api" mkdir -p "$AUTH_DIR" # login 工具:不存在才编译(源码改动后手动 go build -o login ./cmd/login) LOGIN_BIN="./login" if [[ ! -x "$LOGIN_BIN" ]]; then go build -o "$LOGIN_BIN" ./cmd/login fi echo "============================================================" echo " WorkBuddy OAuth 登录" echo "============================================================" echo "" AUTH_URL=$("$LOGIN_BIN" url) echo "请在浏览器中打开以下链接完成登录:" echo "" echo " $AUTH_URL" echo "" if command -v xclip &>/dev/null; then echo -n "$AUTH_URL" | xclip -selection clipboard 2>/dev/null && echo "(已复制到剪贴板)" elif command -v xsel &>/dev/null; then echo -n "$AUTH_URL" | xsel --clipboard 2>/dev/null && echo "(已复制到剪贴板)" fi echo "" read -rp "完成登录后按 y 继续: " ans if [[ "$ans" != "y" && "$ans" != "Y" ]]; then echo "已取消" exit 1 fi echo "" echo "正在获取 token..." RESULT=$("$LOGIN_BIN" poll) || { echo "" echo "获取 token 失败。可能原因:" echo " - 登录还没完成就按了 y(重新运行 ./login.sh 再试)" echo " - 登录页报错(把报错截图发出来排查)" exit 1 } TOKEN=$(echo "$RESULT" | python3 -c "import json,sys; print(json.load(sys.stdin)['access_token'])") REFRESH=$(echo "$RESULT" | python3 -c "import json,sys; print(json.load(sys.stdin)['refresh_token'])") EXPIRES_IN=$(echo "$RESULT" | python3 -c "import json,sys; print(json.load(sys.stdin)['expires_in'])") DOMAIN=$(echo "$RESULT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('domain',''))") USER_ID=$(echo "$RESULT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('uid',''))") ENT_ID=$(echo "$RESULT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('enterprise_id',''))") NICKNAME=$(echo "$RESULT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('nickname',''))") if [[ -z "$USER_ID" ]]; then echo "无法获取 uid,请检查 token 是否有效" exit 1 fi EXPIRES_AT=$(( $(date +%s) + EXPIRES_IN )) # ─── 签到(CN:POST codebuddy.cn/v2/billing/meter/daily-checkin,幂等不阻塞)─── # OAuth 返回字段一律通过环境变量传入 Python,并用带引号 heredoc: # 昵称/domain/token 等值可含引号或换行,拼进 Python 源码会造成注入。 WB2A_LOGIN_TOKEN="$TOKEN" \ WB2A_LOGIN_USER_ID="$USER_ID" \ WB2A_LOGIN_ENT_ID="$ENT_ID" \ WB2A_LOGIN_DOMAIN="$DOMAIN" \ python3 - <<'PYEOF' import json, os, urllib.request, urllib.error token = os.environ["WB2A_LOGIN_TOKEN"] user_id = os.environ["WB2A_LOGIN_USER_ID"] ent_id = os.environ["WB2A_LOGIN_ENT_ID"] domain = os.environ["WB2A_LOGIN_DOMAIN"] req = urllib.request.Request( "https://www.codebuddy.cn/v2/billing/meter/daily-checkin", method="POST", data=b"{}", headers={ "Authorization": "Bearer " + token, "Accept": "application/json", "Content-Type": "application/json", "X-User-Id": user_id, **({"X-Enterprise-Id": ent_id, "X-Tenant-Id": ent_id} if ent_id else {}), **({"X-Domain": domain} if domain else {}), }) try: with urllib.request.urlopen(req, timeout=15) as r: body = json.loads(r.read().decode() or "{}") if body.get("code") == 0: data = body.get("data") or {} print(f"签到: 成功 {json.dumps(data, ensure_ascii=False)[:150]}") else: print(f"签到: {body.get('msg', json.dumps(body)[:150])}") except urllib.error.HTTPError as e: # 已签到等业务错误也走 4xx(实测 code=10001 "今天已签到") try: body = json.loads(e.read().decode() or "{}") print(f"签到: {body.get('msg', 'http %d' % e.code)}") except Exception: print(f"签到: http {e.code}") except Exception as e: print(f"签到: {e}") PYEOF # ─── 落盘 auth 文件(与 internal/auth 读取格式一致)───────────────── AUTH_FILE="$AUTH_DIR/workbuddy-${USER_ID}.json" if [[ -f "$AUTH_FILE" ]]; then echo "账号已存在(uid=${USER_ID}),将覆盖更新凭证" ACTION="覆盖" else echo "新账号(uid=${USER_ID}),新增 auth 文件" ACTION="新增" fi WB2A_LOGIN_TOKEN="$TOKEN" \ WB2A_LOGIN_REFRESH="$REFRESH" \ WB2A_LOGIN_EXPIRES_AT="$EXPIRES_AT" \ WB2A_LOGIN_DOMAIN="$DOMAIN" \ WB2A_LOGIN_USER_ID="$USER_ID" \ WB2A_LOGIN_ENT_ID="$ENT_ID" \ WB2A_LOGIN_NICKNAME="$NICKNAME" \ WB2A_LOGIN_AUTH_FILE="$AUTH_FILE" \ WB2A_LOGIN_ACTION="$ACTION" \ python3 - <<'PYEOF' import json, os auth = { "account": { "uid": os.environ["WB2A_LOGIN_USER_ID"], "enterpriseId": os.environ["WB2A_LOGIN_ENT_ID"], "nickname": os.environ["WB2A_LOGIN_NICKNAME"], }, "auth": { "accessToken": os.environ["WB2A_LOGIN_TOKEN"], "refreshToken": os.environ["WB2A_LOGIN_REFRESH"], "expiresAt": int(os.environ["WB2A_LOGIN_EXPIRES_AT"]), "domain": os.environ["WB2A_LOGIN_DOMAIN"], }, } with open(os.environ["WB2A_LOGIN_AUTH_FILE"], "w") as f: json.dump(auth, f, indent=1) print(f"已保存({os.environ['WB2A_LOGIN_ACTION']}): {os.environ['WB2A_LOGIN_AUTH_FILE']}") PYEOF # ─── 重启服务 ──────────────────────────────────────────── echo "" if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER}$"; then echo "重启 $CONTAINER 加载新账号..." docker restart "$CONTAINER" >/dev/null sleep 2 # API_KEY 从 config.json 读取(该变量在脚本中未定义,fallback 仅为占位,不会通过鉴权) API_KEY=$(python3 -c "import json; print(json.load(open('config.json')).get('api_key',''))" 2>/dev/null) COUNT=$(curl -s http://127.0.0.1:7863/status -H "Authorization: Bearer ${API_KEY:-test_key}" 2>/dev/null | python3 -c "import json,sys; print(len(json.load(sys.stdin).get('accounts',[])))" 2>/dev/null || echo "?") echo "服务已重启,当前账号数: $COUNT" else echo "容器 $CONTAINER 未运行,auth 文件已保存,下次启动自动加载" fi echo "" echo "============================================================" echo " 登录完成!" echo " UID: $USER_ID" echo " Nickname: ${NICKNAME:-(未获取到)}" echo " Token: ${TOKEN:0:30}..." echo " 有效期: $(date -d "@$EXPIRES_AT" '+%Y-%m-%d %H:%M' 2>/dev/null || echo "$EXPIRES_AT")" echo "============================================================"