4
4
"""
5
5
6
6
import asyncio
7
+ import hashlib
7
8
import json
8
9
import os
9
10
import sys
17
18
18
19
load_dotenv ()
19
20
21
+ BALANCE_HASH_FILE = 'balance_hash.txt'
22
+
20
23
21
24
def load_accounts ():
22
25
"""从环境变量加载多账号配置"""
@@ -48,6 +51,32 @@ def load_accounts():
48
51
return None
49
52
50
53
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
+
51
80
def parse_cookies (cookies_data ):
52
81
"""解析 cookies 数据"""
53
82
if isinstance (cookies_data , dict ):
@@ -68,59 +97,63 @@ async def get_waf_cookies_with_playwright(account_name: str):
68
97
print (f'[PROCESSING] { account_name } : Starting browser to get WAF cookies...' )
69
98
70
99
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 ()
86
117
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...' )
89
120
90
- await page .goto ('https://anyrouter.top/login' , wait_until = 'networkidle' )
121
+ await page .goto ('https://anyrouter.top/login' , wait_until = 'networkidle' )
91
122
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 )
96
127
97
- cookies = await page .context .cookies ()
128
+ cookies = await page .context .cookies ()
98
129
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
103
136
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' )
105
138
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 ]
108
141
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
113
146
114
- print (f'[SUCCESS] { account_name } : Successfully got all WAF cookies' )
147
+ print (f'[SUCCESS] { account_name } : Successfully got all WAF cookies' )
115
148
116
- await context .close ()
149
+ await context .close ()
117
150
118
- return waf_cookies
151
+ return waf_cookies
119
152
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
124
157
125
158
126
159
def get_user_info (client , headers ):
@@ -134,10 +167,15 @@ def get_user_info(client, headers):
134
167
user_data = data .get ('data' , {})
135
168
quota = round (user_data .get ('quota' , 0 ) / 500000 , 2 )
136
169
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 } ' }
138
177
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 ]} ...' }
141
179
142
180
143
181
async def check_in_account (account_info , account_index ):
@@ -187,12 +225,11 @@ async def check_in_account(account_info, account_index):
187
225
'new-api-user' : api_user ,
188
226
}
189
227
190
- user_info_text = None
191
-
192
228
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' ))
196
233
197
234
print (f'[NETWORK] { account_name } : Executing check-in' )
198
235
@@ -209,26 +246,26 @@ async def check_in_account(account_info, account_index):
209
246
result = response .json ()
210
247
if result .get ('ret' ) == 1 or result .get ('code' ) == 0 or result .get ('success' ):
211
248
print (f'[SUCCESS] { account_name } : Check-in successful!' )
212
- return True , user_info_text
249
+ return True , user_info
213
250
else :
214
251
error_msg = result .get ('msg' , result .get ('message' , 'Unknown error' ))
215
252
print (f'[FAILED] { account_name } : Check-in failed - { error_msg } ' )
216
- return False , user_info_text
253
+ return False , user_info
217
254
except json .JSONDecodeError :
218
255
# 如果不是 JSON 响应,检查是否包含成功标识
219
256
if 'success' in response .text .lower ():
220
257
print (f'[SUCCESS] { account_name } : Check-in successful!' )
221
- return True , user_info_text
258
+ return True , user_info
222
259
else :
223
260
print (f'[FAILED] { account_name } : Check-in failed - Invalid response format' )
224
- return False , user_info_text
261
+ return False , user_info
225
262
else :
226
263
print (f'[FAILED] { account_name } : Check-in failed - HTTP { response .status_code } ' )
227
- return False , user_info_text
264
+ return False , user_info
228
265
229
266
except Exception as e :
230
267
print (f'[FAILED] { account_name } : Error occurred during check-in process - { str (e )[:50 ]} ...' )
231
- return False , user_info_text
268
+ return False , None
232
269
finally :
233
270
client .close ()
234
271
@@ -246,47 +283,109 @@ async def main():
246
283
247
284
print (f'[INFO] Found { len (accounts )} account configurations' )
248
285
286
+ # 加载余额hash
287
+ last_balance_hash = load_balance_hash ()
288
+
249
289
# 为每个账号执行签到
250
290
success_count = 0
251
291
total_count = len (accounts )
252
292
notification_content = []
293
+ current_balances = {}
294
+ need_notify = False # 是否需要发送通知
295
+ balance_changed = False # 余额是否有变化
253
296
254
297
for i , account in enumerate (accounts ):
298
+ account_key = f'account_{ i + 1 } '
255
299
try :
256
300
success , user_info = await check_in_account (account , i )
257
301
if success :
258
302
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
+
265
328
except Exception as e :
266
329
print (f'[FAILED] Account { i + 1 } processing exception: { e } ' )
330
+ need_notify = True # 异常也需要通知
267
331
notification_content .append (f'[FAIL] Account { i + 1 } exception: { str (e )[:50 ]} ...' )
268
332
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' )
284
379
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" ) } '
286
381
287
- print ( notify_content )
382
+ notify_content = ' \n \n ' . join ([ time_info , ' \n ' . join ( notification_content ), ' \n ' . join ( summary )] )
288
383
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' )
290
389
291
390
# 设置退出码
292
391
sys .exit (0 if success_count > 0 else 1 )
0 commit comments