Skip to content

Commit f9e168e

Browse files
authored
Merge pull request millylee#26 from millylee/feature/notify-limit
签到出错或账号中有余额变动才提醒
2 parents 47bbd29 + 1b51412 commit f9e168e

File tree

2 files changed

+199
-80
lines changed

2 files changed

+199
-80
lines changed

.github/workflows/checkin.yml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,21 @@ jobs:
2121
with:
2222
python-version: '3.11'
2323

24+
- name: 缓存 UV 依赖
25+
uses: actions/cache@v4
26+
id: uv-cache
27+
with:
28+
path: |
29+
~/.cache/uv
30+
.venv
31+
key: ${{ runner.os }}-uv-${{ hashFiles('pyproject.toml', 'uv.lock') }}
32+
restore-keys: |
33+
${{ runner.os }}-uv-
34+
2435
- name: 安装依赖
36+
if: steps.uv-cache.outputs.cache-hit != 'true'
2537
run: |
26-
echo "正在安装项目依赖..."
38+
echo "缓存未命中,开始安装项目依赖..."
2739
uv sync
2840
echo "✅ 环境初始化完成"
2941
@@ -49,6 +61,14 @@ jobs:
4961
echo "缓存未命中,开始安装 Playwright 浏览器..."
5062
uv run playwright install chromium --with-deps
5163
64+
- name: 恢复余额历史缓存
65+
uses: actions/cache@v4
66+
with:
67+
path: balance_hash.txt
68+
key: balance-hash-${{ github.sha }}
69+
restore-keys: |
70+
balance-hash-
71+
5272
- name: 执行签到
5373
env:
5474
ANYROUTER_ACCOUNTS: ${{ secrets.ANYROUTER_ACCOUNTS }}

checkin.py

Lines changed: 178 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55

66
import asyncio
7+
import hashlib
78
import json
89
import os
910
import sys
@@ -17,6 +18,8 @@
1718

1819
load_dotenv()
1920

21+
BALANCE_HASH_FILE = 'balance_hash.txt'
22+
2023

2124
def load_accounts():
2225
"""从环境变量加载多账号配置"""
@@ -48,6 +51,32 @@ def load_accounts():
4851
return None
4952

5053

54+
def load_balance_hash():
55+
"""加载余额hash"""
56+
try:
57+
if os.path.exists(BALANCE_HASH_FILE):
58+
with open(BALANCE_HASH_FILE, 'r', encoding='utf-8') as f:
59+
return f.read().strip()
60+
except Exception:
61+
pass
62+
return None
63+
64+
65+
def save_balance_hash(balance_hash):
66+
"""保存余额hash"""
67+
try:
68+
with open(BALANCE_HASH_FILE, 'w', encoding='utf-8') as f:
69+
f.write(balance_hash)
70+
except Exception as e:
71+
print(f'Warning: Failed to save balance hash: {e}')
72+
73+
74+
def generate_balance_hash(balances):
75+
"""生成余额数据的hash"""
76+
balance_json = json.dumps(balances, sort_keys=True, separators=(',', ':'))
77+
return hashlib.sha256(balance_json.encode('utf-8')).hexdigest()[:16]
78+
79+
5180
def parse_cookies(cookies_data):
5281
"""解析 cookies 数据"""
5382
if isinstance(cookies_data, dict):
@@ -68,59 +97,63 @@ async def get_waf_cookies_with_playwright(account_name: str):
6897
print(f'[PROCESSING] {account_name}: Starting browser to get WAF cookies...')
6998

7099
async with async_playwright() as p:
71-
context = await p.chromium.launch_persistent_context(
72-
user_data_dir=None,
73-
headless=False,
74-
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36',
75-
viewport={'width': 1920, 'height': 1080},
76-
args=[
77-
'--disable-blink-features=AutomationControlled',
78-
'--disable-dev-shm-usage',
79-
'--disable-web-security',
80-
'--disable-features=VizDisplayCompositor',
81-
'--no-sandbox',
82-
],
83-
)
84-
85-
page = await context.new_page()
100+
import tempfile
101+
with tempfile.TemporaryDirectory() as temp_dir:
102+
context = await p.chromium.launch_persistent_context(
103+
user_data_dir=temp_dir,
104+
headless=False,
105+
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36',
106+
viewport={'width': 1920, 'height': 1080},
107+
args=[
108+
'--disable-blink-features=AutomationControlled',
109+
'--disable-dev-shm-usage',
110+
'--disable-web-security',
111+
'--disable-features=VizDisplayCompositor',
112+
'--no-sandbox',
113+
],
114+
)
115+
116+
page = await context.new_page()
86117

87-
try:
88-
print(f'[PROCESSING] {account_name}: Step 1: Access login page to get initial cookies...')
118+
try:
119+
print(f'[PROCESSING] {account_name}: Step 1: Access login page to get initial cookies...')
89120

90-
await page.goto('https://anyrouter.top/login', wait_until='networkidle')
121+
await page.goto('https://anyrouter.top/login', wait_until='networkidle')
91122

92-
try:
93-
await page.wait_for_function('document.readyState === "complete"', timeout=5000)
94-
except Exception:
95-
await page.wait_for_timeout(3000)
123+
try:
124+
await page.wait_for_function('document.readyState === "complete"', timeout=5000)
125+
except Exception:
126+
await page.wait_for_timeout(3000)
96127

97-
cookies = await page.context.cookies()
128+
cookies = await page.context.cookies()
98129

99-
waf_cookies = {}
100-
for cookie in cookies:
101-
if cookie['name'] in ['acw_tc', 'cdn_sec_tc', 'acw_sc__v2']:
102-
waf_cookies[cookie['name']] = cookie['value']
130+
waf_cookies = {}
131+
for cookie in cookies:
132+
cookie_name = cookie.get('name')
133+
cookie_value = cookie.get('value')
134+
if cookie_name in ['acw_tc', 'cdn_sec_tc', 'acw_sc__v2'] and cookie_value is not None:
135+
waf_cookies[cookie_name] = cookie_value
103136

104-
print(f'[INFO] {account_name}: Got {len(waf_cookies)} WAF cookies after step 1')
137+
print(f'[INFO] {account_name}: Got {len(waf_cookies)} WAF cookies after step 1')
105138

106-
required_cookies = ['acw_tc', 'cdn_sec_tc', 'acw_sc__v2']
107-
missing_cookies = [c for c in required_cookies if c not in waf_cookies]
139+
required_cookies = ['acw_tc', 'cdn_sec_tc', 'acw_sc__v2']
140+
missing_cookies = [c for c in required_cookies if c not in waf_cookies]
108141

109-
if missing_cookies:
110-
print(f'[FAILED] {account_name}: Missing WAF cookies: {missing_cookies}')
111-
await context.close()
112-
return None
142+
if missing_cookies:
143+
print(f'[FAILED] {account_name}: Missing WAF cookies: {missing_cookies}')
144+
await context.close()
145+
return None
113146

114-
print(f'[SUCCESS] {account_name}: Successfully got all WAF cookies')
147+
print(f'[SUCCESS] {account_name}: Successfully got all WAF cookies')
115148

116-
await context.close()
149+
await context.close()
117150

118-
return waf_cookies
151+
return waf_cookies
119152

120-
except Exception as e:
121-
print(f'[FAILED] {account_name}: Error occurred while getting WAF cookies: {e}')
122-
await context.close()
123-
return None
153+
except Exception as e:
154+
print(f'[FAILED] {account_name}: Error occurred while getting WAF cookies: {e}')
155+
await context.close()
156+
return None
124157

125158

126159
def get_user_info(client, headers):
@@ -134,10 +167,15 @@ def get_user_info(client, headers):
134167
user_data = data.get('data', {})
135168
quota = round(user_data.get('quota', 0) / 500000, 2)
136169
used_quota = round(user_data.get('used_quota', 0) / 500000, 2)
137-
return f':money: Current balance: ${quota}, Used: ${used_quota}'
170+
return {
171+
'success': True,
172+
'quota': quota,
173+
'used_quota': used_quota,
174+
'display': f':money: Current balance: ${quota}, Used: ${used_quota}'
175+
}
176+
return {'success': False, 'error': f'Failed to get user info: HTTP {response.status_code}'}
138177
except Exception as e:
139-
return f'[FAIL] Failed to get user info: {str(e)[:50]}...'
140-
return None
178+
return {'success': False, 'error': f'Failed to get user info: {str(e)[:50]}...'}
141179

142180

143181
async def check_in_account(account_info, account_index):
@@ -187,12 +225,11 @@ async def check_in_account(account_info, account_index):
187225
'new-api-user': api_user,
188226
}
189227

190-
user_info_text = None
191-
192228
user_info = get_user_info(client, headers)
193-
if user_info:
194-
print(user_info)
195-
user_info_text = user_info
229+
if user_info and user_info.get('success'):
230+
print(user_info['display'])
231+
elif user_info:
232+
print(user_info.get('error', 'Unknown error'))
196233

197234
print(f'[NETWORK] {account_name}: Executing check-in')
198235

@@ -209,26 +246,26 @@ async def check_in_account(account_info, account_index):
209246
result = response.json()
210247
if result.get('ret') == 1 or result.get('code') == 0 or result.get('success'):
211248
print(f'[SUCCESS] {account_name}: Check-in successful!')
212-
return True, user_info_text
249+
return True, user_info
213250
else:
214251
error_msg = result.get('msg', result.get('message', 'Unknown error'))
215252
print(f'[FAILED] {account_name}: Check-in failed - {error_msg}')
216-
return False, user_info_text
253+
return False, user_info
217254
except json.JSONDecodeError:
218255
# 如果不是 JSON 响应,检查是否包含成功标识
219256
if 'success' in response.text.lower():
220257
print(f'[SUCCESS] {account_name}: Check-in successful!')
221-
return True, user_info_text
258+
return True, user_info
222259
else:
223260
print(f'[FAILED] {account_name}: Check-in failed - Invalid response format')
224-
return False, user_info_text
261+
return False, user_info
225262
else:
226263
print(f'[FAILED] {account_name}: Check-in failed - HTTP {response.status_code}')
227-
return False, user_info_text
264+
return False, user_info
228265

229266
except Exception as e:
230267
print(f'[FAILED] {account_name}: Error occurred during check-in process - {str(e)[:50]}...')
231-
return False, user_info_text
268+
return False, None
232269
finally:
233270
client.close()
234271

@@ -246,47 +283,109 @@ async def main():
246283

247284
print(f'[INFO] Found {len(accounts)} account configurations')
248285

286+
# 加载余额hash
287+
last_balance_hash = load_balance_hash()
288+
249289
# 为每个账号执行签到
250290
success_count = 0
251291
total_count = len(accounts)
252292
notification_content = []
293+
current_balances = {}
294+
need_notify = False # 是否需要发送通知
295+
balance_changed = False # 余额是否有变化
253296

254297
for i, account in enumerate(accounts):
298+
account_key = f'account_{i + 1}'
255299
try:
256300
success, user_info = await check_in_account(account, i)
257301
if success:
258302
success_count += 1
259-
# 收集通知内容
260-
status = '[SUCCESS]' if success else '[FAIL]'
261-
account_result = f'{status} Account {i + 1}'
262-
if user_info:
263-
account_result += f'\n{user_info}'
264-
notification_content.append(account_result)
303+
304+
# 检查是否需要通知
305+
should_notify_this_account = False
306+
307+
# 如果签到失败,需要通知
308+
if not success:
309+
should_notify_this_account = True
310+
need_notify = True
311+
print(f'[NOTIFY] Account {i + 1} failed, will send notification')
312+
313+
# 收集余额数据
314+
if user_info and user_info.get('success'):
315+
current_quota = user_info['quota']
316+
current_balances[account_key] = current_quota
317+
318+
# 只有需要通知的账号才收集内容
319+
if should_notify_this_account:
320+
status = '[SUCCESS]' if success else '[FAIL]'
321+
account_result = f'{status} Account {i + 1}'
322+
if user_info and user_info.get('success'):
323+
account_result += f'\n{user_info["display"]}'
324+
elif user_info:
325+
account_result += f'\n{user_info.get("error", "Unknown error")}'
326+
notification_content.append(account_result)
327+
265328
except Exception as e:
266329
print(f'[FAILED] Account {i + 1} processing exception: {e}')
330+
need_notify = True # 异常也需要通知
267331
notification_content.append(f'[FAIL] Account {i + 1} exception: {str(e)[:50]}...')
268332

269-
# 构建通知内容
270-
summary = [
271-
'[STATS] Check-in result statistics:',
272-
f'[SUCCESS] Success: {success_count}/{total_count}',
273-
f'[FAIL] Failed: {total_count - success_count}/{total_count}',
274-
]
275-
276-
if success_count == total_count:
277-
summary.append('[SUCCESS] All accounts check-in successful!')
278-
elif success_count > 0:
279-
summary.append('[WARN] Some accounts check-in successful')
280-
else:
281-
summary.append('[ERROR] All accounts check-in failed')
282-
283-
time_info = f'[TIME] Execution time: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}'
333+
# 检查余额变化
334+
current_balance_hash = generate_balance_hash(current_balances) if current_balances else None
335+
if current_balance_hash:
336+
if last_balance_hash is None:
337+
# 首次运行
338+
balance_changed = True
339+
need_notify = True
340+
print('[NOTIFY] First run detected, will send notification with current balances')
341+
elif current_balance_hash != last_balance_hash:
342+
# 余额有变化
343+
balance_changed = True
344+
need_notify = True
345+
print('[NOTIFY] Balance changes detected, will send notification')
346+
else:
347+
print('[INFO] No balance changes detected')
348+
349+
# 为有余额变化的情况添加所有成功账号到通知内容
350+
if balance_changed:
351+
for i, account in enumerate(accounts):
352+
account_key = f'account_{i + 1}'
353+
if account_key in current_balances:
354+
# 只添加成功获取余额的账号,且避免重复添加
355+
account_result = f'[BALANCE] Account {i + 1}'
356+
account_result += f'\n:money: Current balance: ${current_balances[account_key]}'
357+
# 检查是否已经在通知内容中(避免重复)
358+
if not any(f'Account {i + 1}' in item for item in notification_content):
359+
notification_content.append(account_result)
360+
361+
# 保存当前余额hash
362+
if current_balance_hash:
363+
save_balance_hash(current_balance_hash)
364+
365+
if need_notify and notification_content:
366+
# 构建通知内容
367+
summary = [
368+
'[STATS] Check-in result statistics:',
369+
f'[SUCCESS] Success: {success_count}/{total_count}',
370+
f'[FAIL] Failed: {total_count - success_count}/{total_count}',
371+
]
372+
373+
if success_count == total_count:
374+
summary.append('[SUCCESS] All accounts check-in successful!')
375+
elif success_count > 0:
376+
summary.append('[WARN] Some accounts check-in successful')
377+
else:
378+
summary.append('[ERROR] All accounts check-in failed')
284379

285-
notify_content = '\n\n'.join([time_info, '\n'.join(notification_content), '\n'.join(summary)])
380+
time_info = f'[TIME] Execution time: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}'
286381

287-
print(notify_content)
382+
notify_content = '\n\n'.join([time_info, '\n'.join(notification_content), '\n'.join(summary)])
288383

289-
notify.push_message('AnyRouter Check-in Results', notify_content, msg_type='text')
384+
print(notify_content)
385+
notify.push_message('AnyRouter Check-in Alert', notify_content, msg_type='text')
386+
print('[NOTIFY] Notification sent due to failures or balance changes')
387+
else:
388+
print('[INFO] All accounts successful and no balance changes detected, notification skipped')
290389

291390
# 设置退出码
292391
sys.exit(0 if success_count > 0 else 1)

0 commit comments

Comments
 (0)