├── 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 | 
5 |
6 |
7 | ## ⚠️ 注意
8 | - 如果设置了2FA验证的,请改为Mobile优先验证
9 | - 首次运行:可能需要设备验证,收到 TG 通知后 30 秒内批准
10 | - REPO_TOKEN:需要有 Secrets 写入权限才能自动更新
11 | - Cookie 有效期:每次运行都会更新,保持最新
12 | 
13 |
14 | ### 设置Mobile优先
15 | 
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 |
--------------------------------------------------------------------------------