├── 1.png ├── 2.png ├── 3.png ├── .github └── workflows │ └── keep-alive.yml ├── README.md └── scripts └── auto_login.py /1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oyz8/ClawCloud-Run/HEAD/1.png -------------------------------------------------------------------------------- /2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oyz8/ClawCloud-Run/HEAD/2.png -------------------------------------------------------------------------------- /3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oyz8/ClawCloud-Run/HEAD/3.png -------------------------------------------------------------------------------- /.github/workflows/keep-alive.yml: -------------------------------------------------------------------------------- 1 | name: ClawCloud 自动登录保活 2 | 3 | on: 4 | schedule: 5 | - cron: '0 6 */5 * *' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | auto-login: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 15 12 | 13 | steps: 14 | - name: 检出代码 15 | uses: actions/checkout@v4 16 | 17 | - name: 设置 Python 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: '3.11' 21 | 22 | - name: 安装依赖 23 | run: | 24 | pip install playwright requests pynacl 25 | playwright install chromium 26 | playwright install-deps 27 | 28 | - name: 运行自动登录 29 | env: 30 | GH_USERNAME: ${{ secrets.GH_USERNAME }} 31 | GH_PASSWORD: ${{ secrets.GH_PASSWORD }} 32 | GH_SESSION: ${{ secrets.GH_SESSION }} 33 | TG_BOT_TOKEN: ${{ secrets.TG_BOT_TOKEN }} 34 | TG_CHAT_ID: ${{ secrets.TG_CHAT_ID }} 35 | REPO_TOKEN: ${{ secrets.REPO_TOKEN }} 36 | GITHUB_REPOSITORY: ${{ github.repository }} 37 | run: python scripts/auto_login.py 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⭐ Star 星星走起 动动发财手点点⭐Star 2 | 3 | 4 | ![设备验证](./3.png) 5 | 6 | 7 | ## ⚠️ 注意 8 | - 如果设置了2FA验证的,请改为Mobile优先验证 9 | - 首次运行:可能需要设备验证,收到 TG 通知后 30 秒内批准 10 | - REPO_TOKEN:需要有 Secrets 写入权限才能自动更新 11 | - Cookie 有效期:每次运行都会更新,保持最新 12 | ![设备验证](./1.png) 13 | 14 | ### 设置Mobile优先 15 | ![设置Mobile优先验证](./2.png) 16 | 17 | --- 18 | 19 | ## Secrets 配置 20 | | Secret 名称 | 说明 | 21 | | --- | --- | 22 | | `GH_USERNAME` | GitHub 用户名 | 23 | | `GH_PASSWORD` | GitHub 密码 | 24 | | `GH_SESSION` | Cookie 不用添加自动生成 | 25 | | `TG_BOT_TOKEN` | Telegram Bot Token | 26 | | `TG_CHAT_ID` | Telegram Chat ID | 27 | | `REPO_TOKEN` | GitHub Token(用于自动更新 Secret) | 28 | 29 | --- 30 | 31 | ## 快速开始 32 | 33 | ### 1. Fork 仓库 34 | 35 | 点击右上角 **Fork** 按钮 36 | 37 | ### 2. 配置 Secrets 38 | 39 | 进入 **Settings** → **Secrets and variables** → **Actions**,添加: 40 | 41 | **必需:** 42 | - `GH_USERNAME`: GitHub 用户名 43 | - `GH_PASSWORD`: GitHub 密码 44 | 45 | **推荐:** 46 | - `TG_BOT_TOKEN`: Telegram Bot Token 47 | - `TG_CHAT_ID`: Telegram Chat ID 48 | - `REPO_TOKEN`: GitHub Personal Access Token (自动更新 Cookie) 49 | 50 | ### 3. 启用 Actions 51 | 52 | 进入 **Actions** → 点击 **I understand my workflows** 53 | 54 | ### 4. 手动测试 55 | 56 | 选择 **Auto Login** workflow → **Run workflow** 57 | 58 | --- 59 | ## 流程图 60 | ``` 61 | 开始 62 | ↓ 63 | 加载已保存的 Cookie(如果有) 64 | ↓ 65 | 访问 ClawCloud 66 | ↓ 67 | 已登录? ─是→ 保活 → 提取新 Cookie → 保存 → 完成 68 | ↓否 69 | 点击 GitHub 登录 70 | ↓ 71 | Cookie 有效? ─是→ 直接 OAuth 授权 72 | ↓否 73 | 输入用户名密码 74 | ↓ 75 | 需要设备验证? ─是→ 发送 TG 通知 → 等待 30 秒 76 | ↓ 77 | 登录成功 78 | ↓ 79 | OAuth 授权 80 | ↓ 81 | 重定向到 ClawCloud 82 | ↓ 83 | 保活(访问控制台、应用页面) 84 | ↓ 85 | 提取新的 Session Cookie 86 | ↓ 87 | 自动更新 GH_SESSION Secret 88 | ↓ 89 | 发送 Telegram 通知 90 | ↓ 91 | 完成 ✅ 92 | 93 | ``` 94 | -------------------------------------------------------------------------------- /scripts/auto_login.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | ClawCloud 自动登录脚本 4 | - 等待设备验证批准(30秒) 5 | - 每次登录后自动更新 Cookie 6 | - Telegram 通知 7 | """ 8 | 9 | import os 10 | import sys 11 | import time 12 | import base64 13 | import requests 14 | from playwright.sync_api import sync_playwright 15 | 16 | # ==================== 配置 ==================== 17 | CLAW_CLOUD_URL = "https://eu-central-1.run.claw.cloud" 18 | SIGNIN_URL = f"{CLAW_CLOUD_URL}/signin" 19 | DEVICE_VERIFY_WAIT = 30 20 | 21 | 22 | class Telegram: 23 | """Telegram 通知""" 24 | 25 | def __init__(self): 26 | self.token = os.environ.get('TG_BOT_TOKEN') 27 | self.chat_id = os.environ.get('TG_CHAT_ID') 28 | self.ok = bool(self.token and self.chat_id) 29 | 30 | def send(self, msg): 31 | if not self.ok: 32 | return 33 | try: 34 | requests.post( 35 | f"https://api.telegram.org/bot{self.token}/sendMessage", 36 | data={"chat_id": self.chat_id, "text": msg, "parse_mode": "HTML"}, 37 | timeout=30 38 | ) 39 | except: 40 | pass 41 | 42 | def photo(self, path, caption=""): 43 | if not self.ok or not os.path.exists(path): 44 | return 45 | try: 46 | with open(path, 'rb') as f: 47 | requests.post( 48 | f"https://api.telegram.org/bot{self.token}/sendPhoto", 49 | data={"chat_id": self.chat_id, "caption": caption[:1024]}, 50 | files={"photo": f}, 51 | timeout=60 52 | ) 53 | except: 54 | pass 55 | 56 | 57 | class SecretUpdater: 58 | """GitHub Secret 更新器""" 59 | 60 | def __init__(self): 61 | self.token = os.environ.get('REPO_TOKEN') 62 | self.repo = os.environ.get('GITHUB_REPOSITORY') 63 | self.ok = bool(self.token and self.repo) 64 | if self.ok: 65 | print("✅ Secret 自动更新已启用") 66 | else: 67 | print("⚠️ Secret 自动更新未启用(需要 REPO_TOKEN)") 68 | 69 | def update(self, name, value): 70 | if not self.ok: 71 | return False 72 | try: 73 | from nacl import encoding, public 74 | 75 | headers = { 76 | "Authorization": f"token {self.token}", 77 | "Accept": "application/vnd.github.v3+json" 78 | } 79 | 80 | # 获取公钥 81 | r = requests.get( 82 | f"https://api.github.com/repos/{self.repo}/actions/secrets/public-key", 83 | headers=headers, timeout=30 84 | ) 85 | if r.status_code != 200: 86 | return False 87 | 88 | key_data = r.json() 89 | pk = public.PublicKey(key_data['key'].encode(), encoding.Base64Encoder()) 90 | encrypted = public.SealedBox(pk).encrypt(value.encode()) 91 | 92 | # 更新 Secret 93 | r = requests.put( 94 | f"https://api.github.com/repos/{self.repo}/actions/secrets/{name}", 95 | headers=headers, 96 | json={"encrypted_value": base64.b64encode(encrypted).decode(), "key_id": key_data['key_id']}, 97 | timeout=30 98 | ) 99 | return r.status_code in [201, 204] 100 | except Exception as e: 101 | print(f"更新 Secret 失败: {e}") 102 | return False 103 | 104 | 105 | class AutoLogin: 106 | """自动登录""" 107 | 108 | def __init__(self): 109 | self.username = os.environ.get('GH_USERNAME') 110 | self.password = os.environ.get('GH_PASSWORD') 111 | self.gh_session = os.environ.get('GH_SESSION', '').strip() 112 | self.tg = Telegram() 113 | self.secret = SecretUpdater() 114 | self.shots = [] 115 | self.logs = [] 116 | self.n = 0 117 | 118 | def log(self, msg, level="INFO"): 119 | icons = {"INFO": "ℹ️", "SUCCESS": "✅", "ERROR": "❌", "WARN": "⚠️", "STEP": "🔹"} 120 | line = f"{icons.get(level, '•')} {msg}" 121 | print(line) 122 | self.logs.append(line) 123 | 124 | def shot(self, page, name): 125 | self.n += 1 126 | f = f"{self.n:02d}_{name}.png" 127 | try: 128 | page.screenshot(path=f) 129 | self.shots.append(f) 130 | except: 131 | pass 132 | return f 133 | 134 | def click(self, page, sels, desc=""): 135 | for s in sels: 136 | try: 137 | el = page.locator(s).first 138 | if el.is_visible(timeout=3000): 139 | el.click() 140 | self.log(f"已点击: {desc}", "SUCCESS") 141 | return True 142 | except: 143 | pass 144 | return False 145 | 146 | def get_session(self, context): 147 | """提取 Session Cookie""" 148 | try: 149 | for c in context.cookies(): 150 | if c['name'] == 'user_session' and 'github' in c.get('domain', ''): 151 | return c['value'] 152 | except: 153 | pass 154 | return None 155 | 156 | def save_cookie(self, value): 157 | """保存新 Cookie""" 158 | if not value: 159 | return 160 | 161 | self.log(f"新 Cookie: {value[:15]}...{value[-8:]}", "SUCCESS") 162 | 163 | # 自动更新 Secret 164 | if self.secret.update('GH_SESSION', value): 165 | self.log("已自动更新 GH_SESSION", "SUCCESS") 166 | self.tg.send("🔑 Cookie 已自动更新\n\nGH_SESSION 已保存") 167 | else: 168 | # 通过 Telegram 发送 169 | self.tg.send(f"""🔑 新 Cookie 170 | 171 | 请更新 Secret GH_SESSION: 172 | {value}""") 173 | self.log("已通过 Telegram 发送 Cookie", "SUCCESS") 174 | 175 | def wait_device(self, page): 176 | """等待设备验证""" 177 | self.log(f"需要设备验证,等待 {DEVICE_VERIFY_WAIT} 秒...", "WARN") 178 | self.shot(page, "设备验证") 179 | 180 | self.tg.send(f"""⚠️ 需要设备验证 181 | 182 | 请在 {DEVICE_VERIFY_WAIT} 秒内批准: 183 | 1️⃣ 检查邮箱点击链接 184 | 2️⃣ 或在 GitHub App 批准""") 185 | 186 | if self.shots: 187 | self.tg.photo(self.shots[-1], "设备验证页面") 188 | 189 | for i in range(DEVICE_VERIFY_WAIT): 190 | time.sleep(1) 191 | if i % 5 == 0: 192 | self.log(f" 等待... ({i}/{DEVICE_VERIFY_WAIT}秒)") 193 | url = page.url 194 | if 'verified-device' not in url and 'device-verification' not in url: 195 | self.log("设备验证通过!", "SUCCESS") 196 | self.tg.send("✅ 设备验证通过") 197 | return True 198 | try: 199 | page.reload(timeout=10000) 200 | page.wait_for_load_state('networkidle', timeout=10000) 201 | except: 202 | pass 203 | 204 | if 'verified-device' not in page.url: 205 | return True 206 | 207 | self.log("设备验证超时", "ERROR") 208 | self.tg.send("❌ 设备验证超时") 209 | return False 210 | 211 | def login_github(self, page, context): 212 | """登录 GitHub""" 213 | self.log("登录 GitHub...", "STEP") 214 | self.shot(page, "github_登录页") 215 | 216 | try: 217 | page.locator('input[name="login"]').fill(self.username) 218 | page.locator('input[name="password"]').fill(self.password) 219 | self.log("已输入凭据") 220 | except Exception as e: 221 | self.log(f"输入失败: {e}", "ERROR") 222 | return False 223 | 224 | self.shot(page, "github_已填写") 225 | 226 | try: 227 | page.locator('input[type="submit"], button[type="submit"]').first.click() 228 | except: 229 | pass 230 | 231 | time.sleep(3) 232 | page.wait_for_load_state('networkidle', timeout=30000) 233 | self.shot(page, "github_登录后") 234 | 235 | url = page.url 236 | self.log(f"当前: {url}") 237 | 238 | # 设备验证 239 | if 'verified-device' in url or 'device-verification' in url: 240 | if not self.wait_device(page): 241 | return False 242 | time.sleep(2) 243 | page.wait_for_load_state('networkidle', timeout=30000) 244 | self.shot(page, "验证后") 245 | 246 | # 2FA 247 | if 'two-factor' in page.url: 248 | self.log("需要两步验证!", "ERROR") 249 | self.tg.send("❌ 需要两步验证") 250 | return False 251 | 252 | # 错误 253 | try: 254 | err = page.locator('.flash-error').first 255 | if err.is_visible(timeout=2000): 256 | self.log(f"错误: {err.inner_text()}", "ERROR") 257 | return False 258 | except: 259 | pass 260 | 261 | return True 262 | 263 | def oauth(self, page): 264 | """处理 OAuth""" 265 | if 'github.com/login/oauth/authorize' in page.url: 266 | self.log("处理 OAuth...", "STEP") 267 | self.shot(page, "oauth") 268 | self.click(page, ['button[name="authorize"]', 'button:has-text("Authorize")'], "授权") 269 | time.sleep(3) 270 | page.wait_for_load_state('networkidle', timeout=30000) 271 | 272 | def wait_redirect(self, page, wait=60): 273 | """等待重定向""" 274 | self.log("等待重定向...", "STEP") 275 | for i in range(wait): 276 | url = page.url 277 | if 'claw.cloud' in url and 'signin' not in url.lower(): 278 | self.log("重定向成功!", "SUCCESS") 279 | return True 280 | if 'github.com/login/oauth/authorize' in url: 281 | self.oauth(page) 282 | time.sleep(1) 283 | if i % 10 == 0: 284 | self.log(f" 等待... ({i}秒)") 285 | self.log("重定向超时", "ERROR") 286 | return False 287 | 288 | def keepalive(self, page): 289 | """保活""" 290 | self.log("保活...", "STEP") 291 | for url, name in [(f"{CLAW_CLOUD_URL}/", "控制台"), (f"{CLAW_CLOUD_URL}/apps", "应用")]: 292 | try: 293 | page.goto(url, timeout=30000) 294 | page.wait_for_load_state('networkidle', timeout=15000) 295 | self.log(f"已访问: {name}", "SUCCESS") 296 | time.sleep(2) 297 | except: 298 | pass 299 | self.shot(page, "完成") 300 | 301 | def notify(self, ok, err=""): 302 | if not self.tg.ok: 303 | return 304 | 305 | msg = f"""🤖 ClawCloud 自动登录 306 | 307 | 状态: {"✅ 成功" if ok else "❌ 失败"} 308 | 用户: {self.username} 309 | 时间: {time.strftime('%Y-%m-%d %H:%M:%S')}""" 310 | 311 | if err: 312 | msg += f"\n错误: {err}" 313 | 314 | msg += "\n\n日志:\n" + "\n".join(self.logs[-6:]) 315 | 316 | self.tg.send(msg) 317 | 318 | if self.shots: 319 | if not ok: 320 | for s in self.shots[-3:]: 321 | self.tg.photo(s, s) 322 | else: 323 | self.tg.photo(self.shots[-1], "完成") 324 | 325 | def run(self): 326 | print("\n" + "="*50) 327 | print("🚀 ClawCloud 自动登录") 328 | print("="*50 + "\n") 329 | 330 | self.log(f"用户名: {self.username}") 331 | self.log(f"Session: {'有' if self.gh_session else '无'}") 332 | self.log(f"密码: {'有' if self.password else '无'}") 333 | 334 | if not self.username or not self.password: 335 | self.log("缺少凭据", "ERROR") 336 | self.notify(False, "凭据未配置") 337 | sys.exit(1) 338 | 339 | with sync_playwright() as p: 340 | browser = p.chromium.launch(headless=True, args=['--no-sandbox']) 341 | context = browser.new_context( 342 | viewport={'width': 1920, 'height': 1080}, 343 | user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' 344 | ) 345 | page = context.new_page() 346 | 347 | try: 348 | # 预加载 Cookie 349 | if self.gh_session: 350 | try: 351 | context.add_cookies([ 352 | {'name': 'user_session', 'value': self.gh_session, 'domain': 'github.com', 'path': '/'}, 353 | {'name': 'logged_in', 'value': 'yes', 'domain': 'github.com', 'path': '/'} 354 | ]) 355 | self.log("已加载 Session Cookie", "SUCCESS") 356 | except: 357 | self.log("加载 Cookie 失败", "WARN") 358 | 359 | # 1. 访问 ClawCloud 360 | self.log("步骤1: 打开 ClawCloud", "STEP") 361 | page.goto(SIGNIN_URL, timeout=60000) 362 | page.wait_for_load_state('networkidle', timeout=30000) 363 | time.sleep(2) 364 | self.shot(page, "clawcloud") 365 | 366 | if 'signin' not in page.url.lower(): 367 | self.log("已登录!", "SUCCESS") 368 | self.keepalive(page) 369 | # 提取并保存新 Cookie 370 | new = self.get_session(context) 371 | if new: 372 | self.save_cookie(new) 373 | self.notify(True) 374 | print("\n✅ 成功!\n") 375 | return 376 | 377 | # 2. 点击 GitHub 378 | self.log("步骤2: 点击 GitHub", "STEP") 379 | if not self.click(page, [ 380 | 'button:has-text("GitHub")', 381 | 'a:has-text("GitHub")', 382 | '[data-provider="github"]' 383 | ], "GitHub"): 384 | self.log("找不到按钮", "ERROR") 385 | self.notify(False, "找不到 GitHub 按钮") 386 | sys.exit(1) 387 | 388 | time.sleep(3) 389 | page.wait_for_load_state('networkidle', timeout=30000) 390 | self.shot(page, "点击后") 391 | 392 | url = page.url 393 | self.log(f"当前: {url}") 394 | 395 | # 3. GitHub 登录 396 | self.log("步骤3: GitHub 认证", "STEP") 397 | 398 | if 'github.com/login' in url or 'github.com/session' in url: 399 | if not self.login_github(page, context): 400 | self.shot(page, "登录失败") 401 | self.notify(False, "GitHub 登录失败") 402 | sys.exit(1) 403 | elif 'github.com/login/oauth/authorize' in url: 404 | self.log("Cookie 有效", "SUCCESS") 405 | self.oauth(page) 406 | 407 | # 4. 等待重定向 408 | self.log("步骤4: 等待重定向", "STEP") 409 | if not self.wait_redirect(page): 410 | self.shot(page, "重定向失败") 411 | self.notify(False, "重定向失败") 412 | sys.exit(1) 413 | 414 | self.shot(page, "重定向成功") 415 | 416 | # 5. 验证 417 | self.log("步骤5: 验证", "STEP") 418 | if 'claw.cloud' not in page.url or 'signin' in page.url.lower(): 419 | self.notify(False, "验证失败") 420 | sys.exit(1) 421 | 422 | # 6. 保活 423 | self.keepalive(page) 424 | 425 | # 7. 提取并保存新 Cookie 426 | self.log("步骤6: 更新 Cookie", "STEP") 427 | new = self.get_session(context) 428 | if new: 429 | self.save_cookie(new) 430 | else: 431 | self.log("未获取到新 Cookie", "WARN") 432 | 433 | self.notify(True) 434 | print("\n" + "="*50) 435 | print("✅ 成功!") 436 | print("="*50 + "\n") 437 | 438 | except Exception as e: 439 | self.log(f"异常: {e}", "ERROR") 440 | self.shot(page, "异常") 441 | import traceback 442 | traceback.print_exc() 443 | self.notify(False, str(e)) 444 | sys.exit(1) 445 | finally: 446 | browser.close() 447 | 448 | 449 | if __name__ == "__main__": 450 | AutoLogin().run() 451 | --------------------------------------------------------------------------------