Jelajahi Sumber

feat: update

jerry 1 Minggu lalu
induk
melakukan
1818958ff7
8 mengubah file dengan 147 tambahan dan 137 penghapusan
  1. 15 13
      booker_builtin.py
  2. 17 16
      booker_order.py
  3. 2 2
      config/config.json
  4. 65 0
      configure.py
  5. 7 4
      plugins/tls_plugin.py
  6. 9 8
      sentinel.py
  7. 13 64
      toolkit/vs_cloud_api.py
  8. 19 30
      tools/clash_api.py

+ 15 - 13
booker_builtin.py

@@ -6,6 +6,7 @@ import random
 import redis
 from typing import List, Dict, Callable
 
+import configure
 from vs_types import GroupConfig, VSPlgConfig, Task, VSQueryResult, AppointmentType
 from vs_plg_factory import VSPlgFactory 
 from toolkit.thread_pool import ThreadPool 
@@ -25,7 +26,8 @@ class BuiltinBookerGCO:
         self.m_tasks: List[Task] = []
         self.m_lock = threading.RLock()
         self.m_stop_event = threading.Event()
-        self.redis_client = redis.Redis(**redis_conf)
+        self.redis_com = redis.Redis(**redis_conf)
+        self.redis_sub = redis.Redis(**redis_conf)
         self.m_pending_builtin = 0
         
         self.m_tracker_key = f"vs:worker:tasks_tracker:{self.m_cfg.identifier}"
@@ -142,7 +144,7 @@ class BuiltinBookerGCO:
         while not self.m_stop_event.is_set():
             try:
                 if pubsub is None:
-                    pubsub = self.redis_client.pubsub(ignore_subscribe_messages=False)
+                    pubsub = self.redis_sub.pubsub(ignore_subscribe_messages=False)
                     channels_to_sub = list(channel_to_routing_key.keys())
                     self._log(f"⏳ Sending SUBSCRIBE command to Redis for: {channels_to_sub}")
                     pubsub.subscribe(*channels_to_sub)
@@ -212,12 +214,12 @@ class BuiltinBookerGCO:
         is_rate_limited = False
         
         try:
-            task_data = VSCloudApi.Instance().get_vas_task_pop(queue_name, test=False)
+            task_data = VSCloudApi.Instance().get_vas_task_pop(queue_name)
             if not task_data:
                 return 
             task_id = task_data['id']
             order_id = task_data.get('order_id')
-            self.redis_client.zadd(self.m_tracker_key, {str(task_id): time.time() + self.heartbeat_ttl})
+            self.redis_com.zadd(self.m_tracker_key, {str(task_id): time.time() + self.heartbeat_ttl})
                         
             user_input = task_data.get('user_inputs', {})
             book_res = task.instance.book(query_result, user_input)
@@ -248,7 +250,7 @@ class BuiltinBookerGCO:
                     f"━━━━━━━━━━━━━━━\n"
                 )
                 VSCloudApi.Instance().push_weixin_text(push_content)
-                self.redis_client.zrem(self.m_tracker_key, task_id)
+                self.redis_com.zrem(self.m_tracker_key, task_id)
                 
                 # === 核心:成功次数判断 ===
                 task.successful_bookings += 1
@@ -281,11 +283,11 @@ class BuiltinBookerGCO:
                         
                     t_cd = self.task_backoff.calculate(t_fails)
                     self._log(f"⏳ Task={task_id} (Booking Attempt {t_fails}) suspended for {t_cd:.1f}s.")
-                    self.redis_client.zadd(self.m_tracker_key, {str(task_id): time.time() + t_cd})
+                    self.redis_com.zadd(self.m_tracker_key, {str(task_id): time.time() + t_cd})
             
         finally:
            if not booking_success and task_id is not None and not is_rate_limited:
-                self.redis_client.zadd(self.m_tracker_key, {str(task_id): 0})
+                self.redis_com.zadd(self.m_tracker_key, {str(task_id): 0})
                 self._log(f"♻️ Task={task_id} normal failure. Instantly handed over to Sweeper.")
                 
     def _creator_loop(self):
@@ -294,7 +296,7 @@ class BuiltinBookerGCO:
         group_cd_key = f"vs:group:cooldown:{self.m_cfg.identifier}"
         while not self.m_stop_event.is_set():
             time.sleep(2.0)
-            if self.redis_client.exists(group_cd_key):
+            if self.redis_com.exists(group_cd_key):
                 continue
             with self.m_lock:
                 current = len(self.m_tasks)
@@ -318,13 +320,13 @@ class BuiltinBookerGCO:
                 plg_cfg.session_max_life = self.m_cfg.session_max_life
 
                 if self.m_cfg.need_account:
-                    acc = VSCloudApi.Instance().get_next_account(self.m_cfg.booker.account_pool_id, self.m_cfg.booker.account_cd, test=False)
+                    acc = VSCloudApi.Instance().get_next_account(self.m_cfg.booker.account_pool_id, self.m_cfg.booker.account_cd)
                     plg_cfg.account.id = acc['id']
                     plg_cfg.account.username = acc['username']
                     plg_cfg.account.password = acc['password']
 
                 if self.m_cfg.need_proxy:
-                    proxy = VSCloudApi.Instance().get_next_proxy(self.m_cfg.proxy_pool, self.m_cfg.proxy_cd, test=False)
+                    proxy = VSCloudApi.Instance().get_next_proxy(self.m_cfg.proxy_pool, self.m_cfg.proxy_cd)
                     plg_cfg.proxy.id = proxy['id']
                     plg_cfg.proxy.ip = proxy['ip']
                     plg_cfg.proxy.port = proxy['port']
@@ -353,7 +355,7 @@ class BuiltinBookerGCO:
                     )
                     
                     group_fail_key = f"vs:group:failures:{self.m_cfg.identifier}"
-                    self.redis_client.delete(group_fail_key)
+                    self.redis_com.delete(group_fail_key)
                     
                 self._log(f"+++ Built-in Booker spawned: {plg_cfg.account.username}")
             except Exception as e:
@@ -377,11 +379,11 @@ class BuiltinBookerGCO:
                     group_cd_key = f"vs:group:cooldown:{self.m_cfg.identifier}"
                     
                     # 更新全局(机器组)失败次数
-                    g_fails = self.redis_client.incr(group_fail_key)
+                    g_fails = self.redis_com.incr(group_fail_key)
                     # 计算退避时间
                     g_cd = self.group_backoff.calculate(g_fails)
                     # 设置 Redis 全局冷却保护阀
-                    self.redis_client.set(group_cd_key, "1", ex=int(g_cd))
+                    self.redis_com.set(group_cd_key, "1", ex=int(g_cd))
                     self._log(f"📉 [Rate Limited] Group '{self.m_cfg.identifier}' failed {g_fails} times. Global Backoff: {g_cd:.1f}s.")
 
             finally:

+ 17 - 16
booker_order.py

@@ -26,7 +26,8 @@ class OrderBookerGCO:
         self.m_tasks: List[Task] = []
         self.m_lock = threading.RLock()
         self.m_stop_event = threading.Event()
-        self.redis_client = redis.Redis(**redis_conf)
+        self.redis_com = redis.Redis(**redis_conf)
+        self.redis_sub = redis.Redis(**redis_conf)
         self.m_pending_order_by_queue: Dict[str, int] = {}        
         self.m_last_spawn_times: Dict[str, float] = {}
         self.m_task_data_cache: Dict[str, dict] = {}
@@ -132,7 +133,7 @@ class OrderBookerGCO:
             
             if healthy_tasks:
                 try:
-                    pipeline = self.redis_client.pipeline()
+                    pipeline = self.redis_com.pipeline()
                     new_deadline = time.time() + self.heartbeat_ttl
                     for t in healthy_tasks:
                         if t.task_ref is not None:
@@ -144,7 +145,7 @@ class OrderBookerGCO:
 
             if dead_tasks:
                 try:
-                    pipeline = self.redis_client.pipeline()
+                    pipeline = self.redis_com.pipeline()
                     for t in dead_tasks:
                         if t.task_ref is not None:
                             pipeline.zadd(self.m_tracker_key, {str(t.task_ref): 0})
@@ -205,7 +206,7 @@ class OrderBookerGCO:
         while not self.m_stop_event.is_set():
             try:
                 if pubsub is None:
-                    pubsub = self.redis_client.pubsub(ignore_subscribe_messages=False)
+                    pubsub = self.redis_sub.pubsub(ignore_subscribe_messages=False)
                     channels_to_sub = list(channel_to_routing_key.keys())
                     self._log(f"⏳ Sending SUBSCRIBE command to Redis for: {channels_to_sub}")
                     pubsub.subscribe(*channels_to_sub)
@@ -284,7 +285,7 @@ class OrderBookerGCO:
             if not task_data or task_data.get('status') in ['grabbed', 'pause', 'completed', 'cancelled']:
                 self._log(f"Bound Task={task_id} is no longer valid or already processed. Removing instance.")
                 self._remove_task(task, "bound task no longer valid")
-                self.redis_client.zrem(self.m_tracker_key, task_id)
+                self.redis_com.zrem(self.m_tracker_key, task_id)
                 return
             
             order_id = task_data.get('order_id')
@@ -323,7 +324,7 @@ class OrderBookerGCO:
                 
                 ThreadPool.getInstance().enqueue(_update_cloud_success)
 
-                self.redis_client.zrem(self.m_tracker_key, task_id)
+                self.redis_com.zrem(self.m_tracker_key, task_id)
                 self._remove_task(task, "booking success")
             else:
                 self._log(f"❌ BOOK FAILED for Order: {order_id}. Will retry on next signal.")
@@ -350,7 +351,7 @@ class OrderBookerGCO:
                     ThreadPool.getInstance().enqueue(_update_cloud_meta)   
                     t_cd = self.task_backoff.calculate(t_fails)
                     self._log(f"⏳ Task={task_id} (Booking Attempt {t_fails}) suspended for {t_cd:.1f}s.")
-                    self.redis_client.zadd(self.m_tracker_key, {str(task_id): time.time() + t_cd})
+                    self.redis_com.zadd(self.m_tracker_key, {str(task_id): time.time() + t_cd})
 
     def _creator_loop(self):
         self._log("Creator loop started.")
@@ -362,7 +363,7 @@ class OrderBookerGCO:
                 r_key = apt.routing_key
                 
                 queue_cd_key = f"vs:queue:cooldown:{r_key}"
-                if self.redis_client.exists(queue_cd_key):
+                if self.redis_com.exists(queue_cd_key):
                     continue
                 
                 with self.m_lock:
@@ -387,7 +388,7 @@ class OrderBookerGCO:
             
             try:
                 queue_name = f"auto.{target_routing_key}"
-                task_data = VSCloudApi.Instance().get_vas_task_pop(queue_name, test=False)
+                task_data = VSCloudApi.Instance().get_vas_task_pop(queue_name)
                 if not task_data:
                     return 
                 
@@ -396,7 +397,7 @@ class OrderBookerGCO:
                 with self.m_lock:
                     self.m_task_data_cache[str(task_id)] = task_data
                 
-                self.redis_client.zadd(self.m_tracker_key, {str(task_id): time.time() + 5*60.0})
+                self.redis_com.zadd(self.m_tracker_key, {str(task_id): time.time() + 5*60.0})
                 user_inputs = task_data.get('user_inputs', {})
                 
                 plg_cfg = VSPlgConfig()
@@ -410,7 +411,7 @@ class OrderBookerGCO:
                 
                 acceptable_keys = [target_routing_key]
                 if self.m_cfg.need_proxy:
-                    proxy = VSCloudApi.Instance().get_next_proxy(self.m_cfg.proxy_pool, self.m_cfg.proxy_cd, test=False)
+                    proxy = VSCloudApi.Instance().get_next_proxy(self.m_cfg.proxy_pool, self.m_cfg.proxy_cd)
                     plg_cfg.proxy.id = proxy['id']
                     plg_cfg.proxy.ip = proxy['ip']
                     plg_cfg.proxy.port = proxy['port']
@@ -437,7 +438,7 @@ class OrderBookerGCO:
                         )
                     )
                     queue_fail_key = f"vs:queue:failures:{target_routing_key}"
-                    self.redis_client.delete(queue_fail_key)                    
+                    self.redis_com.delete(queue_fail_key)                    
                 success = True
                 self._log(f"+++ Order Booker spawned: {plg_cfg.account.username} (Target: {acceptable_keys})")
             except Exception as e:
@@ -460,9 +461,9 @@ class OrderBookerGCO:
                     is_rate_limited = True
                     queue_fail_key = f"vs:queue:failures:{target_routing_key}"
                     queue_cd_key = f"vs:queue:cooldown:{target_routing_key}"
-                    q_fails = self.redis_client.incr(queue_fail_key)
+                    q_fails = self.redis_com.incr(queue_fail_key)
                     q_cd = self.queue_backoff.calculate(q_fails)
-                    self.redis_client.set(queue_cd_key, "1", ex=int(q_cd))
+                    self.redis_com.set(queue_cd_key, "1", ex=int(q_cd))
                     self._log(f"📉 [Rate Limited] Queue '{target_routing_key}' failed {q_fails} times. Global Backoff: {q_cd:.1f}s.")
                     if task_id is not None:
                         task_meta = task_data.get('meta') or {}
@@ -476,13 +477,13 @@ class OrderBookerGCO:
                         
                         t_cd = self.account_backoff.calculate(t_fails)
                         self._log(f"⏳ Task={task_id} (Attempt {t_fails}) suspended for {t_cd:.1f}s.")
-                        self.redis_client.zadd(self.m_tracker_key, {str(task_id): time.time() + t_cd})       
+                        self.redis_com.zadd(self.m_tracker_key, {str(task_id): time.time() + t_cd})       
             finally:
                 with self.m_lock: 
                     self.m_pending_order_by_queue[target_routing_key] = max(0, self.m_pending_order_by_queue[target_routing_key] - 1)
                 
                 if not success and task_id is not None and not is_rate_limited:
-                    self.redis_client.zadd(self.m_tracker_key, {str(task_id): 0})
+                    self.redis_com.zadd(self.m_tracker_key, {str(task_id): 0})
                     self._log(f"♻️ Task={task_id} failed normal spawn. Instantly handed over to Sweeper.")
                     
                     with self.m_lock:

+ 2 - 2
config/config.json

@@ -974,13 +974,13 @@
             "enable": true,
             "need_account": true,
             "need_proxy": true,
-            "proxy_pool": ["local"],
+            "proxy_pool": ["proxy-cheap", "decodo"],
             "proxy_cd": 300,
             "session_max_life": 1800,
             "sentinel": {
                 "account_source": "built-in",
                 "account_pool_id": "tls.gb.fr.sentinel",
-                "target_instances": 1,
+                "target_instances": 4,
                 "account_cd": 1800,
                 "signal_ttl": 30
             },

+ 65 - 0
configure.py

@@ -0,0 +1,65 @@
+# -------- 配置部分 --------
+TEST_TASK = None                        # 测试任务,这里配置以后任务轮转函数直接使用这个任务
+# TEST_TASK = {
+#     "status": "running",
+#     "priority": 10,
+#     "config": {
+#         "select": "random",
+#         "alias_email": "mariaandrews2240@gmail-app.com",
+#         "time_filter": ["AM","PM"],
+#         "exclude_dates": [],
+#         "include_today": True,
+#         "allowed_weekdays": [1,2,3,4,5],
+#         "include_tomorrow": True
+#     },
+#     "user_inputs": {
+#         "email": "mariaandrews2240@gmail-app.com",
+#         "password": "Visafly@111",
+#         "username": "mariaandrews2240@gmail-app.com",
+#         "support_pta": False,
+#         "expected_end_date": "2026-06-30",
+#         "expected_start_date": "2026-05-01"
+#     },
+#     "grabbed_history": {},
+#     "meta": None,
+#     "id": 0,
+#     "order_id": "ORD-20260415100806-0fdee58d",
+#     "routing_key": "auto.slot.lon.fr.tourist",
+#     "script_version": "latest",
+#     "attempt_count": 0,
+#     "notify_count": 0,
+#     "created_at": "2000-01-01T01:00:00",
+#     "updated_at": "2000-01-01T01:00:00",
+#     "expire_at":  "2000-01-01T01:00:00"
+# }
+
+TEST_ACCOUNT = None                     # 测试账号,这里配置以后账号轮转函数直接使用这个账号
+# TEST_ACCOUNT = {
+#     "pool_name": "tls.gb.fr.sentinel",
+#     "username": "robertolord2257@gmail-app.com",
+#     "password": "Visafly@111",
+#     "extra_data": {},
+#     "status": "active",
+#     "next_use_time": "2000-01-01T01:00:00",
+#     "id": 0,
+#     "created_at": "2000-01-01T01:00:00",
+#     "updated_at": "2000-01-01T01:00:00"
+# }
+
+TEST_PROXY = None                        # 测试代理,这里配置以后ip轮转函数直接使用这个代理
+# TEST_PROXY = {
+#     "pool_name": "local",
+#     "proto": "http",
+#     "ip": "127.0.0.1",
+#     "port": 7890,
+#     "username": "",
+#     "password": "",
+#     "id": 0
+# }
+
+CHROME_PATH = None                       # Chrome bin 的路径, 这个优先级最高,其次CHROME_BIN 的环境变量,最后系统默认值
+
+CLASH_SWITCH_NODE = False                # 是否要启用Clash 轮换
+CLASH_API_URL = "http://127.0.0.1:9097"  # Clash 本地 API
+CLASH_API_KEY = "esZnx8"                 # Clash API 密钥
+CLASH_GROUP_NAME = "♻️ 手动切换"           # 要轮换的策略组名称

+ 7 - 4
plugins/tls_plugin.py

@@ -10,8 +10,9 @@ from datetime import datetime
 from typing import List, Dict, Optional, Any, Callable
 from urllib.parse import urljoin, urlparse, urlencode, parse_qs
 from concurrent.futures import ThreadPoolExecutor
-
 from DrissionPage import ChromiumPage, ChromiumOptions
+
+import configure
 from vs_plg import IVSPlg
 from vs_types import VSPlgConfig, AppointmentType, VSQueryResult, VSBookResult, AvailabilityStatus, TimeSlot, DateAvailability, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
 from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
@@ -156,7 +157,9 @@ class TlsPlugin(IVSPlg):
         co.set_local_port(debug_port)       
         co.set_user_data_path(self.user_data_path)
         
-        chrome_path = os.getenv("CHROME_BIN")
+        chrome_path = configure.CHROME_PATH
+        if not chrome_path:
+            chrome_path = os.getenv("CHROME_BIN")
         if chrome_path and os.path.exists(chrome_path):
             co.set_paths(browser_path=chrome_path)
         
@@ -239,7 +242,7 @@ class TlsPlugin(IVSPlg):
             self.page.get(tls_url)
             time.sleep(5)
             
-            cf_bypasser = CloudflareBypasser(self.page, log=True)
+            cf_bypasser = CloudflareBypasser(self.page, log=self.config.debug)
             if not cf_bypasser.bypass(max_retry=6):
                 raise BizLogicError("Cloudflare bypass timeout")
             time.sleep(3)
@@ -247,7 +250,7 @@ class TlsPlugin(IVSPlg):
             
             # --- 初始化人类行为模拟工具 ---
             self._log("Init humanize tools...")
-            self.mouse = HumanMouse(self.page, debug=True)
+            self.mouse = HumanMouse(self.page, debug=self.config.debug)
             self.keyboard = HumanKeyboard(self.page)
             viewport_width = self.page.rect.viewport_size[0]
             viewport_height = self.page.rect.viewport_size[1]

+ 9 - 8
sentinel.py

@@ -21,7 +21,8 @@ class SentinelGCO:
         self.m_lock = threading.RLock()
         self.m_stop_event = threading.Event()
         
-        self.redis_client = redis.Redis(**redis_conf)
+        self.redis_com = redis.Redis(**redis_conf)
+        self.redis_pub = redis.Redis(**redis_conf)
         self.m_pending_builtin = 0
         
         # 1. 全局建连退避:起步 1 分钟,封顶 1 小时 (保护登录接口)
@@ -153,7 +154,7 @@ class SentinelGCO:
                                 "timestamp": now
                             }
                             redis_key = self._get_redis_key(apt_type.routing_key)
-                            self.redis_client.publish(redis_key, json.dumps(payload))
+                            self.redis_pub.publish(redis_key, json.dumps(payload))
                             payload["query_result"]["website"] = self.m_cfg.website
                             VSCloudApi.Instance().slot_snapshot_report(payload["query_result"])
                         VSCloudApi.Instance().slot_refresh_success(apt_type.routing_key)
@@ -172,7 +173,7 @@ class SentinelGCO:
             time.sleep(2)
             with self.m_lock:
                 
-                if self.redis_client.exists(group_cd_key):
+                if self.redis_com.exists(group_cd_key):
                     continue
                 
                 current = len(self.m_tasks)
@@ -207,13 +208,13 @@ class SentinelGCO:
                     plg_cfg.account.id = 0
                     plg_cfg.account.username = "Guest"
                 else:
-                    acc = VSCloudApi.Instance().get_next_account(self.m_cfg.sentinel.account_pool_id, self.m_cfg.sentinel.account_cd, test=False)
+                    acc = VSCloudApi.Instance().get_next_account(self.m_cfg.sentinel.account_pool_id, self.m_cfg.sentinel.account_cd)
                     plg_cfg.account.id = acc['id']
                     plg_cfg.account.username = acc['username']
                     plg_cfg.account.password = acc['password']
                 
                 if self.m_cfg.need_proxy:
-                    proxy = VSCloudApi.Instance().get_next_proxy(self.m_cfg.proxy_pool, self.m_cfg.proxy_cd, test=False)
+                    proxy = VSCloudApi.Instance().get_next_proxy(self.m_cfg.proxy_pool, self.m_cfg.proxy_cd)
                     plg_cfg.proxy.id = proxy['id']
                     plg_cfg.proxy.ip = proxy['ip']
                     plg_cfg.proxy.port = proxy['port']
@@ -231,7 +232,7 @@ class SentinelGCO:
                         Task(instance=instance,qw_cfg=self.m_cfg.query_wait,next_run=time.time(), book_allowed=False))
                 
                     group_fail_key = f"vs:group:failures:{self.m_cfg.identifier}"
-                    self.redis_client.delete(group_fail_key)
+                    self.redis_com.delete(group_fail_key)
                 
                 success = True
                 self._log(f"+++ Sentinel spawned: {plg_cfg.account.username}")
@@ -256,9 +257,9 @@ class SentinelGCO:
                     group_fail_key = f"vs:group:failures:{self.m_cfg.identifier}"
                     group_cd_key = f"vs:group:cooldown:{self.m_cfg.identifier}"
                     
-                    g_fails = self.redis_client.incr(group_fail_key)
+                    g_fails = self.redis_com.incr(group_fail_key)
                     g_cd = self.group_backoff.calculate(g_fails)
-                    self.redis_client.set(group_cd_key, "1", ex=int(g_cd))
+                    self.redis_com.set(group_cd_key, "1", ex=int(g_cd))
                     self._log(f"📉 [Rate Limited] Sentinel Spawn failed {g_fails} times. Global Backoff: {g_cd:.1f}s.")
                     
             finally:

+ 13 - 64
toolkit/vs_cloud_api.py

@@ -3,6 +3,7 @@ import requests
 import json
 import time
 import urllib.parse
+import configure
 from datetime import datetime
 from typing import Dict, Any, List, Optional
 from vs_types import NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
@@ -54,40 +55,9 @@ class VSCloudApi:
         else:
             raise BizLogicError(message=f"HTTP Error {resp.status_code}: {resp.text[:100]}")
 
-    def get_vas_task_pop(self, routing_key: str, test=False) -> Optional[Dict]:
-        if test:
-            return  {
-                "status": "running",
-                "priority": 10,
-                "config": {
-                    "select": "random",
-                    "alias_email": "mariaandrews2240@gmail-app.com",
-                    "time_filter": ["AM","PM"],
-                    "exclude_dates": [],
-                    "include_today": True,
-                    "allowed_weekdays": [1,2,3,4,5],
-                    "include_tomorrow": True
-                },
-                "user_inputs": {
-                    "email": "mariaandrews2240@gmail-app.com",
-                    "password": "Visafly@111",
-                    "username": "mariaandrews2240@gmail-app.com",
-                    "support_pta": False,
-                    "expected_end_date": "2026-06-30",
-                    "expected_start_date": "2026-05-01"
-                },
-                "grabbed_history": {},
-                "meta": None,
-                "id": 0,
-                "order_id": "ORD-20260415100806-0fdee58d",
-                "routing_key": "auto.slot.lon.fr.tourist",
-                "script_version": "latest",
-                "attempt_count": 0,
-                "notify_count": 0,
-                "created_at": "2000-01-01T01:00:00",
-                "updated_at": "2000-01-01T01:00:00",
-                "expire_at":  "2000-01-01T01:00:00"
-            }
+    def get_vas_task_pop(self, routing_key: str) -> Optional[Dict]:
+        if configure.TEST_TASK:
+            return configure.TEST_TASK
         url = f"{self.base_url}/api/vas/task/pop"
         params = {
             "queue_name": routing_key,
@@ -336,21 +306,10 @@ class VSCloudApi:
     def get_next_account(
         self,
         pool_name: str,
-        account_cd: int = 60,
-        test: bool = False
+        account_cd: int = 60
     ):
-        if test:
-            return {
-                "pool_name": "tls.gb.fr.sentinel",
-                "username": "robertolord2257@gmail-app.com",
-                "password": "Visafly@111",
-                "extra_data": {},
-                "status": "active",
-                "next_use_time": "2000-01-01T01:00:00",
-                "id": 0,
-                "created_at": "2000-01-01T01:00:00",
-                "updated_at": "2000-01-01T01:00:00"
-            }
+        if configure.TEST_ACCOUNT:
+            return configure.TEST_ACCOUNT
         url = f'{self.base_url}/api/account/next'
         payload = {
             "pool_name": pool_name,
@@ -367,23 +326,13 @@ class VSCloudApi:
     def get_next_proxy(
         self,
         pools: List[str],
-        proxy_cd: int = 60,
-        test: bool = False
+        proxy_cd: int = 60
     ):
-        if test:
-            local = {
-                "pool_name": "local",
-                "proto": "http",
-                "ip": "127.0.0.1",
-                "port": 7890,
-                "username": "",
-                "password": "",
-                "time_zone": "Europe/Dublin",
-                "id": 0
-            }
-            node = switch_next_node()
-            VSC_INFO('-', f'proxy node={node}')
-            return local 
+        if configure.TEST_PROXY:
+            if configure.CLASH_SWITCH_NODE:
+                node = switch_next_node()
+                VSC_INFO('-', f'proxy node={node}')
+            return configure.TEST_PROXY 
         url = f'{self.base_url}/api/proxy/next-ip'
         payload = {
             "pools": pools,

+ 19 - 30
tools/clash_api.py

@@ -1,51 +1,40 @@
 #!/usr/bin/env python3
 import requests
-import json
+import configure
+from vs_types import NotFoundError, BizLogicError 
 
-# -------- 配置部分 --------
-API_URL = "http://127.0.0.1:9097"  # Clash 本地 API
-API_KEY = "esZnx8"                 # Clash API 密钥
-GROUP_NAME = "♻️ 手动切换"               # 要轮换的策略组名称
-# ---------------------------
-
-headers = {
-    "Authorization": f"Bearer {API_KEY}",
-    "Content-Type": "application/json"
-}
-
-def get_proxies():
-    """获取策略组和节点信息"""
-    resp = requests.get(f"{API_URL}/proxies", headers=headers, timeout=5)
-    resp.raise_for_status()
-    return resp.json()
 
 def switch_next_node():
-    proxies = get_proxies().get('proxies')
+    headers = {
+        "Authorization": f"Bearer {configure.CLASH_API_KEY}",
+        "Content-Type": "application/json"
+    }
+    try:
+        resp = requests.get(f"{configure.API_URL}/proxies", headers=headers, timeout=5)
+    except Exception as e:
+        raise BizLogicError(f'Fetch proxies error, e={e}')
+    proxies_data = resp.json()
+    proxies = proxies_data.get('proxies')
     
-    if GROUP_NAME not in proxies:
-        print(f"策略组 '{GROUP_NAME}' 不存在")
-        return
+    if configure.CLASH_GROUP_NAME not in proxies:
+        raise NotFoundError(message=f"Group '{configure.CLASH_GROUP_NAME}' not found")
     
-    group = proxies[GROUP_NAME]
+    group = proxies[configure.CLASH_GROUP_NAME]
     all_nodes = group['all']
     current_node = group['now']
     
-    # 找到下一个节点
     try:
         idx = all_nodes.index(current_node)
         next_idx = (idx + 1) % len(all_nodes)
         next_node = all_nodes[next_idx]
     except ValueError:
-        # 当前节点不在列表里,选择第一个
         next_node = all_nodes[0]
     
-    # 切换节点(注意 API 路径修改)
     data = {"name": next_node}
-    resp = requests.put(f"{API_URL}/proxies/{GROUP_NAME}", headers=headers, json=data, timeout=5)
-    resp.raise_for_status()
-    
-    print(f"[OK] 策略组 '{GROUP_NAME}' 切换完成")
-    print(f"    当前节点: {current_node} → {next_node}")
+    try:
+        resp = requests.put(f"{configure.CLASH_API_URL}/proxies/{configure.CLASH_GROUP_NAME}", headers=headers, json=data, timeout=5)
+    except Exception as e:
+        raise BizLogicError(f'Switch to {next_node} error, e={e}')
     return next_node
 
 if __name__ == "__main__":