jerry 4 mesiacov pred
rodič
commit
09258afc72

+ 446 - 0
config/accounts.json

@@ -1,4 +1,412 @@
 {
+    "au_fr": [
+        {
+            "id": 598,
+            "lock_until": 0,
+            "password": "0cjO4ts6H@",
+            "username": "au_fr_h74mby@gmail-app.com"
+        },
+        {
+            "id": 599,
+            "lock_until": 0,
+            "password": "gR1ORxYNA@",
+            "username": "au_fr_p9iq5r@gmail-app.com"
+        },
+        {
+            "id": 600,
+            "lock_until": 0,
+            "password": "fAl7VmBc8@",
+            "username": "au_fr_47p3jz@gmail-app.com"
+        },
+        {
+            "id": 601,
+            "lock_until": 0,
+            "password": "mey@D4ubD0",
+            "username": "au_fr_k99ky1@gmail-app.com"
+        },
+        {
+            "id": 602,
+            "lock_until": 0,
+            "password": "Xui@Zj0HkN",
+            "username": "au_fr_q87fl0@gmail-app.com"
+        },
+        {
+            "id": 603,
+            "lock_until": 0,
+            "password": "@kYfU9S5NI",
+            "username": "au_fr_4cgp29@gmail-app.com"
+        }
+    ],
+    "gb_it": [
+        {
+            "id": 556,
+            "lock_until": 0,
+            "password": "2v@H2UXx74",
+            "username": "gb_it_0msok0@gmail-app.com"
+        },
+        {
+            "id": 557,
+            "lock_until": 0,
+            "password": "@KMWaBf0w1",
+            "username": "gb_it_0wxymp@gmail-app.com"
+        },
+        {
+            "id": 559,
+            "lock_until": 0,
+            "password": "Rc7@RFfzW3",
+            "username": "gb_it_3j6nwd@gmail-app.com"
+        },
+        {
+            "id": 560,
+            "lock_until": 0,
+            "password": "LVUBntr4@5",
+            "username": "gb_it_b2xhnr@gmail-app.com"
+        },
+        {
+            "id": 561,
+            "lock_until": 0,
+            "password": "B4@Q56dgu7",
+            "username": "gb_it_0m93bm@gmail-app.com"
+        },
+        {
+            "id": 562,
+            "lock_until": 0,
+            "password": "5gQX7h3@VF",
+            "username": "gb_it_ajcwvx@gmail-app.com"
+        },
+        {
+            "id": 563,
+            "lock_until": 0,
+            "password": "6Kc@3Eh@eL",
+            "username": "gb_it_mfn0o4@gmail-app.com"
+        },
+        {
+            "id": 564,
+            "lock_until": 0,
+            "password": "Q9c2x@e6da",
+            "username": "gb_it_tf2ync@gmail-app.com"
+        },
+        {
+            "id": 565,
+            "lock_until": 0,
+            "password": "miz@7FRuk3",
+            "username": "gb_it_bn4j65@gmail-app.com"
+        },
+        {
+            "id": 566,
+            "lock_until": 0,
+            "password": "2s4@5VHYU5",
+            "username": "gb_it_wtlffb@gmail-app.com"
+        },
+        {
+            "id": 567,
+            "lock_until": 0,
+            "password": "R@3s9jgNeO",
+            "username": "gb_it_r5z9g5@gmail-app.com"
+        }
+    ],
+    "gb_nl": [
+        {
+            "id": 620,
+            "lock_until": 0,
+            "password": "INYl7Q@0x1",
+            "username": "gb_nl_l6nv77@gmail-app.com"
+        },
+        {
+            "id": 621,
+            "lock_until": 0,
+            "password": "n1@iw3DfNO",
+            "username": "gb_nl_ztsj28@gmail-app.com"
+        },
+        {
+            "id": 622,
+            "lock_until": 0,
+            "password": "w1RmZHL@7N",
+            "username": "gb_nl_ifp27n@gmail-app.com"
+        },
+        {
+            "id": 623,
+            "lock_until": 0,
+            "password": "uZ@5vtkwJc",
+            "username": "gb_nl_2sx13j@gmail-app.com"
+        },
+        {
+            "id": 624,
+            "lock_until": 0,
+            "password": "yZBEOqIZ@4",
+            "username": "gb_nl_t4wi47@gmail-app.com"
+        },
+        {
+            "id": 625,
+            "lock_until": 0,
+            "password": "5i@GYmqh1x",
+            "username": "gb_nl_rwzr3l@gmail-app.com"
+        }
+    ],
+    "gb_no": [
+        {
+            "id": 632,
+            "lock_until": 0,
+            "password": "qFYL4Kvi@7",
+            "username": "gb_no_8kwb4k@gmail-app.com"
+        },
+        {
+            "id": 633,
+            "lock_until": 0,
+            "password": "LWh26@b1FO",
+            "username": "gb_no_i5fmm5@gmail-app.com"
+        },
+        {
+            "id": 634,
+            "lock_until": 0,
+            "password": "Jn33M3j@Km",
+            "username": "gb_no_31n1v9@gmail-app.com"
+        },
+        {
+            "id": 635,
+            "lock_until": 0,
+            "password": "kysEA@69en",
+            "username": "gb_no_sktwxs@gmail-app.com"
+        },
+        {
+            "id": 636,
+            "lock_until": 0,
+            "password": "A@l8ZX7Vhp",
+            "username": "gb_no_xvh47y@gmail-app.com"
+        },
+        {
+            "id": 637,
+            "lock_until": 0,
+            "password": "2CE76@fMTg",
+            "username": "gb_no_a4xwpu@gmail-app.com"
+        },
+        {
+            "id": 638,
+            "lock_until": 0,
+            "password": "cb@M2j4Y6M",
+            "username": "gb_no_nl9vdl@gmail-app.com"
+        },
+        {
+            "id": 639,
+            "lock_until": 0,
+            "password": "Db0cn2w@2M",
+            "username": "gb_no_iaya2k@gmail-app.com"
+        },
+        {
+            "id": 640,
+            "lock_until": 0,
+            "password": "6iWb@C1vxo",
+            "username": "gb_no_5yumya@gmail-app.com"
+        },
+        {
+            "id": 641,
+            "lock_until": 0,
+            "password": "R@b@43ngZ9",
+            "username": "gb_no_io50uu@gmail-app.com"
+        },
+        {
+            "id": 642,
+            "lock_until": 0,
+            "password": "ePbSJ@x94U",
+            "username": "gb_no_glpu8h@gmail-app.com"
+        },
+        {
+            "id": 643,
+            "lock_until": 0,
+            "password": "u6k6eUTky@",
+            "username": "gb_no_np7oxv@gmail-app.com"
+        }
+    ],
+    "ie_at": [
+        {
+            "id": 592,
+            "lock_until": 0,
+            "password": "P1C7evb@LU",
+            "username": "ie_at_oxjrdp@gmail-app.com"
+        },
+        {
+            "id": 593,
+            "lock_until": 0,
+            "password": "CHt@saBY2S",
+            "username": "ie_at_cy6nst@gmail-app.com"
+        },
+        {
+            "id": 594,
+            "lock_until": 0,
+            "password": "12z8o@DmVc",
+            "username": "ie_at_ivov3v@gmail-app.com"
+        },
+        {
+            "id": 595,
+            "lock_until": 0,
+            "password": "e@iC0fPpxP",
+            "username": "ie_at_fatyy4@gmail-app.com"
+        },
+        {
+            "id": 596,
+            "lock_until": 0,
+            "password": "sLfpeK9@PV",
+            "username": "ie_at_us8e66@gmail-app.com"
+        },
+        {
+            "id": 597,
+            "lock_until": 0,
+            "password": "h@@Qes5WdX",
+            "username": "ie_at_2akgxq@gmail-app.com"
+        }
+    ],
+    "ie_dk": [
+        {
+            "id": 626,
+            "lock_until": 0,
+            "password": "7It@d0S038",
+            "username": "ie_dk_c72y0m@gmail-app.com"
+        },
+        {
+            "id": 627,
+            "lock_until": 0,
+            "password": "TrIt0@eYdM",
+            "username": "ie_dk_w5cr67@gmail-app.com"
+        },
+        {
+            "id": 628,
+            "lock_until": 0,
+            "password": "@7caSKB2l9",
+            "username": "ie_dk_p9eqxz@gmail-app.com"
+        },
+        {
+            "id": 629,
+            "lock_until": 0,
+            "password": "ieS@tLsY24",
+            "username": "ie_dk_4e8gmz@gmail-app.com"
+        },
+        {
+            "id": 630,
+            "lock_until": 0,
+            "password": "x@8ikj3AcL",
+            "username": "ie_dk_2xywt2@gmail-app.com"
+        },
+        {
+            "id": 631,
+            "lock_until": 0,
+            "password": "TmA@SCxRg8",
+            "username": "ie_dk_6c9gsf@gmail-app.com"
+        }
+    ],
+    "ie_fi": [
+        {
+            "id": 586,
+            "lock_until": 0,
+            "password": "FeuyJ@zl20",
+            "username": "ie_fi_1txaxx@gmail-app.com"
+        },
+        {
+            "id": 587,
+            "lock_until": 0,
+            "password": "v22xGkFm@w",
+            "username": "ie_fi_j2zeiw@gmail-app.com"
+        },
+        {
+            "id": 588,
+            "lock_until": 0,
+            "password": "Cw6J@sDpik",
+            "username": "ie_fi_ez7inp@gmail-app.com"
+        },
+        {
+            "id": 589,
+            "lock_until": 0,
+            "password": "wbP@H@Ox2t",
+            "username": "ie_fi_119ulr@gmail-app.com"
+        },
+        {
+            "id": 590,
+            "lock_until": 0,
+            "password": "PY71A7@Cds",
+            "username": "ie_fi_bxc1n3@gmail-app.com"
+        },
+        {
+            "id": 591,
+            "lock_until": 0,
+            "password": "@5RC9t3L1w",
+            "username": "ie_fi_subwbr@gmail-app.com"
+        }
+    ],
+    "ie_hu": [
+        {
+            "id": 604,
+            "lock_until": 0,
+            "password": "VKrh@0qVMN",
+            "username": "ie_hu_ulyvgq@gmail-app.com"
+        },
+        {
+            "id": 605,
+            "lock_until": 0,
+            "password": "T03M@NQuhu",
+            "username": "ie_hu_7gmp17@gmail-app.com"
+        },
+        {
+            "id": 606,
+            "lock_until": 0,
+            "password": "6fGJm281@C",
+            "username": "ie_hu_rkscu3@gmail-app.com"
+        },
+        {
+            "id": 607,
+            "lock_until": 0,
+            "password": "A5j@rKBrcL",
+            "username": "ie_hu_mxpot6@gmail-app.com"
+        },
+        {
+            "id": 608,
+            "lock_until": 0,
+            "password": "4KhzKA@Yxw",
+            "username": "ie_hu_wexkp7@gmail-app.com"
+        },
+        {
+            "id": 609,
+            "lock_until": 0,
+            "password": "5eE@Vws3HZ",
+            "username": "ie_hu_i3fcrs@gmail-app.com"
+        }
+    ],
+    "ie_is": [
+        {
+            "id": 574,
+            "lock_until": 0,
+            "password": "Keu5Rt@o9I",
+            "username": "ie_is_p7ahqa@gmail-app.com"
+        },
+        {
+            "id": 575,
+            "lock_until": 0,
+            "password": "@3fw7HwcSZ",
+            "username": "ie_is_h4yi4y@gmail-app.com"
+        },
+        {
+            "id": 576,
+            "lock_until": 0,
+            "password": "Or1duEJc5@",
+            "username": "ie_is_cwwyxz@gmail-app.com"
+        },
+        {
+            "id": 577,
+            "lock_until": 0,
+            "password": "SMeG3hnD@u",
+            "username": "ie_is_hxik14@gmail-app.com"
+        },
+        {
+            "id": 578,
+            "lock_until": 0,
+            "password": "@jE6IaiZFW",
+            "username": "ie_is_28vm9n@gmail-app.com"
+        },
+        {
+            "id": 579,
+            "lock_until": 0,
+            "password": "fDTgp9ecH@",
+            "username": "ie_is_nn9bjb@gmail-app.com"
+        }
+    ],
     "ie_nl": [
         {
             "id": 423,
@@ -43,6 +451,44 @@
             "username": "ie_nl_x0rh02@gmail-app.com"
         }
     ],
+    "sg_fr": [
+        {
+            "id": 580,
+            "lock_until": 0,
+            "password": "y1wm@hf0Bn",
+            "username": "sg_fr_onoy9i@gmail-app.com"
+        },
+        {
+            "id": 581,
+            "lock_until": 0,
+            "password": "p7@@0NnOR8",
+            "username": "sg_fr_k6vuhb@gmail-app.com"
+        },
+        {
+            "id": 582,
+            "lock_until": 0,
+            "password": "D@4OCh2j3I",
+            "username": "sg_fr_cvq08b@gmail-app.com"
+        },
+        {
+            "id": 583,
+            "lock_until": 0,
+            "password": "xT01gsHw@j",
+            "username": "sg_fr_4cn9xj@gmail-app.com"
+        },
+        {
+            "id": 584,
+            "lock_until": 0,
+            "password": "@aprZh7NbA",
+            "username": "sg_fr_m32a2e@gmail-app.com"
+        },
+        {
+            "id": 585,
+            "lock_until": 0,
+            "password": "SR2hnX@ho5",
+            "username": "sg_fr_p0nz5l@gmail-app.com"
+        }
+    ],
     "gb_fr": [
         {
             "id": 0,

+ 597 - 19
config/groups.json

@@ -1,6 +1,6 @@
 [
     {
-        "identifier": "VFS_DUBLIN_NL",
+        "identifier": "VFS_IE_NL",
         "enable": false,
         "need_account": true,
         "account_pool": "ie_nl",
@@ -22,26 +22,27 @@
         },
         "free_config": {
             "verbose": 0,
-            "missionCode": "nld",
-            "missionName": "Netherlands",
-            "countryCode": "irl",
-            "countryName": "Ireland",
-            "cultureCode": "en-US",
+            "mission_code": "nld",
+            "mission_name": "Netherlands",
+            "country_code": "irl",
+            "country_name": "Ireland",
+            "culture_code": "en-US",
             "language": "en",
             "website": "https://visa.vfsglobal.com/irl/en/nld/login",
-            "appointmentType": [
+            "appointment_types": [
                 {
                     "id": 573,
-                    "routingKey": "slot.dub.nl.tourist",
-                    "centerName": "Netherlands Visa Application Center - Dublin",
+                    "routing_key": "slot.dub.nl.tourist",
+                    "center_name": "Netherlands Visa Application Center - Dublin",
                     "city": "Dublin",
-                    "cityCode": "dub",
+                    "visa_type": "Tourist",
+                    "country": "Netherlands",
                     "address": "Cunningham House, 130 Francis Street, Dublin 8  D08 H48R",
-                    "vacCode": "NTDB",
-                    "categoryName": "All Short stay Categories",
-                    "categoryCode": "TA",
-                    "subcategoryName": "Tourist",
-                    "subcategoryCode": "To",
+                    "vac_code": "NTDB",
+                    "category_name": "All Short stay Categories",
+                    "category_code": "TA",
+                    "subcategory_name": "Tourist",
+                    "subcategory_code": "To",
                     "fee": null,
                     "currency": null
                 }
@@ -49,9 +50,560 @@
         }
     },
     {
-        "identifier": "BLS_DUBLIN_ES",
+        "identifier": "VFS_SG_FR",
         "enable": false,
         "need_account": true,
+        "account_pool": "sg_fr",
+        "need_proxy": true,
+        "proxy_pool": "ireland_proxies",
+        "target_instances": 1,
+        "account_login_interval": 30,
+        "query_wait": {
+            "mode": 2,
+            "fixed_wait": 10,
+            "random_min": 60,
+            "random_max": 300
+        },
+        "plugin_config": {
+            "lib_path": "plugins",
+            "plugin_name": "vfs_plugin",
+            "plugin_bin": "vfs_plugin.py",
+            "plugin_proto": "IVSPlg"
+        },
+        "free_config": {
+            "verbose": 0,
+            "mission_code": "fra",
+            "mission_name": "France",
+            "country_code": "sgp",
+            "country_name": "Singapore",
+            "culture_code": "en-US",
+            "language": "en",
+            "website": "https://visa.vfsglobal.com/sgp/en/fra/login",
+            "appointment_types": [
+                {
+                    "id": 538,
+                    "routing_key": "slot.sin.fr.tourist",
+                    "center_name": "France Visa Application Center, Singapore",
+                    "city": "Singapore",
+                    "visa_type": "Tourist",
+                    "country": "France",
+                    "address": "79 Anson Road #15-01 Singapore 079906",
+                    "vac_code": "FRSN",
+                    "category_name": "Short Stay",
+                    "category_code": "02",
+                    "subcategory_name": "Short Stay Tourist, Family Visit",
+                    "subcategory_code": "Six",
+                    "fee": null,
+                    "currency": null
+                }
+            ]
+        }
+    },
+    {
+        "identifier": "VFS_AU_FR",
+        "enable": false,
+        "need_account": true,
+        "account_pool": "au_fr",
+        "need_proxy": true,
+        "proxy_pool": "ireland_proxies",
+        "target_instances": 1,
+        "account_login_interval": 30,
+        "query_wait": {
+            "mode": 2,
+            "fixed_wait": 10,
+            "random_min": 60,
+            "random_max": 300
+        },
+        "plugin_config": {
+            "lib_path": "plugins",
+            "plugin_name": "vfs_plugin",
+            "plugin_bin": "vfs_plugin.py",
+            "plugin_proto": "IVSPlg"
+        },
+        "free_config": {
+            "verbose": 0,
+            "mission_code": "fra",
+            "mission_name": "France",
+            "country_code": "aus",
+            "country_name": "Australia",
+            "culture_code": "en-US",
+            "language": "en",
+            "website": "https://visa.vfsglobal.com/aus/en/fra/login",
+            "appointment_types": [
+                {
+                    "id": 506,
+                    "routing_key": "slot.syd.fr.tourist",
+                    "center_name": "France Visa Application Center - Sydney",
+                    "city": "Sydney",
+                    "visa_type": "Tourist",
+                    "country": "France",
+                    "address": "France Visa Application Center,Level 6, 88 Pitt Street,Sydney NSW 2000",
+                    "vac_code": "SYD",
+                    "category_name": "VISA",
+                    "category_code": "VISA",
+                    "subcategory_name": "Short Stay Schengen Visa",
+                    "subcategory_code": "ShortStaySchengenVisa",
+                    "fee": null,
+                    "currency": null
+                },
+                {
+                    "id": 513,
+                    "routing_key": "slot.mel.fr.tourist",
+                    "center_name": "France Visa Application Center - Melbourne",
+                    "city": "Melbourne",
+                    "visa_type": "Tourist",
+                    "country": "France",
+                    "address": "Level 5 332 St. Kilda road level 5 Melbourne 3004",
+                    "vac_code": "MEL",
+                    "category_name": "VISA",
+                    "category_code": "VISA",
+                    "subcategory_name": "Short Stay Schengen Visa",
+                    "subcategory_code": "ShortStaySchengenVisa",
+                    "fee": null,
+                    "currency": null
+                }
+            ]
+        }
+    },
+    {
+        "identifier": "VFS_GB_IT",
+        "enable": false,
+        "need_account": true,
+        "account_pool": "gb_it",
+        "need_proxy": true,
+        "proxy_pool": "ireland_proxies",
+        "target_instances": 1,
+        "account_login_interval": 30,
+        "query_wait": {
+            "mode": 2,
+            "fixed_wait": 10,
+            "random_min": 60,
+            "random_max": 300
+        },
+        "plugin_config": {
+            "lib_path": "plugins",
+            "plugin_name": "vfs_plugin",
+            "plugin_bin": "vfs_plugin.py",
+            "plugin_proto": "IVSPlg"
+        },
+        "free_config": {
+            "verbose": 0,
+            "mission_code": "ita",
+            "mission_name": "Italy",
+            "country_code": "gbr",
+            "country_name": "United Kingdom",
+            "culture_code": "en-US",
+            "website": "https://visa.vfsglobal.com/gbr/en/ita/login",
+            "language": "en",
+            "appointment_types": [
+                {
+                    "id": 706,
+                    "routing_key": "slot.lon.it.tourist",
+                    "center_name": "Italy Visa Application Centre, London",
+                    "city": "London",
+                    "visa_type": "Tourist",
+                    "country": "Italy",
+                    "address": "Ground floor, 8- 20  Pocock St London SE1 0BW , United Kingdom",
+                    "vac_code": "ILON",
+                    "category_name": "Italy UK VisaCategory",
+                    "category_code": "UKITVED",
+                    "subcategory_name": "Tourist/ Business/ EU Family",
+                    "subcategory_code": "TBE",
+                    "fee": null,
+                    "currency": null
+                },
+                {
+                    "id": 708,
+                    "routing_key": "slot.man.it.tourist",
+                    "center_name": "Italy Visa Application Centre, Manchester",
+                    "city": "Manchester",
+                    "visa_type": "Tourist",
+                    "country": "Italy",
+                    "address": "50 Devonshire Street North, M12 6JH",
+                    "website": "https://visa.vfsglobal.com/gbr/en/ita/login",
+                    "vac_code": "IMAN",
+                    "category_name": "Italy UK VisaCategory",
+                    "category_code": "UKITVED",
+                    "subcategory_name": "Tourist/ Business/ EU Family",
+                    "subcategory_code": "TBE",
+                    "language": "en",
+                    "fee": null,
+                    "currency": null
+                }
+            ]
+        }
+    },
+    {
+        "identifier": "VFS_GB_NL",
+        "enable": false,
+        "need_account": true,
+        "account_pool": "gb_nl",
+        "need_proxy": true,
+        "proxy_pool": "ireland_proxies",
+        "target_instances": 1,
+        "account_login_interval": 30,
+        "query_wait": {
+            "mode": 2,
+            "fixed_wait": 10,
+            "random_min": 60,
+            "random_max": 300
+        },
+        "plugin_config": {
+            "lib_path": "plugins",
+            "plugin_name": "vfs_plugin",
+            "plugin_bin": "vfs_plugin.py",
+            "plugin_proto": "IVSPlg"
+        },
+        "free_config": {
+            "verbose": 0,
+            "mission_code": "nld",
+            "mission_name": "Netherland",
+            "country_code": "gbr",
+            "country_name": "United Kingdom",
+            "culture_code": "en-US",
+            "language": "en",
+            "website": "https://visa.vfsglobal.com/gbr/en/nld/login",
+            "appointment_types": [
+                {
+                    "id": 715,
+                    "routing_key": "slot.lon.nl.tourist",
+                    "center_name": "Netherlands Visa application centre - London",
+                    "city": "London",
+                    "visa_type": "Tourist",
+                    "country": "Netherlands",
+                    "address": "66 Wilson Street, EC2A 2BT",
+                    "vac_code": "NAKN",
+                    "category_name": "Schengen Visa",
+                    "category_code": "Schengen Visa",
+                    "subcategory_name": "Tourism",
+                    "subcategory_code": "TA",
+                    "fee": null,
+                    "currency": null
+                },
+                {
+                    "id": 723,
+                    "routing_key": "slot.man.it.tourist",
+                    "center_name": "Netherlands Visa application centre - Manchester",
+                    "city": "Manchester",
+                    "visa_type": "Tourist",
+                    "country": "Netherlands",
+                    "address": "50 Devonshire Street North, M12 6JH",
+                    "website": "https://visa.vfsglobal.com/gbr/en/nld/login",
+                    "vac_code": "NAKT",
+                    "category_name": "Schengen Visa",
+                    "category_code": "Schengen Visa",
+                    "subcategory_name": "Tourism",
+                    "subcategory_code": "TA",
+                    "fee": null,
+                    "currency": null
+                }
+            ]
+        }
+    },
+    {
+        "identifier": "VFS_GB_NO",
+        "enable": false,
+        "need_account": true,
+        "account_pool": "gb_no",
+        "need_proxy": true,
+        "proxy_pool": "ireland_proxies",
+        "target_instances": 1,
+        "account_login_interval": 30,
+        "query_wait": {
+            "mode": 2,
+            "fixed_wait": 10,
+            "random_min": 60,
+            "random_max": 300
+        },
+        "plugin_config": {
+            "lib_path": "plugins",
+            "plugin_name": "vfs_plugin",
+            "plugin_bin": "vfs_plugin.py",
+            "plugin_proto": "IVSPlg"
+        },
+        "free_config": {
+            "verbose": 0,
+            "mission_code": "nor",
+            "mission_name": "Norway",
+            "country_code": "gbr",
+            "country_name": "United Kingdom",
+            "culture_code": "en-US",
+            "language": "en",
+            "website": "https://visa.vfsglobal.com/gbr/en/nor/login",
+            "appointment_types": [
+                {
+                    "id": 731,
+                    "routing_key": "slot.lon.no.tourist",
+                    "center_name": "Norway Visa Application Centre, London",
+                    "city": "London",
+                    "visa_type": "Tourist",
+                    "country": "Norway",
+                    "address": "66 Wilson street, EC2A 2BT",
+                    "vac_code": "NLON",
+                    "category_name": "Schengen Visa C",
+                    "category_code": "SCHVISA",
+                    "subcategory_name": "Tourist Visa",
+                    "subcategory_code": "TOU",
+                    "fee": null,
+                    "currency": null
+                }
+            ]
+        }
+    },
+    {
+        "identifier": "VFS_IE_AT",
+        "enable": false,
+        "need_account": true,
+        "account_pool": "ie_at",
+        "need_proxy": true,
+        "proxy_pool": "ireland_proxies",
+        "target_instances": 1,
+        "account_login_interval": 30,
+        "query_wait": {
+            "mode": 2,
+            "fixed_wait": 10,
+            "random_min": 60,
+            "random_max": 300
+        },
+        "plugin_config": {
+            "lib_path": "plugins",
+            "plugin_name": "vfs_plugin",
+            "plugin_bin": "vfs_plugin.py",
+            "plugin_proto": "IVSPlg"
+        },
+        "free_config": {
+            "verbose": 0,
+            "mission_code": "aut",
+            "mission_name": "Austria",
+            "country_code": "irl",
+            "country_name": "Ireland",
+            "culture_code": "en-US",
+            "language": "en",
+            "website": "https://visa.vfsglobal.com/irl/en/aut/login",
+            "appointment_types": [
+                {
+                    "id": 551,
+                    "routing_key": "slot.dub.at.tourist",
+                    "center_name": "Austria / Switzerland / Liechtenstein/ Slovenia Visa Application Center, Dublin",
+                    "city": "Dublin",
+                    "visa_type": "Tourist",
+                    "country": "Austria",
+                    "address": "Cunningham House, 130 Francis Street, Dublin 8 D08 H48R",
+                    "vac_code": "AUT-DUB",
+                    "category_name": "Other Visas",
+                    "category_code": "Default_Austria_Ireland ",
+                    "subcategory_name": "All Visas ",
+                    "subcategory_code": "TA",
+                    "fee": null,
+                    "currency": null
+                }
+            ]
+        }
+    },
+    {
+        "identifier": "VFS_IE_DK",
+        "enable": false,
+        "need_account": true,
+        "account_pool": "ie_dk",
+        "need_proxy": true,
+        "proxy_pool": "ireland_proxies",
+        "target_instances": 1,
+        "account_login_interval": 30,
+        "query_wait": {
+            "mode": 2,
+            "fixed_wait": 10,
+            "random_min": 60,
+            "random_max": 300
+        },
+        "plugin_config": {
+            "lib_path": "plugins",
+            "plugin_name": "vfs_plugin",
+            "plugin_bin": "vfs_plugin.py",
+            "plugin_proto": "IVSPlg"
+        },
+        "free_config": {
+            "verbose": 0,
+            "mission_code": "dnk",
+            "mission_name": "Denmark",
+            "country_code": "irl",
+            "country_name": "Ireland",
+            "culture_code": "en-US",
+            "language": "en",
+            "website": "https://visa.vfsglobal.com/irl/en/dnk/login",
+            "appointment_types": [
+                {
+                    "id": 556,
+                    "routing_key": "slot.dub.dk.tourist",
+                    "center_name": "Denmark Visa Application Center, Dublin ",
+                    "city": "Dublin",
+                    "visa_type": "Tourist",
+                    "country": "Denmark",
+                    "address": "Cunningham House, 130 Francis Street, Dublin 8 D08 H48R",
+                    "vac_code": "DIDUB",
+                    "category_name": "Schengen Visa",
+                    "category_code": "SV",
+                    "subcategory_name": "Tourism",
+                    "subcategory_code": "TV",
+                    "fee": null,
+                    "currency": null
+                }
+            ]
+        }
+    },
+    {
+        "identifier": "VFS_IE_FI",
+        "enable": false,
+        "need_account": true,
+        "account_pool": "ie_fi",
+        "need_proxy": true,
+        "proxy_pool": "ireland_proxies",
+        "target_instances": 1,
+        "account_login_interval": 30,
+        "query_wait": {
+            "mode": 2,
+            "fixed_wait": 10,
+            "random_min": 60,
+            "random_max": 300
+        },
+        "plugin_config": {
+            "lib_path": "plugins",
+            "plugin_name": "vfs_plugin",
+            "plugin_bin": "vfs_plugin.py",
+            "plugin_proto": "IVSPlg"
+        },
+        "free_config": {
+            "verbose": 0,
+            "mission_code": "fin",
+            "mission_name": "Finland",
+            "country_code": "irl",
+            "country_name": "Ireland",
+            "culture_code": "en-US",
+            "language": "en",
+            "website": "https://visa.vfsglobal.com/irl/en/fin/login",
+            "appointment_types": [
+                {
+                    "id": 559,
+                    "routing_key": "slot.dub.fi.tourist",
+                    "center_name": "Application Centre, Dublin",
+                    "city": "Dublin",
+                    "visa_type": "Tourist",
+                    "country": "Finland",
+                    "address": "Cunningham House, 130 Francis Street, Dublin 8 D08 H48R",
+                    "vac_code": "Dubb",
+                    "category_name": "VISA",
+                    "category_code": "S S",
+                    "subcategory_name": "Tourist Category",
+                    "subcategory_code": "Tourist Category",
+                    "fee": null,
+                    "currency": null
+                }
+            ]
+        }
+    },
+    {
+        "identifier": "VFS_IE_HU",
+        "enable": false,
+        "need_account": true,
+        "account_pool": "ie_hu",
+        "need_proxy": true,
+        "proxy_pool": "ireland_proxies",
+        "target_instances": 1,
+        "account_login_interval": 30,
+        "query_wait": {
+            "mode": 2,
+            "fixed_wait": 10,
+            "random_min": 60,
+            "random_max": 300
+        },
+        "plugin_config": {
+            "lib_path": "plugins",
+            "plugin_name": "vfs_plugin",
+            "plugin_bin": "vfs_plugin.py",
+            "plugin_proto": "IVSPlg"
+        },
+        "free_config": {
+            "verbose": 0,
+            "mission_code": "hun",
+            "mission_name": "Hungary",
+            "country_code": "irl",
+            "country_name": "Ireland",
+            "culture_code": "en-US",
+            "language": "en",
+            "website": "https://visa.vfsglobal.com/irl/en/hun/login",
+            "appointment_types": [
+                {
+                    "id": 566,
+                    "routing_key": "slot.dub.hu.tourist",
+                    "center_name": "Ireland Visa Application Center,Dublin",
+                    "city": "Dublin",
+                    "visa_type": "Tourist",
+                    "country": "Hungary",
+                    "address": "Cunningham House, 130 Francis Street Dublin",
+                    "vac_code": "DUB",
+                    "category_name": "Short Stay",
+                    "category_code": "SS",
+                    "subcategory_name": "Schengen Visa",
+                    "subcategory_code": "Schengen Visa",
+                    "fee": null,
+                    "currency": null
+                }
+            ]
+        }
+    },
+    {
+        "identifier": "VFS_IE_IS",
+        "enable": false,
+        "need_account": true,
+        "account_pool": "ie_is",
+        "need_proxy": true,
+        "proxy_pool": "ireland_proxies",
+        "target_instances": 1,
+        "account_login_interval": 30,
+        "query_wait": {
+            "mode": 2,
+            "fixed_wait": 10,
+            "random_min": 60,
+            "random_max": 300
+        },
+        "plugin_config": {
+            "lib_path": "plugins",
+            "plugin_name": "vfs_plugin",
+            "plugin_bin": "vfs_plugin.py",
+            "plugin_proto": "IVSPlg"
+        },
+        "free_config": {
+            "verbose": 0,
+            "mission_code": "isl",
+            "mission_name": "Iceland",
+            "country_code": "irl",
+            "country_name": "Ireland",
+            "culture_code": "en-US",
+            "language": "en",
+            "website": "https://visa.vfsglobal.com/irl/en/isl/login",
+            "appointment_types": [
+                {
+                    "id": 569,
+                    "routing_key": "slot.dub.is.tourist",
+                    "center_name": "Iceland Visa Application Center- Dublin",
+                    "city": "Dublin",
+                    "visa_type": "Tourist",
+                    "country": "Iceland",
+                    "address": "Cunningham House, 130 Francis Street, Dublin, Ireland- DO8 H48R",
+                    "vac_code": "DUB",
+                    "category_name": "C-Visa",
+                    "category_code": "CVI",
+                    "subcategory_name": "Tourism",
+                    "subcategory_code": "OTT",
+                    "fee": null,
+                    "currency": null
+                }
+            ]
+        }
+    },
+    {
+        "identifier": "BLS_IE_ES",
+        "enable": true,
+        "need_account": true,
         "account_pool": "ie_es",
         "need_proxy": true,
         "proxy_pool": "local",
@@ -69,7 +621,7 @@
         },
         "free_config": {
             "domain": "ireland.blsspainglobal.com", 
-            "ocr_service_url": "http://127.0.0.1:8085/predict/vfcode?model=pytorch", 
+            "ocr_service_url": "http://127.0.0.1:8085/predict/bls?model=pytorch", 
             
             "location": "Dublin",
             "jurisdiction": null,
@@ -79,8 +631,8 @@
         }
     },
     {
-        "identifier": "TLS_LONDON_FR",
-        "enable": true,
+        "identifier": "TLS_GB_FR",
+        "enable": false,
         "need_account": true,
         "account_pool": "gb_fr",
         "need_proxy": true,
@@ -116,5 +668,31 @@
             "target_labels": ["", "pta"],
             "website": "https://visas-fr.tlscontact.com/country/gb/vac/gbLON2fr/"
         }
+    },
+    {
+        "identifier": "VISAMETRIC_IE_DE",
+        "enable": false,
+        "need_account": false,
+        "account_pool": "",
+        "need_proxy": true,
+        "proxy_pool": "ireland_proxies",
+        "target_instances": 1,
+        "query_wait": {
+            "mode": 2,
+            "random_min": 60,
+            "random_max": 300
+        },
+        "plugin_config": {
+            "lib_path": "plugins",
+            "plugin_name": "de_plugin",
+            "plugin_bin": "de_plugin.py",
+            "plugin_proto": "IVSPlg"
+        },
+        "free_config": {
+            "verbose": 0,
+            "base_url": "https://ie-appointment.visametric.com",
+            "ocr_service_url": "http://127.0.0.1:8085/predict/visametric", 
+            "consularid": 1
+        }
     }
 ]

+ 19 - 18
group_coordinator.py

@@ -154,7 +154,7 @@ class GroupCoordinator:
                     else:
                         VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Query failed, No availability found")
                 except Exception as e:
-                    VSC_ERROR("coordinator", f"[{self.m_cfg.identifier}] Exception during query: {e}")
+                    VSC_ERROR("coordinator", f"[{self.m_cfg.identifier}] Exception during query: {e.message}")
 
                 # 计算下次运行时间
                 # 如果刚刚触发了抢票(无论成功失败),建议强制加长一点冷却时间,防止反爬
@@ -292,12 +292,12 @@ class GroupCoordinator:
         return plg_cfg
 
     def _create_instance(self, plg_cfg: VSPlgConfig) -> Optional[IVSPlg]:
-        """
-        @brief 创建并初始化单个插件实例。
-        这个方法在 creator_loop 的线程池中执行。
-        """
-        VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Creating plugin instance (plugin={self.m_cfg.plugin_config.plugin_name})...")
-        try:
+        # """
+        # @brief 创建并初始化单个插件实例。
+        # 这个方法在 creator_loop 的线程池中执行。
+        # """
+        # VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Creating plugin instance (plugin={self.m_cfg.plugin_config.plugin_name})...")
+        # try:
             inst = self.m_factory.create(self.m_cfg.identifier, self.m_cfg.plugin_config.plugin_name)
             inst.set_config(plg_cfg)
             inst.create_session()
@@ -306,9 +306,9 @@ class GroupCoordinator:
                     self.m_cfg.account_pool, plg_cfg.account.id, self.m_cfg.account_login_interval * 60)
             VSC_INFO("coordinator", f"[{self.m_cfg.identifier}] Plugin instance created and session established.")
             return inst
-        except Exception as e:
-            VSC_ERROR("coordinator", f"[{self.m_cfg.identifier}] Error creating plugin instance: {e}")
-        return None
+        # except Exception as e:
+        #     VSC_ERROR("coordinator", f"[{self.m_cfg.identifier}] Error creating plugin instance: {e}")
+        # return None
 
     def on_query_result(self, sptr: IVSPlg, query_result: VSQueryResult):
         VSC_INFO("coordinator", f"[{self.m_cfg.identifier}] Query result received: {str(query_result)}. BLOCKING monitor loop for booking...")
@@ -346,8 +346,8 @@ class GroupCoordinator:
                         self.push_callback_(100, f"Booking Success: {order_id}".encode('utf-8'), 0) 
                     
                     # 4. 成功逻辑:更新任务状态为 grabbed
-                    # 构造历史记录详情对象
-                    history_detail = {
+                    # 包含后端需要的关键信息
+                    current_grab_info = {
                         "account": book_res.account,
                         "session_id": book_res.session_id,
                         "slot_date": book_res.book_date,
@@ -355,20 +355,21 @@ class GroupCoordinator:
                         "timestamp": int(time.time()),
                         "payment_link": book_res.payment_link,
                     }
-
+                    
                     update_data = {
                         "status": "grabbed",
-                        # 修改点:grabbed_history 是一个字符串列表
-                        # 我们将详情对象序列化为 JSON 字符串放入列表中
-                        "grabbed_history": [json.dumps(history_detail)]
+                        # === 修改点:直接覆盖 ===
+                        # 直接发送字典对象,requests 会自动序列化为 JSON Object
+                        # 满足后端 "type":"dict_type" 的校验
+                        "grabbed_history": current_grab_info
                     }
                     
                     VSCloudApi.Instance().update_vas_task(task_id, update_data)
                     VSC_INFO("coordinator", f"[{inst.get_group_id()}] Task {task_id} marked as GRABBED.")
                     # 成功后 task_id 置空,防止 finally 块再次将其重置为 pending
                     task_id = None 
-            # except Exception as e:
-            #     VSC_ERROR("coordinator", f"[{inst.get_group_id()}] Exception during booking: {e}")
+            except Exception as e:
+                VSC_ERROR("coordinator", f"[{inst.get_group_id()}] Exception during booking: {e.message}")
             
             finally:
                 # 5. Return to Queue (回滚机制)

+ 14 - 4
plugins/bls_plugin.py

@@ -18,7 +18,7 @@ from cryptography.hazmat.primitives.asymmetric import padding
 from cryptography.hazmat.backends import default_backend
 
 # 框架依赖
-from vs_plg import IVSPlg, VSError 
+from vs_plg import IVSPlg 
 from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError
 from vs_log_macros import VSC_INFO, VSC_ERROR, VSC_DEBUG, VSC_WARN 
 from toolkit.vs_cloud_api import VSCloudApi 
@@ -37,11 +37,10 @@ class BlsPlugin(IVSPlg):
         
         # 运行时状态
         self.book_params: Dict = {} 
-        self.last_error = VSError(0, "OK")
         self.is_healthy = True
         
         # OCR 服务地址默认值
-        self.ocr_service_url = "http://127.0.0.1:8085/predict/vfcode?model=pytorch"
+        self.ocr_service_url = "http://127.0.0.1:8085/predict/bls?model=pytorch"
         self.browser = get_browser()
 
     def get_group_id(self) -> str:
@@ -311,6 +310,17 @@ class BlsPlugin(IVSPlg):
         VSC_INFO("bls_plg", "[%s] Book Success. Liveness URL: %s", self.group_id, res.payment_link)
         return res
     
+    def _get_proxy_url(self):
+            # 构造代理
+        proxy_url = ""
+        if self.config.proxy.ip:
+            s = self.config.proxy
+            if s.username:
+                proxy_url = f"{s.scheme}://{s.username}:{s.password}@{s.ip}:{s.port}"
+            else:
+                proxy_url = f"{s.scheme}://{s.ip}:{s.port}"
+        return proxy_url
+    
     def _save_debug_html(self, content: str, prefix: str = "debug"):
         save_dir = "debug_pages"
         if not os.path.exists(save_dir):
@@ -731,7 +741,7 @@ class BlsPlugin(IVSPlg):
         # Headers 需要 Token
         headers = {
             'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0.0.0 Safari/537.36',
-            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
+            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
             "Referer": f"https://{domain}/{referer}",
             "X-Requested-With": "XMLHttpRequest",
             "requestverificationtoken": token

+ 540 - 0
plugins/de_plugin.py

@@ -0,0 +1,540 @@
+import time
+import json
+import random
+import re
+import os
+import base64
+from datetime import datetime
+from typing import List, Dict, Optional, Any
+from urllib.parse import urljoin
+
+from curl_cffi import requests, const
+from bs4 import BeautifulSoup
+
+
+# 框架依赖
+from vs_plg import IVSPlg 
+from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
+from vs_log_macros import VSC_INFO, VSC_ERROR, VSC_DEBUG, VSC_WARN 
+from toolkit.vs_cloud_api import VSCloudApi 
+
+
+def get_alias_email(email: str, new_domain: str = "gmail-app.com") -> str:
+    """
+    将邮箱域名替换为指定域名(默认 gmail-app.com)
+    """
+    if "@" not in email:
+        raise ValueError(f"Invalid email: {email}")
+
+    local_part, _ = email.rsplit("@", 1)
+    return f"{local_part}@{new_domain}"
+
+class DePlugin(IVSPlg):
+    """
+    Germany (Visametric) 签证预约插件
+    适配 Visametric Ireland -> Germany 流程
+    """
+
+    def __init__(self, group_id: str):
+        self.group_id = group_id
+        self.config: Optional[VSPlgConfig] = None
+        self.free_config: Dict[str, Any] = {}
+        
+        self.session: Optional[requests.Session] = None
+        self.user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
+        
+        # 状态
+        self.is_healthy = True
+        
+        # 关键上下文变量 (从页面提取)
+        self.base_url = "https://ie-appointment.visametric.com" 
+        self.csrf_token = ""
+        self.personal_info_val = ""
+        self.email_val_control = ""
+        
+        # 默认 OCR 服务地址
+        self.ocr_service_url = "http://127.0.0.1:8085/predict/visametric"
+
+    def get_group_id(self) -> str:
+        return self.group_id
+
+    def set_config(self, config: VSPlgConfig):
+        self.config = config
+        try:
+            self.free_config = json.loads(config.free_config) if config.free_config else {}
+        except:
+            self.free_config = {}
+            
+        if self.free_config.get("base_url"):
+            self.base_url = self.free_config["base_url"].rstrip('/')
+            
+        if self.free_config.get("ocr_service_url"):
+            self.ocr_service_url = self.free_config["ocr_service_url"]
+
+    def health_check(self) -> bool:
+        return self.is_healthy
+
+    def create_session(self):
+        """
+        初始化会话:过盾 -> 获取 CSRF -> 识别图片验证码 -> 提交验证码 -> 获取上下文
+        """
+        # 1. 初始化 Session
+        curlopt = {
+            const.CurlOpt.MAXAGE_CONN: 1800,
+            const.CurlOpt.MAXLIFETIME_CONN: 1800,
+            const.CurlOpt.VERBOSE: False,
+        }
+        
+        proxy_url = ""
+        if self.config.proxy.ip:
+            s = self.config.proxy
+            if s.username:
+                proxy_url = f"{s.scheme}://{s.username}:{s.password}@{s.ip}:{s.port}"
+            else:
+                proxy_url = f"{s.scheme}://{s.ip}:{s.port}"
+
+        self.session = requests.Session(
+            proxy=proxy_url,
+            impersonate="chrome124",
+            curl_options=curlopt,
+            use_thread_local_curl=False,
+            http_version=const.CurlHttpVersion.V2TLS
+        )
+        
+        # 2. 访问首页,获取 CSRF 和 Captcha 图片
+        # Visametric 首页通常有 Cloudflare
+        url_home = f"{self.base_url}/en"
+        
+        # 尝试过盾
+        self._solve_cloudflare5S_challenge()
+
+        default_headers = self._get_headers()
+        default_headers.pop("X-Requested-With")
+        
+        resp = self._perform_request('GET', url_home, headers=default_headers)
+        
+        html = resp.text
+        soup = BeautifulSoup(html, 'html.parser')
+        meta = soup.find('meta', {'name': 'csrf-token'})
+        if not meta:
+            raise NotFoundError(message='Missing csrf-token in html')
+        self.csrf_token = meta.get('content', '')
+
+        # 提取验证码图片 Base64, 正则匹配: "data:image/png;base64," + "..."
+        match = re.search(r'"data:image/png;base64,"\s*\+\s*"(.*?)"', html)
+        if not match:
+            raise NotFoundError(message="Captcha image not found")
+  
+        captcha_b64 = base64.b64decode(match.group(1))
+
+        # 3. 识别验证码
+        resp = requests.post(
+            self.ocr_service_url, 
+            data=captcha_b64, 
+            headers={"Content-Type": "application/octet-stream"},
+            timeout=10
+        )
+        if resp.status_code != 200:
+            raise BizLogicError(message='Captcha ocr server failed')
+        captcha_code = resp.json().get('data', '').replace('$', '')
+        VSC_INFO("de_plg", "[%s] Captcha recognized: %s", self.group_id, captcha_code)
+
+        # 4. 提交验证码 (/appointment-form)
+        # 这一步是为了让服务器验证 Session,并返回包含 personalinfo 的页面
+        self._submit_captcha(captcha_code)
+        VSC_INFO("de_plg", "[%s] Session created successfully.", self.group_id)
+
+    def query(self) -> VSQueryResult:
+        """
+        查询可用日期 (/getdate)
+        """
+        res = VSQueryResult()
+
+        # 构造 Payload (参考 get_slot_day) 这里的 ID 需要根据实际情况配置,或者使用原代码的默认值
+        consular_id = self.free_config.get("consularid", "1") # 1=Ireland?
+        
+        url = f"{self.base_url}/en/getdate"
+        payload = {
+            "consularid": consular_id,
+            "exitid": "1",
+            "servicetypeid": "1",
+            "calendarType": "2",
+            "totalperson": "1"
+        }
+        
+        default_headers = self._get_headers()
+        resp = self._perform_request('POST', url, data=payload, headers=default_headers)
+
+        # Visametric 返回 JSON: {"getDateEnable": ["15-01-2026", "16-01-2026"]}
+        j = resp.json()
+        dates = j.get("getDateEnable", [])
+        
+        if dates:
+            res.success = True
+            res.availability_status = AvailabilityStatus.Available
+            # Visametric 返回 DD-MM-YYYY, 标准化为 DD/MM/YYYY
+            res.earliest_date = dates[0].replace("-", "/") 
+            
+            for d in dates:
+                da = VSQueryResult.DateAvailability()
+                da.date = d.replace("-", "/")
+                da.times.append(VSQueryResult.DateAvailability.TimeSlot(time="00:00", label="Available"))
+                res.availability.append(da)
+        else:
+            res.success = False
+            res.availability_status = AvailabilityStatus.NoneAvailable
+        return res
+
+    def book(self, slot_info: VSQueryResult, user_inputs: Dict) -> VSBookResult:
+        """
+        执行预约:选择日期 -> 选择时间 -> 发邮件 -> 填表 -> 提交
+        """
+        res = VSBookResult()
+        
+        # 1. 筛选日期
+        available_dates = [da.date.replace("/", "-") for da in slot_info.availability]
+        exp_start = user_inputs.get('expected_start_date', '')
+        exp_end = user_inputs.get('expected_end_date', '')
+        
+        valid_dates = self._filter_dates(available_dates, exp_start, exp_end)
+        if not valid_dates:
+            raise NotFoundError(message="No dates match user constraints")
+        
+        target_date = random.choice(valid_dates)
+        VSC_INFO("de_plg", "[%s] Selected date: %s", self.group_id, target_date)
+        
+        # 2. 获取时间 (/senddate)
+        time_slot = self._get_slot_time(target_date)
+        VSC_INFO("de_plg", "[%s] Selected time: %s", self.group_id, time_slot['time'])
+
+        # 3. 触发邮件流程 (Step 1: /jky45fgd)
+        alias_email = get_alias_email(user_inputs.get("email"), new_domain='gmail-app.com')
+        self._send_email_step1(alias_email)
+            
+        # 4. 触发邮件流程 (Step 2: /confirmCodeSendMail) 这一步会发送包含验证码的邮件 根据原代码逻辑: send_email("0") 触发发送
+        self._send_email_step2("0")
+
+        # 5. 读取 OTP
+        # Visametric 邮件发送者
+        recipient = alias_email
+        otp_code = self._read_otp_email(recipient)
+            
+        # 6. 提交验证码并确认 (/personal/appointment/create)
+        book_res_html = self._confirm_appointment(target_date, time_slot, user_inputs, otp_code, alias_email)
+            
+        if "complete all required fields" in book_res_html.lower():
+            raise BizLogicError(message='Comfirm appointment response <complete all required fields>')
+    
+        # 7. 提取结果
+        match = re.search(r'https:\/\/checkout\.stripe\.com\/c\/pay\/[^\s"]+', book_res_html)
+        
+        
+        res.success = True
+        res.fee_amount = 3000
+        res.fee_currency = 'EUR'
+        res.book_date = target_date
+        res.book_time = time_slot['time']
+        
+        if match:
+            res.payment_link = match.group(0)
+            VSC_INFO("de_plg", "[%s] Payment Link Found: %s", self.group_id, res.payment_link)
+        return res
+
+    # ---------------------------------------------------------
+    # 辅助方法
+    # ---------------------------------------------------------
+    
+    def _get_headers(self) -> Dict[str, str]:
+        """基础 Header"""
+        return {
+            "User-Agent": self.user_agent,
+            "Accept": "*/*",
+            "Accept-Language": "en,zh-CN;q=0.9,zh;q=0.8",
+            "Origin": self.base_url,
+            "Referer": f"{self.base_url}/en/appointment-form", # 默认 Referer
+            "X-Requested-With": "XMLHttpRequest"
+        }
+
+    def _submit_captcha(self, code):
+        url = f"{self.base_url}/en/appointment-form"
+        payload = {
+            '_token': self.csrf_token,
+            'cpJvnsControl': '',
+            'mailConfirmCode': code
+        }
+        headers = self._get_headers()
+        headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
+        
+        resp = self._perform_request('POST', url, data=payload, headers=headers)
+  
+        # 关键:提交验证码后,返回的 HTML 中包含了后续需要的加密参数        
+        match_pi = re.search(r"personalinfo:\s*'([^']*)'", resp.text)
+        if match_pi:
+            self.personal_info_val = match_pi.group(1)
+        
+        # emailValControl: '...'
+        match_ev = re.search(r"emailValControl:\s*'([^']*)'", resp.text)
+        if match_ev:
+            self.email_val_control = match_ev.group(1)
+            
+        if not self.personal_info_val:
+            raise NotFoundError(message="Personalinfo not found in captcha response")
+   
+    def _get_slot_time(self, date) -> Optional[Dict]:
+        url = f"{self.base_url}/en/senddate"
+        payload = {
+            "fulldate": date,
+            "totalperson": "1",
+            "set_new_consular_id": self.free_config.get("consularid", "1"),
+            "set_new_exit_office_id": "1",
+            "calendarType": "2",
+            "set_new_service_type_id": "1",
+            "personalinfo": self.personal_info_val
+        }
+        headers = self._get_headers()
+        headers['X-CSRF-TOKEN'] = self.csrf_token # 这里需要 CSRF
+        headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
+        
+        resp = self._perform_request('POST', url, data=payload, headers=headers)
+        soup = BeautifulSoup(resp.text, 'html.parser')
+        buttons = soup.find_all('button')
+        slots = []
+        for btn in buttons:
+            i_tag = btn.find('i')
+            if i_tag:
+                time_val = i_tag.next_sibling.strip()
+                slots.append({
+                    'time': time_val,
+                    'data_id': btn.get('data-id'),
+                    'data_all': btn.get('data-all')
+                })
+        if slots:
+            return random.choice(slots)
+        else:
+            raise NotFoundError(message='Not slot time available')
+   
+
+    def _send_email_step1(self, email):
+        url = f"{self.base_url}/en/jky45fgd"
+        payload = {
+            "emailCheck": email,
+            "personalinfo": self.personal_info_val
+        }
+        headers = self._get_headers()
+        headers['X-CSRF-TOKEN'] = self.csrf_token
+        headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
+        self._perform_request('POST', url, data=payload, headers=headers)
+        
+
+    def _send_email_step2(self, code_val):
+        url = f"{self.base_url}/en/confirmCodeSendMail"
+        payload = {
+            "confirmCode": code_val,
+            "emailValControl": self.email_val_control
+        }
+        headers = self._get_headers()
+        headers['X-CSRF-TOKEN'] = self.csrf_token
+        headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
+        self._perform_request('POST', url, data=payload, headers=headers)
+    
+    def _read_otp_email(self, recipient) -> str:
+        """
+        读取 OTP 邮件
+        """
+        master_email = "visafly666@gmail.com"
+        sender = 'Visametric - verify at visametric.com'
+        subject_keywords = 'Verification Code'
+        body_keywords = 'Verification code'
+
+        now_utc = datetime.utcnow()
+        formatted_utc_time = now_utc.strftime("%Y-%m-%d %H:%M:%S")
+
+        VSC_INFO("de_plg", "[%s] Waiting for OTP email sent after %s...", self.group_id, formatted_utc_time)
+
+        # 3. 轮询查收
+        for i in range(12):
+            content_out = VSCloudApi.Instance().fetch_mail_content(
+                master_email,
+                sender,
+                recipient,
+                subject_keywords,
+                body_keywords,
+                formatted_utc_time,
+                300
+            )
+
+            if content_out:
+                match = re.search(r'\b\d{6}\b', content_out)
+                if match:
+                    otp = match.group(0)
+                    VSC_INFO("de_plg", "[%s] OTP code found: %s", self.group_id, otp)
+                    return otp
+            
+            time.sleep(5)
+        raise NotFoundError(message="OTP email not found (timeout)")
+
+    def _confirm_appointment(self, date, slot_data, user_inputs, otp, alias_email):
+        url = f"{self.base_url}/en/personal/appointment/create"
+        
+        # 处理日期格式 YYYY-MM-DD
+        def _get_dob(d_str):
+            try: return datetime.strptime(d_str[:10], "%Y-%m-%d")
+            except: return datetime.now()
+            
+        dob = _get_dob(user_inputs.get('birthday', ''))
+        
+        payload = {
+            "_token": self.csrf_token,
+            "country": str(self.free_config.get("consularid", "1")),
+            "visitingcountry": str(self.free_config.get("consularid", "1")),
+            "city": "6", # Dublin? 需配置
+            "office": "1",
+            "officetype": "1",
+            "totalPerson": "1",
+            
+            "name1": user_inputs.get('first_name', '').upper(),
+            "surname1": user_inputs.get('last_name', '').upper(),
+            "nationality1": "2", # 假设值
+            
+            "birthday1": str(dob.day),
+            "birthmonth1": str(dob.month),
+            "birthyear1": str(dob.year),
+            
+            "passport1": user_inputs.get('passport_no'),
+            # 原代码 passport_expried 是 DD-MM-YYYY
+            "passportExpirationDate1": datetime.strptime(user_inputs.get('passport_expiry_date', '')[:10], "%Y-%m-%d").strftime("%d-%m-%Y"),
+            "email1": alias_email,
+            "phone1": user_inputs.get('phone_no'),
+            "alternativephone1": "",
+            
+            # 其他 person 留空
+            "name2": "", "surname2": "", "nationality2": "0", "birthday2": "0", "birthmonth2": "0", "birthyear2": "0", "passport2": "", "passportExpirationDate2": "", "email2": alias_email, "phone2": user_inputs.get('phone_no'), "alternativephone2": "",
+            "name3": "", "surname3": "", "nationality3": "0", "birthday3": "0", "birthmonth3": "0", "birthyear3": "0", "passport3": "", "passportExpirationDate3": "", "email3": alias_email, "phone3": user_inputs.get('phone_no'), "alternativephone3": "",
+            "name4": "", "surname4": "", "nationality4": "0", "birthday4": "0", "birthmonth4": "0", "birthyear4": "0", "passport4": "", "passportExpirationDate4": "", "email4": alias_email, "phone4": user_inputs.get('phone_no'), "alternativephone4": "",
+
+            "mailConfirmCode": otp,
+            "ctval": slot_data['data_id'],
+            "qtallvert": slot_data['data_all'],
+            
+            "oldofficetype": "1",
+            "oldtotalperson": "1",
+            "rePaymentControl": "0",
+            
+            # 关键:View Set
+            "view_set_app_country": "Schengen - Tourism/Family&Friend Visit/Transit Visa/Other Purposes", # 需配置
+            "view_set_app_office": "Dublin",
+            "view_set_app_service_type": "NORMAL",
+            
+            "cargoactive": "0",
+            "setnewcalendarstatus": "2",
+            "availableDaycontrol": "0",
+            
+            "travelStartDate": datetime.strptime(user_inputs.get('travel_date', '')[:10], "%Y-%m-%d").strftime("%d-%m-%Y"),
+            "personalapproveTerms": "1"
+        }
+        
+        headers = self._get_headers()
+        headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
+        
+        resp = self._perform_request('POST', url, data=payload, headers=headers)
+        return resp.text
+
+    def _filter_dates(self, dates: List[str], start_str: str, end_str: str) -> List[str]:
+        """
+        根据用户的期望范围筛选可用日期
+        
+        :param dates: API 返回的可用日期列表 (通常是 DD-MM-YYYY 或 DD/MM/YYYY)
+        :param start_str: 用户期望开始日期 (YYYY-MM-DD)
+        :param end_str: 用户期望结束日期 (YYYY-MM-DD)
+        :return: 符合要求的日期列表
+        """
+        # 如果没有设置范围,则不过滤,返回所有日期
+        if not start_str or not end_str:
+            return dates
+            
+        valid_dates = []
+        try:
+            # 1. 解析用户期望的范围 (通常是 YYYY-MM-DD)
+            # 截取前10位以防带有时分秒
+            s_date = datetime.strptime(start_str[:10], "%Y-%m-%d")
+            e_date = datetime.strptime(end_str[:10], "%Y-%m-%d")
+            
+            for date_str in dates:
+                try:
+                    # 2. 解析 API 返回的日期
+                    # Visametric 通常返回 DD-MM-YYYY,但为了稳健,尝试两种常见分隔符
+                    # 替换 / 为 - 统一格式处理
+                    clean_date_str = date_str.replace("/", "-")
+                    curr_date = datetime.strptime(clean_date_str, "%d-%m-%Y")
+                    
+                    # 3. 比较范围 (闭区间)
+                    if s_date <= curr_date <= e_date:
+                        valid_dates.append(date_str)
+                except ValueError:
+                    # 如果某个日期格式解析失败,跳过该日期,不影响其他
+                    VSC_DEBUG("gmy_plg", f"Date parse error for slot: {date_str}")
+                    continue
+                    
+        except ValueError as e:
+            # 如果用户配置的日期格式不对,记录警告并返回所有日期(或者空,视业务需求)
+            # 这里选择返回所有日期,避免因配置错误导致一直无法下单
+            VSC_WARN("gmy_plg", f"User date range format error: {e}. Returning all slots.")
+            return dates
+
+        return valid_dates
+    
+    def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None):
+        """
+        统一 HTTP 请求封装,严格复刻 C++ 逻辑:
+        1. 发送 OPTIONS 请求
+        2. 发送实际请求
+        """
+        print(f'[perform request] {method} {url}')
+        resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
+        VSC_INFO('tls_plg', resp.text)
+        if resp.status_code == 200:
+            return resp
+        elif resp.status_code == 401:
+            self.is_healthy = False
+            raise SessionExpiredOrInvalidError()
+        elif resp.status_code == 403:
+            raise PermissionDeniedError()
+        elif resp.status_code == 429:
+            self.is_healthy = False
+            raise RateLimiteddError()
+        else:
+            raise BizLogicError(message=f"HTTP Error {resp.status_code}: {resp.text[:100]}")
+
+    def _solve_cloudflare5S_challenge(self):
+        """
+        解决 Cloudflare 5s 盾
+        """
+        VSC_INFO("de_plg", f"[{self.group_id}] Solving Cloudflare 5s...")
+        website_url = f'{self.base_url}/en'
+        
+        # 1. 格式化代理字符串, 这里的接口要求格式通常是: host:port:user:pass (根据你的脚本示例)
+        p = self.config.proxy
+        if p.username:
+            proxy_str = f"{p.ip}:{p.port}:{p.username}:{p.password}"
+        else:
+            proxy_str = f"{p.ip}:{p.port}"
+        # 2. 提交任务
+        task = VSCloudApi.Instance().submit_anticloudflare_task(proxy_str, website_url)
+        # 3. 等待结果
+        task_id = str(task['id'])
+        result = VSCloudApi.Instance().get_anticloudflare_result(task_id)
+        parsed = json.loads(result.get('result', '{}'))
+        cookies_list = parsed.get('cookies', [])
+        for cookie in cookies_list:
+            if cookie['name'] in ['__cf_bm', 'cf_clearance']:
+                self.session.cookies.set(
+                    cookie['name'], 
+                    cookie['value'], 
+                    domain=cookie['domain'], 
+                    path='/'
+                )
+        ua = parsed.get('userAgent')
+        if ua:
+            self.user_agent = ua
+            self.session.headers['User-Agent'] = ua
+        VSC_INFO("de_plg", "[%s] Cloudflare 5s challenge solved.", self.group_id)

+ 1 - 1
plugins/tls_plugin.py

@@ -11,7 +11,7 @@ from curl_cffi import requests, const
 from bs4 import BeautifulSoup
 
 from vs_plg import IVSPlg
-from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatus,  NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
+from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
 from vs_log_macros import VSC_INFO, VSC_ERROR, VSC_DEBUG, VSC_WARN
 from toolkit.vs_cloud_api import VSCloudApi
 

+ 206 - 184
plugins/vfs_plugin.py

@@ -8,7 +8,7 @@ import urllib.parse
 from datetime import datetime
 from typing import Dict, Any, Optional, List, Tuple
 
-from curl_cffi import requests
+from curl_cffi import requests, const
 # 加密库
 from cryptography.hazmat.primitives import serialization, hashes
 from cryptography.hazmat.primitives.asymmetric import padding
@@ -18,7 +18,6 @@ from vs_plg import IVSPlg
 from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
 from vs_log_macros import VSC_INFO, VSC_ERROR, VSC_DEBUG, VSC_WARN 
 from toolkit.vs_cloud_api import VSCloudApi 
-from toolkit.rule_engine import RuleEngine 
 
 # ----------------- 静态常量与辅助数据 -----------------
 
@@ -52,17 +51,18 @@ def get_alias_email(email: str, new_domain: str = "gmail-app.com") -> str:
     return f"{local_part}@{new_domain}"
 
 
-# ----------------- VfsPlugin 类 -----------------
+def to_yyyymmdd(data_str: str, date_str_format: str, target_format: str="%Y-%m-%d"):
+    # 转换日期到YYYY-MM-DD 固定格式
+    dt = datetime.strptime(data_str, date_str_format)
+    return dt.strftime("%Y-%m-%d")
 
 class VfsPlugin(IVSPlg):
     def __init__(self, group_id: str):
         self.group_id = group_id
         self.config: Optional[VSPlgConfig] = None
         self.free_config: Dict[str, Any] = {}
-        
-        self.session = requests.Session()
-        # 模拟 Chrome 124
-        self.session.impersonate = "chrome124"
+
+        self.session: Optional[requests.Session] = None
         
         self.jwt_token = ""
         self.user_agent = ""
@@ -88,23 +88,28 @@ class VfsPlugin(IVSPlg):
             self.free_config = json.loads(config.free_config) if config.free_config else {}
         except:
             self.free_config = {}
-        
-        # 设置代理
-        if config.proxy.ip:
-            proxy_str = f"{config.proxy.scheme}://"
-            if config.proxy.username:
-                proxy_str += f"{config.proxy.username}:{config.proxy.password}@"
-            proxy_str += f"{config.proxy.ip}:{config.proxy.port}"
-            self.session.proxies = {"http": proxy_str, "https": proxy_str}
-            VSC_DEBUG("vfs_plg", "[%s] Proxy set: %s", self.group_id, config.proxy.ip)
 
     def health_check(self) -> bool:
-        # 直接利用 VSError 的逻辑
         return self.is_healthy
 
     def create_session(self) -> None:
-        """登录流程"""
-        VSC_INFO("vfs_plg", "[%s] Starting login...", self.group_id)
+        # 初始化 Session
+        curlopt = {
+            const.CurlOpt.MAXAGE_CONN: 1800,
+            const.CurlOpt.MAXLIFETIME_CONN: 1800,
+            const.CurlOpt.VERBOSE: False,
+        }
+
+        self.session = requests.Session(
+            proxy=self._get_proxy_url(),
+            impersonate="chrome124",
+            curl_options=curlopt,
+            use_thread_local_curl=False,
+            http_version=const.CurlHttpVersion.V2TLS
+        )
+        
+        # 获取真实IP
+        self.real_ip = self._get_realnetwork_ip()
         
         # 1. Cloudflare Turnstile
         cf_token = self._handle_cloudflare_challenge()
@@ -114,8 +119,8 @@ class VfsPlugin(IVSPlg):
         password = self.config.account.password
         enc_password = self._encrypt_password(password)
         
-        mission_code = self.free_config.get("missionCode", "")
-        country_code = self.free_config.get("countryCode", "")
+        mission_code = self.free_config.get("mission_code", "")
+        country_code = self.free_config.get("country_code", "")
         
         client_src = self._get_client_source()
         orange_src = self._get_orange_source(email)
@@ -141,7 +146,7 @@ class VfsPlugin(IVSPlg):
         # 3. 发送登录请求 (包含 OPTIONS)
         resp = self._perform_request("POST", url, headers=headers, data=data)
         resp_json = resp.json()
-        if "accessToken" in resp_json and resp_json["accessToken"]:
+        if resp_json.get('accessToken', ''):
             self.jwt_token = resp_json["accessToken"]
             VSC_INFO("vfs_plg", "[%s] Login successful, JWT obtained.", self.group_id)
             return
@@ -157,17 +162,18 @@ class VfsPlugin(IVSPlg):
     def query(self) -> VSQueryResult:
         """查询可预约 Slot"""
         result = VSQueryResult() 
-        appt_types = self.free_config.get("appointmentType", [])
+        appt_types = self.free_config.get("appointment_types", [])
         if not appt_types:
             raise NotFoundError(message="No matching appointment configuration found.")
         apt_config = random.choice(appt_types)
         self._fetch_configurations(apt_config)
         earliest_date = self._query_earliest_slot(apt_config)
         result.success = False
-        result.visa_type = apt_config.get("subcategoryCode", "")
-        result.city = apt_config.get("vacCode", "")
-        result.country = self.free_config.get("countryCode", "")
-        result.routing_key = apt_config.get("routingKey", "")
+        result.availability_status = AvailabilityStatus.NoneAvailable
+        result.visa_type = apt_config.get("visa_type", "")
+        result.city = apt_config.get("city", "")
+        result.country = apt_config.get("country", "")
+        result.routing_key = apt_config.get("routing_key", "")
         if earliest_date:
             if "WaitList" in earliest_date:
                 result.success = True
@@ -177,65 +183,30 @@ class VfsPlugin(IVSPlg):
                 result.success = True
                 result.availability_status = AvailabilityStatus.Available
                 result.earliest_date = earliest_date
+                
                 VSC_INFO("vfs_plg", "[%s] Found Slot: %s", self.group_id, earliest_date)
                 
                 day_info = VSQueryResult.DateAvailability()
                 day_info.date = earliest_date
                 result.availability.append(day_info)
         return result
-    
-    def _get_filtered_covered_months(self, start_date, end_date, from_date) -> List[str]:
-        """
-        计算需要查询的月份列表,格式 YYYY-MM-DD (每月1号)
-        """
-        fmt = "%Y-%m-%d"
-        # 默认值处理
-        try:
-            dt_start = datetime.strptime(start_date, fmt) if start_date else datetime.now()
-            dt_end = datetime.strptime(end_date, fmt) if end_date else datetime.now().replace(year=datetime.now().year + 1)
-            
-            # from_date 格式可能是 DD/MM/YYYY (从 slot_info 来)
-            try:
-                dt_from = datetime.strptime(from_date, "%d/%m/%Y")
-            except:
-                dt_from = datetime.now()
-        except:
-            return []
-
-        # 归一化到月初
-        dt_start = dt_start.replace(day=1)
-        dt_end = dt_end.replace(day=1)
-        dt_from = dt_from.replace(day=1)
-        
-        # 起始点取 max(start, from)
-        curr = max(dt_start, dt_from)
-        
-        months = []
-        while curr <= dt_end:
-            months.append(curr.strftime(fmt))
-            # 下个月
-            if curr.month == 12:
-                curr = curr.replace(year=curr.year + 1, month=1)
-            else:
-                curr = curr.replace(month=curr.month + 1)
-        return months
 
     def book(self, slot_info: VSQueryResult, user_inputs) -> VSBookResult:
         """
         执行完整的预约流程,包含:上传文档 -> 添加申请人 -> OTP -> 选时间 -> 锁定 -> 支付
         """
-        user_email = user_inputs.get('email', 'get_visa_666@example.com')
+        user_email = user_inputs.get('email')
         user_inputs['alias_email'] = get_alias_email(user_email, new_domain="gmail-app.com")
         
         res = VSBookResult()
         slot_routing_key = slot_info.routing_key
         
-        from_date = slot_info.earliest_date if slot_info.earliest_date else datetime.now().strftime("%d/%m/%Y")
+        from_date = slot_info.earliest_date if slot_info.earliest_date else datetime.now().strftime("%Y-%m-%d")
         
         apt_config = None
-        appt_types = self.free_config.get("appointmentType", [])
+        appt_types = self.free_config.get("appointment_types", [])
         for apt in appt_types:
-            if apt.get("routingKey") == slot_routing_key or len(appt_types) == 1:
+            if apt.get("routing_key") == slot_routing_key:
                 apt_config = apt
                 break
         
@@ -244,7 +215,7 @@ class VfsPlugin(IVSPlg):
 
         self._fetch_configurations(apt_config)
 
-        sub_cc = apt_config.get("subcategoryCode")
+        sub_cc = apt_config.get("subcategory_code")
         sub_conf = self.subcategory_conf.get(sub_cc, {})
 
         # OCR 识别 / 文档上传
@@ -265,20 +236,20 @@ class VfsPlugin(IVSPlg):
         is_waitlist = (slot_info.availability_status == AvailabilityStatus.Waitlist)
         
         add_primary_retry = 0
-        MAX_RETRY = 3
-        success_add = False
+        MAX_RETRY = 6
         
         while add_primary_retry < MAX_RETRY:
             try:
                 final_urn = self._add_primary_applicant(apt_config, user_inputs, is_waitlist, ocr_enabled, enable_reference_number)
-                success_add = True
+                if not final_urn:
+                    raise NotFoundError(message="URN not found")
                 break
             except Exception as e:
                 VSC_WARN("vfs_plg", "[%s] Add Applicant retry %d...", self.group_id, add_primary_retry)
-                time.sleep(5)
+                time.sleep(10)
                 add_primary_retry += 1
         
-        if not success_add:
+        if not final_urn:
             raise BizLogicError(message="Failed to add primary applicant (Slot likely taken)")
 
         VSC_INFO("vfs_plg", "[%s] Applicant Added. URN: %s", self.group_id, final_urn)
@@ -305,13 +276,8 @@ class VfsPlugin(IVSPlg):
             raise BizLogicError(message='confirm waitlist failed')
 
         # 规则引擎与日期筛选 (核心步骤 3)
-        rules_str = user_inputs.get("rules", "")
-        rule_engine = RuleEngine(rules_str)
-        
         expected_start = user_inputs.get("expected_start_date", "")
         expected_end = user_inputs.get("expected_end_date", "")
-        rule_engine.set_date_range_start(expected_start)
-        rule_engine.set_date_range_end(expected_end)
         
         # 计算需要扫描的月份, 如果 expected_start/end 为空,默认使用 from_date 所在月
         months = self._get_filtered_covered_months(expected_start, expected_end, from_date)
@@ -329,11 +295,7 @@ class VfsPlugin(IVSPlg):
         
         # 遍历月份寻找 Slot
         for m_str in months:
-            # 需要 DD/MM/YYYY
-            dt_m = datetime.strptime(m_str, "%Y-%m-%d")
-            converted_date = dt_m.strftime("%d/%m/%Y")
-
-            ads = not self._query_slot_calendar(apt_config, final_urn, converted_date)
+            ads = self._query_slot_calendar(apt_config, final_urn, m_str)
                 
             # 过滤已知的 slots
             new_ads = [d for d in ads if d not in all_ads]
@@ -345,7 +307,8 @@ class VfsPlugin(IVSPlg):
                 avail_candidates = [d for d in list(all_ads) if d not in forbidden_dates]
                 
                 # 规则筛选
-                sel_dates = rule_engine.select_date(avail_candidates, "%d/%m/%Y")
+                sel_dates = self._filter_dates(avail_candidates, expected_start, expected_end)
+                print(f'avail_candidates={avail_candidates}, sel_dates={sel_dates}')
                 if not sel_dates:
                     break
                 
@@ -378,7 +341,7 @@ class VfsPlugin(IVSPlg):
                 break
                 
         if not found_slot:
-            VSC_INFO("vfs_plg", "[%s] No valid slots found after Rule Engine filtering.", self.group_id) 
+            VSC_INFO("vfs_plg", "[%s] No valid slots found.", self.group_id) 
             res.success = False
             return res
 
@@ -391,7 +354,7 @@ class VfsPlugin(IVSPlg):
         
         schedule_res = self._schedule(apt_config, final_urn, amount, currency, selected_slot_id)
         
-        if schedule_res.get("IsAppointmentBooked"):
+        if not schedule_res.get("IsAppointmentBooked"):
             VSC_INFO("vfs_plg", "[%s] IsAppointmentBooked is false", self.group_id) 
             res.success = False
             return res
@@ -417,6 +380,61 @@ class VfsPlugin(IVSPlg):
                     res.session_id = saved_session['session_id']
         return res
     
+    def _get_proxy_url(self):
+            # 构造代理
+        proxy_url = ""
+        if self.config.proxy.ip:
+            s = self.config.proxy
+            if s.username:
+                proxy_url = f"{s.scheme}://{s.username}:{s.password}@{s.ip}:{s.port}"
+            else:
+                proxy_url = f"{s.scheme}://{s.ip}:{s.port}"
+        return proxy_url
+    
+    def _get_filtered_covered_months(self, start_date, end_date, from_date) -> List[str]:
+        """
+        计算需要查询的月份列表,格式 YYYY-MM-DD (每月1号)
+        """
+        fmt = "%Y-%m-%d"
+        # 默认值处理
+        try:
+            dt_start = datetime.strptime(start_date, fmt) if start_date else datetime.now()
+            dt_end = datetime.strptime(end_date, fmt) if end_date else datetime.now().replace(year=datetime.now().year + 1)
+            
+            try:
+                dt_from = datetime.strptime(from_date, fmt)
+            except:
+                dt_from = datetime.now()
+        except:
+            return []
+
+        # 归一化到月初
+        dt_start = dt_start.replace(day=1)
+        dt_end = dt_end.replace(day=1)
+        dt_from = dt_from.replace(day=1)
+        
+        # 起始点取 max(start, from)
+        curr = max(dt_start, dt_from)
+        
+        months = []
+        while curr <= dt_end:
+            months.append(curr.strftime(fmt))
+            # 下个月
+            if curr.month == 12:
+                curr = curr.replace(year=curr.year + 1, month=1)
+            else:
+                curr = curr.replace(month=curr.month + 1)
+        return months
+    
+    def _get_realnetwork_ip(self):
+        url = "https://api.ipify.org/?format=json"
+        headers = {
+            'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
+            'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
+        }
+        resp = self._perform_request('GET', url, headers=headers)
+        return resp.json()['ip']
+    
     def _confirm_waitlist(self, apt_config: Dict[str, Any], urn: str) -> bool:
         """
         确认加入候补名单 (对应 C++ VFSApi::confirm_waitlist)
@@ -426,9 +444,9 @@ class VfsPlugin(IVSPlg):
         headers["content-type"] = "application/json;charset=UTF-8"
 
         data = {
-            "missionCode": self.free_config.get("missionCode"),
-            "countryCode": self.free_config.get("countryCode"),
-            "centerCode": apt_config.get("vacCode"),
+            "missionCode": self.free_config.get("mission_code"),
+            "countryCode": self.free_config.get("country_code"),
+            "centerCode": apt_config.get("vac_code"),
             "loginUser": self.config.account.username,
             "urn": urn,
             "notificationType": "none",
@@ -454,20 +472,18 @@ class VfsPlugin(IVSPlg):
         headers["content-type"] = "application/json;charset=UTF-8"
         
         data = {
-            "missioncode": self.free_config.get("missionCode"),
-            "countryCode": self.free_config.get("countryCode"),
-            "centerCode": apt_config.get("vacCode"),
+            "missioncode": self.free_config.get("mission_code"),
+            "countryCode": self.free_config.get("country_code"),
+            "centerCode": apt_config.get("vac_code"),
             "loginUser": self.config.account.username,
             "languageCode": "en-US",
-            "visaCategoryCode": apt_config.get("subcategoryCode"),
+            "visaCategoryCode": apt_config.get("subcategory_code"),
             "fileBytes": b64_str,
             "selfiImageFileBytes": ""
         }
         
-        result = {}
         resp = self._perform_request("POST", url, headers=headers, json_data=data)
-        j = resp.json()
-        result.update(j)
+        result = resp.json()
         result["passportImageFilename"] = "passport_img.jpg"
         result["passportImageFileBytes"] = b64_str
         return result
@@ -571,7 +587,7 @@ class VfsPlugin(IVSPlg):
             "Retryleft": "",
             
             # 真实 IP 注入
-            "ipAddress": self.real_ip or "127.0.0.1"
+            "ipAddress": self.real_ip
         }
 
         # --- 处理 Reference Number (Cover Letter) ---
@@ -588,11 +604,11 @@ class VfsPlugin(IVSPlg):
 
         # --- 构造最外层 Payload ---
         payload = {
-            "countryCode": self.free_config.get("countryCode"),
-            "missionCode": self.free_config.get("missionCode"),
-            "centerCode": apt_config.get("vacCode"),
+            "countryCode": self.free_config.get("country_code"),
+            "missionCode": self.free_config.get("mission_code"),
+            "centerCode": apt_config.get("vac_code"),
             "loginUser": self.config.account.username,
-            "visaCategoryCode": apt_config.get("subcategoryCode"),
+            "visaCategoryCode": apt_config.get("subcategory_code"),
             "applicantList": [applicant], # 数组形式
             
             "languageCode": "en-US",
@@ -619,9 +635,9 @@ class VfsPlugin(IVSPlg):
         data = {
             "urn": urn,
             "loginUser": self.config.account.username,
-            "missionCode": self.free_config.get("missionCode"),
-            "countryCode": self.free_config.get("countryCode"),
-            "centerCode": apt_config.get("vacCode"),
+            "missionCode": self.free_config.get("mission_code"),
+            "countryCode": self.free_config.get("country_code"),
+            "centerCode": apt_config.get("vac_code"),
             "OTP": "",
             "otpAction": "GENERATE",
             "languageCode": "en-US"
@@ -633,16 +649,15 @@ class VfsPlugin(IVSPlg):
     def _applicant_otp_verify(self, apt_config, urn, otp) -> bool:
         url = "https://lift-api.vfsglobal.com/appointment/applicantotp"
         headers = self._get_common_headers(with_auth=True)
-        # C++ specific: datacenter header
         headers["datacenter"] = "GERMANY" 
         headers["content-type"] = "application/json;charset=UTF-8"
         
         data = {
             "urn": urn,
             "loginUser": self.config.account.username,
-            "missionCode": self.free_config.get("missionCode"),
-            "countryCode": self.free_config.get("countryCode"),
-            "centerCode": apt_config.get("vacCode"),
+            "missionCode": self.free_config.get("mission_code"),
+            "countryCode": self.free_config.get("country_code"),
+            "centerCode": apt_config.get("vac_code"),
             "OTP": otp,
             "otpAction": "VALIDATE",
             "languageCode": "en-US"
@@ -655,14 +670,15 @@ class VfsPlugin(IVSPlg):
         url = "https://lift-api.vfsglobal.com/appointment/calendar"
         headers = self._get_common_headers(with_auth=True)
         headers["content-type"] = "application/json;charset=UTF-8"
-        
+        dt_m = datetime.strptime(from_date, "%Y-%m-%d")
+        converted_date = dt_m.strftime("%d/%m/%Y")
         data = {
-            "missionCode": self.free_config.get("missionCode"),
-            "countryCode": self.free_config.get("countryCode"),
-            "centerCode": apt_config.get("vacCode"),
+            "missionCode": self.free_config.get("mission_code"),
+            "countryCode": self.free_config.get("country_code"),
+            "centerCode": apt_config.get("vac_code"),
             "loginUser": self.config.account.username,
-            "visaCategoryCode": apt_config.get("subcategoryCode"),
-            "fromDate": from_date,
+            "visaCategoryCode": apt_config.get("subcategory_code"),
+            "fromDate": converted_date,
             "urn": urn,
             "payCode": ""
         }
@@ -672,11 +688,9 @@ class VfsPlugin(IVSPlg):
         if calendars:
             ads_out = []
             for item in calendars:
-                # C++ assumes "MM/DD/YYYY" -> "DD/MM/YYYY"
+                # "MM/DD/YYYY" -> "YYYY-MM-DD"
                 raw = item.get("date")
-                # Normalize to DD/MM/YYYY
-                dObj = datetime.strptime(raw, "%m/%d/%Y")
-                ads_out.append(dObj.strftime("%d/%m/%Y"))
+                ads_out.append(to_yyyymmdd(raw, "%m/%d/%Y"))
             return ads_out
         return []
      
@@ -684,14 +698,15 @@ class VfsPlugin(IVSPlg):
         url = "https://lift-api.vfsglobal.com/appointment/timeslot"
         headers = self._get_common_headers(with_auth=True)
         headers["content-type"] = "application/json;charset=UTF-8"
-        
+        dt_m = datetime.strptime(slot_date, "%Y-%m-%d")
+        converted_date = dt_m.strftime("%d/%m/%Y")
         data = {
-            "missionCode": self.free_config.get("missionCode"),
-            "countryCode": self.free_config.get("countryCode"),
-            "centerCode": apt_config.get("vacCode"),
+            "missionCode": self.free_config.get("mission_code"),
+            "countryCode": self.free_config.get("country_code"),
+            "centerCode": apt_config.get("vac_code"),
             "loginUser": self.config.account.username,
-            "visaCategoryCode": apt_config.get("subcategoryCode"),
-            "slotDate": slot_date,
+            "visaCategoryCode": apt_config.get("subcategory_code"),
+            "slotDate": converted_date,
             "urn": urn
         }
         resp = self._perform_request("POST", url, headers=headers, json_data=data)
@@ -703,22 +718,18 @@ class VfsPlugin(IVSPlg):
         headers["content-type"] = "application/json;charset=UTF-8"
         
         # ISO format conversion
-        try:
-             dt = datetime.strptime(earliest_date, "%d/%m/%Y")
-             iso_date = dt.strftime("%Y-%m-%dT%H:%M:%S")
-        except:
-             iso_date = earliest_date
+        dt = datetime.strptime(earliest_date, "%Y-%m-%d")
 
         data = {
-            "missionCode": self.free_config.get("missionCode"),
-            "countryCode": self.free_config.get("countryCode"),
-            "centerCode": apt_config.get("vacCode"),
+            "missionCode": self.free_config.get("mission_code"),
+            "countryCode": self.free_config.get("country_code"),
+            "centerCode": apt_config.get("vac_code"),
             "loginUser": self.config.account.username,
             "urn": urn,
-            "firstEarliestSlotDate": earliest_date,
+            "firstEarliestSlotDate": dt.strftime("%d/%m/%Y"),
             "action": "schedule",
-            "ipAddress": self.real_ip or "127.0.0.1",
-            "eadAppointmentDetail": iso_date
+            "ipAddress": self.real_ip,
+            "eadAppointmentDetail": dt.strftime("%Y-%m-%dT%H:%M:%S")
         }
         resp = self._perform_request("POST", url, headers=headers, json_data=data)
         return resp.json().get("isSavedSuccess", False)
@@ -729,7 +740,7 @@ class VfsPlugin(IVSPlg):
         1. 发送 OPTIONS 请求
         2. 发送实际请求
         """
-        print(f'[perform request] {method} {url}')
+        print(f'[perform request] {method} {url} {data} {json_data} {params}')
         # --- 1. 发送 OPTIONS 请求 ---
         try:
             # OPTIONS 请求使用相同的 URL 和 headers (部分 header 如 content-length 会被自动处理)
@@ -786,21 +797,16 @@ class VfsPlugin(IVSPlg):
         """
         完整实现的 Cloudflare Turnstile 验证逻辑
         """
-        mission = self.free_config.get("missionCode", "")
-        country = self.free_config.get("countryCode", "")
+        mission = self.free_config.get("mission_code", "")
+        country = self.free_config.get("country_code", "")
         if not mission or not country:
-            raise NotFoundError(message="Missing missionCode or countryCode in free_config")
+            raise NotFoundError(message="Missing mission_code or country_code in free_config")
 
         website_url = f"https://visa.vfsglobal.com/{country}/en/{mission}/login"
         
         # 构造代理字符串传给打码平台 (格式: http://user:pass@ip:port)
-        proxy_str = ""
-        if self.config.proxy.ip:
-            proxy_str = f"{self.config.proxy.scheme}://"
-            if self.config.proxy.username:
-                proxy_str += f"{self.config.proxy.username}:{self.config.proxy.password}@"
-            proxy_str += f"{self.config.proxy.ip}:{self.config.proxy.port}"
-
+        proxy_str = self._get_proxy_url()
+        
         # 2. 提交任务
         VSC_INFO("vfs_plg", "[%s] Submitting Turnstile task for %s...", self.group_id, website_url)
         task_out = VSCloudApi.Instance().submit_anti_turnstile_task(proxy_str, website_url)
@@ -876,8 +882,8 @@ class VfsPlugin(IVSPlg):
         raise BizLogicError(message="Captcha task timeout (120s)")
 
     def _get_common_headers(self, with_auth=True) -> Dict[str, str]:
-        mission = self.free_config.get("missionCode", "")
-        country = self.free_config.get("countryCode", "")
+        mission = self.free_config.get("mission_code", "")
+        country = self.free_config.get("country_code", "")
         lang = self.free_config.get("language", "en")
         route = f"{country}/{lang}/{mission}"
         
@@ -905,10 +911,10 @@ class VfsPlugin(IVSPlg):
         max_retries = self.free_config.get("slot_query_max_retries", 2)
 
         data = {
-            "missioncode": self.free_config.get("missionCode"),
-            "countrycode": self.free_config.get("countryCode"),
-            "vacCode": apt_config.get("vacCode"),
-            "visaCategoryCode": apt_config.get("subcategoryCode"),
+            "missioncode": self.free_config.get("mission_code"),
+            "countrycode": self.free_config.get("country_code"),
+            "vacCode": apt_config.get("vac_code"),
+            "visaCategoryCode": apt_config.get("subcategory_code"),
             "roleName": "Individual",
             "loginUser": self.config.account.username,
             "payCode": ""
@@ -949,26 +955,17 @@ class VfsPlugin(IVSPlg):
         j = resp.json()
         if j.get("earliestSlotLists"):
             raw_date = j["earliestSlotLists"][0]["date"]
-            dt = datetime.strptime(raw_date, "%m/%d/%Y %H:%M:%S")
-            return dt.strftime("%m/%d/%Y")
-
+            return to_yyyymmdd(raw_date, "%m/%d/%Y %H:%M:%S")
         return ""
 
-
-    def _fmt_date(self, yyyy_mm_dd):
-        try:
-            return datetime.strptime(yyyy_mm_dd, "%Y-%m-%d").strftime("%d/%m/%Y")
-        except:
-            return yyyy_mm_dd
-
     def _fetch_configurations(self, apt_config: Dict[str, Any]):
         # 1. 获取所有中心配置 (query_center)
         if not self.center_conf:
             self.center_conf = self._query_center()
 
         # 2. 获取 Visa Category 配置
-        vac_code = apt_config.get("vacCode")
-        category_code = apt_config.get("categoryCode")
+        vac_code = apt_config.get("vac_code")
+        category_code = apt_config.get("category_code")
         
         # 检查目标 category_code 是否已在缓存中
         if category_code not in self.category_conf:
@@ -987,7 +984,7 @@ class VfsPlugin(IVSPlg):
                 raise NotFoundError(message=f"{self.group_id} Category code {category_code} not found in VAC {vac_code}")
 
         # 3. 获取 Visa SubCategory 配置
-        sub_category_code = apt_config.get("subcategoryCode")
+        sub_category_code = apt_config.get("subcategory_code")
         if sub_category_code not in self.subcategory_conf:
             visa_subcategories = self._query_visa_sub_category(vac_code, category_code)
             
@@ -1002,16 +999,16 @@ class VfsPlugin(IVSPlg):
                 raise NotFoundError(message=f"{self.group_id} SubCategory code {sub_category_code} not found")
 
     def _query_center(self) -> List:
-        mission = self.free_config.get("missionCode")
-        country = self.free_config.get("countryCode")
+        mission = self.free_config.get("mission_code")
+        country = self.free_config.get("country_code")
         url = f"https://lift-api.vfsglobal.com/master/center/{mission}/{country}/en-US"
         headers = self._get_common_headers(with_auth=False)
         resp = self._perform_request("GET", url, headers=headers)
         return resp.json()
    
     def _query_visa_category(self, center_code: str) -> List:
-        mission = self.free_config.get("missionCode")
-        country = self.free_config.get("countryCode")
+        mission = self.free_config.get("mission_code")
+        country = self.free_config.get("country_code")
         enc_center = urllib.parse.quote(center_code)
         url = f"https://lift-api.vfsglobal.com/master/visacategory/{mission}/{country}/{enc_center}/en-US"
         headers = self._get_common_headers(with_auth=False)
@@ -1020,8 +1017,8 @@ class VfsPlugin(IVSPlg):
  
 
     def _query_visa_sub_category(self, center_code: str, category_code: str) -> List:
-        mission = self.free_config.get("missionCode")
-        country = self.free_config.get("countryCode")
+        mission = self.free_config.get("mission_code")
+        country = self.free_config.get("country_code")
         enc_center = urllib.parse.quote(center_code)
         enc_cat = urllib.parse.quote(category_code)
         url = f"https://lift-api.vfsglobal.com/master/subvisacategory/{mission}/{country}/{enc_center}/{enc_cat}/en-US"
@@ -1077,8 +1074,8 @@ class VfsPlugin(IVSPlg):
         password = self.config.account.password
         enc_password = self._encrypt_password(password)
         
-        mission_code = self.free_config.get("missionCode", "")
-        country_code = self.free_config.get("countryCode", "")
+        mission_code = self.free_config.get("mission_code", "")
+        country_code = self.free_config.get("country_code", "")
         
         # 2. 生成加密 Source (每次请求时间戳不同,建议重新生成)
         client_src = self._get_client_source()
@@ -1124,8 +1121,8 @@ class VfsPlugin(IVSPlg):
         
         data = {
             "loginUser": self.config.account.username,
-            "missionCode": self.free_config.get("missionCode"),
-            "countryCode": self.free_config.get("countryCode"),
+            "missionCode": self.free_config.get("mission_code"),
+            "countryCode": self.free_config.get("country_code"),
             "urn": urn,
             "applicants": []
         }
@@ -1138,9 +1135,9 @@ class VfsPlugin(IVSPlg):
         headers["content-type"] = "application/json;charset=UTF-8"
         
         data = {
-            "missionCode": self.free_config.get("missionCode"),
-            "countryCode": self.free_config.get("countryCode"),
-            "centerCode": apt_config.get("vacCode"),
+            "missionCode": self.free_config.get("mission_code"),
+            "countryCode": self.free_config.get("country_code"),
+            "centerCode": apt_config.get("vac_code"),
             "loginUser": self.config.account.username,
             "urn": urn,
             "languageCode": "en-US"
@@ -1148,7 +1145,7 @@ class VfsPlugin(IVSPlg):
         
         resp = self._perform_request("POST", url, headers=headers, json_data=data)
         j = resp.json()
-        return float(j.get("totalamount", 0.0)), j["feeDetails"][0].get("currency", "")
+        return j.get("totalamount"), j["feeDetails"][0].get("currency")
 
 
     def _schedule(self, apt_config, urn, amount, currency, slot_id) -> Dict:
@@ -1157,9 +1154,9 @@ class VfsPlugin(IVSPlg):
         headers["content-type"] = "application/json;charset=UTF-8"
         
         data = {
-            "missionCode": self.free_config.get("missionCode"),
-            "countryCode": self.free_config.get("countryCode"),
-            "centerCode": apt_config.get("vacCode"),
+            "missionCode": self.free_config.get("mission_code"),
+            "countryCode": self.free_config.get("country_code"),
+            "centerCode": apt_config.get("vac_code"),
             "loginUser": self.config.account.username,
             "urn": urn,
             "notificationType": "none",
@@ -1168,10 +1165,10 @@ class VfsPlugin(IVSPlg):
                 "RequestRefNo": "",
                 "clientId": "",
                 "merchantId": "",
-                "amount": amount,
+                "amount":  amount,
                 "currency": currency
             },
-            "allocationId": slot_id,
+            "allocationId": str(slot_id),
             "CanVFSReachoutToApplicant": True
         }
         
@@ -1198,6 +1195,31 @@ class VfsPlugin(IVSPlg):
         else:
             raise NotFoundError(message='payment link not found')
 
+    def _filter_dates(self, dates: List[str], start_str: str, end_str: str) -> List[str]:
+        """
+        根据用户的期望范围筛选可用日期
+        
+        :param dates: API 返回的可用日期列表 (YYYY-MM-DD)
+        :param start_str: 用户期望开始日期 (YYYY-MM-DD)
+        :param end_str: 用户期望结束日期 (YYYY-MM-DD)
+        :return: 符合要求的日期列表
+        """
+        # 如果没有设置范围,则不过滤,返回所有日期
+        if not start_str or not end_str:
+            return dates
+            
+        valid_dates = []
+        # 截取前10位以防带有时分秒
+        s_date = datetime.strptime(start_str[:10], "%Y-%m-%d")
+        e_date = datetime.strptime(end_str[:10], "%Y-%m-%d")
+        
+        for date_str in dates:
+            curr_date = datetime.strptime(date_str, "%Y-%m-%d")
+            # 比较范围 (闭区间)
+            if s_date <= curr_date <= e_date:
+                valid_dates.append(date_str)
+        random.shuffle(valid_dates)
+        return valid_dates
 
     def _save_http_session(self, page_url):
         """

+ 84 - 66
predict_server.py

@@ -6,6 +6,7 @@ import json
 import string
 import socket
 import traceback
+import io  # 新增
 from http.server import BaseHTTPRequestHandler, HTTPServer
 from io import BytesIO
 from collections import OrderedDict
@@ -14,7 +15,7 @@ from urllib.parse import urlparse, parse_qs
 # 图像处理依赖
 import cv2
 import numpy as np
-from PIL import Image
+from PIL import Image, ImageFilter # 新增 ImageFilter
 
 # 深度学习依赖
 import torch
@@ -29,58 +30,32 @@ except ImportError:
     print("[WARNING] ddddocr not installed. Run 'pip install ddddocr'")
     HAS_DDDDOCR = False
 
-# ================= 核心优化:图像去噪 =================
+# ================= 核心优化:图像去噪 (BLS专用) =================
 def advanced_denoise(image_bytes):
     """
-    针对 BLS 验证码的去噪流程:
-    1. 转灰度
-    2. 中值滤波 (关键:去除椒盐噪点)
-    3. 自适应二值化 (剥离彩色背景)
-    4. 连通域过滤 (去除残留的微小噪点)
+    针对 BLS 验证码的去噪流程
     """
     try:
-        # 1. 字节流转 OpenCV 格式
         nparr = np.frombuffer(image_bytes, np.uint8)
         img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
-        
-        # 2. 灰度化
         gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
-        
-        # 3. 中值滤波 (Median Blur) - 去除椒盐噪声的神器
-        # ksize=3 表示 3x3 区域,能过滤掉独立的黑点,保留较粗的笔画
         gray_blur = cv2.medianBlur(gray, 3)
-        
-        # 4. 自适应二值化
-        # 使用 Gaussian 方法,BlockSize=11, C=2 经验参数
         binary = cv2.adaptiveThreshold(
             gray_blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
             cv2.THRESH_BINARY, 11, 2
         )
-        
-        # 5. 连通域降噪 (Contour Filter)
-        # 找到所有的黑色块(文字和残留噪点)
-        # 注意:OpenCV findContours 找的是白色块,所以先反转
         contours, _ = cv2.findContours(255 - binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
-        
-        # 创建一个纯白背景
         clean_img = np.ones(binary.shape, dtype="uint8") * 255
-        
         for cnt in contours:
             area = cv2.contourArea(cnt)
-            # 过滤逻辑:保留面积在 30 到 1000 像素之间的色块 (文字)
-            # 小于 30 的通常是残留噪点,大于 1000 的可能是边框
             if 30 < area < 1000:
-                cv2.drawContours(clean_img, [cnt], -1, 0, -1) # 在白底上画黑色文字
-
-        # 6. 转回 PIL Image
+                cv2.drawContours(clean_img, [cnt], -1, 0, -1)
         return Image.fromarray(clean_img)
-        
     except Exception as e:
         print(f"[Denoise] Error: {e}")
-        # 出错时回退到原始图片
         return Image.open(BytesIO(image_bytes))
 
-# ================= PyTorch 模型结构 (保持不变) =================
+# ================= PyTorch 模型结构 =================
 class Model(nn.Module):
     def __init__(self, n_classes, input_shape=(3, 64, 128)):
         super(Model, self).__init__()
@@ -161,12 +136,13 @@ class PyTorchEngine:
         if not self.ready:
             return "Error: Model not loaded"
         try:
-            # 使用高级去噪
-            image = advanced_denoise(image_bytes)
+            # === 恢复:直接使用 PIL 打开图片,移除 advanced_denoise ===
+            image = Image.open(BytesIO(image_bytes))
             image = image.convert('RGB')
             
             if self.transforms_func is not None:
                 image = self.transforms_func(image)
+            
             with torch.no_grad():
                 output = self.model(image.unsqueeze(0).cpu())
                 
@@ -177,11 +153,10 @@ class PyTorchEngine:
             print(f"[PyTorch] Inference error: {e}")
             return ""
 
-# ================= 引擎2: DDDDOCR (已优化) =================
+# ================= 引擎2: DDDDOCR =================
 class DddOcrEngine:
     def __init__(self):
         if HAS_DDDDOCR:
-            # show_ad=False 关闭广告, beta=True 启用旧版模型(通常对纯数字更稳)
             self.ocr = ddddocr.DdddOcr(show_ad=False, beta=True)
             print("[DDDDOCR] Initialized successfully")
             self.ready = True
@@ -190,10 +165,11 @@ class DddOcrEngine:
             self.ready = False
 
     def inference_bytes(self, image_bytes):
+        """ 原有的 VFCode 识别逻辑 """
         if not self.ready:
             return "Error: ddddocr not installed"
         try:
-            # 1. 预处理:去噪、二值化、过滤
+            # 1. VF 专用预处理
             img_pil = advanced_denoise(image_bytes)
             
             # 2. 转 bytes 传给 ddddocr
@@ -208,6 +184,31 @@ class DddOcrEngine:
             print(f"[DDDDOCR] Inference error: {e}")
             return ""
 
+    def inference_captcha(self, image_bytes):
+        """ 
+        [新增] 适配你提供的预处理逻辑
+        路径: /predict/visametric
+        """
+        if not self.ready:
+            return "Error: ddddocr not installed"
+        try:
+            # 1. 打开图片
+            image = Image.open(io.BytesIO(image_bytes))
+
+            # 2. 自定义预处理: 灰度 -> 中值滤波 -> 二值化
+            gray_img = image.convert("L").filter(ImageFilter.MedianFilter(size=3))
+            binary_img = gray_img.point(lambda p: 255 if p > 128 else 0)
+
+            # 3. 转 bytes 并识别
+            with io.BytesIO() as img_buffer:
+                binary_img.save(img_buffer, format="PNG")
+                processed_bytes = img_buffer.getvalue()
+                return self.ocr.classification(processed_bytes)
+
+        except Exception as e:
+            print(f"[DDDDOCR-Captcha] Inference error: {e}")
+            return ""
+
 # ================= HTTP 处理 =================
 engines = {}
 
@@ -219,27 +220,30 @@ class RequestHandler(BaseHTTPRequestHandler):
         self.wfile.write(content)
 
     def log_message(self, format, *args):
-        # 屏蔽 HTTP 请求日志,只打印识别结果
         return
 
     def do_POST(self):
         parsed_path = urlparse(self.path)
         path = parsed_path.path
         query_params = parse_qs(parsed_path.query)
-        
-        # 默认使用 ddddocr,因为加上去噪后效果通常好于未针对性训练的 pytorch 模型
-        model_type = query_params.get('model', ['ddddocr'])[0]
 
-        if path == '/predict/vfcode':
-            try:
-                content_length = int(self.headers.get('Content-Length', 0))
-                if content_length == 0:
-                    self._send_response(400, 'application/json', json.dumps({'code': 400, 'msg': 'Empty body'}).encode())
-                    return
-
-                file_content = self.rfile.read(content_length)
+        # 获取 Content-Length
+        try:
+            content_length = int(self.headers.get('Content-Length', 0))
+            if content_length == 0:
+                self._send_response(400, 'application/json', json.dumps({'code': 400, 'msg': 'Empty body'}).encode())
+                return
+            file_content = self.rfile.read(content_length)
+        except Exception:
+            self._send_response(400, 'application/json', json.dumps({'code': 400, 'msg': 'Read body failed'}).encode())
+            return
 
-                result_string = ""
+        result_string = ""
+        
+        try:
+            # === 路由 1: 原有的 VFCode 识别 ===
+            if path == '/predict/bls':
+                model_type = query_params.get('model', ['ddddocr'])[0]
                 if model_type == 'ddddocr':
                     if 'ddddocr' in engines:
                         result_string = engines['ddddocr'].inference_bytes(file_content)
@@ -251,33 +255,45 @@ class RequestHandler(BaseHTTPRequestHandler):
                     else:
                         result_string = "Error: pytorch model not available"
                 
-                response = {
-                    'data': result_string,
-                    'msg': "success",
-                    'code': 200
-                }
-                self._send_response(200, 'application/json', json.dumps(response).encode())
+                print(f"[VFCode] [{model_type}] Result: {result_string}")
+
+            # === 路由 2: 新增的通用 Captcha 识别 ===
+            elif path == '/predict/visametric':
+                if 'ddddocr' in engines:
+                    # 使用新增的预处理逻辑
+                    result_string = engines['ddddocr'].inference_captcha(file_content)
+                else:
+                    result_string = "Error: ddddocr not available"
                 
-                # 打印简洁的识别日志
-                print(f"[{model_type}] Result: {result_string}")
+                print(f"[Captcha] Result: {result_string}")
 
-            except Exception as e:
-                traceback.print_exc()
-                response = {'data': '', 'msg': 'failed', 'code': 500}
-                self._send_response(500, 'application/json', json.dumps(response).encode())
-        else:
-            self._send_response(404, 'text/plain', b'Not Found')
+            else:
+                self._send_response(404, 'text/plain', b'Not Found')
+                return
+
+            # 返回成功响应
+            response = {
+                'data': result_string,
+                'msg': "success",
+                'code': 200
+            }
+            self._send_response(200, 'application/json', json.dumps(response).encode())
+
+        except Exception as e:
+            traceback.print_exc()
+            response = {'data': '', 'msg': 'failed', 'code': 500}
+            self._send_response(500, 'application/json', json.dumps(response).encode())
 
 if __name__ == '__main__':
     MODEL_PATH = 'data/ctc.pth'
     PORT = 8085
     
-    # 1. PyTorch
+    # 初始化 PyTorch 引擎
     pytorch_engine = PyTorchEngine(MODEL_PATH)
     if pytorch_engine.ready:
         engines['pytorch'] = pytorch_engine
         
-    # 2. ddddocr
+    # 初始化 DDDDOCR 引擎
     ddd_engine = DddOcrEngine()
     if ddd_engine.ready:
         engines['ddddocr'] = ddd_engine
@@ -285,7 +301,9 @@ if __name__ == '__main__':
     server_address = ('0.0.0.0', PORT)
     httpd = HTTPServer(server_address, RequestHandler)
     print(f'OCR Server running on port {PORT}...')
-    print(f'Active engines: {list(engines.keys())}')
+    print(f'Routes available:')
+    print(f'  POST /predict/bls?model=ddddocr|pytorch')
+    print(f'  POST /predict/visametric (Uses specific preprocessing)')
     
     try:
         httpd.serve_forever()

+ 0 - 55
toolkit/rule_engine.py

@@ -1,55 +0,0 @@
-# toolkit/rule_engine.py
-import json
-from datetime import datetime
-from typing import List
-
-class RuleEngine:
-    def __init__(self, rules_json_str: str):
-        self.rules = {}
-        if rules_json_str:
-            try:
-                self.rules = json.loads(rules_json_str)
-            except:
-                pass
-        self.start_date = None
-        self.end_date = None
-
-    def set_date_range_start(self, date_str: str):
-        # Format: YYYY-MM-DD
-        if date_str:
-            try:
-                self.start_date = datetime.strptime(date_str, "%Y-%m-%d")
-            except:
-                pass
-
-    def set_date_range_end(self, date_str: str):
-        # Format: YYYY-MM-DD
-        if date_str:
-            try:
-                self.end_date = datetime.strptime(date_str, "%Y-%m-%d")
-            except:
-                pass
-
-    # 修正:将 list[str] 改为 List[str]
-    def select_date(self, available_dates: List[str], fmt: str = "%d/%m/%Y") -> List[str]:
-        """
-        从可用日期中筛选符合规则的日期
-        """
-        valid_dates = []
-        for d_str in available_dates:
-            try:
-                dt = datetime.strptime(d_str, fmt)
-                
-                # 检查日期范围
-                if self.start_date and dt < self.start_date:
-                    continue
-                if self.end_date and dt > self.end_date:
-                    continue
-                
-                # 这里可以添加星期几排除等逻辑
-                
-                valid_dates.append(d_str)
-            except:
-                continue
-        
-        return valid_dates

+ 17 - 10
vs_types.py

@@ -64,14 +64,6 @@ class PluginConfig:
     plugin_bin: str = ""         # 动态库文件名/Python文件名
     plugin_proto: str = ""       # 接口协议
 
-@dataclass
-class CenterInfo:
-    id: int = 0                  # 签证中心的唯一ID
-    mission_code: str = ""       # 目的地国名字
-    country_code: str = ""       # 递交国名字
-    provider: str = ""           # 服务商
-    website: str = ""            # 服务商网站
-
 @dataclass
 class GroupConfig:
     enable: bool = False
@@ -86,7 +78,6 @@ class GroupConfig:
     
     query_wait: QueryWaitConfig = field(default_factory=QueryWaitConfig)
     plugin_config: PluginConfig = field(default_factory=PluginConfig)
-    center_info: CenterInfo = field(default_factory=CenterInfo)
     free_config: str = "{}"
 
 @dataclass
@@ -117,30 +108,46 @@ class VSQueryResult:
     class DateAvailability:
         @dataclass
         class TimeSlot:
+            # "hh:mm"
             time: str = ""
             label: str = ""
         date: str = ""
         times: List[TimeSlot] = field(default_factory=list)
     
+    # 可用状态[可用, 等候列表, 不可用]
     availability_status: AvailabilityStatus = AvailabilityStatus.NoneAvailable
+    # 最早可用日期
     earliest_date: str = ""
+    # 业务routing_key
     routing_key: str = ""
+    # 签证类型
     visa_type: str = ""
+    # 递交城市
     city: str = ""
+    # 被申请国
     country: str = ""
+    # 所有有效的日期和时间
     availability: List[DateAvailability] = field(default_factory=list)
-    
+    # 查询到有Slot 为True, 无Slot 为False
     success: bool = False 
 
 @dataclass
 class VSBookResult:
+    # 抢位成功 True, 抢位失败 False
     success: bool = False
+    # 会话ID
     session_id: str = ""
+    # 预定成功的账号
     account: str = ""
+    # 预定的日期
     book_date: str = ""
+    # 预定的时间
     book_time: str = ""
+    # 费用 单位分
     fee_amount: int = 0
+    # 货币单位
     fee_currency: str = ""
+    # 支付链接
     payment_link: str = ""
 
 # --- 内部任务结构 ---