diff --git a/Node_Exit_Ip_Cache/🇯🇵AWS日本01 _ 电信移动联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇯🇵AWS日本01 _ 电信移动联通推荐/public_ip.yaml index 22e7ae3..f0d9af7 100644 --- a/Node_Exit_Ip_Cache/🇯🇵AWS日本01 _ 电信移动联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇯🇵AWS日本01 _ 电信移动联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 103.62.49.138 public_ipv4: 103.62.49.138 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:27Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:08Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇯🇵AWS日本02 _ 电信移动联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇯🇵AWS日本02 _ 电信移动联通推荐/public_ip.yaml index f838c5c..1cd1db8 100644 --- a/Node_Exit_Ip_Cache/🇯🇵AWS日本02 _ 电信移动联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇯🇵AWS日本02 _ 电信移动联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 103.62.49.162 public_ipv4: 103.62.49.162 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:29Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:09Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇯🇵AWS日本03 _ 电信移动联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇯🇵AWS日本03 _ 电信移动联通推荐/public_ip.yaml index 6ff3251..4610ea9 100644 --- a/Node_Exit_Ip_Cache/🇯🇵AWS日本03 _ 电信移动联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇯🇵AWS日本03 _ 电信移动联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 103.62.49.170 public_ipv4: 103.62.49.170 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:30Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:10Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇯🇵AWS日本04 _ 电信移动联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇯🇵AWS日本04 _ 电信移动联通推荐/public_ip.yaml index 9771465..df41e09 100644 --- a/Node_Exit_Ip_Cache/🇯🇵AWS日本04 _ 电信移动联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇯🇵AWS日本04 _ 电信移动联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 103.62.49.178 public_ipv4: 103.62.49.178 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:30Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:10Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇯🇵AWS日本05 _ 电信移动联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇯🇵AWS日本05 _ 电信移动联通推荐/public_ip.yaml index 52f3344..c1f882e 100644 --- a/Node_Exit_Ip_Cache/🇯🇵AWS日本05 _ 电信移动联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇯🇵AWS日本05 _ 电信移动联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 103.62.49.154 public_ipv4: 103.62.49.154 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:31Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:11Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇯🇵AWS日本06 _ 电信移动联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇯🇵AWS日本06 _ 电信移动联通推荐/public_ip.yaml index b5f22fc..e5d44fd 100644 --- a/Node_Exit_Ip_Cache/🇯🇵AWS日本06 _ 电信移动联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇯🇵AWS日本06 _ 电信移动联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 103.62.49.154 public_ipv4: 103.62.49.154 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:31Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:12Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇯🇵AWS日本07 _ 电信移动联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇯🇵AWS日本07 _ 电信移动联通推荐/public_ip.yaml index abf9bf5..dc4fc3c 100644 --- a/Node_Exit_Ip_Cache/🇯🇵AWS日本07 _ 电信移动联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇯🇵AWS日本07 _ 电信移动联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 103.62.49.178 public_ipv4: 103.62.49.178 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:32Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:12Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇯🇵AWS日本08 _ 电信移动联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇯🇵AWS日本08 _ 电信移动联通推荐/public_ip.yaml index eca532c..9a5816d 100644 --- a/Node_Exit_Ip_Cache/🇯🇵AWS日本08 _ 电信移动联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇯🇵AWS日本08 _ 电信移动联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 103.62.49.170 public_ipv4: 103.62.49.170 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:32Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:13Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇯🇵AWS日本09 _ 电信移动联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇯🇵AWS日本09 _ 电信移动联通推荐/public_ip.yaml index 3f611b6..54feb0c 100644 --- a/Node_Exit_Ip_Cache/🇯🇵AWS日本09 _ 电信移动联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇯🇵AWS日本09 _ 电信移动联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 103.62.49.162 public_ipv4: 103.62.49.162 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:33Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:14Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇯🇵AWS日本10 _ 电信移动联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇯🇵AWS日本10 _ 电信移动联通推荐/public_ip.yaml index dd7e734..2c2f1f6 100644 --- a/Node_Exit_Ip_Cache/🇯🇵AWS日本10 _ 电信移动联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇯🇵AWS日本10 _ 电信移动联通推荐/public_ip.yaml @@ -1,8 +1,10 @@ proxy_name: 🇯🇵AWS日本10 | 电信移动联通推荐 username: n0021 mixed_port: 7897 +public_ip: 103.62.49.138 +public_ipv4: 103.62.49.138 public_ipv4_error: ipify 请求失败:SSL connection failed public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:43Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:14Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇯🇵日本东京01-0.1倍 _ 电信联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇯🇵日本东京01-0.1倍 _ 电信联通推荐/public_ip.yaml index a40fdd6..5ccdb52 100644 --- a/Node_Exit_Ip_Cache/🇯🇵日本东京01-0.1倍 _ 电信联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇯🇵日本东京01-0.1倍 _ 电信联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 103.62.49.138 public_ipv4: 103.62.49.138 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:02Z +public_ipv6_cached_at_utc: 2026-08-12T01:46:52Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇯🇵日本东京02-0.1倍 _ 电信联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇯🇵日本东京02-0.1倍 _ 电信联通推荐/public_ip.yaml index 86eda7b..ca1a135 100644 --- a/Node_Exit_Ip_Cache/🇯🇵日本东京02-0.1倍 _ 电信联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇯🇵日本东京02-0.1倍 _ 电信联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 103.62.49.162 public_ipv4: 103.62.49.162 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:03Z +public_ipv6_cached_at_utc: 2026-08-12T01:46:53Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇯🇵日本东京03-0.1倍 _ 电信联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇯🇵日本东京03-0.1倍 _ 电信联通推荐/public_ip.yaml index 1c866fa..0aee8b4 100644 --- a/Node_Exit_Ip_Cache/🇯🇵日本东京03-0.1倍 _ 电信联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇯🇵日本东京03-0.1倍 _ 电信联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 103.62.49.170 public_ipv4: 103.62.49.170 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:05Z +public_ipv6_cached_at_utc: 2026-08-12T01:46:55Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇯🇵日本东京04-0.1倍 _ 电信联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇯🇵日本东京04-0.1倍 _ 电信联通推荐/public_ip.yaml index 80422a5..1a4a232 100644 --- a/Node_Exit_Ip_Cache/🇯🇵日本东京04-0.1倍 _ 电信联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇯🇵日本东京04-0.1倍 _ 电信联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 103.62.49.178 public_ipv4: 103.62.49.178 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:07Z +public_ipv6_cached_at_utc: 2026-08-12T01:46:56Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇯🇵日本东京05-0.1倍 _ 电信联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇯🇵日本东京05-0.1倍 _ 电信联通推荐/public_ip.yaml index 4caa023..22110d2 100644 --- a/Node_Exit_Ip_Cache/🇯🇵日本东京05-0.1倍 _ 电信联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇯🇵日本东京05-0.1倍 _ 电信联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 103.62.49.154 public_ipv4: 103.62.49.154 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:09Z +public_ipv6_cached_at_utc: 2026-08-12T01:46:58Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇯🇵日本东京06-0.1倍 _ 高速专线推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇯🇵日本东京06-0.1倍 _ 高速专线推荐/public_ip.yaml index 3d36465..a102b97 100644 --- a/Node_Exit_Ip_Cache/🇯🇵日本东京06-0.1倍 _ 高速专线推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇯🇵日本东京06-0.1倍 _ 高速专线推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 103.62.49.162 public_ipv4: 103.62.49.162 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:13Z +public_ipv6_cached_at_utc: 2026-08-12T01:46:59Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇯🇵日本东京07-0.1倍 _ 高速专线推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇯🇵日本东京07-0.1倍 _ 高速专线推荐/public_ip.yaml index b2f826a..496da7a 100644 --- a/Node_Exit_Ip_Cache/🇯🇵日本东京07-0.1倍 _ 高速专线推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇯🇵日本东京07-0.1倍 _ 高速专线推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 103.62.49.170 public_ipv4: 103.62.49.170 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:17Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:01Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇯🇵日本东京08-0.1倍 _ 高速专线推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇯🇵日本东京08-0.1倍 _ 高速专线推荐/public_ip.yaml index 28641bb..699fcd8 100644 --- a/Node_Exit_Ip_Cache/🇯🇵日本东京08-0.1倍 _ 高速专线推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇯🇵日本东京08-0.1倍 _ 高速专线推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 103.62.49.178 public_ipv4: 103.62.49.178 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:20Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:02Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇯🇵日本东京09-0.1倍 _ 高速专线推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇯🇵日本东京09-0.1倍 _ 高速专线推荐/public_ip.yaml index a710d9f..53a5ac0 100644 --- a/Node_Exit_Ip_Cache/🇯🇵日本东京09-0.1倍 _ 高速专线推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇯🇵日本东京09-0.1倍 _ 高速专线推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 103.62.49.154 public_ipv4: 103.62.49.154 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:23Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:03Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇸🇬AWS新加坡01 _ 电信移动联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇸🇬AWS新加坡01 _ 电信移动联通推荐/public_ip.yaml index 4ae9b98..6e48e25 100644 --- a/Node_Exit_Ip_Cache/🇸🇬AWS新加坡01 _ 电信移动联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇸🇬AWS新加坡01 _ 电信移动联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 67.159.48.146 public_ipv4: 67.159.48.146 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:53Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:23Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇸🇬AWS新加坡02 _ 电信移动联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇸🇬AWS新加坡02 _ 电信移动联通推荐/public_ip.yaml index a5ad671..670f37b 100644 --- a/Node_Exit_Ip_Cache/🇸🇬AWS新加坡02 _ 电信移动联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇸🇬AWS新加坡02 _ 电信移动联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 67.159.48.147 public_ipv4: 67.159.48.147 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:54Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:24Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇸🇬AWS新加坡03 _ 电信移动联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇸🇬AWS新加坡03 _ 电信移动联通推荐/public_ip.yaml index 135f548..59f726c 100644 --- a/Node_Exit_Ip_Cache/🇸🇬AWS新加坡03 _ 电信移动联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇸🇬AWS新加坡03 _ 电信移动联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 67.159.48.148 public_ipv4: 67.159.48.148 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:54Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:26Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇸🇬AWS新加坡04 _ 电信移动联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇸🇬AWS新加坡04 _ 电信移动联通推荐/public_ip.yaml index d2a9dc6..e9d937f 100644 --- a/Node_Exit_Ip_Cache/🇸🇬AWS新加坡04 _ 电信移动联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇸🇬AWS新加坡04 _ 电信移动联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 67.159.48.149 public_ipv4: 67.159.48.149 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:55Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:26Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇸🇬AWS新加坡05 _ 电信移动联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇸🇬AWS新加坡05 _ 电信移动联通推荐/public_ip.yaml index 26831b7..f958492 100644 --- a/Node_Exit_Ip_Cache/🇸🇬AWS新加坡05 _ 电信移动联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇸🇬AWS新加坡05 _ 电信移动联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 67.159.48.150 public_ipv4: 67.159.48.150 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:55Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:31Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇸🇬新加坡 _ 高速专线-hy2/public_ip.yaml b/Node_Exit_Ip_Cache/🇸🇬新加坡 _ 高速专线-hy2/public_ip.yaml index 13c8f79..86dd28d 100644 --- a/Node_Exit_Ip_Cache/🇸🇬新加坡 _ 高速专线-hy2/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇸🇬新加坡 _ 高速专线-hy2/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 67.159.48.146 public_ipv4: 67.159.48.146 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:23:05Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:34Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇸🇬新加坡01 _ 电信联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇸🇬新加坡01 _ 电信联通推荐/public_ip.yaml index bd8fdfd..6b25ddb 100644 --- a/Node_Exit_Ip_Cache/🇸🇬新加坡01 _ 电信联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇸🇬新加坡01 _ 电信联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 203.27.106.146 public_ipv4: 203.27.106.146 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:25Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:05Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇸🇬新加坡01-0.1倍 _ 电信联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇸🇬新加坡01-0.1倍 _ 电信联通推荐/public_ip.yaml index a356559..5a371b5 100644 --- a/Node_Exit_Ip_Cache/🇸🇬新加坡01-0.1倍 _ 电信联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇸🇬新加坡01-0.1倍 _ 电信联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 67.159.48.146 public_ipv4: 67.159.48.146 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:45Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:15Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇸🇬新加坡02 _ 电信联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇸🇬新加坡02 _ 电信联通推荐/public_ip.yaml index d2a17f1..4966e75 100644 --- a/Node_Exit_Ip_Cache/🇸🇬新加坡02 _ 电信联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇸🇬新加坡02 _ 电信联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 203.27.106.146 public_ipv4: 203.27.106.146 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:26Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:07Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇸🇬新加坡02-0.1倍 _ 电信联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇸🇬新加坡02-0.1倍 _ 电信联通推荐/public_ip.yaml index d6a6333..b37e84e 100644 --- a/Node_Exit_Ip_Cache/🇸🇬新加坡02-0.1倍 _ 电信联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇸🇬新加坡02-0.1倍 _ 电信联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 67.159.48.147 public_ipv4: 67.159.48.147 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:46Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:17Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇸🇬新加坡03-0.1倍 _ 电信联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇸🇬新加坡03-0.1倍 _ 电信联通推荐/public_ip.yaml index 8f57e5c..a92acc9 100644 --- a/Node_Exit_Ip_Cache/🇸🇬新加坡03-0.1倍 _ 电信联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇸🇬新加坡03-0.1倍 _ 电信联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 67.159.48.148 public_ipv4: 67.159.48.148 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:48Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:18Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇸🇬新加坡04-0.1倍 _ 电信联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇸🇬新加坡04-0.1倍 _ 电信联通推荐/public_ip.yaml index 787e7ed..c630094 100644 --- a/Node_Exit_Ip_Cache/🇸🇬新加坡04-0.1倍 _ 电信联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇸🇬新加坡04-0.1倍 _ 电信联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 67.159.48.149 public_ipv4: 67.159.48.149 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:50Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:20Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇸🇬新加坡05-0.1倍 _ 电信联通推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇸🇬新加坡05-0.1倍 _ 电信联通推荐/public_ip.yaml index 36d1860..5a99fd2 100644 --- a/Node_Exit_Ip_Cache/🇸🇬新加坡05-0.1倍 _ 电信联通推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇸🇬新加坡05-0.1倍 _ 电信联通推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 67.159.48.150 public_ipv4: 67.159.48.150 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:22:51Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:22Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇸🇬新加坡2 _ 高速专线-hy2/public_ip.yaml b/Node_Exit_Ip_Cache/🇸🇬新加坡2 _ 高速专线-hy2/public_ip.yaml index 00bceb8..0f3c9fa 100644 --- a/Node_Exit_Ip_Cache/🇸🇬新加坡2 _ 高速专线-hy2/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇸🇬新加坡2 _ 高速专线-hy2/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 67.159.48.146 public_ipv4: 67.159.48.146 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:23:06Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:35Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇸🇬新加坡3 _ 高速专线-hy2/public_ip.yaml b/Node_Exit_Ip_Cache/🇸🇬新加坡3 _ 高速专线-hy2/public_ip.yaml index 4357cf1..a20fae9 100644 --- a/Node_Exit_Ip_Cache/🇸🇬新加坡3 _ 高速专线-hy2/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇸🇬新加坡3 _ 高速专线-hy2/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 203.27.106.146 public_ipv4: 203.27.106.146 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:23:10Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:36Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇸🇬新加坡4 _ 高速专线-hy2/public_ip.yaml b/Node_Exit_Ip_Cache/🇸🇬新加坡4 _ 高速专线-hy2/public_ip.yaml index dd62ef9..78d6833 100644 --- a/Node_Exit_Ip_Cache/🇸🇬新加坡4 _ 高速专线-hy2/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇸🇬新加坡4 _ 高速专线-hy2/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 203.27.106.146 public_ipv4: 203.27.106.146 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:23:11Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:42Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇸🇬新加坡5 _ 高速专线-hy2/public_ip.yaml b/Node_Exit_Ip_Cache/🇸🇬新加坡5 _ 高速专线-hy2/public_ip.yaml index 3701421..14bc0cb 100644 --- a/Node_Exit_Ip_Cache/🇸🇬新加坡5 _ 高速专线-hy2/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇸🇬新加坡5 _ 高速专线-hy2/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 146.235.17.47 public_ipv4: 146.235.17.47 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:23:11Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:45Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇸🇬新加坡6 _ 高速专线-hy2/public_ip.yaml b/Node_Exit_Ip_Cache/🇸🇬新加坡6 _ 高速专线-hy2/public_ip.yaml index 6baed70..fa6e4cc 100644 --- a/Node_Exit_Ip_Cache/🇸🇬新加坡6 _ 高速专线-hy2/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇸🇬新加坡6 _ 高速专线-hy2/public_ip.yaml @@ -3,6 +3,7 @@ username: n0045 mixed_port: 7897 public_ipv4_error: ipify 请求失败:SSL connection failed public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:23:21Z +public_ipv4_cached_at_utc: 2026-08-12T01:47:45Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:50Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇹🇼台湾 _ 避免晚高峰使用/public_ip.yaml b/Node_Exit_Ip_Cache/🇹🇼台湾 _ 避免晚高峰使用/public_ip.yaml index d930dfc..e0f9404 100644 --- a/Node_Exit_Ip_Cache/🇹🇼台湾 _ 避免晚高峰使用/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇹🇼台湾 _ 避免晚高峰使用/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 114.37.199.26 public_ipv4: 114.37.199.26 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:23:40Z +public_ipv6_cached_at_utc: 2026-08-12T01:48:01Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇹🇼台湾2 _ 避免晚高峰使用/public_ip.yaml b/Node_Exit_Ip_Cache/🇹🇼台湾2 _ 避免晚高峰使用/public_ip.yaml index c2791ed..4356869 100644 --- a/Node_Exit_Ip_Cache/🇹🇼台湾2 _ 避免晚高峰使用/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇹🇼台湾2 _ 避免晚高峰使用/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 111.249.67.172 public_ipv4: 111.249.67.172 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:23:46Z +public_ipv6_cached_at_utc: 2026-08-12T01:48:00Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇹🇼台湾3 _ 避免晚高峰使用/public_ip.yaml b/Node_Exit_Ip_Cache/🇹🇼台湾3 _ 避免晚高峰使用/public_ip.yaml index 7b3dc98..73a2f0c 100644 --- a/Node_Exit_Ip_Cache/🇹🇼台湾3 _ 避免晚高峰使用/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇹🇼台湾3 _ 避免晚高峰使用/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 36.231.97.241 public_ipv4: 36.231.97.241 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:23:52Z +public_ipv6_cached_at_utc: 2026-08-12T01:48:08Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇺🇸美国01 _ 合适下载使用-0.01倍/public_ip.yaml b/Node_Exit_Ip_Cache/🇺🇸美国01 _ 合适下载使用-0.01倍/public_ip.yaml index 484b812..a19be96 100644 --- a/Node_Exit_Ip_Cache/🇺🇸美国01 _ 合适下载使用-0.01倍/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇺🇸美国01 _ 合适下载使用-0.01倍/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 108.181.24.43 public_ipv4: 108.181.24.43 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:23:53Z +public_ipv6_cached_at_utc: 2026-08-12T01:48:14Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇺🇸美国02 _ 合适下载使用-0.01倍/public_ip.yaml b/Node_Exit_Ip_Cache/🇺🇸美国02 _ 合适下载使用-0.01倍/public_ip.yaml index 1fda80b..8815020 100644 --- a/Node_Exit_Ip_Cache/🇺🇸美国02 _ 合适下载使用-0.01倍/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇺🇸美国02 _ 合适下载使用-0.01倍/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 108.181.23.235 public_ipv4: 108.181.23.235 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:23:59Z +public_ipv6_cached_at_utc: 2026-08-12T01:48:15Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇺🇸美国03 _ 合适下载使用-0.01倍/public_ip.yaml b/Node_Exit_Ip_Cache/🇺🇸美国03 _ 合适下载使用-0.01倍/public_ip.yaml index 2b474d9..7993cef 100644 --- a/Node_Exit_Ip_Cache/🇺🇸美国03 _ 合适下载使用-0.01倍/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇺🇸美国03 _ 合适下载使用-0.01倍/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 208.87.242.97 public_ipv4: 208.87.242.97 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:24:00Z +public_ipv6_cached_at_utc: 2026-08-12T01:48:16Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇺🇸美国04 _ 合适下载使用-0.01倍/public_ip.yaml b/Node_Exit_Ip_Cache/🇺🇸美国04 _ 合适下载使用-0.01倍/public_ip.yaml index 538bf8c..0b628c8 100644 --- a/Node_Exit_Ip_Cache/🇺🇸美国04 _ 合适下载使用-0.01倍/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇺🇸美国04 _ 合适下载使用-0.01倍/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 108.181.24.47 public_ipv4: 108.181.24.47 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:24:01Z +public_ipv6_cached_at_utc: 2026-08-12T01:48:17Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇺🇸美国05 _ 合适下载使用-0.01倍/public_ip.yaml b/Node_Exit_Ip_Cache/🇺🇸美国05 _ 合适下载使用-0.01倍/public_ip.yaml index 6b55cfc..b830f5f 100644 --- a/Node_Exit_Ip_Cache/🇺🇸美国05 _ 合适下载使用-0.01倍/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇺🇸美国05 _ 合适下载使用-0.01倍/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 108.181.24.41 public_ipv4: 108.181.24.41 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:24:02Z +public_ipv6_cached_at_utc: 2026-08-12T01:48:18Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇺🇸美国08-0.1倍 _ 电信联通移动推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇺🇸美国08-0.1倍 _ 电信联通移动推荐/public_ip.yaml index e74cf61..f94e43b 100644 --- a/Node_Exit_Ip_Cache/🇺🇸美国08-0.1倍 _ 电信联通移动推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇺🇸美国08-0.1倍 _ 电信联通移动推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 203.10.96.139 public_ipv4: 203.10.96.139 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:23:04Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:34Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇺🇸美国洛杉矶08 _ 三网推荐/public_ip.yaml b/Node_Exit_Ip_Cache/🇺🇸美国洛杉矶08 _ 三网推荐/public_ip.yaml index 601d234..ce6447f 100644 --- a/Node_Exit_Ip_Cache/🇺🇸美国洛杉矶08 _ 三网推荐/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇺🇸美国洛杉矶08 _ 三网推荐/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 203.10.96.139 public_ipv4: 203.10.96.139 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:24:13Z +public_ipv6_cached_at_utc: 2026-08-12T01:48:19Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/Node_Exit_Ip_Cache/🇺🇸美国洛杉矶08-0.1倍/public_ip.yaml b/Node_Exit_Ip_Cache/🇺🇸美国洛杉矶08-0.1倍/public_ip.yaml index 210cc23..80a99eb 100644 --- a/Node_Exit_Ip_Cache/🇺🇸美国洛杉矶08-0.1倍/public_ip.yaml +++ b/Node_Exit_Ip_Cache/🇺🇸美国洛杉矶08-0.1倍/public_ip.yaml @@ -4,6 +4,6 @@ mixed_port: 7897 public_ip: 203.10.96.139 public_ipv4: 203.10.96.139 public_ipv6_error: ipify 请求失败:SSL connection failed -cached_at_utc: 2026-07-21T02:23:40Z +public_ipv6_cached_at_utc: 2026-08-12T01:47:55Z ipv4_source: https://api4.ipify.org/?format=json ipv6_source: https://api6.ipify.org/?format=json \ No newline at end of file diff --git a/config/config.yaml b/config/config.yaml index d6564b7..74bbafb 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -53,13 +53,10 @@ tun: - 192.168.0.0/16 - 127.0.0.0/8 exclude-interface: - - et_11_6lqc + - easy_tier - VMware Network Adapter VMnet1 - VMware Network Adapter VMnet8 - ZeroTier One -# 网卡改名 -# Get-NetAdapter -InterfaceDescription "ZeroTier Virtual Port" | -# Rename-NetAdapter -NewName "ZeroTier" proxy-providers: 赔钱机场: type: http diff --git a/src/base/config.h b/src/base/config.h new file mode 100644 index 0000000..3ecb836 --- /dev/null +++ b/src/base/config.h @@ -0,0 +1,8 @@ +#pragma once +// 出口 IPv4 缓存有效期,单位秒;0 表示永不过期。 +inline constexpr int public_ipv4_cache_ttl_seconds = 7 * 24 * 60 * 60; +// 出口 IPv6 缓存有效期,单位秒;0 表示永不过期。 +inline constexpr int public_ipv6_cache_ttl_seconds = 7 * 24 * 60 * 60; +// true 表示对应缓存过期就重查;false 表示过期且缺少对应 IP 才重查。 +inline constexpr bool radical_ipv4 = false; +inline constexpr bool radical_ipv6 = false; diff --git a/src/node_service/Node_Service.h b/src/node_service/Node_Service.h deleted file mode 100644 index 3954e49..0000000 --- a/src/node_service/Node_Service.h +++ /dev/null @@ -1,308 +0,0 @@ -#pragma once -#ifndef NOMINMAX -#define NOMINMAX -#endif -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#ifndef CPPHTTPLIB_OPENSSL_SUPPORT -#define CPPHTTPLIB_OPENSSL_SUPPORT -#endif -#include "base/Async_Blocking.h" -#include "base/export.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef _WIN32 -#include -#endif -namespace fs = std::filesystem; -using json = nlohmann::json; -inline constexpr double ewma_rtt_alpha = 0.125; -inline constexpr double ewma_rttvar_beta = 0.25; -inline constexpr size_t recent_result_window = 100; -inline constexpr long mihomo_timeout_ms = 5000; -inline constexpr long mihomo_request_timeout_ms = 8000; -inline constexpr int min_check_interval_seconds = 5; -inline constexpr int max_check_interval_seconds = 3600; -struct Test_Target { - std::string name; - std::string url; - long timeout_ms = mihomo_timeout_ms; -}; -struct Auto_Switch_Config { - bool enabled = true; - bool skip_direct = true; - int switch_cooldown_seconds = 120; - int fail_switch_count = 2; - int better_confirm_rounds = 3; - double keep_score_threshold = 75.0; - double weak_score_threshold = 60.0; - double weak_improve_margin = 15.0; - double better_improve_margin = 10.0; - std::string default_strategy = "stability"; - std::unordered_map strategy_parameters; - std::vector managed_groups; - std::unordered_map group_strategies; -}; -struct Table_Layout_Config { - std::unordered_map> column_orders; - std::unordered_map> sticky_columns; -}; -struct Service_Config { - std::string listen_host = "127.0.0.1"; - int listen_port = 18088; - std::string controller_origin = "http://127.0.0.1:7898"; - std::string controller_secret; - fs::path config_path; - fs::path state_dir; - fs::path web_dir; - fs::path service_config_path; - bool http_request_log_enabled = false; - int check_interval_seconds = 60; - int first_delay_ms = 1000; - size_t worker_threads = 16; - Auto_Switch_Config auto_switch; - Table_Layout_Config table_layout; - std::vector targets; -}; -struct Delay_Result { - bool success = false; - int delay_ms = 0; - int http_status = 0; - std::string error; - json raw = json::object(); -}; -struct Probe_Result { - std::string target; - std::string url; - Delay_Result result; -}; -struct Node_State { - std::string name; - std::string type; - std::string provider; - std::string exit_ipv4; - std::string exit_ipv6; - int ai_score = -1; - int risk = -1; - std::string risk_level; - double multiplier = -1.0; - std::string multiplier_text; - std::string shared_users; - std::string ip_type; - std::string native_type; - std::string country; - bool alive = false; - bool initialized = false; - double srtt_ms = 0.0; - double rttvar_ms = 0.0; - double success_ewma = 0.0; - uint64_t samples = 0; - uint64_t success_count = 0; - uint64_t failure_count = 0; - uint64_t consecutive_failures = 0; - std::vector recent_results; - int last_delay_ms = 0; - int last_success_delay_ms = 0; - double stability_score = 0.0; - std::string last_error; - std::string last_check_time; - std::string updated_time; - std::vector probes; -}; -struct Proxy_Group_Def { - std::string name; - std::string type; - std::vector proxies; -}; -struct Group_Switch_State { - std::string name; - std::string type; - std::vector proxies; - std::vector candidates; - std::string current; - std::string best_candidate; - int better_rounds = 0; - uint64_t switch_count = 0; - int64_t last_switch_unix = 0; - std::string last_switch_time; - std::string last_decision; - std::string last_error; - bool switchable = false; - bool managed = false; - std::string strategy; -}; -struct Controller_Proxy_Info { - bool found = false; - bool alive = false; - std::string type; - std::string provider; - json raw = json::object(); -}; -struct Service_State { - Service_Config config; - std::mutex mutex; - std::unordered_map nodes; - std::vector node_order; - std::unordered_map groups; - std::vector group_order; - std::atomic_bool running = true; - std::atomic_bool checking = false; - std::atomic_bool force_refresh = false; - std::atomic_size_t refresh_done_nodes = 0; - uint64_t round = 0; - uint64_t refresh_request_count = 0; - size_t refresh_total_nodes = 0; - std::string started_time; - std::string status_text; - std::string last_refresh_request_time; - std::string last_refresh_begin_time; - std::string last_refresh_end_time; -}; -fs::path path_from_utf8_string(std::string_view value); -std::string path_to_utf8_string(const fs::path& path); -std::string now_string(); -int64_t unix_seconds(); -std::string trim(std::string value); -std::string read_env(std::string_view key); -std::string url_encode(std::string_view value); -std::string make_safe_file_name(std::string_view value); -std::vector split_parts(std::string_view value, char delimiter); -bool starts_with(std::string_view value, std::string_view prefix); -bool ends_with(std::string_view value, std::string_view suffix); -int parse_int_prefix(std::string_view value, size_t& offset); -std::string to_string(std::string_view value); -std::string compact_country_from_name(std::string_view name); -void parse_node_metadata(Node_State& node); -bool is_label_metadata_part(std::string_view part); -std::string original_proxy_name_from_label(std::string_view name); -std::optional yaml_string_value(const YAML::Node& node, const char* key); -void load_node_ip_cache(Node_State& node); -void enrich_node_metadata(Node_State& node); -void write_text_atomic(const fs::path& path, std::string_view content); -json read_json_file(const fs::path& path); -void save_json_file(const fs::path& path, const json& value); -std::string read_binary_text(const fs::path& path); -std::string mime_type(const fs::path& path); -void set_file_response(const fs::path& file, httplib::Response& res); -void service_log(std::string_view message); -int normalize_check_interval(int value); -double normalize_score_threshold(double value); -int normalize_positive_int(int value, int min_value, int max_value); -json switch_strategy_definitions_to_json(); -json switch_strategy_options_to_json(); -json auto_switch_config_to_json(const Auto_Switch_Config& config); -void load_auto_switch_config(Auto_Switch_Config& config, const json& data); -json table_layout_config_to_json(const Table_Layout_Config& config); -void load_table_layout_config(Table_Layout_Config& config, const json& data); -json runtime_config_to_json(const Service_Config& config); -void load_runtime_config(Service_Config& config); -void save_runtime_config(const Service_Config& config); -fs::path default_config_path(); -fs::path default_web_dir(); -Service_Config make_config(int argc, char** argv); -fs::path node_state_path(const Service_State& service, std::string_view name); -json target_to_json(const Test_Target& target); -json probe_to_json(const Probe_Result& probe); -json group_to_json(const Group_Switch_State& group); -json enum_options_to_json(); -json dynamic_filter_options_to_json(const Service_State& service); -json node_to_json(const Node_State& node); -void load_node_state(Node_State& node, const fs::path& path); -void save_service_index(Service_State& service); -double clamp_score(double value); -void update_node_statistics(Node_State& node, std::optional delay_ms); -void update_score(Node_State& node); -std::vector read_proxy_names(const fs::path& config_path); -std::vector read_proxy_groups(const fs::path& config_path); -bool contains_name(const std::vector& list, const std::string& name); -bool should_manage_group(const Auto_Switch_Config& config, const Proxy_Group_Def& group); -void reload_switch_groups_locked(Service_State& service); -void reload_switch_groups(Service_State& service); -httplib::Headers controller_headers(const Service_Config& config); -json controller_get_json(const Service_Config& config, const std::string& path); -Controller_Proxy_Info request_proxy_info(const Service_Config& config, const std::string& name); -Delay_Result request_proxy_delay(const Service_Config& config, const std::string& name, const Test_Target& target); -std::vector request_node_probes(const Service_Config& config, const std::string& name); -void controller_put_json(const Service_Config& config, const std::string& path, const json& body); -std::string request_group_current(const Service_Config& config, const std::string& group_name); -void switch_group_to_node(const Service_Config& config, const std::string& group_name, const std::string& node_name); -std::optional find_best_candidate_node(const Auto_Switch_Config& config, const std::unordered_map& nodes, const Group_Switch_State& group); -void reset_better_tracking(Group_Switch_State& group); -std::string score_text(const Node_State& node); -bool group_switch_cooldown_finished(const Auto_Switch_Config& config, const Group_Switch_State& group, int64_t now); -std::string normalize_switch_strategy(std::string value); -std::string group_switch_strategy(const Auto_Switch_Config& config, const std::string& group_name); -double switch_strategy_parameter(const Auto_Switch_Config& config, const std::string& key); -void normalize_switch_strategy_parameters(Auto_Switch_Config& config); -double node_switch_score(const Auto_Switch_Config& config, const Node_State& node, const std::string& strategy); -void note_group_decision(Service_State& service, const Group_Switch_State& group); -void perform_auto_switch(Service_State& service); -void refresh_switch_group_status(Service_State& service, const std::optional& group_name = std::nullopt); -Service_Config config_snapshot(Service_State& service); -asio::awaitable>> check_node(Service_State& service, asio::thread_pool& pool, std::string name); -std::optional best_delay(const std::vector& probes); -std::string join_probe_errors(const std::vector& probes); -void update_node_state(Service_State& service, const std::string& name, const Controller_Proxy_Info& info, std::vector probes); -asio::awaitable check_node_task(Service_State& service, asio::thread_pool& pool, std::string name, std::shared_ptr pending, asio::steady_timer& signal); -int current_check_interval_seconds(Service_State& service); -asio::awaitable check_all_nodes_once(Service_State& service, asio::thread_pool& pool); -asio::awaitable run_auto_switch_once(Service_State& service, asio::thread_pool& pool); -asio::awaitable monitor_refresh_loop(Service_State& service, asio::thread_pool& pool); - -class Table_Protocol_Registry { -public: - static Table_Protocol_Registry& instance(); - json protocol() const; - json protocol(const Service_State& service) const; - json rows(const Service_State& service) const; - json proxy_node_rows(const Service_State& service) const; - json proxy_group_rows(const Service_State& service) const; - json switch_global_setting_rows(const Service_State& service) const; - json switch_strategy_rows(const Service_State& service) const; - json switch_strategy_setting_rows(const Service_State& service) const; -}; -json table_protocol_snapshot(Service_State& service); -json table_rows_snapshot(Service_State& service); -void update_table_column_order(Service_State& service, const std::string& table_id, const std::vector& column_order); -void update_table_sticky_columns(Service_State& service, const std::string& table_id, const std::vector& sticky_columns); -void update_table_cell(Service_State& service, const std::string& table_id, const std::string& row_id, const std::string& column_id, const json& value); -json state_snapshot(Service_State& service); -std::string html_page(); -void start_web_server(Service_State& service, asio::io_context& io, httplib::Server& server); -void initialize_nodes(Service_State& service); diff --git a/src/node_service/Node_Service_Auto_Switch.cpp b/src/node_service/Node_Service_Auto_Switch.cpp deleted file mode 100644 index ddc058b..0000000 --- a/src/node_service/Node_Service_Auto_Switch.cpp +++ /dev/null @@ -1,319 +0,0 @@ -#include "Node_Service.h" -namespace { -json strategy_parameter_def(std::string_view key, std::string_view label, double default_value, double min_value, double max_value, double step, std::string_view unit, std::string_view help) { - return json{{"key", std::string(key)}, {"label", std::string(label)}, {"type", "number"}, {"default", default_value}, {"min", min_value}, {"max", max_value}, {"step", step}, {"unit", std::string(unit)}, {"help", std::string(help)}}; -} -json strategy_def(std::string_view id, std::string_view name, std::string_view description, std::string_view formula, json parameters) { - return json{{"id", std::string(id)}, {"name", std::string(name)}, {"description", std::string(description)}, {"formula_latex", std::string(formula)}, {"parameters", std::move(parameters)}}; -} -std::unordered_map default_strategy_parameters() { - return {{"stability_weight", 1.0}, {"risk_penalty_weight", 0.5}, {"multiplier_penalty_weight", 10.0}, {"ai_bonus_weight", 2.0}}; -} -std::set strategy_parameter_keys() { - return {"stability_weight", "risk_penalty_weight", "multiplier_penalty_weight", "ai_bonus_weight"}; -} -} -json switch_strategy_definitions_to_json() { - json result = json::array(); - result.push_back(strategy_def("none", "不操作", "不执行自动切换,只保留当前选择。", "", json::array())); - result.push_back(strategy_def("stability", "稳定性优先", "只按节点稳定性评分选择候选节点。", R"LATEX(S = stability)LATEX", json::array())); - json parameters = json::array(); - parameters.push_back(strategy_parameter_def("stability_weight", "稳定性权重", 1.0, 0.0, 10.0, 0.1, "", "稳定性评分权重。")); - parameters.push_back(strategy_parameter_def("risk_penalty_weight", "风控惩罚权重", 0.5, 0.0, 10.0, 0.1, "", "风控风险越高扣分越多。")); - parameters.push_back(strategy_parameter_def("multiplier_penalty_weight", "倍率惩罚权重", 10.0, 0.0, 100.0, 0.1, "", "倍率越高扣分越多,优先低倍率节点。")); - parameters.push_back(strategy_parameter_def("ai_bonus_weight", "AI星级奖励权重", 2.0, 0.0, 20.0, 0.1, "", "AI星级越高加分越多。")); - result.push_back(strategy_def("risk_multiplier", "稳定性 + 倍率 + 风控", "同时考虑稳定性、风控、倍率和 AI 星级。", R"LATEX(S = stability \times w_s - risk \times w_r - multiplier \times w_m + ai \times w_a)LATEX", std::move(parameters))); - return result; -} -json switch_strategy_options_to_json() { - json result = json::array(); - for (const auto& item : switch_strategy_definitions_to_json()) { - result.push_back(json{{"value", item.value("id", std::string{})}, {"label", item.value("name", std::string{})}}); - } - return result; -} -std::string normalize_switch_strategy(std::string value) { - value = trim(std::move(value)); - for (const auto& item : switch_strategy_definitions_to_json()) { - if (value == item.value("id", std::string{})) { - return value; - } - } - return "stability"; -} -double switch_strategy_parameter(const Auto_Switch_Config& config, const std::string& key) { - const auto item = config.strategy_parameters.find(key); - if (item != config.strategy_parameters.end()) { - return item->second; - } - const auto defaults = default_strategy_parameters(); - const auto def = defaults.find(key); - return def == defaults.end() ? 0.0 : def->second; -} -void normalize_switch_strategy_parameters(Auto_Switch_Config& config) { - const auto defaults = default_strategy_parameters(); - for (const auto& [key, value] : defaults) { - if (!config.strategy_parameters.contains(key)) { - config.strategy_parameters.emplace(key, value); - } - } - for (auto it = config.strategy_parameters.begin(); it != config.strategy_parameters.end();) { - if (!strategy_parameter_keys().contains(it->first)) { - it = config.strategy_parameters.erase(it); - } - else { - it->second = std::clamp(it->second, 0.0, it->first == "multiplier_penalty_weight" ? 100.0 : 20.0); - ++it; - } - } -} -std::string group_switch_strategy(const Auto_Switch_Config& config, const std::string& group_name) { - const auto item = config.group_strategies.find(group_name); - if (item != config.group_strategies.end()) { - return normalize_switch_strategy(item->second); - } - return normalize_switch_strategy(config.default_strategy); -} -double node_switch_score(const Auto_Switch_Config& config, const Node_State& node, const std::string& strategy) { - const auto normalized = normalize_switch_strategy(strategy); - if (normalized == "none") { - return 0.0; - } - if (normalized == "stability") { - return node.stability_score; - } - const double risk = node.risk < 0 ? 50.0 : static_cast(node.risk); - const double multiplier = node.multiplier < 0.0 ? 1.0 : node.multiplier; - const double ai_bonus = node.ai_score < 0 ? 0.0 : static_cast(node.ai_score) * switch_strategy_parameter(config, "ai_bonus_weight"); - return node.stability_score * switch_strategy_parameter(config, "stability_weight") - risk * switch_strategy_parameter(config, "risk_penalty_weight") - multiplier * switch_strategy_parameter(config, "multiplier_penalty_weight") + ai_bonus; -} -std::optional find_best_candidate_node(const Auto_Switch_Config& config, const std::unordered_map& nodes, const Group_Switch_State& group) { - std::optional best; - const auto strategy = group.strategy.empty() ? group_switch_strategy(config, group.name) : group.strategy; - for (const auto& name : group.candidates) { - const auto item = nodes.find(name); - if (item == nodes.end() || !item->second.alive) { - continue; - } - if (!best || node_switch_score(config, item->second, strategy) > node_switch_score(config, *best, strategy)) { - best = item->second; - } - } - return best; -} -void reset_better_tracking(Group_Switch_State& group) { - group.best_candidate.clear(); - group.better_rounds = 0; -} -std::string score_text(const Node_State& node) { - const auto multiplier = node.multiplier_text.empty() ? std::string("倍率未知") : node.multiplier_text; - const auto risk = node.risk < 0 ? std::string("风险未知") : std::format("风控{}", node.risk); - return std::format("{} 稳定{:.1f}分 {} {} 连续失败{}", node.name, node.stability_score, multiplier, risk, node.consecutive_failures); -} -bool group_switch_cooldown_finished(const Auto_Switch_Config& config, const Group_Switch_State& group, int64_t now) { - return config.switch_cooldown_seconds <= 0 || group.last_switch_unix == 0 || now - group.last_switch_unix >= config.switch_cooldown_seconds; -} -void note_group_decision(Service_State& service, const Group_Switch_State& group) { - auto& stored = service.groups[group.name]; - stored = group; -} -void perform_auto_switch(Service_State& service) { - Service_Config config; - std::unordered_map nodes; - std::vector groups; - { - std::lock_guard lock(service.mutex); - config = service.config; - nodes = service.nodes; - for (const auto& name : service.group_order) { - const auto item = service.groups.find(name); - if (item != service.groups.end()) { - groups.push_back(item->second); - } - } - } - service_log(std::format("自动切换判定:{} 个代理组", groups.size())); - if (!config.auto_switch.enabled) { - service_log("自动切换判定跳过:未启用"); - std::lock_guard lock(service.mutex); - for (auto& group : groups) { - group.last_decision = "自动切换未启用"; - note_group_decision(service, group); - } - save_service_index(service); - return; - } - const auto now = unix_seconds(); - for (auto& group : groups) { - group.last_error.clear(); - group.strategy = group_switch_strategy(config.auto_switch, group.name); - if (!group.managed) { - group.last_decision = "不受自动切换管理"; - std::lock_guard lock(service.mutex); - note_group_decision(service, group); - service_log(std::format("代理组 {}:{}", group.name, group.last_decision)); - continue; - } - if (group.strategy == "none") { - try { - group.current = request_group_current(config, group.name); - group.last_decision = "策略不操作,保持当前选择"; - reset_better_tracking(group); - } - catch (const std::exception& error) { - group.last_error = error_text_to_utf8(error.what()); - group.last_decision = "策略不操作,但读取当前选择失败"; - } - std::lock_guard lock(service.mutex); - note_group_decision(service, group); - service_log(std::format("代理组 {}:{}", group.name, group.last_decision)); - continue; - } - if (!group.switchable) { - group.last_decision = "没有可切换的直接节点候选"; - group.last_error = "代理组 proxies 里没有直接节点名"; - std::lock_guard lock(service.mutex); - note_group_decision(service, group); - service_log(std::format("代理组 {}:{},{}", group.name, group.last_decision, group.last_error)); - continue; - } - try { - group.current = request_group_current(config, group.name); - } - catch (const std::exception& error) { - group.last_error = error_text_to_utf8(error.what()); - group.last_decision = "读取当前选择失败"; - std::lock_guard lock(service.mutex); - note_group_decision(service, group); - service_log(std::format("代理组 {}:{},{}", group.name, group.last_decision, group.last_error)); - continue; - } - const auto best = find_best_candidate_node(config.auto_switch, nodes, group); - if (!best) { - group.last_decision = "没有 alive 候选节点"; - group.last_error = "候选节点全部不可用"; - std::lock_guard lock(service.mutex); - note_group_decision(service, group); - service_log(std::format("代理组 {}:{},{}", group.name, group.last_decision, group.last_error)); - continue; - } - const auto current_item = nodes.find(group.current); - const bool current_is_node = current_item != nodes.end(); - const bool can_switch_now = group_switch_cooldown_finished(config.auto_switch, group, now); - const auto best_score = node_switch_score(config.auto_switch, *best, group.strategy); - auto switch_to_best = [&](std::string_view reason) { - if (group.current == best->name) { - group.last_decision = std::format("{},当前已经是最佳节点 {},策略 {},策略分 {:.1f}", reason, score_text(*best), group.strategy, best_score); - reset_better_tracking(group); - return; - } - if (!can_switch_now) { - group.last_decision = std::format("{},但冷却中,当前 {},最佳 {},策略 {},策略分 {:.1f}", reason, group.current, score_text(*best), group.strategy, best_score); - return; - } - switch_group_to_node(config, group.name, best->name); - group.current = best->name; - ++group.switch_count; - group.last_switch_unix = now; - group.last_switch_time = now_string(); - group.last_decision = std::format("{},切换到 {},策略 {},策略分 {:.1f}", reason, score_text(*best), group.strategy, best_score); - reset_better_tracking(group); - }; - if (!current_is_node) { - switch_to_best("当前选择不是受监控直接节点"); - } - else { - const auto& current = current_item->second; - const auto current_score = node_switch_score(config.auto_switch, current, group.strategy); - if (current.alive && current_score >= config.auto_switch.keep_score_threshold) { - group.last_decision = std::format("当前节点策略分稳定,不切换:{},策略 {},策略分 {:.1f}", score_text(current), group.strategy, current_score); - reset_better_tracking(group); - } - else if (current.consecutive_failures >= static_cast(config.auto_switch.fail_switch_count)) { - switch_to_best(std::format("当前节点连续失败达到 {} 次", current.consecutive_failures)); - } - else if (current_score < config.auto_switch.weak_score_threshold && best_score >= current_score + config.auto_switch.weak_improve_margin) { - switch_to_best(std::format("当前节点低于弱分阈值:当前策略分 {:.1f},最佳策略分 {:.1f}", current_score, best_score)); - } - else if (best_score >= current_score + config.auto_switch.better_improve_margin) { - if (group.best_candidate == best->name) { - ++group.better_rounds; - } - else { - group.best_candidate = best->name; - group.better_rounds = 1; - } - if (group.better_rounds >= config.auto_switch.better_confirm_rounds) { - switch_to_best(std::format("最佳节点连续 {} 轮明显更好:当前策略分 {:.1f},最佳策略分 {:.1f}", group.better_rounds, current_score, best_score)); - } - else { - group.last_decision = std::format("最佳节点更好但未达到确认轮数:{}/{},策略 {},当前策略分 {:.1f},最佳策略分 {:.1f},最佳 {}", group.better_rounds, config.auto_switch.better_confirm_rounds, group.strategy, current_score, best_score, score_text(*best)); - } - } - else { - group.last_decision = std::format("不满足切换阈值:策略 {},当前策略分 {:.1f},最佳策略分 {:.1f},当前 {},最佳 {}", group.strategy, current_score, best_score, score_text(current), score_text(*best)); - reset_better_tracking(group); - } - } - service_log(std::format("代理组 {}:{}", group.name, group.last_decision)); - std::lock_guard lock(service.mutex); - note_group_decision(service, group); - save_service_index(service); - } -} -void refresh_switch_group_status(Service_State& service, const std::optional& group_name) { - Service_Config config; - std::vector names; - { - std::lock_guard lock(service.mutex); - reload_switch_groups_locked(service); - config = service.config; - if (group_name) { - names.push_back(*group_name); - } - else { - names = service.group_order; - } - service.status_text = group_name ? std::format("代理组状态刷新开始:{}", *group_name) : "代理组状态刷新开始:全部代理组"; - save_service_index(service); - } - service_log(group_name ? std::format("代理组状态刷新开始:{}", *group_name) : std::format("代理组状态刷新开始:{} 个代理组", names.size())); - for (const auto& name : names) { - Group_Switch_State updated; - bool exists = false; - { - std::lock_guard lock(service.mutex); - const auto item = service.groups.find(name); - if (item != service.groups.end()) { - updated = item->second; - exists = true; - } - } - if (!exists) { - service_log(std::format("代理组状态刷新跳过:{} 不在受控代理组列表", name)); - continue; - } - try { - updated.strategy = group_switch_strategy(config.auto_switch, name); - updated.current = request_group_current(config, name); - updated.last_error.clear(); - updated.last_decision = "代理组当前选择已刷新"; - service_log(std::format("代理组状态刷新完成:{} 当前选择 {} 策略 {}", name, updated.current, updated.strategy)); - } - catch (const std::exception& error) { - updated.last_error = error_text_to_utf8(error.what()); - updated.last_decision = "代理组当前选择刷新失败"; - service_log(std::format("代理组状态刷新失败:{},{}", name, updated.last_error)); - } - { - std::lock_guard lock(service.mutex); - service.groups[name] = updated; - save_service_index(service); - } - } - { - std::lock_guard lock(service.mutex); - service.status_text = group_name ? std::format("代理组状态刷新完成:{}", *group_name) : "代理组状态刷新完成:全部代理组"; - save_service_index(service); - } -} diff --git a/src/node_service/Node_Service_Config.cpp b/src/node_service/Node_Service_Config.cpp deleted file mode 100644 index b14aa43..0000000 --- a/src/node_service/Node_Service_Config.cpp +++ /dev/null @@ -1,243 +0,0 @@ -#include "Node_Service.h" -int normalize_check_interval(int value) { - return std::clamp(value, min_check_interval_seconds, max_check_interval_seconds); -} -double normalize_score_threshold(double value) { - return std::clamp(value, 0.0, 100.0); -} -int normalize_positive_int(int value, int min_value, int max_value) { - return std::clamp(value, min_value, max_value); -} -json auto_switch_config_to_json(const Auto_Switch_Config& config) { - json result; - result["enabled"] = config.enabled; - result["skip_direct"] = config.skip_direct; - result["switch_cooldown_seconds"] = config.switch_cooldown_seconds; - result["fail_switch_count"] = config.fail_switch_count; - result["better_confirm_rounds"] = config.better_confirm_rounds; - result["keep_score_threshold"] = config.keep_score_threshold; - result["weak_score_threshold"] = config.weak_score_threshold; - result["weak_improve_margin"] = config.weak_improve_margin; - result["better_improve_margin"] = config.better_improve_margin; - result["default_strategy"] = normalize_switch_strategy(config.default_strategy); - result["strategy_parameters"] = json::object(); - for (const auto& item : switch_strategy_definitions_to_json()) { - for (const auto& parameter : item.value("parameters", json::array())) { - const auto key = parameter.value("key", std::string{}); - if (!key.empty()) { - result["strategy_parameters"][key] = switch_strategy_parameter(config, key); - } - } - } - result["managed_groups"] = config.managed_groups; - result["group_strategies"] = json::object(); - for (const auto& [name, strategy] : config.group_strategies) { - result["group_strategies"][name] = normalize_switch_strategy(strategy); - } - return result; -} -void load_auto_switch_config(Auto_Switch_Config& config, const json& data) { - normalize_switch_strategy_parameters(config); - if (!data.is_object()) { - return; - } - config.enabled = data.value("enabled", config.enabled); - config.skip_direct = data.value("skip_direct", config.skip_direct); - config.switch_cooldown_seconds = normalize_positive_int(data.value("switch_cooldown_seconds", config.switch_cooldown_seconds), 0, 3600); - config.fail_switch_count = normalize_positive_int(data.value("fail_switch_count", config.fail_switch_count), 1, 100); - config.better_confirm_rounds = normalize_positive_int(data.value("better_confirm_rounds", config.better_confirm_rounds), 1, 100); - config.keep_score_threshold = normalize_score_threshold(data.value("keep_score_threshold", config.keep_score_threshold)); - config.weak_score_threshold = normalize_score_threshold(data.value("weak_score_threshold", config.weak_score_threshold)); - config.weak_improve_margin = normalize_score_threshold(data.value("weak_improve_margin", config.weak_improve_margin)); - config.better_improve_margin = normalize_score_threshold(data.value("better_improve_margin", config.better_improve_margin)); - config.default_strategy = normalize_switch_strategy(data.value("default_strategy", config.default_strategy)); - if (data.contains("strategy_parameters") && data["strategy_parameters"].is_object()) { - config.strategy_parameters.clear(); - for (const auto& item : data["strategy_parameters"].items()) { - if (item.value().is_number()) { - config.strategy_parameters[item.key()] = item.value().get(); - } - } - } - else { - if (data.contains("combined_stability_weight") && data["combined_stability_weight"].is_number()) { - config.strategy_parameters["stability_weight"] = data["combined_stability_weight"].get(); - } - if (data.contains("combined_risk_penalty_weight") && data["combined_risk_penalty_weight"].is_number()) { - config.strategy_parameters["risk_penalty_weight"] = data["combined_risk_penalty_weight"].get(); - } - if (data.contains("combined_multiplier_penalty_weight") && data["combined_multiplier_penalty_weight"].is_number()) { - config.strategy_parameters["multiplier_penalty_weight"] = data["combined_multiplier_penalty_weight"].get(); - } - if (data.contains("combined_ai_bonus_weight") && data["combined_ai_bonus_weight"].is_number()) { - config.strategy_parameters["ai_bonus_weight"] = data["combined_ai_bonus_weight"].get(); - } - } - normalize_switch_strategy_parameters(config); - if (data.contains("group_strategies") && data["group_strategies"].is_object()) { - config.group_strategies.clear(); - for (const auto& item : data["group_strategies"].items()) { - const auto strategy = normalize_switch_strategy(item.value().is_string() ? item.value().get() : std::string{}); - if (!item.key().empty()) { - config.group_strategies.emplace(item.key(), strategy); - } - } - } - if (data.contains("managed_groups") && data["managed_groups"].is_array()) { - config.managed_groups.clear(); - for (const auto& item : data["managed_groups"]) { - if (item.is_string()) { - auto name = trim(item.get()); - if (!name.empty()) { - config.managed_groups.push_back(std::move(name)); - } - } - } - } -} - -json table_layout_config_to_json(const Table_Layout_Config& config) { - json result = json::object(); - for (const auto& [table_id, order] : config.column_orders) { - result[table_id]["column_order"] = order; - } - for (const auto& [table_id, columns] : config.sticky_columns) { - result[table_id]["sticky_columns"] = columns; - } - return result; -} -std::vector load_string_array(const json& value) { - std::vector result; - if (!value.is_array()) { - return result; - } - for (const auto& item : value) { - if (!item.is_string()) { - continue; - } - auto id = trim(item.get()); - if (!id.empty()) { - result.push_back(std::move(id)); - } - } - return result; -} -void load_table_layout_config(Table_Layout_Config& config, const json& data) { - config.column_orders.clear(); - config.sticky_columns.clear(); - if (!data.is_object()) { - return; - } - for (const auto& item : data.items()) { - const auto& table = item.value(); - if (!table.is_object()) { - continue; - } - if (table.contains("column_order")) { - auto order = load_string_array(table["column_order"]); - if (!order.empty()) { - config.column_orders[item.key()] = std::move(order); - } - } - if (table.contains("sticky_columns")) { - config.sticky_columns[item.key()] = load_string_array(table["sticky_columns"]); - } - } -} -json runtime_config_to_json(const Service_Config& config) { - json result; - result["listen_host"] = config.listen_host; - result["listen_port"] = config.listen_port; - result["controller_origin"] = config.controller_origin; - result["config_path"] = path_to_utf8_string(config.config_path); - result["state_dir"] = path_to_utf8_string(config.state_dir); - result["web_dir"] = path_to_utf8_string(config.web_dir); - result["service_config_path"] = path_to_utf8_string(config.service_config_path); - result["http_request_log_enabled"] = config.http_request_log_enabled; - result["check_interval_seconds"] = config.check_interval_seconds; - result["delay_refresh_interval_seconds"] = config.check_interval_seconds; - result["first_delay_ms"] = config.first_delay_ms; - result["worker_threads"] = config.worker_threads; - result["auto_switch"] = auto_switch_config_to_json(config.auto_switch); - result["table_layouts"] = table_layout_config_to_json(config.table_layout); - result["switch_strategy_definitions"] = switch_strategy_definitions_to_json(); - result["targets"] = json::array(); - for (const auto& target : config.targets) { - result["targets"].push_back(target_to_json(target)); - } - return result; -} -void load_runtime_config(Service_Config& config) { - const auto data = read_json_file(config.service_config_path); - if (!data.is_object()) { - return; - } - if (data.contains("http_request_log_enabled") && data["http_request_log_enabled"].is_boolean()) { - config.http_request_log_enabled = data["http_request_log_enabled"].get(); - } - if (data.contains("check_interval_seconds") && data["check_interval_seconds"].is_number_integer()) { - config.check_interval_seconds = normalize_check_interval(data["check_interval_seconds"].get()); - } - else if (data.contains("delay_refresh_interval_seconds") && data["delay_refresh_interval_seconds"].is_number_integer()) { - config.check_interval_seconds = normalize_check_interval(data["delay_refresh_interval_seconds"].get()); - } - if (data.contains("auto_switch")) { - load_auto_switch_config(config.auto_switch, data["auto_switch"]); - } - if (data.contains("table_layouts")) { - load_table_layout_config(config.table_layout, data["table_layouts"]); - } -} -void save_runtime_config(const Service_Config& config) { - save_json_file(config.service_config_path, runtime_config_to_json(config)); -} -fs::path default_config_path() { - const fs::path root = path_from_utf8_string(get_executable_dir()); - const fs::path out_config = (root / "../out_yml/config.yaml").lexically_normal(); - if (fs::is_regular_file(out_config)) { - return out_config; - } - return (root / "../yml/origin.yaml").lexically_normal(); -} -fs::path default_web_dir() { - const fs::path exe_dir = path_from_utf8_string(get_executable_dir()); - const std::vector candidates{ - (exe_dir / "../node_service_web/dist").lexically_normal(), - (exe_dir / "../node_service_web").lexically_normal(), - (exe_dir / "../src/node_service/web/dist").lexically_normal(), - (fs::current_path() / "../src/node_service/web/dist").lexically_normal(), - (fs::current_path() / "src/node_service/web/dist").lexically_normal(), - (fs::current_path() / "web/dist").lexically_normal() - }; - for (const auto& candidate : candidates) { - if (fs::is_regular_file(candidate / "index.html")) { - return candidate; - } - } - return candidates.front(); -} -Service_Config make_config(int argc, char** argv) { - Service_Config config; - const fs::path root = path_from_utf8_string(get_executable_dir()); - config.config_path = argc > 1 ? path_from_utf8_string(argv[1]) : default_config_path(); - config.state_dir = argc > 2 ? path_from_utf8_string(argv[2]) : (root / "../serer_state/node_state").lexically_normal(); - config.service_config_path = (root / "../serer_state/node_service_config" / "config.json").lexically_normal(); - config.web_dir = argc > 5 ? path_from_utf8_string(argv[5]) : default_web_dir(); - if (argc > 3) { - config.controller_origin = argv[3]; - } - if (argc > 4) { - config.listen_port = std::stoi(argv[4]); - } - const auto secret = read_env("MIHOMO_SECRET"); - if (!secret.empty()) { - config.controller_secret = secret; - } - config.targets.push_back({"gstatic", "https://www.gstatic.com/generate_204", 5000}); - config.targets.push_back({"cloudflare", "https://cp.cloudflare.com/generate_204", 5000}); - config.targets.push_back({"connectivity", "https://www.google.com/generate_204", 5000}); - load_runtime_config(config); - normalize_switch_strategy_parameters(config.auto_switch); - save_runtime_config(config); - return config; -} diff --git a/src/node_service/Node_Service_Controller.cpp b/src/node_service/Node_Service_Controller.cpp deleted file mode 100644 index 74bde0a..0000000 --- a/src/node_service/Node_Service_Controller.cpp +++ /dev/null @@ -1,98 +0,0 @@ -#include "Node_Service.h" -httplib::Headers controller_headers(const Service_Config& config) { - httplib::Headers headers; - if (!config.controller_secret.empty()) { - headers.emplace("Authorization", "Bearer " + config.controller_secret); - } - return headers; -} -static long long elapsed_ms(std::chrono::steady_clock::time_point begin) { - return std::chrono::duration_cast(std::chrono::steady_clock::now() - begin).count(); -} -static void log_controller_http(const Service_Config& config, std::string_view text) { - if (config.http_request_log_enabled) { - service_log(text); - } -} -json controller_get_json(const Service_Config& config, const std::string& path) { - const auto begin = std::chrono::steady_clock::now(); - log_controller_http(config, std::format("内核 HTTP GET 开始:{}{}", config.controller_origin, path)); - httplib::Client client(config.controller_origin); - client.set_follow_location(true); - client.set_connection_timeout(std::chrono::milliseconds(3000)); - client.set_read_timeout(std::chrono::milliseconds(mihomo_request_timeout_ms)); - const auto response = client.Get(path, controller_headers(config)); - if (!response) { - const auto message = httplib::to_string(response.error()); - log_controller_http(config, std::format("内核 HTTP GET 失败:{},耗时 {}ms,错误 {}", path, elapsed_ms(begin), message)); - throw std::runtime_error(message); - } - log_controller_http(config, std::format("内核 HTTP GET 返回:{},状态 {},耗时 {}ms,响应 {} 字节", path, response->status, elapsed_ms(begin), response->body.size())); - if (response->status < 200 || response->status >= 300) { - throw std::runtime_error(std::format("mihomo http status {}", response->status)); - } - return json::parse(response->body, nullptr, true, true); -} -Controller_Proxy_Info request_proxy_info(const Service_Config& config, const std::string& name) { - Controller_Proxy_Info info; - auto data = controller_get_json(config, "/proxies/" + url_encode(name)); - info.found = true; - info.raw = std::move(data); - info.alive = info.raw.value("alive", false); - info.type = info.raw.value("type", std::string{}); - info.provider = info.raw.value("provider", std::string{}); - return info; -} -Delay_Result request_proxy_delay(const Service_Config& config, const std::string& name, const Test_Target& target) { - Delay_Result result; - try { - const std::string path = "/proxies/" + url_encode(name) + "/delay?timeout=" + std::to_string(target.timeout_ms) + "&url=" + url_encode(target.url); - result.raw = controller_get_json(config, path); - result.delay_ms = result.raw.value("delay", 0); - result.success = result.delay_ms > 0; - result.http_status = 200; - if (!result.success) { - result.error = "delay is zero"; - } - } - catch (const std::exception& error) { - result.success = false; - result.error = error_text_to_utf8(error.what()); - } - return result; -} -std::vector request_node_probes(const Service_Config& config, const std::string& name) { - std::vector probes; - probes.reserve(config.targets.size()); - for (const auto& target : config.targets) { - probes.push_back({target.name, target.url, request_proxy_delay(config, name, target)}); - } - return probes; -} -void controller_put_json(const Service_Config& config, const std::string& path, const json& body) { - const auto begin = std::chrono::steady_clock::now(); - log_controller_http(config, std::format("内核 HTTP PUT 开始:{}{} body={}", config.controller_origin, path, body.dump())); - httplib::Client client(config.controller_origin); - client.set_follow_location(true); - client.set_connection_timeout(std::chrono::milliseconds(3000)); - client.set_read_timeout(std::chrono::milliseconds(mihomo_request_timeout_ms)); - auto headers = controller_headers(config); - headers.emplace("Content-Type", "application/json"); - const auto response = client.Put(path, headers, body.dump(), "application/json"); - if (!response) { - const auto message = httplib::to_string(response.error()); - log_controller_http(config, std::format("内核 HTTP PUT 失败:{},耗时 {}ms,错误 {}", path, elapsed_ms(begin), message)); - throw std::runtime_error(message); - } - log_controller_http(config, std::format("内核 HTTP PUT 返回:{},状态 {},耗时 {}ms,响应 {} 字节", path, response->status, elapsed_ms(begin), response->body.size())); - if (response->status < 200 || response->status >= 300) { - throw std::runtime_error(std::format("mihomo http status {}", response->status)); - } -} -std::string request_group_current(const Service_Config& config, const std::string& group_name) { - const auto data = controller_get_json(config, "/proxies/" + url_encode(group_name)); - return data.value("now", std::string{}); -} -void switch_group_to_node(const Service_Config& config, const std::string& group_name, const std::string& node_name) { - controller_put_json(config, "/proxies/" + url_encode(group_name), json{{"name", node_name}}); -} diff --git a/src/node_service/Node_Service_Monitor.cpp b/src/node_service/Node_Service_Monitor.cpp deleted file mode 100644 index b5717a8..0000000 --- a/src/node_service/Node_Service_Monitor.cpp +++ /dev/null @@ -1,176 +0,0 @@ -#include "Node_Service.h" -Service_Config config_snapshot(Service_State& service) { - std::lock_guard lock(service.mutex); - return service.config; -} -asio::awaitable>> check_node(Service_State& service, asio::thread_pool& pool, std::string name) { - const auto config = config_snapshot(service); - co_return co_await WyC::async_blocking(pool, [config, name = std::move(name)] { - Controller_Proxy_Info info; - try { - info = request_proxy_info(config, name); - } - catch (...) { - info.found = false; - info.alive = false; - } - auto probes = request_node_probes(config, name); - return std::make_pair(std::move(info), std::move(probes)); - }); -} -std::optional best_delay(const std::vector& probes) { - std::optional value; - for (const auto& probe : probes) { - if (!probe.result.success) { - continue; - } - if (!value || probe.result.delay_ms < *value) { - value = probe.result.delay_ms; - } - } - return value; -} -std::string join_probe_errors(const std::vector& probes) { - std::string result; - for (const auto& probe : probes) { - if (probe.result.success) { - continue; - } - if (!result.empty()) { - result += "; "; - } - result += probe.target + ":" + probe.result.error; - } - return result; -} -void update_node_state(Service_State& service, const std::string& name, const Controller_Proxy_Info& info, std::vector probes) { - std::string log_text; - { - std::lock_guard lock(service.mutex); - auto& node = service.nodes[name]; - node.name = name; - enrich_node_metadata(node); - node.alive = info.alive; - node.type = info.type; - node.provider = info.provider; - node.probes = std::move(probes); - node.last_check_time = now_string(); - node.updated_time = node.last_check_time; - const auto delay = best_delay(node.probes); - node.last_delay_ms = delay.value_or(0); - node.last_error = delay ? std::string{} : join_probe_errors(node.probes); - update_node_statistics(node, delay); - update_score(node); - log_text = std::format("节点 delay 完成:{} alive={} delay={}ms score={:.1f} fail={} {}", node.name, node.alive, node.last_delay_ms, node.stability_score, node.consecutive_failures, node.last_error.empty() ? "" : node.last_error); - save_json_file(node_state_path(service, name), node_to_json(node)); - save_service_index(service); - } - service_log(log_text); -} -asio::awaitable check_node_task(Service_State& service, asio::thread_pool& pool, std::string name, std::shared_ptr pending, asio::steady_timer& signal) { - try { - auto [info, probes] = co_await check_node(service, pool, name); - update_node_state(service, name, info, std::move(probes)); - } - catch (const std::exception& error) { - Controller_Proxy_Info info; - info.found = false; - info.alive = false; - std::vector probes; - Probe_Result probe; - probe.target = "node_service"; - probe.result.success = false; - probe.result.error = error_text_to_utf8(error.what()); - probes.push_back(std::move(probe)); - update_node_state(service, name, info, std::move(probes)); - } - const auto done = service.refresh_done_nodes.fetch_add(1) + 1; - --*pending; - if (done == service.refresh_total_nodes || done % 5 == 0) { - service_log(std::format("全量 delay 进度:{}/{}", done, service.refresh_total_nodes)); - } - signal.cancel(); -} -int current_check_interval_seconds(Service_State& service) { - std::lock_guard lock(service.mutex); - return service.config.check_interval_seconds; -} -asio::awaitable check_all_nodes_once(Service_State& service, asio::thread_pool& pool) { - std::vector names; - { - std::lock_guard lock(service.mutex); - names = service.node_order; - service.refresh_total_nodes = names.size(); - service.refresh_done_nodes.store(0); - service.last_refresh_begin_time = now_string(); - service.status_text = names.empty() ? "没有节点需要刷新" : std::format("全量 delay 开始:{} 个节点", names.size()); - save_service_index(service); - } - service_log(names.empty() ? "全量 delay 跳过:没有节点" : std::format("全量 delay 开始:{} 个节点", names.size())); - if (names.empty()) { - co_return; - } - auto executor = co_await asio::this_coro::executor; - asio::steady_timer signal(executor); - auto pending = std::make_shared(names.size()); - for (auto& name : names) { - asio::co_spawn(executor, check_node_task(service, pool, std::move(name), pending, signal), asio::detached); - } - while (*pending != 0) { - signal.expires_after(std::chrono::hours(24)); - std::error_code error; - co_await signal.async_wait(asio::redirect_error(asio::use_awaitable, error)); - } - { - std::lock_guard lock(service.mutex); - ++service.round; - service.last_refresh_end_time = now_string(); - service.status_text = std::format("全量 delay 完成:轮次 {},节点 {}", service.round, names.size()); - save_service_index(service); - } - service_log(std::format("全量 delay 完成:轮次 {}", service.round)); -} -asio::awaitable run_auto_switch_once(Service_State& service, asio::thread_pool& pool) { - co_await WyC::async_blocking(pool, [&service] { - perform_auto_switch(service); - }); -} -asio::awaitable monitor_refresh_loop(Service_State& service, asio::thread_pool& pool) { - auto executor = co_await asio::this_coro::executor; - asio::steady_timer timer(executor); - timer.expires_after(std::chrono::milliseconds(service.config.first_delay_ms)); - std::error_code error; - co_await timer.async_wait(asio::redirect_error(asio::use_awaitable, error)); - while (service.running.load()) { - service.force_refresh.store(false); - service.checking.store(true); - try { - co_await check_all_nodes_once(service, pool); - service_log("自动切换判定开始"); - co_await run_auto_switch_once(service, pool); - service_log("自动切换判定结束"); - } - catch (const std::exception& err) { - const auto text = error_text_to_utf8(err.what()); - { - std::lock_guard lock(service.mutex); - service.status_text = std::format("全量 delay 异常结束:{}", text); - service.last_refresh_end_time = now_string(); - save_service_index(service); - } - service_log(std::format("全量 delay 异常结束:{}", text)); - } - service.checking.store(false); - int elapsed = 0; - while (service.running.load() && elapsed < current_check_interval_seconds(service)) { - if (service.force_refresh.exchange(false)) { - service_log("收到立即刷新请求,提前进入下一轮全量 delay"); - break; - } - timer.expires_after(std::chrono::seconds(1)); - error.clear(); - co_await timer.async_wait(asio::redirect_error(asio::use_awaitable, error)); - ++elapsed; - } - } -} diff --git a/src/node_service/Node_Service_State.cpp b/src/node_service/Node_Service_State.cpp deleted file mode 100644 index 20c5495..0000000 --- a/src/node_service/Node_Service_State.cpp +++ /dev/null @@ -1,562 +0,0 @@ -#include "Node_Service.h" -fs::path node_state_path(const Service_State& service, std::string_view name) { - return service.config.state_dir / path_from_utf8_string(make_safe_file_name(name) + ".json"); -} -json target_to_json(const Test_Target& target) { - json result; - result["name"] = target.name; - result["url"] = target.url; - result["timeout_ms"] = target.timeout_ms; - return result; -} -json probe_to_json(const Probe_Result& probe) { - json result; - result["target"] = probe.target; - result["url"] = probe.url; - result["success"] = probe.result.success; - result["delay_ms"] = probe.result.delay_ms; - result["http_status"] = probe.result.http_status; - result["error"] = probe.result.error; - result["raw"] = probe.result.raw; - return result; -} -json group_to_json(const Group_Switch_State& group) { - json result; - result["name"] = group.name; - result["type"] = group.type; - result["proxies"] = group.proxies; - result["candidates"] = group.candidates; - result["current"] = group.current; - result["best_candidate"] = group.best_candidate; - result["better_rounds"] = group.better_rounds; - result["switch_count"] = group.switch_count; - result["last_switch_unix"] = group.last_switch_unix; - result["last_switch_time"] = group.last_switch_time; - result["last_decision"] = group.last_decision; - result["last_error"] = group.last_error; - result["switchable"] = group.switchable; - result["managed"] = group.managed; - result["strategy"] = group.strategy; - return result; -} - -namespace { -json enum_item(std::string_view value, std::string_view label) { - return json{{"value", std::string(value)}, {"label", std::string(label)}}; -} -bool text_has(std::string_view text, std::string_view key) { - return text.find(key) != std::string_view::npos; -} -std::string score_category(double value) { - if (value >= 80.0) { - return "excellent"; - } - if (value >= 50.0) { - return "normal"; - } - return "poor"; -} -std::string rate_category(double value, uint64_t count) { - if (count == 0) { - return "unknown"; - } - if (value >= 95.0) { - return "excellent"; - } - if (value >= 80.0) { - return "usable"; - } - return "poor"; -} -std::string ai_category(int value) { - if (value < 0) { - return "unknown"; - } - if (value >= 4) { - return "high"; - } - if (value >= 3) { - return "usable"; - } - return "weak"; -} -std::string ip_type_category(std::string_view value) { - if (value.empty()) { - return "unknown"; - } - if (text_has(value, "IDC") || text_has(value, "机房") || text_has(value, "数据中心")) { - return "idc"; - } - if (text_has(value, "家庭") || text_has(value, "住宅") || text_has(value, "家宽") || text_has(value, "宽带")) { - return "residential"; - } - if (text_has(value, "移动") || text_has(value, "手机") || text_has(value, "蜂窝")) { - return "mobile"; - } - if (text_has(value, "教育") || text_has(value, "学校")) { - return "education"; - } - return "other"; -} -std::string risk_category(int risk, std::string_view level) { - if (text_has(level, "纯净")) { - return "clean"; - } - if (text_has(level, "高风险") || text_has(level, "危险")) { - return "high"; - } - if (text_has(level, "风险")) { - return "risk"; - } - if (risk < 0) { - return "unknown"; - } - if (risk >= 80) { - return "high"; - } - if (risk >= 50) { - return "risk"; - } - if (risk >= 25) { - return "normal"; - } - return "clean"; -} -std::string shared_category(std::string_view value) { - if (value.empty()) { - return "unknown"; - } - if (text_has(value, "极好") || text_has(value, "1 - 10") || text_has(value, "1-10")) { - return "excellent"; - } - if (text_has(value, "良好") || text_has(value, "10 - 50") || text_has(value, "10-50")) { - return "good"; - } - if (text_has(value, "差") || text_has(value, "多") || text_has(value, "100") || text_has(value, "1000")) { - return "high"; - } - return "normal"; -} -std::string native_category(std::string_view value) { - if (value.empty()) { - return "unknown"; - } - if (text_has(value, "原生") && !text_has(value, "非原生")) { - return "native"; - } - if (text_has(value, "广播")) { - return "broadcast"; - } - if (text_has(value, "非原生")) { - return "non_native"; - } - return "other"; -} -std::string ip_version_category(const Node_State& node) { - const bool has_ipv4 = !node.exit_ipv4.empty(); - const bool has_ipv6 = !node.exit_ipv6.empty() && node.exit_ipv6 != "无"; - if (has_ipv4 && has_ipv6) { - return "dual"; - } - if (has_ipv4) { - return "ipv4_only"; - } - if (has_ipv6) { - return "ipv6_only"; - } - return "none"; -} -std::string enum_label(const json& options, std::string_view name, std::string_view value) { - const auto item = options.find(std::string(name)); - if (item == options.end() || !item->is_array()) { - return std::string(value); - } - for (const auto& option : *item) { - if (option.value("value", std::string{}) == std::string(value)) { - return option.value("label", std::string(value)); - } - } - return std::string(value); -} -} -json enum_options_to_json() { - json result; - result["alive_category"] = json::array({enum_item("alive", "alive"), enum_item("dead", "dead"), enum_item("special", "特殊")}); - result["stability_category"] = json::array({enum_item("excellent", "稳定 ≥ 80"), enum_item("normal", "稳定 50~80"), enum_item("poor", "稳定 < 50"), enum_item("unknown", "特殊/未知")}); - result["rate_category"] = json::array({enum_item("excellent", "通过率 ≥ 95%"), enum_item("usable", "通过率 80~95%"), enum_item("poor", "通过率 < 80%"), enum_item("unknown", "未检测")}); - result["ai_category"] = json::array({enum_item("high", "AI 4~5 星"), enum_item("usable", "AI 3 星"), enum_item("weak", "AI < 3 星"), enum_item("unknown", "AI 未知")}); - result["ip_type_category"] = json::array({enum_item("idc", "IDC/机房"), enum_item("residential", "家庭宽带"), enum_item("mobile", "移动网络"), enum_item("education", "教育网络"), enum_item("other", "其他"), enum_item("unknown", "未知")}); - result["risk_category"] = json::array({enum_item("clean", "纯净/低风险"), enum_item("normal", "普通"), enum_item("risk", "风险"), enum_item("high", "高风险"), enum_item("unknown", "未知")}); - result["shared_category"] = json::array({enum_item("excellent", "极好"), enum_item("good", "良好"), enum_item("normal", "普通"), enum_item("high", "共享偏高"), enum_item("unknown", "未知")}); - result["native_category"] = json::array({enum_item("native", "原生"), enum_item("broadcast", "广播"), enum_item("non_native", "非原生"), enum_item("other", "其他"), enum_item("unknown", "未知")}); - result["ip_version_category"] = json::array({enum_item("dual", "IPv4 + IPv6"), enum_item("ipv4_only", "仅 IPv4"), enum_item("ipv6_only", "仅 IPv6"), enum_item("none", "无 IP 缓存")}); - result["switch_strategy"] = switch_strategy_options_to_json(); - return result; -} -json dynamic_filter_options_to_json(const Service_State& service) { - std::set countries; - std::set providers; - for (const auto& name : service.node_order) { - const auto item = service.nodes.find(name); - if (item == service.nodes.end()) { - continue; - } - if (!item->second.country.empty()) { - countries.insert(item->second.country); - } - if (!item->second.provider.empty()) { - providers.insert(item->second.provider); - } - } - json result; - result["country"] = json::array(); - result["provider"] = json::array(); - for (const auto& country : countries) { - result["country"].push_back(enum_item(country, country)); - } - for (const auto& provider : providers) { - result["provider"].push_back(enum_item(provider, provider)); - } - return result; -} -json node_to_json(const Node_State& node) { - json result; - result["name"] = node.name; - result["type"] = node.type; - result["provider"] = node.provider; - result["exit_ipv4"] = node.exit_ipv4; - result["exit_ipv6"] = node.exit_ipv6; - result["ai_score"] = node.ai_score; - result["risk"] = node.risk; - result["risk_level"] = node.risk_level; - result["multiplier"] = node.multiplier; - result["multiplier_text"] = node.multiplier_text; - result["shared_users"] = node.shared_users; - result["ip_type"] = node.ip_type; - result["native_type"] = node.native_type; - result["country"] = node.country; - const auto options = enum_options_to_json(); - const auto recent_pass_for_category = static_cast(std::count(node.recent_results.begin(), node.recent_results.end(), 1)); - const double total_rate_for_category = node.samples == 0 ? 0.0 : static_cast(node.success_count) * 100.0 / static_cast(node.samples); - const double recent_rate_for_category = node.recent_results.empty() ? 0.0 : static_cast(recent_pass_for_category) * 100.0 / static_cast(node.recent_results.size()); - const auto alive_enum = node.alive ? std::string("alive") : std::string("dead"); - const auto stability_enum = score_category(node.stability_score); - const auto total_rate_enum = rate_category(total_rate_for_category, node.samples); - const auto recent_rate_enum = rate_category(recent_rate_for_category, node.recent_results.size()); - const auto ai_enum = ai_category(node.ai_score); - const auto ip_type_enum = ip_type_category(node.ip_type); - const auto risk_enum = risk_category(node.risk, node.risk_level); - const auto shared_enum = shared_category(node.shared_users); - const auto native_enum = native_category(node.native_type); - const auto ip_version_enum = ip_version_category(node); - result["alive"] = node.alive; - result["alive_category"] = alive_enum; - result["alive_category_label"] = enum_label(options, "alive_category", alive_enum); - result["stability_category"] = stability_enum; - result["stability_category_label"] = enum_label(options, "stability_category", stability_enum); - result["total_rate_category"] = total_rate_enum; - result["total_rate_category_label"] = enum_label(options, "rate_category", total_rate_enum); - result["recent_rate_category"] = recent_rate_enum; - result["recent_rate_category_label"] = enum_label(options, "rate_category", recent_rate_enum); - result["ai_category"] = ai_enum; - result["ai_category_label"] = enum_label(options, "ai_category", ai_enum); - result["ip_type_category"] = ip_type_enum; - result["ip_type_category_label"] = enum_label(options, "ip_type_category", ip_type_enum); - result["risk_category"] = risk_enum; - result["risk_category_label"] = enum_label(options, "risk_category", risk_enum); - result["shared_category"] = shared_enum; - result["shared_category_label"] = enum_label(options, "shared_category", shared_enum); - result["native_category"] = native_enum; - result["native_category_label"] = enum_label(options, "native_category", native_enum); - result["ip_version_category"] = ip_version_enum; - result["ip_version_category_label"] = enum_label(options, "ip_version_category", ip_version_enum); - result["initialized"] = node.initialized; - result["srtt_ms"] = node.srtt_ms; - result["rttvar_ms"] = node.rttvar_ms; - result["success_ewma"] = node.success_ewma; - result["samples"] = node.samples; - result["check_count"] = node.samples; - result["success_count"] = node.success_count; - result["pass_count"] = node.success_count; - result["failure_count"] = node.failure_count; - result["consecutive_failures"] = node.consecutive_failures; - const auto recent_pass = static_cast(std::count(node.recent_results.begin(), node.recent_results.end(), 1)); - result["success_rate"] = node.samples == 0 ? 0.0 : static_cast(node.success_count) * 100.0 / static_cast(node.samples); - result["recent_check_count"] = node.recent_results.size(); - result["recent_pass_count"] = recent_pass; - result["recent_success_rate"] = node.recent_results.empty() ? 0.0 : static_cast(recent_pass) * 100.0 / static_cast(node.recent_results.size()); - result["last_delay_ms"] = node.last_delay_ms; - result["last_success_delay_ms"] = node.last_success_delay_ms; - result["stability_score"] = node.stability_score; - result["last_error"] = node.last_error; - result["last_check_time"] = node.last_check_time; - result["updated_time"] = node.updated_time; - result["recent_results"] = node.recent_results; - result["probes"] = json::array(); - for (const auto& probe : node.probes) { - result["probes"].push_back(probe_to_json(probe)); - } - return result; -} -void load_node_state(Node_State& node, const fs::path& path) { - const auto data = read_json_file(path); - if (!data.is_object()) { - return; - } - node.initialized = data.value("initialized", false); - node.srtt_ms = data.value("srtt_ms", 0.0); - node.rttvar_ms = data.value("rttvar_ms", 0.0); - node.success_ewma = data.value("success_ewma", 0.0); - node.samples = data.value("samples", uint64_t{}); - node.success_count = data.value("success_count", uint64_t{}); - node.failure_count = data.value("failure_count", uint64_t{}); - node.consecutive_failures = data.value("consecutive_failures", uint64_t{}); - node.recent_results.clear(); - if (data.contains("recent_results") && data["recent_results"].is_array()) { - for (const auto& item : data["recent_results"]) { - node.recent_results.push_back(item.get() == 0 ? 0 : 1); - } - if (node.recent_results.size() > recent_result_window) { - node.recent_results.erase(node.recent_results.begin(), node.recent_results.end() - static_cast(recent_result_window)); - } - } - node.last_delay_ms = data.value("last_delay_ms", 0); - node.last_success_delay_ms = data.value("last_success_delay_ms", 0); - node.stability_score = data.value("stability_score", 0.0); - node.last_error = data.value("last_error", std::string{}); - node.last_check_time = data.value("last_check_time", std::string{}); - node.updated_time = data.value("updated_time", std::string{}); - node.alive = data.value("alive", false); - if (data.contains("probes") && data["probes"].is_array()) { - node.probes.clear(); - for (const auto& item : data["probes"]) { - Probe_Result probe; - probe.target = item.value("target", std::string{}); - probe.url = item.value("url", std::string{}); - probe.result.success = item.value("success", false); - probe.result.delay_ms = item.value("delay_ms", 0); - probe.result.http_status = item.value("http_status", 0); - probe.result.error = item.value("error", std::string{}); - node.probes.push_back(std::move(probe)); - } - } -} -void save_service_index(Service_State& service) { - json result; - result["started_time"] = service.started_time; - result["updated_time"] = now_string(); - result["controller_origin"] = service.config.controller_origin; - result["check_interval_seconds"] = service.config.check_interval_seconds; - result["delay_refresh_interval_seconds"] = service.config.check_interval_seconds; - result["status_text"] = service.status_text; - result["checking"] = service.checking.load(); - result["force_refresh"] = service.force_refresh.load(); - result["refresh_done_nodes"] = service.refresh_done_nodes.load(); - result["refresh_total_nodes"] = service.refresh_total_nodes; - result["refresh_request_count"] = service.refresh_request_count; - result["last_refresh_request_time"] = service.last_refresh_request_time; - result["last_refresh_begin_time"] = service.last_refresh_begin_time; - result["last_refresh_end_time"] = service.last_refresh_end_time; - result["state_dir"] = path_to_utf8_string(service.config.state_dir); - result["web_dir"] = path_to_utf8_string(service.config.web_dir); - result["service_config_path"] = path_to_utf8_string(service.config.service_config_path); - result["targets"] = json::array(); - for (const auto& target : service.config.targets) { - result["targets"].push_back(target_to_json(target)); - } - result["round"] = service.round; - result["enum_options"] = enum_options_to_json(); - result["dynamic_filter_options"] = dynamic_filter_options_to_json(service); - result["auto_switch"] = auto_switch_config_to_json(service.config.auto_switch); - result["table_protocol"] = Table_Protocol_Registry::instance().protocol(service); - result["table_rows"] = Table_Protocol_Registry::instance().rows(service); - result["groups"] = json::array(); - for (const auto& name : service.group_order) { - const auto item = service.groups.find(name); - if (item != service.groups.end()) { - result["groups"].push_back(group_to_json(item->second)); - } - } - result["nodes"] = json::array(); - for (const auto& name : service.node_order) { - const auto item = service.nodes.find(name); - if (item != service.nodes.end()) { - result["nodes"].push_back(node_to_json(item->second)); - } - } - save_json_file(service.config.state_dir / "index.json", result); -} -double clamp_score(double value) { - return std::clamp(value, 0.0, 100.0); -} -void update_node_statistics(Node_State& node, std::optional delay_ms) { - ++node.samples; - node.recent_results.push_back(delay_ms ? 1 : 0); - if (node.recent_results.size() > recent_result_window) { - node.recent_results.erase(node.recent_results.begin()); - } - if (!delay_ms) { - ++node.failure_count; - ++node.consecutive_failures; - node.success_ewma = node.samples == 1 ? 0.0 : node.success_ewma * 0.95; - return; - } - const double rtt = static_cast(*delay_ms); - ++node.success_count; - node.consecutive_failures = 0; - node.last_success_delay_ms = *delay_ms; - node.success_ewma = node.samples == 1 ? 1.0 : node.success_ewma * 0.95 + 0.05; - if (!node.initialized) { - node.srtt_ms = rtt; - node.rttvar_ms = rtt / 2.0; - node.initialized = true; - } - else { - node.rttvar_ms = (1.0 - ewma_rttvar_beta) * node.rttvar_ms + ewma_rttvar_beta * std::abs(node.srtt_ms - rtt); - node.srtt_ms = (1.0 - ewma_rtt_alpha) * node.srtt_ms + ewma_rtt_alpha * rtt; - } -} -void update_score(Node_State& node) { - const auto recent_pass = static_cast(std::count(node.recent_results.begin(), node.recent_results.end(), 1)); - const double recent_rate = node.recent_results.empty() ? node.success_ewma : static_cast(recent_pass) / static_cast(node.recent_results.size()); - const double latency_penalty = node.initialized ? std::min(30.0, node.srtt_ms / 50.0) : 30.0; - const double jitter_penalty = std::min(25.0, node.rttvar_ms / 50.0); - const double failure_penalty = std::min(40.0, static_cast(node.consecutive_failures) * 15.0); - node.stability_score = clamp_score(recent_rate * 100.0 - latency_penalty - jitter_penalty - failure_penalty); -} -std::vector read_proxy_names(const fs::path& config_path) { - std::ifstream stream(config_path, std::ios::binary); - if (!stream) { - throw std::runtime_error(std::format("无法打开配置文件:{}", path_to_utf8_string(config_path))); - } - YAML::Node config = YAML::Load(stream); - const auto proxies = config["proxies"]; - std::vector names; - std::set seen; - if (!proxies || !proxies.IsSequence()) { - return names; - } - for (const auto& proxy : proxies) { - if (!proxy["name"]) { - continue; - } - const auto name = proxy["name"].as(); - if (contain(name, "剩余流量") || contain(name, "套餐到期")) { - continue; - } - if (seen.insert(name).second) { - names.push_back(name); - } - } - return names; -} -std::vector read_proxy_groups(const fs::path& config_path) { - std::ifstream stream(config_path, std::ios::binary); - if (!stream) { - throw std::runtime_error(std::format("无法打开配置文件:{}", path_to_utf8_string(config_path))); - } - YAML::Node config = YAML::Load(stream); - const auto groups = config["proxy-groups"]; - std::vector result; - if (!groups || !groups.IsSequence()) { - return result; - } - for (const auto& item : groups) { - if (!item["name"] || !item["type"]) { - continue; - } - Proxy_Group_Def group; - group.name = item["name"].as(); - group.type = item["type"].as(); - const auto proxies = item["proxies"]; - if (proxies && proxies.IsSequence()) { - std::set seen; - for (const auto& proxy : proxies) { - const auto name = proxy.as(); - if (seen.insert(name).second) { - group.proxies.push_back(name); - } - } - } - result.push_back(std::move(group)); - } - return result; -} -bool contains_name(const std::vector& list, const std::string& name) { - return std::find(list.begin(), list.end(), name) != list.end(); -} -bool should_manage_group(const Auto_Switch_Config& config, const Proxy_Group_Def& group) { - if (group.type != "select") { - return false; - } - return config.managed_groups.empty() || contains_name(config.managed_groups, group.name); -} -void reload_switch_groups_locked(Service_State& service) { - const auto groups = read_proxy_groups(service.config.config_path); - std::set node_names(service.node_order.begin(), service.node_order.end()); - std::unordered_map next_groups; - std::vector next_order; - for (const auto& def : groups) { - Group_Switch_State group; - const auto old = service.groups.find(def.name); - if (old != service.groups.end()) { - group = old->second; - } - group.name = def.name; - group.type = def.type; - group.proxies = def.proxies; - group.managed = should_manage_group(service.config.auto_switch, def); - group.strategy = group_switch_strategy(service.config.auto_switch, group.name); - group.candidates.clear(); - for (const auto& proxy : def.proxies) { - if (service.config.auto_switch.skip_direct && proxy == "DIRECT") { - continue; - } - if (node_names.contains(proxy)) { - group.candidates.push_back(proxy); - } - } - group.switchable = group.managed && !group.candidates.empty(); - if (!group.managed) { - group.last_error.clear(); - if (group.last_decision.empty()) { - group.last_decision = "不受自动切换管理"; - } - } - else if (group.switchable) { - group.last_error.clear(); - if (group.last_decision.empty()) { - group.last_decision = "等待自动切换判定"; - } - } - else { - group.last_error = "代理组没有直接节点候选,不能由稳定性服务切换"; - } - next_order.push_back(group.name); - next_groups.emplace(group.name, std::move(group)); - } - service.group_order = std::move(next_order); - service.groups = std::move(next_groups); -} -void reload_switch_groups(Service_State& service) { - std::lock_guard lock(service.mutex); - reload_switch_groups_locked(service); - save_service_index(service); -} -void initialize_nodes(Service_State& service) { - fs::create_directories(service.config.state_dir); - const auto names = read_proxy_names(service.config.config_path); - std::lock_guard lock(service.mutex); - service.status_text = "服务初始化完成,等待第一轮全量 delay"; - service.node_order = names; - for (const auto& name : names) { - Node_State node; - node.name = name; - enrich_node_metadata(node); - load_node_state(node, node_state_path(service, name)); - service.nodes.emplace(name, std::move(node)); - } - reload_switch_groups_locked(service); - save_service_index(service); - service_log(std::format("节点初始化完成:{} 个节点,{} 个受控代理组", service.node_order.size(), service.group_order.size())); -} diff --git a/src/node_service/Node_Service_Table.cpp b/src/node_service/Node_Service_Table.cpp deleted file mode 100644 index b9ddc8f..0000000 --- a/src/node_service/Node_Service_Table.cpp +++ /dev/null @@ -1,567 +0,0 @@ -#include "Node_Service.h" -namespace { -json enum_item(std::string_view value, std::string_view label) { - return json{{"value", std::string(value)}, {"label", std::string(label)}}; -} -json column(std::string_view id, std::string_view label, std::string_view type, int width, bool visible, bool sortable, bool filterable, std::string_view sort_rule = "none", std::string_view filter_type = "none", std::string_view enum_name = "", std::string_view label_field = "", std::string_view align = "left", bool editable = false, std::string_view sticky = "", int sticky_order = -1) { - json result; - result["id"] = std::string(id); - result["label"] = std::string(label); - result["type"] = std::string(type); - result["width"] = width; - result["visible"] = visible; - result["sortable"] = sortable; - result["filterable"] = filterable; - result["sort_rule"] = std::string(sort_rule); - result["filter_type"] = std::string(filter_type); - result["align"] = std::string(align); - result["editable"] = editable; - if (!enum_name.empty()) { - result["enum"] = std::string(enum_name); - } - if (!label_field.empty()) { - result["label_field"] = std::string(label_field); - } - if (!sticky.empty()) { - result["sticky"] = std::string(sticky); - result["sticky_order"] = sticky_order; - } - return result; -} -json table(std::string_view id, std::string_view name, std::string_view row_id_field, std::string_view default_sort_column, std::string_view default_sort_direction, json columns, json keyword_fields) { - json result; - result["id"] = std::string(id); - result["name"] = std::string(name); - result["row_id_field"] = std::string(row_id_field); - result["default_sort"] = json{{"column", std::string(default_sort_column)}, {"direction", std::string(default_sort_direction)}}; - result["columns"] = std::move(columns); - result["keyword_fields"] = std::move(keyword_fields); - result["column_order_editable"] = true; - result["sticky_editable"] = true; - result["layout"] = json{{"sticky_header", true}, {"top_scrollbar", true}}; - return result; -} -void apply_column_order(json& table, const Table_Layout_Config& layout) { - const auto table_id = table.value("id", std::string{}); - const auto item = layout.column_orders.find(table_id); - if (item == layout.column_orders.end() || !table.contains("columns") || !table["columns"].is_array()) { - return; - } - std::unordered_map columns; - for (const auto& column : table["columns"]) { - const auto id = column.value("id", std::string{}); - if (!id.empty()) { - columns[id] = column; - } - } - json ordered = json::array(); - std::set used; - for (const auto& id : item->second) { - const auto found = columns.find(id); - if (found != columns.end()) { - ordered.push_back(found->second); - used.insert(id); - } - } - for (const auto& column : table["columns"]) { - const auto id = column.value("id", std::string{}); - if (!id.empty() && !used.contains(id)) { - ordered.push_back(column); - } - } - table["columns"] = std::move(ordered); -} -void apply_sticky_columns(json& table, const Table_Layout_Config& layout) { - const auto table_id = table.value("id", std::string{}); - const auto item = layout.sticky_columns.find(table_id); - if (item == layout.sticky_columns.end() || !table.contains("columns") || !table["columns"].is_array()) { - return; - } - std::set sticky_ids(item->second.begin(), item->second.end()); - int order = 0; - for (auto& column : table["columns"]) { - const auto id = column.value("id", std::string{}); - if (!sticky_ids.contains(id)) { - column.erase("sticky"); - column.erase("sticky_order"); - continue; - } - column["sticky"] = "left"; - column["sticky_order"] = order++; - } -} -json edit_schema(std::string_view type, std::string_view enum_name = "", double min_value = 0.0, double max_value = 0.0, double step = 1.0) { - json result; - result["type"] = std::string(type); - if (!enum_name.empty()) { - result["enum"] = std::string(enum_name); - } - if (type == "number") { - result["min"] = min_value; - result["max"] = max_value; - result["step"] = step; - } - return result; -} -std::string risk_text_from_node(const json& node) { - const int risk = node.value("risk", -1); - const auto level = node.value("risk_level", std::string{}); - if (risk < 0) { - return level.empty() ? std::string("-") : level; - } - return level.empty() ? std::format("{}", risk) : std::format("{} {}", risk, level); -} -std::string ai_text_from_node(const json& node) { - const int score = node.value("ai_score", -1); - return score < 0 ? std::string("-") : std::format("{} 星", score); -} -json rate_value_from_node(const json& node) { - json result; - result["check_count"] = node.value("check_count", node.value("samples", 0)); - result["pass_count"] = node.value("pass_count", node.value("success_count", 0)); - result["success_rate"] = node.value("success_rate", 0.0); - result["recent_check_count"] = node.value("recent_check_count", 0); - result["recent_pass_count"] = node.value("recent_pass_count", 0); - result["recent_success_rate"] = node.value("recent_success_rate", 0.0); - return result; -} -json node_row_values(const Node_State& node, const std::vector& groups) { - const auto raw = node_to_json(node); - json values = raw; - values["id"] = node.name; - values["risk_text"] = risk_text_from_node(raw); - values["ai_text"] = ai_text_from_node(raw); - values["rate_summary"] = rate_value_from_node(raw); - values["proxy_groups"] = groups; - values["probe_summary"] = raw.value("probes", json::array()); - return values; -} -json group_row_values(const Group_Switch_State& group) { - const auto raw = group_to_json(group); - json values = raw; - values["id"] = group.name; - values["proxy_count"] = group.proxies.size(); - values["candidate_count"] = group.candidates.size(); - values["managed_category"] = group.managed ? "managed" : "unmanaged"; - values["managed_category_label"] = group.managed ? "受控" : "未受控"; - values["switchable_category"] = group.switchable ? "switchable" : "not_switchable"; - values["switchable_category_label"] = group.switchable ? "可切换" : "不可切换"; - values["type_category"] = group.type.empty() ? "unknown" : group.type; - values["type_category_label"] = group.type.empty() ? "未知" : group.type; - values["_edit"] = json::object(); - values["_edit"]["strategy"] = edit_schema("enum", "switch_strategy"); - return values; -} -json row(std::string_view id, json values, json raw) { - json result; - result["id"] = std::string(id); - result["values"] = std::move(values); - result["raw"] = std::move(raw); - return result; -} -std::unordered_map> make_node_group_map(const Service_State& service) { - std::unordered_map> result; - for (const auto& group_name : service.group_order) { - const auto group_item = service.groups.find(group_name); - if (group_item == service.groups.end()) { - continue; - } - for (const auto& node_name : group_item->second.candidates) { - result[node_name].push_back(group_item->second.name); - } - } - return result; -} -json proxy_group_options(const Service_State& service) { - json result = json::array(); - for (const auto& name : service.group_order) { - result.push_back(enum_item(name, name)); - } - return result; -} -json protocol_enums(const Service_State* service) { - json result = enum_options_to_json(); - result["rate_category"] = result.value("rate_category", json::array()); - result["proxy_group"] = service ? proxy_group_options(*service) : json::array(); - result["group_managed_category"] = json::array({enum_item("managed", "受控"), enum_item("unmanaged", "未受控")}); - result["group_switchable_category"] = json::array({enum_item("switchable", "可切换"), enum_item("not_switchable", "不可切换")}); - result["proxy_group_type"] = json::array({enum_item("select", "select"), enum_item("url-test", "url-test"), enum_item("fallback", "fallback"), enum_item("load-balance", "load-balance"), enum_item("unknown", "未知")}); - result["switch_strategy"] = switch_strategy_options_to_json(); - result["switch_setting_type"] = json::array({enum_item("basic", "基础"), enum_item("decision", "切换判定"), enum_item("parameter", "策略参数")}); - return result; -} -json proxy_node_columns() { - return json::array({ - column("action", "操作", "action", 150, true, false, false, "none", "none", "", "", "left", false, "left", 0), - column("name", "节点", "text", 420, true, true, false, "locale_asc", "none", "", "", "left", false, "left", 1), - column("alive_category", "存活", "enum", 92, true, true, true, "enum_order", "enum", "alive_category", "alive_category_label"), - column("exit_ipv4", "IPv4", "text", 150, true, true, false, "locale_asc"), - column("exit_ipv6", "IPv6", "text", 220, true, true, false, "locale_asc"), - column("ip_version_category", "IP版本", "enum", 120, true, true, true, "enum_order", "enum", "ip_version_category", "ip_version_category_label"), - column("ai_category", "AI", "enum", 120, true, true, true, "enum_order", "enum", "ai_category", "ai_category_label"), - column("ip_type_category", "IDC属性", "enum", 150, true, true, true, "enum_order", "enum", "ip_type_category", "ip_type_category_label"), - column("risk_category", "风控", "enum", 150, true, true, true, "enum_order", "enum", "risk_category", "risk_category_label"), - column("multiplier", "倍率", "number", 110, true, true, false, "number_asc", "none", "", "", "right"), - column("shared_category", "共享", "enum", 140, true, true, true, "enum_order", "enum", "shared_category", "shared_category_label"), - column("native_category", "原生", "enum", 130, true, true, true, "enum_order", "enum", "native_category", "native_category_label"), - column("stability_score", "稳定性", "score", 140, true, true, false, "number_desc", "none", "", "", "right"), - column("stability_category", "稳定性", "enum", 150, false, true, true, "enum_order", "enum", "stability_category", "stability_category_label"), - column("rate_summary", "通过率", "rate_summary", 240, true, true, false, "number_desc"), - column("recent_rate_category", "近期通过率", "enum", 170, false, true, true, "enum_order", "enum", "rate_category", "recent_rate_category_label"), - column("srtt_ms", "SRTT", "ms", 90, true, true, false, "number_asc", "none", "", "", "right"), - column("rttvar_ms", "RTTVAR", "ms", 90, true, true, false, "number_asc", "none", "", "", "right"), - column("last_delay_ms", "最新延迟", "ms", 100, true, true, false, "number_asc", "none", "", "", "right"), - column("consecutive_failures", "连续失败", "integer", 100, true, true, false, "number_desc", "none", "", "", "right"), - column("probe_summary", "探测", "probe_icons", 150, true, false, false), - column("updated_time", "更新时间", "datetime", 190, true, true, false, "locale_desc"), - column("country", "国家", "text", 130, false, true, true, "locale_asc", "dynamic_enum", "country"), - column("provider", "Provider", "text", 150, false, true, true, "locale_asc", "dynamic_enum", "provider"), - column("proxy_groups", "代理组", "multi_enum", 180, false, false, true, "none", "multi_enum", "proxy_group") - }); -} -json proxy_group_columns() { - return json::array({ - column("action", "操作", "action", 120, true, false, false, "none", "none", "", "", "left", false, "left", 0), - column("name", "代理组", "text", 260, true, true, false, "locale_asc", "none", "", "", "left", false, "left", 1), - column("type_category", "类型", "enum", 120, true, true, true, "enum_order", "enum", "proxy_group_type", "type_category_label"), - column("managed_category", "管理", "enum", 110, true, true, true, "enum_order", "enum", "group_managed_category", "managed_category_label"), - column("switchable_category", "可切换", "enum", 110, true, true, true, "enum_order", "enum", "group_switchable_category", "switchable_category_label"), - column("strategy", "策略", "enum", 190, true, true, true, "enum_order", "enum", "switch_strategy", "", "left", true), - column("current", "当前选择", "text", 300, true, true, false, "locale_asc"), - column("proxy_count", "成员", "integer", 90, true, true, false, "number_desc", "none", "", "", "right"), - column("candidate_count", "自动候选", "integer", 110, true, true, false, "number_desc", "none", "", "", "right"), - column("best_candidate", "最佳候选", "text", 300, true, true, false, "locale_asc"), - column("better_rounds", "更好确认", "integer", 120, true, true, false, "number_desc", "none", "", "", "right"), - column("switch_count", "切换次数", "integer", 100, true, true, false, "number_desc", "none", "", "", "right"), - column("last_switch_time", "上次切换", "datetime", 190, true, true, false, "locale_desc"), - column("last_decision", "决策", "text", 460, true, false, false), - column("last_error", "错误", "text", 300, true, false, false) - }); -} -json form_control(std::string_view key, std::string_view label, std::string_view type, json value, std::string_view help = "", std::string_view enum_name = "", double min_value = 0.0, double max_value = 0.0, double step = 1.0, std::string_view unit = "") { - json result; - result["key"] = std::string(key); - result["label"] = std::string(label); - result["type"] = std::string(type); - result["value"] = std::move(value); - result["help"] = std::string(help); - result["unit"] = std::string(unit); - if (!enum_name.empty()) { - result["enum"] = std::string(enum_name); - } - if (type == "number") { - result["min"] = min_value; - result["max"] = max_value; - result["step"] = step; - } - return result; -} -json form_schema(std::string_view id, std::string_view title, json controls, std::string_view layout = "inline") { - json result; - result["id"] = std::string(id); - result["title"] = std::string(title); - result["layout"] = std::string(layout); - result["controls"] = std::move(controls); - return result; -} -json switch_global_setting_columns() { - return json::array({ - column("name", "表单", "text", 180, true, true, false, "locale_asc"), - column("settings", "配置项", "form", 1120, true, false, false, "none", "none", "", "", "left", true), - column("description", "说明", "text", 420, true, false, false) - }); -} -json switch_strategy_columns() { - return json::array({ - column("name", "策略", "text", 220, true, true, false, "locale_asc"), - column("formula_latex", "LaTeX公式", "text", 420, true, false, false), - column("settings", "策略参数", "form", 760, true, false, false, "none", "none", "", "", "left", true), - column("description", "说明", "text", 460, true, false, false) - }); -} -json switch_global_setting_table() { - return table("switch_global_settings", "自动切换全局配置", "id", "name", "asc", switch_global_setting_columns(), json::array({"name", "description"})); -} -json switch_strategy_table() { - return table("switch_strategies", "策略配置", "id", "name", "asc", switch_strategy_columns(), json::array({"name", "formula_latex", "description"})); -} -json bool_label_json(bool value) { - return value ? json("是") : json("否"); -} -json switch_global_form(const Auto_Switch_Config& config) { - json controls = json::array(); - controls.push_back(form_control("enabled", "启用自动切换", "boolean", config.enabled, "控制自动切换是否执行。")); - controls.push_back(form_control("skip_direct", "跳过 DIRECT", "boolean", config.skip_direct, "自动选择候选时跳过 DIRECT。")); - controls.push_back(form_control("default_strategy", "默认策略", "enum", config.default_strategy, "代理组没有单独指定策略时使用这个策略。", "switch_strategy")); - controls.push_back(form_control("switch_cooldown_seconds", "切换冷却秒数", "number", config.switch_cooldown_seconds, "切换之后多久内不再切换。", "", 0, 3600, 1, "秒")); - controls.push_back(form_control("fail_switch_count", "连续失败切换次数", "number", config.fail_switch_count, "当前节点连续失败达到该值后切换。", "", 1, 100, 1)); - controls.push_back(form_control("better_confirm_rounds", "更好节点确认轮数", "number", config.better_confirm_rounds, "更好候选连续成立多少轮才切换。", "", 1, 100, 1)); - controls.push_back(form_control("keep_score_threshold", "当前节点不切阈值", "number", config.keep_score_threshold, "当前节点分数不低于该值时保持不切。", "", 0, 100, 0.1)); - controls.push_back(form_control("weak_score_threshold", "弱节点阈值", "number", config.weak_score_threshold, "当前节点低于该值时允许按优势切换。", "", 0, 100, 0.1)); - controls.push_back(form_control("weak_improve_margin", "弱节点切换优势", "number", config.weak_improve_margin, "弱节点场景下候选需要高出多少分。", "", 0, 100, 0.1)); - controls.push_back(form_control("better_improve_margin", "明显更好优势", "number", config.better_improve_margin, "正常场景下候选需要高出多少分。", "", 0, 100, 0.1)); - return form_schema("auto_switch.global", "自动切换全局配置", std::move(controls)); -} -json strategy_form(const Auto_Switch_Config& config, const json& definition) { - json controls = json::array(); - const auto strategy_id = definition.value("id", std::string{}); - for (const auto& parameter : definition.value("parameters", json::array())) { - const auto key = parameter.value("key", std::string{}); - if (key.empty()) { - continue; - } - const auto value = switch_strategy_parameter(config, key); - controls.push_back(form_control(key, parameter.value("label", key), parameter.value("type", std::string("number")), value, parameter.value("help", std::string{}), parameter.value("enum", std::string{}), parameter.value("min", 0.0), parameter.value("max", 100.0), parameter.value("step", 0.1), parameter.value("unit", std::string{}))); - } - return form_schema(std::format("strategy.{}", strategy_id), "策略参数", std::move(controls)); -} -json form_values_from_schema(const json& form) { - json result = json::object(); - for (const auto& control : form.value("controls", json::array())) { - const auto key = control.value("key", std::string{}); - if (!key.empty()) { - result[key] = control.value("value", json{}); - } - } - return result; -} -json switch_global_setting_row(const Auto_Switch_Config& config) { - auto form = switch_global_form(config); - json values; - values["id"] = "global"; - values["name"] = "自动切换全局配置"; - values["settings"] = form_values_from_schema(form); - values["description"] = "开关、默认策略、冷却时间和切换判定阈值。"; - values["_forms"] = json::object({{"settings", std::move(form)}}); - return row("global", values, values); -} -json switch_strategy_row(const Auto_Switch_Config& config, const json& definition) { - const auto id = definition.value("id", std::string{}); - auto form = strategy_form(config, definition); - json values; - values["id"] = id; - values["name"] = definition.value("name", id); - values["formula_latex"] = definition.value("formula_latex", std::string{}); - values["settings"] = form_values_from_schema(form); - values["description"] = definition.value("description", std::string{}); - values["_forms"] = json::object({{"settings", std::move(form)}}); - return row(id, values, definition); -} -std::optional json_to_bool(const json& value) { - if (value.is_boolean()) { - return value.get(); - } - if (value.is_string()) { - const auto text = trim(value.get()); - if (text == "true" || text == "1" || text == "是") { - return true; - } - if (text == "false" || text == "0" || text == "否") { - return false; - } - } - return std::nullopt; -} -int json_to_int(const json& value) { - if (value.is_number_integer()) { - return value.get(); - } - if (value.is_number()) { - return static_cast(value.get()); - } - return std::stoi(value.get()); -} -double json_to_double(const json& value) { - if (value.is_number()) { - return value.get(); - } - return std::stod(value.get()); -} -void apply_global_form_value(Auto_Switch_Config& config, const std::string& key, const json& value) { - if (key == "enabled") { - if (auto parsed = json_to_bool(value)) { - config.enabled = *parsed; - } - } - else if (key == "skip_direct") { - if (auto parsed = json_to_bool(value)) { - config.skip_direct = *parsed; - } - } - else if (key == "default_strategy") { - config.default_strategy = normalize_switch_strategy(value.is_string() ? value.get() : std::string{}); - } - else if (key == "switch_cooldown_seconds") { - config.switch_cooldown_seconds = normalize_positive_int(json_to_int(value), 0, 3600); - } - else if (key == "fail_switch_count") { - config.fail_switch_count = normalize_positive_int(json_to_int(value), 1, 100); - } - else if (key == "better_confirm_rounds") { - config.better_confirm_rounds = normalize_positive_int(json_to_int(value), 1, 100); - } - else if (key == "keep_score_threshold") { - config.keep_score_threshold = normalize_score_threshold(json_to_double(value)); - } - else if (key == "weak_score_threshold") { - config.weak_score_threshold = normalize_score_threshold(json_to_double(value)); - } - else if (key == "weak_improve_margin") { - config.weak_improve_margin = normalize_score_threshold(json_to_double(value)); - } - else if (key == "better_improve_margin") { - config.better_improve_margin = normalize_score_threshold(json_to_double(value)); - } -} -void apply_strategy_form_value(Auto_Switch_Config& config, const std::string& key, const json& value) { - config.strategy_parameters[key] = json_to_double(value); - normalize_switch_strategy_parameters(config); -} -json generic_form_protocol() { - return json{{"version", 1}, {"control_types", json::array({"number", "boolean", "string", "enum"})}, {"submit", "cell"}}; -} -json ordered_protocol(Service_State const* service) { - json result; - result["version"] = 1; - result["enums"] = protocol_enums(service); - result["form_protocol"] = generic_form_protocol(); - result["tables"] = json::object(); - result["tables"]["proxy_nodes"] = table("proxy_nodes", "代理节点", "id", "stability_score", "desc", proxy_node_columns(), json::array({"name", "exit_ipv4", "exit_ipv6", "ip_type", "risk_level", "native_type", "shared_users", "provider", "country", "ai_category_label", "ip_type_category_label", "risk_category_label", "shared_category_label", "native_category_label", "ip_version_category_label"})); - result["tables"]["proxy_groups"] = table("proxy_groups", "代理组", "id", "name", "asc", proxy_group_columns(), json::array({"name", "type", "current", "best_candidate", "strategy", "last_decision", "last_error"})); - result["tables"]["switch_global_settings"] = switch_global_setting_table(); - result["tables"]["switch_strategies"] = switch_strategy_table(); - if (service) { - for (auto& item : result["tables"].items()) { - apply_column_order(item.value(), service->config.table_layout); - apply_sticky_columns(item.value(), service->config.table_layout); - } - } - return result; -} -} -Table_Protocol_Registry& Table_Protocol_Registry::instance() { - static Table_Protocol_Registry registry; - return registry; -} -json Table_Protocol_Registry::protocol() const { - return ordered_protocol(nullptr); -} -json Table_Protocol_Registry::protocol(const Service_State& service) const { - return ordered_protocol(&service); -} -json Table_Protocol_Registry::rows(const Service_State& service) const { - json result; - result["proxy_nodes"] = proxy_node_rows(service); - result["proxy_groups"] = proxy_group_rows(service); - result["switch_global_settings"] = switch_global_setting_rows(service); - result["switch_strategies"] = switch_strategy_rows(service); - return result; -} -json Table_Protocol_Registry::proxy_node_rows(const Service_State& service) const { - const auto group_map = make_node_group_map(service); - json result = json::array(); - for (const auto& name : service.node_order) { - const auto item = service.nodes.find(name); - if (item == service.nodes.end()) { - continue; - } - const auto groups = group_map.contains(name) ? group_map.at(name) : std::vector{}; - result.push_back(row(name, node_row_values(item->second, groups), node_to_json(item->second))); - } - return result; -} -json Table_Protocol_Registry::proxy_group_rows(const Service_State& service) const { - json result = json::array(); - for (const auto& name : service.group_order) { - const auto item = service.groups.find(name); - if (item == service.groups.end()) { - continue; - } - result.push_back(row(name, group_row_values(item->second), group_to_json(item->second))); - } - return result; -} -json Table_Protocol_Registry::switch_global_setting_rows(const Service_State& service) const { - json result = json::array(); - result.push_back(switch_global_setting_row(service.config.auto_switch)); - return result; -} -json Table_Protocol_Registry::switch_strategy_rows(const Service_State& service) const { - json result = json::array(); - for (const auto& definition : switch_strategy_definitions_to_json()) { - result.push_back(switch_strategy_row(service.config.auto_switch, definition)); - } - return result; -} -json Table_Protocol_Registry::switch_strategy_setting_rows(const Service_State& service) const { - json result = json::array(); - for (const auto& row : switch_global_setting_rows(service)) { - result.push_back(row); - } - for (const auto& row : switch_strategy_rows(service)) { - result.push_back(row); - } - return result; -} -json table_protocol_snapshot(Service_State& service) { - std::lock_guard lock(service.mutex); - return Table_Protocol_Registry::instance().protocol(service); -} -json table_rows_snapshot(Service_State& service) { - std::lock_guard lock(service.mutex); - return Table_Protocol_Registry::instance().rows(service); -} -void update_table_column_order(Service_State& service, const std::string& table_id, const std::vector& column_order) { - std::lock_guard lock(service.mutex); - service.config.table_layout.column_orders[table_id] = column_order; - save_runtime_config(service.config); - save_service_index(service); -} -void update_table_sticky_columns(Service_State& service, const std::string& table_id, const std::vector& sticky_columns) { - std::lock_guard lock(service.mutex); - service.config.table_layout.sticky_columns[table_id] = sticky_columns; - save_runtime_config(service.config); - save_service_index(service); -} -void update_table_cell(Service_State& service, const std::string& table_id, const std::string& row_id, const std::string& column_id, const json& value) { - std::lock_guard lock(service.mutex); - if (table_id == "proxy_groups" && column_id == "strategy") { - service.config.auto_switch.group_strategies[row_id] = normalize_switch_strategy(value.is_string() ? value.get() : std::string{}); - reload_switch_groups_locked(service); - } - else if (table_id == "switch_global_settings" && row_id == "global" && column_id == "settings") { - if (!value.is_object()) { - throw std::runtime_error("自动切换全局配置表单值必须是对象"); - } - auto& config = service.config.auto_switch; - for (const auto& item : value.items()) { - apply_global_form_value(config, item.key(), item.value()); - } - reload_switch_groups_locked(service); - } - else if (table_id == "switch_strategies" && column_id == "settings") { - if (!value.is_object()) { - throw std::runtime_error("策略参数表单值必须是对象"); - } - auto& config = service.config.auto_switch; - for (const auto& item : value.items()) { - apply_strategy_form_value(config, item.key(), item.value()); - } - reload_switch_groups_locked(service); - } - else if (table_id == "switch_strategy_settings" && column_id == "value") { - auto& config = service.config.auto_switch; - if (row_id == "enabled" || row_id == "skip_direct" || row_id == "default_strategy" || row_id == "switch_cooldown_seconds" || row_id == "fail_switch_count" || row_id == "better_confirm_rounds" || row_id == "keep_score_threshold" || row_id == "weak_score_threshold" || row_id == "weak_improve_margin" || row_id == "better_improve_margin") { - apply_global_form_value(config, row_id, value); - } - else if (starts_with(row_id, "param:")) { - apply_strategy_form_value(config, row_id.substr(6), value); - } - reload_switch_groups_locked(service); - } - else { - throw std::runtime_error(std::format("不支持编辑表格单元:{}.{}.{}", table_id, row_id, column_id)); - } - save_runtime_config(service.config); - save_service_index(service); -} diff --git a/src/node_service/Node_Service_Util.cpp b/src/node_service/Node_Service_Util.cpp deleted file mode 100644 index dd4934c..0000000 --- a/src/node_service/Node_Service_Util.cpp +++ /dev/null @@ -1,350 +0,0 @@ -#include "Node_Service.h" -fs::path path_from_utf8_string(std::string_view value) { - return path_from_utf8(value); -} -std::string path_to_utf8_string(const fs::path& path) { - return path_to_utf8(path); -} -std::string now_string() { - const auto china_now = std::chrono::system_clock::now() + std::chrono::hours(8); - const auto value = std::chrono::system_clock::to_time_t(china_now); - std::tm china{}; -#ifdef _WIN32 - gmtime_s(&china, &value); -#else - gmtime_r(&value, &china); -#endif - return std::format("{:04}-{:02}-{:02} {:02}:{:02}:{:02} 中国时间", china.tm_year + 1900, china.tm_mon + 1, china.tm_mday, china.tm_hour, china.tm_min, china.tm_sec); -} -int64_t unix_seconds() { - return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); -} -std::string trim(std::string value) { - const auto first = value.find_first_not_of(" \t\r\n"); - if (first == std::string::npos) { - return {}; - } - const auto last = value.find_last_not_of(" \t\r\n"); - return value.substr(first, last - first + 1); -} -std::string read_env(std::string_view key) { -#ifdef _WIN32 - const std::string name(key); - const DWORD required = GetEnvironmentVariableA(name.c_str(), nullptr, 0); - if (required == 0) { - return {}; - } - std::string value(required, '\0'); - const DWORD written = GetEnvironmentVariableA(name.c_str(), value.data(), required); - if (written == 0) { - return {}; - } - value.resize(written); - return trim(native_to_utf8(value)); -#else - const auto* value = std::getenv(std::string(key).c_str()); - return value ? trim(value) : std::string{}; -#endif -} -std::string url_encode(std::string_view value) { - static constexpr char hex[] = "0123456789ABCDEF"; - std::string out; - out.reserve(value.size() * 3); - for (const unsigned char ch : value) { - const bool safe = (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '-' || ch == '_' || ch == '.' || ch == '~'; - if (safe) { - out.push_back(static_cast(ch)); - } - else { - out.push_back('%'); - out.push_back(hex[ch >> 4]); - out.push_back(hex[ch & 0x0F]); - } - } - return out; -} -std::string make_safe_file_name(std::string_view value) { - std::string out; - out.reserve(value.size()); - for (const unsigned char ch : value) { - if (ch < 32 || ch == '"' || ch == '*' || ch == '/' || ch == ':' || ch == '<' || ch == '>' || ch == '?' || ch == '\\' || ch == '|') { - out.push_back('_'); - } - else { - out.push_back(static_cast(ch)); - } - } - while (!out.empty() && (out.back() == ' ' || out.back() == '.')) { - out.pop_back(); - } - if (out.empty()) { - return "unnamed"; - } - return out; -} -std::vector split_parts(std::string_view value, char delimiter) { - std::vector result; - size_t start = 0; - while (start <= value.size()) { - const auto pos = value.find(delimiter, start); - if (pos == std::string_view::npos) { - result.push_back(value.substr(start)); - break; - } - result.push_back(value.substr(start, pos - start)); - start = pos + 1; - } - return result; -} -bool starts_with(std::string_view value, std::string_view prefix) { - return value.size() >= prefix.size() && value.substr(0, prefix.size()) == prefix; -} -bool ends_with(std::string_view value, std::string_view suffix) { - return value.size() >= suffix.size() && value.substr(value.size() - suffix.size()) == suffix; -} -int parse_int_prefix(std::string_view value, size_t& offset) { - int result = 0; - bool found = false; - while (offset < value.size() && std::isdigit(static_cast(value[offset]))) { - found = true; - result = result * 10 + value[offset] - '0'; - ++offset; - } - return found ? result : -1; -} -std::string to_string(std::string_view value) { - return std::string(value.data(), value.size()); -} -std::optional> parse_multiplier_from_text(std::string_view text) { - const auto pos = text.find("倍"); - if (pos == std::string_view::npos) { - return std::nullopt; - } - size_t begin = pos; - while (begin > 0) { - const auto ch = static_cast(text[begin - 1]); - if (!std::isdigit(ch) && ch != '.') { - break; - } - --begin; - } - if (begin == pos) { - return std::nullopt; - } - auto value_text = to_string(text.substr(begin, pos - begin)); - try { - return std::pair{std::stod(value_text), value_text + "倍"}; - } - catch (...) { - return std::nullopt; - } -} -std::string compact_country_from_name(std::string_view name) { - static constexpr std::array countries{ - "日本", "新加坡", "美国", "香港", "台湾", "韩国", "英国", "德国", "法国", "加拿大", "澳大利亚", "印度", "泰国", "越南" - }; - for (const auto country : countries) { - if (name.find(country) != std::string_view::npos) { - return to_string(country); - } - } - return {}; -} -void parse_node_metadata(Node_State& node) { - node.exit_ipv4.clear(); - node.exit_ipv6.clear(); - node.ai_score = -1; - node.risk = -1; - node.risk_level.clear(); - node.multiplier = -1.0; - node.multiplier_text.clear(); - node.shared_users.clear(); - node.ip_type.clear(); - node.native_type.clear(); - node.country = compact_country_from_name(node.name); - if (const auto multiplier = parse_multiplier_from_text(node.name)) { - node.multiplier = multiplier->first; - node.multiplier_text = multiplier->second; - } - for (const auto part : split_parts(node.name, '_')) { - if (starts_with(part, "ipv4")) { - node.exit_ipv4 = to_string(part.substr(4)); - continue; - } - if (starts_with(part, "ipv6")) { - const auto value = part.substr(4); - if (value != "无") { - node.exit_ipv6 = to_string(value); - } - continue; - } - if (starts_with(part, "AI") && ends_with(part, "星")) { - size_t offset = 2; - node.ai_score = parse_int_prefix(part, offset); - continue; - } - if (starts_with(part, "风控")) { - size_t offset = 6; - node.risk = parse_int_prefix(part, offset); - node.risk_level = to_string(part.substr(offset)); - continue; - } - if (starts_with(part, "共享")) { - node.shared_users = to_string(part.substr(6)); - continue; - } - if (part.find("原生") != std::string_view::npos || part.find("广播") != std::string_view::npos) { - node.native_type = to_string(part); - continue; - } - if (part.find("IDC") != std::string_view::npos || part.find("机房") != std::string_view::npos || part.find("家庭宽带") != std::string_view::npos || part.find("住宅") != std::string_view::npos) { - node.ip_type = to_string(part); - continue; - } - } -} -bool is_label_metadata_part(std::string_view part) { - return starts_with(part, "ipv4") || starts_with(part, "ipv6") || starts_with(part, "AI") && ends_with(part, "星") || starts_with(part, "风控") || starts_with(part, "共享") || part.find("原生") != std::string_view::npos || part.find("广播") != std::string_view::npos || part.find("IDC") != std::string_view::npos || part.find("机房") != std::string_view::npos || part.find("家庭宽带") != std::string_view::npos || part.find("住宅") != std::string_view::npos; -} -std::string original_proxy_name_from_label(std::string_view name) { - const auto parts = split_parts(name, '_'); - for (size_t i = 0; i < parts.size(); ++i) { - if (!is_label_metadata_part(parts[i])) { - continue; - } - std::string result; - for (size_t j = 0; j < i; ++j) { - if (j != 0) { - result.push_back('_'); - } - result += to_string(parts[j]); - } - return result.empty() ? to_string(name) : result; - } - return to_string(name); -} -std::optional yaml_string_value(const YAML::Node& node, const char* key) { - if (!node || !node.IsMap()) { - return std::nullopt; - } - const auto value = node[key]; - if (!value || !value.IsScalar()) { - return std::nullopt; - } - const auto text = trim(value.as()); - return text.empty() ? std::nullopt : std::optional{text}; -} -void load_node_ip_cache(Node_State& node) { - const fs::path root = path_from_utf8_string(get_executable_dir()); - const auto cache_dir = (root / "../Node_Exit_Ip_Cache").lexically_normal(); - const auto original_name = original_proxy_name_from_label(node.name); - const std::array candidates{original_name, node.name}; - for (const auto& name : candidates) { - const auto path = cache_dir / path_from_utf8_string(make_safe_file_name(name)) / "public_ip.yaml"; - if (!fs::is_regular_file(path)) { - continue; - } - try { - std::ifstream stream(path, std::ios::binary); - const auto data = YAML::Load(stream); - auto ipv4 = yaml_string_value(data, "public_ipv4"); - if (!ipv4) { - ipv4 = yaml_string_value(data, "public_ip"); - } - const auto ipv6 = yaml_string_value(data, "public_ipv6"); - if (ipv4) { - node.exit_ipv4 = *ipv4; - } - if (ipv6) { - node.exit_ipv6 = *ipv6; - } - return; - } - catch (...) { - return; - } - } -} -void enrich_node_metadata(Node_State& node) { - parse_node_metadata(node); - load_node_ip_cache(node); -} -void write_text_atomic(const fs::path& path, std::string_view content) { - fs::create_directories(path.parent_path()); - auto temp = path; - temp += L".tmp"; - { - std::ofstream stream(temp, std::ios::binary | std::ios::trunc); - if (!stream) { - throw std::runtime_error(std::format("无法写入文件:{}", path_to_utf8_string(temp))); - } - stream.write(content.data(), static_cast(content.size())); - } - std::error_code ec; - fs::rename(temp, path, ec); - if (ec) { - fs::remove(path, ec); - ec.clear(); - fs::rename(temp, path, ec); - if (ec) { - fs::remove(temp); - throw std::runtime_error(std::format("无法替换文件:{},{}", path_to_utf8_string(path), error_text_to_utf8(ec.message()))); - } - } -} -json read_json_file(const fs::path& path) { - std::ifstream stream(path, std::ios::binary); - if (!stream) { - return json::object(); - } - try { - return json::parse(stream, nullptr, true, true); - } - catch (...) { - return json::object(); - } -} -void save_json_file(const fs::path& path, const json& value) { - write_text_atomic(path, value.dump(2)); -} -std::string read_binary_text(const fs::path& path) { - std::ifstream stream(path, std::ios::binary); - if (!stream) { - return {}; - } - return std::string((std::istreambuf_iterator(stream)), std::istreambuf_iterator()); -} -std::string mime_type(const fs::path& path) { - const auto ext = path_to_utf8_string(path.extension()); - if (ext == ".html") { - return "text/html; charset=utf-8"; - } - if (ext == ".css") { - return "text/css; charset=utf-8"; - } - if (ext == ".js") { - return "application/javascript; charset=utf-8"; - } - if (ext == ".json") { - return "application/json; charset=utf-8"; - } - if (ext == ".svg") { - return "image/svg+xml"; - } - return "application/octet-stream"; -} -void set_file_response(const fs::path& file, httplib::Response& res) { - const auto content = read_binary_text(file); - if (content.empty() && !fs::is_regular_file(file)) { - res.status = 404; - res.set_content("not found", "text/plain; charset=utf-8"); - return; - } - res.set_header("Cache-Control", "no-store"); - res.set_content(content, mime_type(file)); -} -void service_log(std::string_view message) { - static std::mutex log_mutex; - std::lock_guard lock(log_mutex); - std::cout << std::format("[{}] {}\n", now_string(), message) << std::flush; -} diff --git a/src/node_service/Node_Service_Web.cpp b/src/node_service/Node_Service_Web.cpp deleted file mode 100644 index 741ae30..0000000 --- a/src/node_service/Node_Service_Web.cpp +++ /dev/null @@ -1,387 +0,0 @@ -#include "Node_Service.h" -json state_snapshot(Service_State& service) { - std::lock_guard lock(service.mutex); - json result; - result["started_time"] = service.started_time; - result["updated_time"] = now_string(); - result["controller_origin"] = service.config.controller_origin; - result["config_path"] = path_to_utf8_string(service.config.config_path); - result["state_dir"] = path_to_utf8_string(service.config.state_dir); - result["web_dir"] = path_to_utf8_string(service.config.web_dir); - result["service_config_path"] = path_to_utf8_string(service.config.service_config_path); - result["http_request_log_enabled"] = service.config.http_request_log_enabled; - result["check_interval_seconds"] = service.config.check_interval_seconds; - result["delay_refresh_interval_seconds"] = service.config.check_interval_seconds; - result["status_text"] = service.status_text; - result["refresh_done_nodes"] = service.refresh_done_nodes.load(); - result["refresh_total_nodes"] = service.refresh_total_nodes; - result["refresh_request_count"] = service.refresh_request_count; - result["last_refresh_request_time"] = service.last_refresh_request_time; - result["last_refresh_begin_time"] = service.last_refresh_begin_time; - result["last_refresh_end_time"] = service.last_refresh_end_time; - result["checking"] = service.checking.load(); - result["force_refresh"] = service.force_refresh.load(); - result["targets"] = json::array(); - for (const auto& target : service.config.targets) { - result["targets"].push_back(target_to_json(target)); - } - result["round"] = service.round; - result["enum_options"] = enum_options_to_json(); - result["dynamic_filter_options"] = dynamic_filter_options_to_json(service); - result["auto_switch"] = auto_switch_config_to_json(service.config.auto_switch); - result["switch_strategy_definitions"] = switch_strategy_definitions_to_json(); - result["table_protocol"] = Table_Protocol_Registry::instance().protocol(service); - result["table_rows"] = Table_Protocol_Registry::instance().rows(service); - result["groups"] = json::array(); - for (const auto& name : service.group_order) { - const auto item = service.groups.find(name); - if (item != service.groups.end()) { - result["groups"].push_back(group_to_json(item->second)); - } - } - result["nodes"] = json::array(); - for (const auto& name : service.node_order) { - const auto item = service.nodes.find(name); - if (item != service.nodes.end()) { - result["nodes"].push_back(node_to_json(item->second)); - } - } - return result; -} -std::string html_page() { - return R"HTML( - - - - -节点稳定性监控 - - - -
节点稳定性监控
loading
-
- - -)HTML"; -} -void start_web_server(Service_State& service, asio::io_context& io, httplib::Server& server) { - server.Get("/", [&service](const httplib::Request&, httplib::Response& res) { - const auto index = service.config.web_dir / "index.html"; - if (fs::is_regular_file(index)) { - set_file_response(index, res); - return; - } - res.set_content(html_page(), "text/html; charset=utf-8"); - }); - server.Get(R"(/assets/(.+))", [&service](const httplib::Request& req, httplib::Response& res) { - const auto name = req.matches[1].str(); - if (name.find("..") != std::string::npos || name.find('/') != std::string::npos || name.find('\\') != std::string::npos) { - res.status = 400; - res.set_content("bad asset path", "text/plain; charset=utf-8"); - return; - } - set_file_response(service.config.web_dir / "assets" / name, res); - }); - server.Get("/api/state", [&service](const httplib::Request&, httplib::Response& res) { - res.set_content(state_snapshot(service).dump(), "application/json; charset=utf-8"); - }); - server.Get("/api/config", [&service](const httplib::Request&, httplib::Response& res) { - std::lock_guard lock(service.mutex); - res.set_content(runtime_config_to_json(service.config).dump(), "application/json; charset=utf-8"); - }); - server.Get("/api/strategies", [](const httplib::Request&, httplib::Response& res) { - res.set_content(json{{"strategies", switch_strategy_definitions_to_json()}, {"options", switch_strategy_options_to_json()}}.dump(), "application/json; charset=utf-8"); - }); - server.Get("/api/table-protocol", [&service](const httplib::Request&, httplib::Response& res) { - res.set_content(table_protocol_snapshot(service).dump(), "application/json; charset=utf-8"); - }); - server.Get("/api/table-rows", [&service](const httplib::Request&, httplib::Response& res) { - res.set_content(table_rows_snapshot(service).dump(), "application/json; charset=utf-8"); - }); - - server.Post("/api/table/order", [&service](const httplib::Request& req, httplib::Response& res) { - try { - const auto body = json::parse(req.body, nullptr, true, true); - const auto table_id = body.at("table").get(); - std::vector order; - for (const auto& item : body.at("column_order")) { - if (item.is_string()) { - auto id = trim(item.get()); - if (!id.empty()) { - order.push_back(std::move(id)); - } - } - } - update_table_column_order(service, table_id, order); - service_log(std::format("表格列顺序已保存:{},{} 项", table_id, order.size())); - res.set_content(json{{"success", true}, {"message", "表格列顺序已保存"}, {"state", state_snapshot(service)}}.dump(), "application/json; charset=utf-8"); - } - catch (const std::exception& error) { - res.status = 400; - res.set_content(json{{"success", false}, {"error", error_text_to_utf8(error.what())}}.dump(), "application/json; charset=utf-8"); - } - }); - server.Post("/api/table/sticky", [&service](const httplib::Request& req, httplib::Response& res) { - try { - const auto body = json::parse(req.body, nullptr, true, true); - const auto table_id = body.at("table").get(); - std::vector columns; - for (const auto& item : body.at("sticky_columns")) { - if (item.is_string()) { - auto id = trim(item.get()); - if (!id.empty()) { - columns.push_back(std::move(id)); - } - } - } - update_table_sticky_columns(service, table_id, columns); - service_log(std::format("表格固定列已保存:{},{} 项", table_id, columns.size())); - res.set_content(json{{"success", true}, {"message", "表格固定列已保存"}, {"state", state_snapshot(service)}}.dump(), "application/json; charset=utf-8"); - } - catch (const std::exception& error) { - res.status = 400; - res.set_content(json{{"success", false}, {"error", error_text_to_utf8(error.what())}}.dump(), "application/json; charset=utf-8"); - } - }); - server.Post("/api/table/cell", [&service](const httplib::Request& req, httplib::Response& res) { - try { - const auto body = json::parse(req.body, nullptr, true, true); - const auto table_id = body.at("table").get(); - const auto row_id = body.at("row").get(); - const auto column_id = body.at("column").get(); - update_table_cell(service, table_id, row_id, column_id, body.at("value")); - service_log(std::format("表格单元已保存:{}.{}.{}", table_id, row_id, column_id)); - res.set_content(json{{"success", true}, {"message", "表格单元已保存"}, {"state", state_snapshot(service)}}.dump(), "application/json; charset=utf-8"); - } - catch (const std::exception& error) { - res.status = 400; - res.set_content(json{{"success", false}, {"error", error_text_to_utf8(error.what())}}.dump(), "application/json; charset=utf-8"); - } - }); - server.Post("/api/config", [&service](const httplib::Request& req, httplib::Response& res) { - try { - const auto body = json::parse(req.body, nullptr, true, true); - std::lock_guard lock(service.mutex); - if (body.contains("http_request_log_enabled") && body["http_request_log_enabled"].is_boolean()) { - service.config.http_request_log_enabled = body["http_request_log_enabled"].get(); - } - if (body.contains("check_interval_seconds") && body["check_interval_seconds"].is_number_integer()) { - service.config.check_interval_seconds = normalize_check_interval(body["check_interval_seconds"].get()); - } - else if (body.contains("delay_refresh_interval_seconds") && body["delay_refresh_interval_seconds"].is_number_integer()) { - service.config.check_interval_seconds = normalize_check_interval(body["delay_refresh_interval_seconds"].get()); - } - if (body.contains("auto_switch")) { - load_auto_switch_config(service.config.auto_switch, body["auto_switch"]); - reload_switch_groups_locked(service); - } - if (body.contains("table_layouts")) { - load_table_layout_config(service.config.table_layout, body["table_layouts"]); - } - service.status_text = "服务配置已保存"; - save_runtime_config(service.config); - save_service_index(service); - service_log(std::format("服务配置已保存:全量 delay 间隔 {} 秒,自动切换 {},后台 HTTP 日志 {}", service.config.check_interval_seconds, service.config.auto_switch.enabled ? "启用" : "关闭", service.config.http_request_log_enabled ? "启用" : "关闭")); - res.set_content(json{{"success", true}, {"message", "配置已保存"}, {"config", runtime_config_to_json(service.config)}}.dump(), "application/json; charset=utf-8"); - } - catch (const std::exception& error) { - res.status = 400; - res.set_content(json{{"success", false}, {"error", error_text_to_utf8(error.what())}}.dump(), "application/json; charset=utf-8"); - } - }); - server.Post("/api/refresh", [&service](const httplib::Request&, httplib::Response& res) { - std::string message; - { - std::lock_guard lock(service.mutex); - service.force_refresh.store(true); - ++service.refresh_request_count; - service.last_refresh_request_time = now_string(); - message = service.checking.load() ? "全量 delay 正在执行,本次请求已记录" : "已请求立即全量 delay"; - service.status_text = message; - save_service_index(service); - } - service_log(message); - res.set_content(json{{"success", true}, {"message", message}, {"checking", service.checking.load()}, {"state", state_snapshot(service)}}.dump(), "application/json; charset=utf-8"); - }); - server.Post("/api/node/refresh", [&service](const httplib::Request& req, httplib::Response& res) { - try { - const auto body = json::parse(req.body, nullptr, true, true); - const auto name = body.at("name").get(); - Service_Config config; - { - std::lock_guard lock(service.mutex); - config = service.config; - service.status_text = std::format("单节点 delay 开始:{}", name); - save_service_index(service); - } - service_log(std::format("收到单节点 delay 请求:{}", name)); - Controller_Proxy_Info info; - try { - info = request_proxy_info(config, name); - } - catch (const std::exception& error) { - info.found = false; - info.alive = false; - service_log(std::format("单节点信息请求失败:{},{}", name, error_text_to_utf8(error.what()))); - } - auto probes = request_node_probes(config, name); - update_node_state(service, name, info, std::move(probes)); - { - std::lock_guard lock(service.mutex); - service.status_text = std::format("单节点 delay 完成:{}", name); - save_service_index(service); - } - service_log(std::format("单节点 delay 完成:{}", name)); - res.set_content(json{{"success", true}, {"message", "节点刷新完成"}, {"name", name}, {"state", state_snapshot(service)}}.dump(), "application/json; charset=utf-8"); - } - catch (const std::exception& error) { - const auto text = error_text_to_utf8(error.what()); - service_log(std::format("单节点 delay 请求失败:{}", text)); - res.status = 400; - res.set_content(json{{"success", false}, {"error", text}}.dump(), "application/json; charset=utf-8"); - } - }); - server.Post("/api/group/switch", [&service](const httplib::Request& req, httplib::Response& res) { - try { - const auto body = json::parse(req.body, nullptr, true, true); - const auto group_name = trim(body.at("group").get()); - const auto node_name = trim(body.at("node").get()); - if (group_name.empty() || node_name.empty()) { - throw std::runtime_error("代理组名和节点名不能为空"); - } - Service_Config config; - { - std::lock_guard lock(service.mutex); - const auto group = service.groups.find(group_name); - if (group == service.groups.end()) { - throw std::runtime_error("代理组不存在:" + group_name); - } - if (!contains_name(group->second.candidates, node_name)) { - throw std::runtime_error("节点不在该代理组候选列表:" + node_name); - } - config = service.config; - } - service_log(std::format("手动切换代理组开始:{} -> {}", group_name, node_name)); - switch_group_to_node(config, group_name, node_name); - refresh_switch_group_status(service, std::optional{group_name}); - { - std::lock_guard lock(service.mutex); - auto& group = service.groups[group_name]; - group.current = node_name; - group.last_error.clear(); - group.last_decision = "手动切换到 " + node_name; - group.last_switch_unix = unix_seconds(); - group.last_switch_time = now_string(); - ++group.switch_count; - save_service_index(service); - } - service_log(std::format("手动切换代理组完成:{} -> {}", group_name, node_name)); - res.set_content(json{{"success", true}, {"message", "代理组已手动切换"}, {"state", state_snapshot(service)}}.dump(), "application/json; charset=utf-8"); - } - catch (const std::exception& error) { - res.status = 400; - res.set_content(json{{"success", false}, {"error", error_text_to_utf8(error.what())}}.dump(), "application/json; charset=utf-8"); - } - }); - - server.Post("/api/groups/refresh", [&service](const httplib::Request& req, httplib::Response& res) { - try { - std::optional name; - if (!req.body.empty()) { - const auto body = json::parse(req.body, nullptr, true, true); - if (body.contains("name") && body["name"].is_string()) { - auto text = trim(body["name"].get()); - if (!text.empty()) { - name = std::move(text); - } - } - } - refresh_switch_group_status(service, name); - res.set_content(json{{"success", true}, {"message", name ? "代理组刷新完成" : "全部代理组刷新完成"}, {"state", state_snapshot(service)}}.dump(), "application/json; charset=utf-8"); - } - catch (const std::exception& error) { - res.status = 400; - res.set_content(json{{"success", false}, {"error", error_text_to_utf8(error.what())}}.dump(), "application/json; charset=utf-8"); - } - }); - server.Post("/api/switch", [&service](const httplib::Request&, httplib::Response& res) { - try { - service_log("收到手动自动切换判定请求"); - perform_auto_switch(service); - service_log("手动自动切换判定完成"); - res.set_content(json{{"success", true}, {"message", "切换判定完成"}, {"state", state_snapshot(service)}}.dump(), "application/json; charset=utf-8"); - } - catch (const std::exception& error) { - res.status = 400; - res.set_content(json{{"success", false}, {"error", error_text_to_utf8(error.what())}}.dump(), "application/json; charset=utf-8"); - } - }); - server.Post("/api/report", [&service](const httplib::Request& req, httplib::Response& res) { - try { - const auto body = json::parse(req.body, nullptr, true, true); - const auto name = body.at("name").get(); - std::lock_guard lock(service.mutex); - if (service.nodes.find(name) == service.nodes.end()) { - service.node_order.push_back(name); - } - auto& node = service.nodes[name]; - node.name = name; - enrich_node_metadata(node); - node.updated_time = now_string(); - node.last_check_time = node.updated_time; - if (body.contains("type") && body["type"].is_string()) { - node.type = body["type"].get(); - } - if (body.contains("provider") && body["provider"].is_string()) { - node.provider = body["provider"].get(); - } - if (body.contains("alive")) { - node.alive = body["alive"].get(); - } - if (body.contains("probes") && body["probes"].is_array()) { - node.probes.clear(); - for (const auto& item : body["probes"]) { - Probe_Result probe; - probe.target = item.value("target", std::string{}); - probe.url = item.value("url", std::string{}); - probe.result.success = item.value("success", false); - probe.result.delay_ms = item.value("delay_ms", 0); - probe.result.http_status = item.value("http_status", 0); - probe.result.error = item.value("error", std::string{}); - node.probes.push_back(std::move(probe)); - } - } - if (body.contains("delay_ms") && body["delay_ms"].is_number_integer()) { - node.last_delay_ms = body["delay_ms"].get(); - update_node_statistics(node, node.last_delay_ms); - } - else if (body.contains("alive") && !node.alive) { - update_node_statistics(node, std::nullopt); - } - if (body.contains("error") && body["error"].is_string()) { - node.last_error = body["error"].get(); - } - update_score(node); - save_json_file(node_state_path(service, name), node_to_json(node)); - save_service_index(service); - service_log(std::format("收到节点上报:{} alive={} delay={}ms score={:.1f}", node.name, node.alive, node.last_delay_ms, node.stability_score)); - res.set_content(R"({"success":true})", "application/json; charset=utf-8"); - } - catch (const std::exception& error) { - res.status = 400; - res.set_content(json{{"success", false}, {"error", error_text_to_utf8(error.what())}}.dump(), "application/json; charset=utf-8"); - } - }); - server.Post("/api/stop", [&service, &server, &io](const httplib::Request&, httplib::Response& res) { - service.running.store(false); - service_log("收到停止服务请求"); - res.set_content(R"({"success":true})", "application/json; charset=utf-8"); - server.stop(); - asio::post(io, [&io] { - io.stop(); - }); - }); -} diff --git a/src/node_service/main.cpp b/src/node_service/main.cpp deleted file mode 100644 index ec75885..0000000 --- a/src/node_service/main.cpp +++ /dev/null @@ -1,46 +0,0 @@ -#include "Node_Service.h" -int main(int argc, char** argv) { -#ifdef _WIN32 - SetConsoleOutputCP(CP_UTF8); - SetConsoleCP(CP_UTF8); -#endif - try { - Service_State service; - service.config = make_config(argc, argv); - service.started_time = now_string(); - initialize_nodes(service); - asio::io_context io(1); - asio::thread_pool pool(service.config.worker_threads); - asio::post(pool, [&service] { - refresh_switch_group_status(service); - }); - httplib::Server server; - start_web_server(service, io, server); - std::thread web_thread([&service, &server] { - service_log(std::format("HTTP 服务监听:{}:{}", service.config.listen_host, service.config.listen_port)); - if (!server.listen(service.config.listen_host, service.config.listen_port)) { - service_log("HTTP 服务监听失败"); - } - }); - asio::co_spawn(io, monitor_refresh_loop(service, pool), asio::detached); - service_log(std::format("节点服务启动:http://{}:{}", service.config.listen_host, service.config.listen_port)); - service_log(std::format("配置文件:{}", path_to_utf8_string(service.config.config_path))); - service_log(std::format("状态目录:{}", path_to_utf8_string(service.config.state_dir))); - service_log(std::format("前端目录:{}", path_to_utf8_string(service.config.web_dir))); - service_log(std::format("服务配置:{}", path_to_utf8_string(service.config.service_config_path))); - io.run(); - service.running.store(false); - server.stop(); - if (web_thread.joinable()) { - web_thread.join(); - } - pool.stop(); - pool.join(); - service_log("节点服务退出"); - return 0; - } - catch (const std::exception& error) { - std::cerr << std::format("node_service 启动失败:{}\n", error_text_to_utf8(error.what())) << std::flush; - return 1; - } -} diff --git a/src/node_service/web/README.md b/src/node_service/web/README.md deleted file mode 100644 index 3b2825c..0000000 --- a/src/node_service/web/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# node_service Web - -这是 node_service 的 React + Vite + Material Design 前端。 - -```powershell -pwsh ./build_frontend.ps1 -``` - -构建产物在 `dist` 目录,node_service 会优先加载 `web/dist/index.html`。 diff --git a/src/node_service/web/build_frontend.ps1 b/src/node_service/web/build_frontend.ps1 deleted file mode 100644 index 56350f8..0000000 --- a/src/node_service/web/build_frontend.ps1 +++ /dev/null @@ -1,9 +0,0 @@ -$ErrorActionPreference = "Stop" -Push-Location $PSScriptRoot -try { - npm install - npm run build -} -finally { - Pop-Location -} diff --git a/src/node_service/web/index.html b/src/node_service/web/index.html deleted file mode 100644 index 9dd7b6f..0000000 --- a/src/node_service/web/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - 节点稳定性监控 - - -
- - - diff --git a/src/node_service/web/package-lock.json b/src/node_service/web/package-lock.json deleted file mode 100644 index 7f72ada..0000000 --- a/src/node_service/web/package-lock.json +++ /dev/null @@ -1,2329 +0,0 @@ -{ - "name": "tizi-node-service-web", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "tizi-node-service-web", - "version": "1.0.0", - "dependencies": { - "@emotion/react": "^11.13.5", - "@emotion/styled": "^11.13.5", - "@mui/material": "^6.1.8", - "@vitejs/plugin-react": "^4.3.3", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "vite": "^5.4.11" - }, - "devDependencies": {} - }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "license": "MIT" - }, - "node_modules/@babel/generator": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", - "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", - "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", - "license": "MIT", - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.7" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", - "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", - "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@emotion/babel-plugin": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", - "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/serialize": "^1.3.3", - "babel-plugin-macros": "^3.1.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^4.0.0", - "find-root": "^1.1.0", - "source-map": "^0.5.7", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/cache": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", - "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", - "license": "MIT", - "dependencies": { - "@emotion/memoize": "^0.9.0", - "@emotion/sheet": "^1.4.0", - "@emotion/utils": "^1.4.2", - "@emotion/weak-memoize": "^0.4.0", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/hash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", - "license": "MIT" - }, - "node_modules/@emotion/is-prop-valid": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", - "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", - "license": "MIT", - "dependencies": { - "@emotion/memoize": "^0.9.0" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", - "license": "MIT" - }, - "node_modules/@emotion/react": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", - "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.13.5", - "@emotion/cache": "^11.14.0", - "@emotion/serialize": "^1.3.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", - "@emotion/utils": "^1.4.2", - "@emotion/weak-memoize": "^0.4.0", - "hoist-non-react-statics": "^3.3.1" - }, - "peerDependencies": { - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/serialize": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", - "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", - "license": "MIT", - "dependencies": { - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/unitless": "^0.10.0", - "@emotion/utils": "^1.4.2", - "csstype": "^3.0.2" - } - }, - "node_modules/@emotion/sheet": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", - "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", - "license": "MIT" - }, - "node_modules/@emotion/styled": { - "version": "11.14.1", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", - "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.13.5", - "@emotion/is-prop-valid": "^1.3.0", - "@emotion/serialize": "^1.3.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", - "@emotion/utils": "^1.4.2" - }, - "peerDependencies": { - "@emotion/react": "^11.0.0-rc.0", - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/unitless": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", - "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", - "license": "MIT" - }, - "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", - "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@emotion/utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", - "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", - "license": "MIT" - }, - "node_modules/@emotion/weak-memoize": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", - "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", - "license": "MIT" - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mui/core-downloads-tracker": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.5.0.tgz", - "integrity": "sha512-LGb8t8i6M2ZtS3Drn3GbTI1DVhDY6FJ9crEey2lZ0aN2EMZo8IZBZj9wRf4vqbZHaWjsYgtbOnJw5V8UWbmK2Q==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - } - }, - "node_modules/@mui/material": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.5.0.tgz", - "integrity": "sha512-yjvtXoFcrPLGtgKRxFaH6OQPtcLPhkloC0BML6rBG5UeldR0nPULR/2E2BfXdo5JNV7j7lOzrrLX2Qf/iSidow==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/core-downloads-tracker": "^6.5.0", - "@mui/system": "^6.5.0", - "@mui/types": "~7.2.24", - "@mui/utils": "^6.4.9", - "@popperjs/core": "^2.11.8", - "@types/react-transition-group": "^4.4.12", - "clsx": "^2.1.1", - "csstype": "^3.1.3", - "prop-types": "^15.8.1", - "react-is": "^19.0.0", - "react-transition-group": "^4.4.5" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^6.5.0", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@mui/material-pigment-css": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/private-theming": { - "version": "6.4.9", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.9.tgz", - "integrity": "sha512-LktcVmI5X17/Q5SkwjCcdOLBzt1hXuc14jYa7NPShog0GBDCDvKtcnP0V7a2s6EiVRlv7BzbWEJzH6+l/zaCxw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/utils": "^6.4.9", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/styled-engine": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.5.0.tgz", - "integrity": "sha512-8woC2zAqF4qUDSPIBZ8v3sakj+WgweolpyM/FXf8jAx6FMls+IE4Y8VDZc+zS805J7PRz31vz73n2SovKGaYgw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@emotion/cache": "^11.13.5", - "@emotion/serialize": "^1.3.3", - "@emotion/sheet": "^1.4.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } - } - }, - "node_modules/@mui/system": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.5.0.tgz", - "integrity": "sha512-XcbBYxDS+h/lgsoGe78ExXFZXtuIlSBpn/KsZq8PtZcIkUNJInkuDqcLd2rVBQrDC1u+rvVovdaWPf2FHKJf3w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/private-theming": "^6.4.9", - "@mui/styled-engine": "^6.5.0", - "@mui/types": "~7.2.24", - "@mui/utils": "^6.4.9", - "clsx": "^2.1.1", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/types": { - "version": "7.2.24", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz", - "integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/utils": { - "version": "6.4.9", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.9.tgz", - "integrity": "sha512-Y12Q9hbK9g+ZY0T3Rxrx9m2m10gaphDuUMgWxyV5kNJevVxXYCLclYUCC9vXaIk1/NdNDTcW2Yfr2OGvNFNmHg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/types": "~7.2.24", - "@types/prop-types": "^15.7.14", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "react-is": "^19.0.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", - "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", - "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", - "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", - "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", - "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", - "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", - "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", - "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", - "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", - "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", - "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", - "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", - "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", - "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", - "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", - "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", - "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", - "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", - "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", - "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", - "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", - "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", - "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", - "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", - "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "license": "MIT" - }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "license": "MIT" - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", - "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "license": "MIT", - "peer": true, - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-transition-group": { - "version": "4.4.12", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", - "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.44", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.44.tgz", - "integrity": "sha512-T3ghW+sl/ZJ8w1v/yQx3qvJ9040DWoLBz8JT/CILbAKcFyG9b2MRe75v6W5uXjv6uH1lumK2Kv46y2zSkcej0Q==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/browserslist": { - "version": "4.28.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", - "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.42", - "caniuse-lite": "^1.0.30001803", - "electron-to-chromium": "^1.5.389", - "node-releases": "^2.0.51", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001806", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", - "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "license": "MIT" - }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.394", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.394.tgz", - "integrity": "sha512-Wmt2Gm0o8JWBuGgmc4XZ0u9s1RaCRqhxP47phplmfg04+qypTUurpeJGP45A7Fhv7jdrrVH44PLlR9qXo37cVQ==", - "license": "ISC" - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "license": "MIT" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, - "node_modules/is-core-module": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", - "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.51", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", - "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "license": "MIT" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/postcss": { - "version": "8.5.20", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz", - "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.16", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-is": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", - "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", - "license": "MIT" - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, - "node_modules/resolve": { - "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/rollup": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", - "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.9" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.62.2", - "@rollup/rollup-android-arm64": "4.62.2", - "@rollup/rollup-darwin-arm64": "4.62.2", - "@rollup/rollup-darwin-x64": "4.62.2", - "@rollup/rollup-freebsd-arm64": "4.62.2", - "@rollup/rollup-freebsd-x64": "4.62.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", - "@rollup/rollup-linux-arm-musleabihf": "4.62.2", - "@rollup/rollup-linux-arm64-gnu": "4.62.2", - "@rollup/rollup-linux-arm64-musl": "4.62.2", - "@rollup/rollup-linux-loong64-gnu": "4.62.2", - "@rollup/rollup-linux-loong64-musl": "4.62.2", - "@rollup/rollup-linux-ppc64-gnu": "4.62.2", - "@rollup/rollup-linux-ppc64-musl": "4.62.2", - "@rollup/rollup-linux-riscv64-gnu": "4.62.2", - "@rollup/rollup-linux-riscv64-musl": "4.62.2", - "@rollup/rollup-linux-s390x-gnu": "4.62.2", - "@rollup/rollup-linux-x64-gnu": "4.62.2", - "@rollup/rollup-linux-x64-musl": "4.62.2", - "@rollup/rollup-openbsd-x64": "4.62.2", - "@rollup/rollup-openharmony-arm64": "4.62.2", - "@rollup/rollup-win32-arm64-msvc": "4.62.2", - "@rollup/rollup-win32-ia32-msvc": "4.62.2", - "@rollup/rollup-win32-x64-gnu": "4.62.2", - "@rollup/rollup-win32-x64-msvc": "4.62.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", - "license": "MIT" - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "license": "ISC" - }, - "node_modules/yaml": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", - "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", - "license": "ISC", - "engines": { - "node": ">= 6" - } - } - } -} diff --git a/src/node_service/web/package.json b/src/node_service/web/package.json deleted file mode 100644 index 664a5c9..0000000 --- a/src/node_service/web/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "tizi-node-service-web", - "version": "1.0.0", - "private": true, - "type": "module", - "scripts": { - "dev": "vite --host 127.0.0.1 --port 5173", - "build": "vite build", - "preview": "vite preview --host 127.0.0.1 --port 4173" - }, - "dependencies": { - "@emotion/react": "^11.13.5", - "@emotion/styled": "^11.13.5", - "@mui/material": "^6.1.8", - "@vitejs/plugin-react": "^4.3.3", - "vite": "^5.4.11", - "react": "^18.3.1", - "react-dom": "^18.3.1" - }, - "devDependencies": { - "typescript": "^5.6.3", - "@types/react": "^18.3.12", - "@types/react-dom": "^18.3.1" - } -} \ No newline at end of file diff --git a/src/node_service/web/src/App.tsx b/src/node_service/web/src/App.tsx deleted file mode 100644 index ddb7551..0000000 --- a/src/node_service/web/src/App.tsx +++ /dev/null @@ -1,431 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { - Alert, - AppBar, - Box, - Button, - CircularProgress, - Container, - Snackbar, - Stack, - Tab, - Tabs, - Toolbar, - Typography, -} from "@mui/material"; -import type { - AutoSwitchConfig, - NodeState, - PageName, - ProxyGroup, - ServiceState, -} from "./types.tsx"; -import { - readState, - requestAutoSwitch, - requestFullDelay, - requestGroupRefresh, - requestGroupSwitch, - requestNodeRefresh, - saveConfig, - updateTableCell, - updateTableOrder, - updateTableSticky, -} from "./api.tsx"; -import { ConfigPage } from "./pages/ConfigPage.tsx"; -import { GroupsPage } from "./pages/GroupsPage.tsx"; -import { NodesPage } from "./pages/NodesPage.tsx"; -function TopBar({ - page, - setPage, - nodeDetailCollapsed, - setNodeDetailCollapsed, - groupListCollapsed, - setGroupListCollapsed, -}: { - page: PageName; - setPage: (page: PageName) => void; - nodeDetailCollapsed: boolean; - setNodeDetailCollapsed: (value: boolean) => void; - groupListCollapsed: boolean; - setGroupListCollapsed: (value: boolean) => void; -}) { - return ( - - - setPage(value)} - variant="scrollable" - scrollButtons="auto" - sx={{ flex: 1 }} - > - - - - - - {page === "nodes" ? ( - - ) : null} - {page === "groups" ? ( - - ) : null} - - - - ); -} -export default function App() { - const [state, setState] = useState(null); - const [selected, setSelected] = useState(null); - const [selectedGroup, setSelectedGroup] = useState(null); - const [page, setPage] = useState("nodes"); - const [nodeDetailCollapsed, setNodeDetailCollapsed] = useState(false); - const [groupListCollapsed, setGroupListCollapsed] = useState(false); - const [intervalValue, setIntervalValue] = useState("60"); - const [httpLog, setHttpLog] = useState(false); - const [autoRefresh, setAutoRefresh] = useState(true); - const [busy, setBusy] = useState(false); - const [fullRefreshRequesting, setFullRefreshRequesting] = useState(false); - const [refreshingNodes, setRefreshingNodes] = useState>( - new Set(), - ); - const [refreshingGroups, setRefreshingGroups] = useState>( - new Set(), - ); - const [switchingNodes, setSwitchingNodes] = useState>( - new Set(), - ); - const [notice, setNotice] = useState("等待操作"); - const [error, setError] = useState(""); - const reload = async () => { - const data = await readState(); - setState(data); - setIntervalValue( - String( - data.delay_refresh_interval_seconds || - data.check_interval_seconds || - 60, - ), - ); - setHttpLog(!!data.http_request_log_enabled); - setSelected((prev) => - prev - ? (data.nodes || []).find((node) => node.name === prev.name) || null - : prev, - ); - setSelectedGroup((prev) => - prev - ? (data.groups || []).find((group) => group.name === prev.name) || null - : (data.groups || [])[0] || null, - ); - }; - useEffect(() => { - reload().catch((err) => setError(err.message)); - }, []); - useEffect(() => { - const timer = setInterval(() => { - if (autoRefresh) reload().catch((err) => setError(err.message)); - }, 5000); - return () => clearInterval(timer); - }, [autoRefresh]); - const saveInterval = async () => { - setBusy(true); - try { - const seconds = Number(intervalValue); - const result = await saveConfig({ - check_interval_seconds: seconds, - delay_refresh_interval_seconds: seconds, - http_request_log_enabled: httpLog, - }); - setNotice(result.message || "间隔已保存"); - await reload(); - } finally { - setBusy(false); - } - }; - const saveHttpLog = async (value: boolean) => { - setBusy(true); - try { - const result = await saveConfig({ - check_interval_seconds: Number(intervalValue), - delay_refresh_interval_seconds: Number(intervalValue), - http_request_log_enabled: value, - }); - setNotice(value ? "后台 HTTP 日志已开启" : "后台 HTTP 日志已关闭"); - if (result.config) - setHttpLog(!!(result.config as any).http_request_log_enabled); - await reload(); - } finally { - setBusy(false); - } - }; - const runRefresh = async () => { - setFullRefreshRequesting(true); - try { - setNotice("正在请求刷新全部节点"); - const result = await requestFullDelay(); - setNotice(result.message || "已请求刷新全部节点"); - if (result.state) setState(result.state); - else await reload(); - } finally { - setFullRefreshRequesting(false); - } - }; - const refreshNode = async (name: string) => { - setRefreshingNodes((prev) => new Set([...prev, name])); - setNotice(`正在刷新节点:${name}`); - try { - const result = await requestNodeRefresh(name); - setNotice(`${result.message || "节点刷新完成"}:${name}`); - if (result.state) { - setState(result.state); - setSelected((prev) => - prev - ? (result.state?.nodes || []).find( - (node) => node.name === prev.name, - ) || null - : prev, - ); - setSelectedGroup((prev) => - prev - ? (result.state?.groups || []).find( - (group) => group.name === prev.name, - ) || null - : prev, - ); - } else await reload(); - } finally { - setRefreshingNodes((prev) => { - const next = new Set(prev); - next.delete(name); - return next; - }); - } - }; - const refreshGroup = async (name?: string) => { - const key = name || "__all__"; - setRefreshingGroups((prev) => new Set([...prev, key])); - setNotice(name ? `正在刷新代理组:${name}` : "正在刷新全部代理组"); - try { - const result = await requestGroupRefresh(name); - setNotice(name ? `代理组刷新完成:${name}` : "全部代理组刷新完成"); - if (result.state) { - setState(result.state); - setSelectedGroup((prev) => - prev - ? (result.state?.groups || []).find( - (group) => group.name === prev.name, - ) || null - : prev, - ); - } else await reload(); - } finally { - setRefreshingGroups((prev) => { - const next = new Set(prev); - next.delete(key); - return next; - }); - } - }; - - const switchGroupNode = async (group: string, node: string) => { - const key = `${group}\n${node}`; - setSwitchingNodes((prev) => new Set([...prev, node])); - setNotice(`正在切换代理组:${group} -> ${node}`); - try { - const result = await requestGroupSwitch(group, node); - setNotice(result.message || `代理组已切换:${group}`); - if (result.state) { - setState(result.state); - setSelected((result.state.nodes || []).find((item) => item.name === node) || null); - setSelectedGroup((result.state.groups || []).find((item) => item.name === group) || null); - } else await reload(); - } finally { - setSwitchingNodes((prev) => { - const next = new Set(prev); - next.delete(node); - return next; - }); - void key; - } - }; - - const saveSwitch = async (autoSwitch: AutoSwitchConfig) => { - setBusy(true); - try { - const result = await saveConfig({ - check_interval_seconds: Number(intervalValue), - delay_refresh_interval_seconds: Number(intervalValue), - http_request_log_enabled: httpLog, - auto_switch: autoSwitch, - }); - setNotice(result.message || "切换策略已保存"); - await reload(); - } finally { - setBusy(false); - } - }; - - const editTableCell = async (table: string, row: string, column: string, value: unknown) => { - setBusy(true); - try { - const result = await updateTableCell(table, row, column, value); - setNotice(result.message || "表格单元已保存"); - if (result.state) setState(result.state); - else await reload(); - } finally { - setBusy(false); - } - }; - const editTableOrder = async (table: string, columnOrder: string[]) => { - setBusy(true); - try { - const result = await updateTableOrder(table, columnOrder); - setNotice(result.message || "表格列顺序已保存"); - if (result.state) setState(result.state); - else await reload(); - } finally { - setBusy(false); - } - }; - const editTableSticky = async (table: string, stickyColumns: string[]) => { - setBusy(true); - try { - const result = await updateTableSticky(table, stickyColumns); - setNotice(result.message || "表格固定列已保存"); - if (result.state) setState(result.state); - else await reload(); - } finally { - setBusy(false); - } - }; - const runSwitch = async () => { - setBusy(true); - try { - const result = await requestAutoSwitch(); - setNotice(result.message || "切换判定完成"); - if (result.state) setState(result.state); - } finally { - setBusy(false); - } - }; - const safeRun = (fn: () => Promise) => - fn().catch((err) => setError(err.message)); - return ( - - - - {state ? ( - <> - {page === "nodes" ? ( - safeRun(() => refreshNode(name))} - refreshingNodes={refreshingNodes} - onRefreshAll={() => safeRun(runRefresh)} - fullRefreshRequesting={fullRefreshRequesting} - detailCollapsed={nodeDetailCollapsed} - onEditTableCell={(table,row,column,value) => safeRun(() => editTableCell(table,row,column,value))} - onEditTableOrder={(table,columnOrder) => safeRun(() => editTableOrder(table,columnOrder))} - onEditTableSticky={(table,stickyColumns) => safeRun(() => editTableSticky(table,stickyColumns))} - /> - ) : null} - {page === "groups" ? ( - safeRun(() => refreshGroup(name))} - refreshingGroups={refreshingGroups} - onRefreshNode={(name) => safeRun(() => refreshNode(name))} - refreshingNodes={refreshingNodes} - groupListCollapsed={groupListCollapsed} - onEditTableCell={(table,row,column,value) => safeRun(() => editTableCell(table,row,column,value))} - onEditTableOrder={(table,columnOrder) => safeRun(() => editTableOrder(table,columnOrder))} - onEditTableSticky={(table,stickyColumns) => safeRun(() => editTableSticky(table,stickyColumns))} - onSwitchGroupNode={(group,node) => safeRun(() => switchGroupNode(group,node))} - switchingNodes={switchingNodes} - /> - ) : null} - {page === "config" ? ( - safeRun(saveInterval)} - onSaveHttpLog={(value) => safeRun(() => saveHttpLog(value))} - busy={busy} - onSaveSwitch={(autoSwitch) => - safeRun(() => saveSwitch(autoSwitch)) - } - onRunSwitch={() => safeRun(runSwitch)} - onRefresh={() => safeRun(runRefresh)} - fullRefreshRequesting={fullRefreshRequesting} - onReload={() => safeRun(reload)} - autoRefresh={autoRefresh} - setAutoRefresh={setAutoRefresh} - onEditTableCell={(table,row,column,value) => safeRun(() => editTableCell(table,row,column,value))} - onEditTableOrder={(table,columnOrder) => safeRun(() => editTableOrder(table,columnOrder))} - onEditTableSticky={(table,stickyColumns) => safeRun(() => editTableSticky(table,stickyColumns))} - /> - ) : null} - - ) : ( - - - 正在读取服务状态 - - )} - - setNotice("等待操作")} - > - setNotice("等待操作")}> - {notice} - - - setError("")} - > - setError("")}> - {error} - - - - ); -} diff --git a/src/node_service/web/src/api.tsx b/src/node_service/web/src/api.tsx deleted file mode 100644 index 05041be..0000000 --- a/src/node_service/web/src/api.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import type {ApiResult,ServiceState} from './types.tsx'; -async function readJsonResponse(response:Response):Promise { - const data=await response.json(); - if (!response.ok || data.success === false) { - throw new Error(data.error || data.message || `HTTP ${response.status}`); - } - return data; -} -export async function readState():Promise { - const response=await fetch('/api/state',{cache:'no-store'}); - return await readJsonResponse(response); -} -export async function saveConfig(config:unknown):Promise { - const response=await fetch('/api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(config)}); - return await readJsonResponse(response); -} -export async function requestFullDelay():Promise { - const response=await fetch('/api/refresh',{method:'POST'}); - return await readJsonResponse(response); -} -export async function requestNodeRefresh(name:string):Promise { - const response=await fetch('/api/node/refresh',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name})}); - return await readJsonResponse(response); -} -export async function requestGroupRefresh(name?:string):Promise { - const body=name ? JSON.stringify({name}) : JSON.stringify({}); - const response=await fetch('/api/groups/refresh',{method:'POST',headers:{'Content-Type':'application/json'},body}); - return await readJsonResponse(response); -} - -export async function requestGroupSwitch(group:string,node:string):Promise { - const response=await fetch('/api/group/switch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({group,node})}); - return await readJsonResponse(response); -} - -export async function requestAutoSwitch():Promise { - const response=await fetch('/api/switch',{method:'POST'}); - return await readJsonResponse(response); -} - -export async function updateTableOrder(table:string,columnOrder:string[]):Promise { - const response=await fetch('/api/table/order',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({table,column_order:columnOrder})}); - return await readJsonResponse(response); -} -export async function updateTableSticky(table:string,stickyColumns:string[]):Promise { - const response=await fetch('/api/table/sticky',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({table,sticky_columns:stickyColumns})}); - return await readJsonResponse(response); -} -export async function updateTableCell(table:string,row:string,column:string,value:unknown):Promise { - const response=await fetch('/api/table/cell',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({table,row,column,value})}); - return await readJsonResponse(response); -} diff --git a/src/node_service/web/src/components/Display.tsx b/src/node_service/web/src/components/Display.tsx deleted file mode 100644 index 124e867..0000000 --- a/src/node_service/web/src/components/Display.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import {Box,Button,Card,CardContent,Chip,Stack,Typography} from '@mui/material'; -import type {NodeState,Probe} from '../types.tsx'; -import {nodeProbeResult,scoreLevel,scoreText} from '../utils.tsx'; -export function CategoryChip({label,category}:{label?:string;category?:string}) { - const color=(['excellent','high','usable','clean','native','residential','alive','dual','ipv4_only','ipv6_only'].includes(category || '')) ? 'success' : (['normal','good','other','broadcast'].includes(category || '')) ? 'warning' : (['dead','poor','weak','risk','high','non_native','none'].includes(category || '')) ? 'error' : 'default'; - return ; -} -export function ScoreCell({score}:{score?:number}) { - const value=Number(score || 0); - return {scoreText(value)}; -} -export function ProbeIcons({probes}:{probes?:Probe[]}) { - const list=probes || []; - if (!list.length) return <>-; - return {list.map((probe,index)=>{const result=nodeProbeResult(probe);return {result.success ? '✅' : '❌'};})}; -} -export function StatCard({label,value,hint,color}:{label:string;value:React.ReactNode;hint?:React.ReactNode;color?:string}) { - return {label}{value}{hint}; -} -export function NodeRefreshButton({node,onRefresh,refreshing}:{node:NodeState;onRefresh:(name:string)=>void;refreshing:boolean}) { - return ; -} diff --git a/src/node_service/web/src/components/GroupListPanel.tsx b/src/node_service/web/src/components/GroupListPanel.tsx deleted file mode 100644 index feaa491..0000000 --- a/src/node_service/web/src/components/GroupListPanel.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import {Button,Chip,MenuItem,Paper,Select,Stack,Typography} from '@mui/material'; -import type {ProxyGroup,ServiceState,TableColumnProtocol,TableRowObject} from '../types.tsx'; -import {ProtocolObjectTable} from './ProtocolObjectTable.tsx'; -import {switchStrategyLabel,tableProtocol,tableRows,timeText} from '../utils.tsx'; -function GroupRefreshButton({name,onRefresh,refreshing}:{name?:string;onRefresh:(name?:string)=>void;refreshing:boolean}) { - return ; -} -export function GroupListPanel({state,selectedGroup,setSelectedGroup,onRefreshGroup,refreshingGroups,onEditTableCell,onEditTableOrder,onEditTableSticky}:{state:ServiceState;selectedGroup?:ProxyGroup | null;setSelectedGroup:(group:ProxyGroup)=>void;onRefreshGroup:(name?:string)=>void;refreshingGroups:Set;onEditTableCell:(table:string,row:string,column:string,value:unknown)=>void;onEditTableOrder:(table:string,columnOrder:string[])=>void;onEditTableSticky:(table:string,stickyColumns:string[])=>void}) { - const protocol=tableProtocol(state,'proxy_groups'); - const rows=tableRows(state,'proxy_groups'); - if (!protocol) { - return 后端没有返回代理组表格协议。; - } - return state={state} protocol={protocol} rows={rows} selectedId={selectedGroup?.name} onSelectRow={(row)=>setSelectedGroup(row.raw)} actionRenderer={(row:TableRowObject)=>} storageKey="node_service_group_table_widths" title="代理组表格" description="代理组字段、过滤、排序和策略编辑同样由后端协议定义。" fillHeight extraToolbar={} onEditCell={(row:TableRowObject,column:TableColumnProtocol,value:unknown)=>onEditTableCell(protocol.id,row.id,column.id,value)} onColumnOrderChange={onEditTableOrder} onColumnStickyChange={onEditTableSticky}/>; -} -export function SelectedGroupHeader({group,state,onEditStrategy,onLocateCurrent,onRefreshGroup}:{group?:ProxyGroup | null;state?:ServiceState;onEditStrategy?:(strategy:string)=>void;onLocateCurrent?:()=>void;onRefreshGroup?:(name:string)=>void}) { - if (!group) return 未选择代理组从右侧代理组表格选择一个代理组。; - const options=state?.switch_strategy_definitions || []; - return {group.name}{onRefreshGroup ? : null}决策:{group.last_decision || '等待自动切换判定'}{group.last_error ? `,错误:${group.last_error}` : ''}策略选择是代理组页顶部快捷控件;代理组表格里的策略列仍可编辑并持久化。; -} diff --git a/src/node_service/web/src/components/NodeDetailPanel.tsx b/src/node_service/web/src/components/NodeDetailPanel.tsx deleted file mode 100644 index 42faa24..0000000 --- a/src/node_service/web/src/components/NodeDetailPanel.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import React, { useState } from "react"; -import { - Accordion, - AccordionDetails, - AccordionSummary, - Card, - CardContent, - Chip, - Divider, - Paper, - Stack, - Typography, -} from "@mui/material"; -import type { NodeState } from "../types.tsx"; -import { NodeRefreshButton } from "./Display.tsx"; -import { - aiText, - msText, - multiplierText, - nodeProbeResult, - rateSummaryText, - riskText, - scoreText, - timeText, - valueText, -} from "../utils.tsx"; -function DetailItem({ - label, - value, -}: { - label: string; - value: React.ReactNode; -}) { - return ( - - - {label} - - - {valueText(value)} - - - ); -} -export function NodeDetailPanel({ - node, - onRefreshNode, - refreshingNodes, -}: { - node?: NodeState | null; - onRefreshNode: (name: string) => void; - refreshingNodes: Set; -}) { - const [probeOpen, setProbeOpen] = useState(false); - if (!node) - return ( - - 节点详情 - - 点击左侧节点查看探测详情。 - - - ); - return ( - - - - 节点详情 - - - - - {node.name} - - - - - - - - - - - - - - - - - - - - - - - setProbeOpen(expanded)} - disableGutters - variant="outlined" - > - ⌄}> - - - 探测详情 - - - - - - {(node.probes || []).map((probe, index) => { - const result = nodeProbeResult(probe); - return ( - - - - - {probe.target} - - - - - {probe.url} - - {result.error ? ( - - {result.error} - - ) : null} - - - ); - })} - - - - ); -} diff --git a/src/node_service/web/src/components/NodeFilters.tsx b/src/node_service/web/src/components/NodeFilters.tsx deleted file mode 100644 index be4d286..0000000 --- a/src/node_service/web/src/components/NodeFilters.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import React from 'react'; -export function NodeFilters() { - return <>; -} diff --git a/src/node_service/web/src/components/NodeTable.tsx b/src/node_service/web/src/components/NodeTable.tsx deleted file mode 100644 index 0b3da1d..0000000 --- a/src/node_service/web/src/components/NodeTable.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React,{useMemo} from 'react'; -import {Button,Paper,Stack,Typography} from '@mui/material'; -import type {NodeState,ServiceState,TableColumnProtocol,TableRowObject} from '../types.tsx'; -import {NodeRefreshButton} from './Display.tsx'; -import {ProtocolObjectTable} from './ProtocolObjectTable.tsx'; -import {tableProtocol,tableRows} from '../utils.tsx'; -function NodeActionCell({node,onRefreshNode,refreshing,onSwitchNode,switching,isCurrent}:{node:NodeState;onRefreshNode:(name:string)=>void;refreshing:boolean;onSwitchNode?:(name:string)=>void;switching?:boolean;isCurrent?:boolean}) { - const refreshable=!node.special_proxy_kind; - return event.stopPropagation()}>{refreshable ? : null}{onSwitchNode ? : null}; -} -function specialRow(name:string,state:ServiceState):TableRowObject { - const group=(state.groups || []).find(item=>item.name === name); - const kind=name === 'DIRECT' ? 'direct' : group ? 'group' : 'unknown'; - const label=kind === 'direct' ? 'DIRECT' : group ? '代理组' : '特殊项'; - const type=kind === 'direct' ? 'DIRECT' : group ? `proxy_group:${group.type || 'unknown'}` : 'unknown'; - const raw:NodeState={name,special_proxy_kind:kind,type,provider:label,alive:true,stability_score:0,success_rate:0,recent_success_rate:0,check_count:0,pass_count:0,recent_check_count:0,recent_pass_count:0,probes:[]}; - const values={...raw,id:name,alive_category:'special',alive_category_label:label,stability_category:'unknown',stability_category_label:label,total_rate_category:'unknown',total_rate_category_label:label,recent_rate_category:'unknown',recent_rate_category_label:label,ai_category:'unknown',ai_category_label:'-',ip_type_category:'unknown',ip_type_category_label:label,risk_category:'unknown',risk_category_label:'-',shared_category:'unknown',shared_category_label:'-',native_category:'unknown',native_category_label:'-',ip_version_category:'none',ip_version_category_label:'-',risk_text:'-',ai_text:'-',rate_summary:{check_count:0,pass_count:0,success_rate:0,recent_check_count:0,recent_pass_count:0,recent_success_rate:0},proxy_groups:group ? [group.name] : [],probe_summary:[]}; - return {id:name,values,raw}; -} -function orderedRows(state:ServiceState,rows:TableRowObject[],names:string[]):TableRowObject[] { - const rowMap=new Map(rows.map(row=>[row.id,row])); - const result:TableRowObject[]=[]; - const used=new Set(); - for (const name of names) { - if (used.has(name)) continue; - used.add(name); - result.push(rowMap.get(name) || specialRow(name,state)); - } - return result; -} -export function NodeTable({state,sourceNodes,sourceNames,selected,setSelected,onRefreshNode,refreshingNodes,maxHeight='calc(100vh - 330px)',fillHeight=true,storageKey='node_service_node_table_widths',title='节点表格',onEditTableCell,onEditTableOrder,onEditTableSticky,highlightedName,scrollToName,scrollSignal,onSwitchNode,switchingNodes}:{state:ServiceState;sourceNodes:NodeState[];sourceNames?:string[];selected?:NodeState | null;setSelected?:(node:NodeState)=>void;onRefreshNode:(name:string)=>void;refreshingNodes:Set;maxHeight?:string;fillHeight?:boolean;storageKey?:string;title?:string;onEditTableCell?:(table:string,row:string,column:string,value:unknown)=>void;onEditTableOrder?:(table:string,columnOrder:string[])=>void;onEditTableSticky?:(table:string,stickyColumns:string[])=>void;highlightedName?:string | null;scrollToName?:string | null;scrollSignal?:number;onSwitchNode?:(name:string)=>void;switchingNodes?:Set}) { - const protocol=tableProtocol(state,'proxy_nodes'); - const rows=tableRows(state,'proxy_nodes'); - const names=useMemo(()=>sourceNames || sourceNodes.map(node=>node.name),[sourceNames,sourceNodes]); - const filteredRows=useMemo(()=>orderedRows(state,rows,names),[state,rows,names]); - if (!protocol) { - return 后端没有返回代理节点表格协议。; - } - return state={state} protocol={protocol} rows={filteredRows} selectedId={selected?.name} highlightedId={highlightedName} scrollToId={scrollToName} scrollSignal={scrollSignal} onSelectRow={(row)=>setSelected?.(row.raw)} actionRenderer={(row:TableRowObject)=>} storageKey={storageKey} title={title} description="表格列、枚举、过滤、排序规则都由后端协议定义。" fillHeight={fillHeight} maxHeight={maxHeight} onEditCell={onEditTableCell ? (row:TableRowObject,column:TableColumnProtocol,value:unknown)=>onEditTableCell(protocol.id,row.id,column.id,value) : undefined} onColumnOrderChange={onEditTableOrder} onColumnStickyChange={onEditTableSticky}/>; -} diff --git a/src/node_service/web/src/components/ProtocolObjectTable.tsx b/src/node_service/web/src/components/ProtocolObjectTable.tsx deleted file mode 100644 index e748bd1..0000000 --- a/src/node_service/web/src/components/ProtocolObjectTable.tsx +++ /dev/null @@ -1,216 +0,0 @@ -import React,{useEffect,useMemo,useRef,useState} from 'react'; -import {Box,Button,Chip,FormControlLabel,Menu,MenuItem,Select,Stack,Switch,TableCell,TableRow,TextField,Typography} from '@mui/material'; -import type {FormControlProtocol,FormProtocol,ServiceState,TableCellEditSchema,TableColumn,TableColumnProtocol,TableProtocol,TableRowObject,TableSortDirection} from '../types.tsx'; -import {CategoryChip,ProbeIcons,ScoreCell} from './Display.tsx'; -import {HeaderResizeGrip,ResizableObjectTable,stickyOffsets,tableCellSx,usePersistentColumns} from './ResizableObjectTable.tsx'; -import {columnDefaultDirection,defaultSortValue,enumOptions,msText,protocolFilterMatch,protocolKeywordMatch,rateText,rowLabel,rowValue,sortProtocolRows,timeText,valueText} from '../utils.tsx'; -function widthDefaults(protocol:TableProtocol):Record { - const result:Record={}; - for (const column of protocol.columns) result[column.id]=Number(column.width || 120); - return result; -} -function splitSort(value:string):{column:string;direction:TableSortDirection} { - const [column,direction]=value.split(':'); - return {column,direction:direction === 'desc' ? 'desc' : 'asc'}; -} -let measureCanvas:HTMLCanvasElement | undefined; -function measureText(text:string):number { - if (typeof document === 'undefined') return text.length * 12; - const canvas=measureCanvas || (measureCanvas=document.createElement('canvas')); - const context=canvas.getContext('2d'); - if (!context) return text.length * 12; - context.font='14px Arial,"Microsoft YaHei",sans-serif'; - return context.measureText(text).width; -} -function cellWidthText(row:TableRowObject,column:TableColumnProtocol,state:ServiceState):string { - const value=rowValue(row,column); - if (column.type === 'action') return '刷新 选择 当前'; - if (column.type === 'rate_summary') return `总 ${Number(value?.pass_count || 0)}/${Number(value?.check_count || 0)} ${rateText(value?.success_rate)} 近 ${Number(value?.recent_pass_count || 0)}/${Number(value?.recent_check_count || 0)} ${rateText(value?.recent_success_rate)}`; - if (column.type === 'probe_icons') return Array.isArray(value) ? value.map(item=>item?.success ? '成功' : '失败').join(' ') : '-'; - if (column.type === 'ms') return msText(value); - if (column.type === 'percent') return rateText(value); - if (column.type === 'datetime') return timeText(value); - if (column.type === 'form') return rowLabel(row,column,state); - return rowLabel(row,column,state); -} -function autoColumnWidths(protocol:TableProtocol,columns:TableColumnProtocol[],rows:TableRowObject[],state:ServiceState):Record { - const result:Record={}; - for (const column of columns) { - const header=measureText(column.label) + 100; - const maxCell=rows.reduce((max,row)=>Math.max(max,measureText(cellWidthText(row,column,state)) + 48),0); - const base=Number(column.width || 120); - const max=Math.max(header,maxCell,base); - result[column.id]=Math.max(70,Math.min(column.type === 'form' ? 1100 : column.type === 'text' ? 620 : 420,Math.ceil(max))); - } - for (const column of protocol.columns) if (!result[column.id]) result[column.id]=Number(column.width || 120); - return result; -} -function renderRateSummary(value:any) { - return 总 {Number(value?.pass_count || 0)}/{Number(value?.check_count || 0)} {rateText(value?.success_rate)}近 {Number(value?.recent_pass_count || 0)}/{Number(value?.recent_check_count || 0)} {rateText(value?.recent_success_rate)}; -} -function enumChip(row:TableRowObject,column:TableColumnProtocol,state:ServiceState) { - const value=String(rowValue(row,column) ?? ''); - return ; -} -function multiEnumCell(row:TableRowObject,column:TableColumnProtocol,state:ServiceState) { - const values=rowValue(row,column); - if (!Array.isArray(values) || !values.length) return <>-; - const options=enumOptions(state,column.enum); - return {values.slice(0,3).map((value:string)=>option.value === value)?.label || value}/>) }{values.length > 3 ? : null}; -} -function renderCell(row:TableRowObject,column:TableColumnProtocol,state:ServiceState,actionRenderer?:((row:TableRowObject)=>React.ReactNode)) { - const value=rowValue(row,column); - if (column.type === 'action') return actionRenderer ? actionRenderer(row) : null; - if (column.type === 'enum') return enumChip(row,column,state); - if (column.type === 'multi_enum') return multiEnumCell(row,column,state); - if (column.type === 'score') return ; - if (column.type === 'ms') return msText(value); - if (column.type === 'percent') return rateText(value); - if (column.type === 'datetime') return timeText(value); - if (column.type === 'form') return rowLabel(row,column,state); - if (column.type === 'rate_summary') return renderRateSummary(value); - if (column.type === 'probe_icons') return ; - if (column.type === 'boolean') return value ? '是' : '否'; - if (column.type === 'integer') return Number(value || 0); - if (column.type === 'number') return value === undefined || value === null || value === '' ? '-' : Number(value).toString(); - return valueText(rowLabel(row,column,state)); -} -function activeFilterLabel(column:TableColumnProtocol,value:string | undefined,state:ServiceState):string { - if (!value || value === 'all') return '过滤'; - if ((column.filter_type === 'enum' || column.filter_type === 'dynamic_enum' || column.filter_type === 'multi_enum') && column.enum) return enumOptions(state,column.enum).find(option=>option.value === value)?.label || value; - return value; -} -function editSchema(row:TableRowObject,column:TableColumnProtocol):TableCellEditSchema | undefined { - if (row.values?._edit?.[column.id]) return row.values._edit[column.id]; - if (!column.editable) return undefined; - if (column.type === 'enum') return {type:'enum',enum:column.enum}; - if (column.type === 'boolean') return {type:'boolean'}; - if (column.type === 'number' || column.type === 'integer' || column.type === 'score') return {type:'number'}; - return {type:'string'}; -} -function controlValue(values:Record,control:FormControlProtocol):unknown { - return values?.[control.key] ?? control.value ?? ''; -} -function nextFormValues(values:Record,control:FormControlProtocol,value:unknown):Record { - return {...(values || {}),[control.key]:value}; -} -function ProtocolForm({form,values,state,onChange}:{form:FormProtocol;values:Record;state:ServiceState;onChange:(value:Record)=>void}) { - if (!form.controls?.length) return 无需参数; - const controlSx={minWidth:120,maxWidth:180}; - const numberSx={width:120}; - const renderControl=(control:FormControlProtocol)=>{ - const value=controlValue(values,control); - if (control.type === 'enum') { - const options=enumOptions(state,control.enum); - return ; - } - if (control.type === 'boolean') { - return event.stopPropagation()} control={onChange(nextFormValues(values,control,event.target.checked))}/>} label={control.label} sx={{mr:1,whiteSpace:'nowrap'}}/>; - } - if (control.type === 'number') { - return event.stopPropagation()} onChange={event=>{const next=Number(event.target.value);if (Number.isFinite(next)) onChange(nextFormValues(values,control,next));}} sx={numberSx}/>; - } - return event.stopPropagation()} onChange={event=>onChange(nextFormValues(values,control,event.target.value))} sx={controlSx}/>; - }; - return {form.controls.map(renderControl)}; -} -function EditableCell({row,column,state,onEditCell,actionRenderer}:{row:TableRowObject;column:TableColumnProtocol;state:ServiceState;onEditCell?:(row:TableRowObject,column:TableColumnProtocol,value:unknown)=>void;actionRenderer?:((row:TableRowObject)=>React.ReactNode)}) { - const schema=editSchema(row,column); - const raw=rowValue(row,column); - const [value,setValue]=useState(String(raw ?? '')); - useEffect(()=>setValue(String(raw ?? '')),[raw]); - if (column.type === 'form') { - const form=row.values?._forms?.[column.id]; - if (!form || !onEditCell) return <>{renderCell(row,column,state,actionRenderer)}; - return } state={state} onChange={next=>onEditCell(row,column,next)}/>; - } - if (!schema || !onEditCell) return <>{renderCell(row,column,state,actionRenderer)}; - if (schema.type === 'enum') { - const options=enumOptions(state,schema.enum || column.enum); - return ; - } - if (schema.type === 'boolean') { - return ; - } - if (schema.type === 'number') { - return event.stopPropagation()} onChange={event=>setValue(event.target.value)} onBlur={()=>{const next=Number(value);if (Number.isFinite(next) && next !== Number(raw ?? 0)) onEditCell(row,column,next);}} onKeyDown={event=>{if (event.key === 'Enter') (event.target as HTMLInputElement).blur();}} sx={{width:130}}/>; - } - return event.stopPropagation()} onChange={event=>setValue(event.target.value)} onBlur={()=>{if (value !== String(raw ?? '')) onEditCell(row,column,value);}} onKeyDown={event=>{if (event.key === 'Enter') (event.target as HTMLInputElement).blur();}}/>; -} -function orderedIdsAfterMove(protocol:TableProtocol,columnId:string,direction:-1 | 1):string[] { - const ids=protocol.columns.map(column=>column.id); - const index=ids.indexOf(columnId); - const next=index + direction; - if (index < 0 || next < 0 || next >= ids.length) return ids; - [ids[index],ids[next]]=[ids[next],ids[index]]; - return ids; -} -function stickyIdsAfterToggle(protocol:TableProtocol,columnId:string):string[] { - const enabled=new Set(protocol.columns.filter(column=>column.sticky === 'left').map(column=>column.id)); - if (enabled.has(columnId)) enabled.delete(columnId); - else enabled.add(columnId); - return protocol.columns.filter(column=>column.visible !== false && enabled.has(column.id)).map(column=>column.id); -} -function stickyColumnCount(protocol:TableProtocol):number { - return protocol.columns.filter(column=>column.visible !== false && column.sticky === 'left').length; -} -export function ProtocolObjectTable({state,protocol,rows,selectedId,highlightedId,scrollToId,scrollSignal,onSelectRow,actionRenderer,storageKey,title,description,fillHeight=true,maxHeight='calc(100vh - 320px)',extraToolbar,onEditCell,onColumnOrderChange,onColumnStickyChange}:{state:ServiceState;protocol:TableProtocol;rows:TableRowObject[];selectedId?:string | null;highlightedId?:string | null;scrollToId?:string | null;scrollSignal?:number;onSelectRow?:(row:TableRowObject)=>void;actionRenderer?:(row:TableRowObject)=>React.ReactNode;storageKey:string;title?:string;description?:string;fillHeight?:boolean;maxHeight?:string;extraToolbar?:React.ReactNode;onEditCell?:(row:TableRowObject,column:TableColumnProtocol,value:unknown)=>void;onColumnOrderChange?:(tableId:string,columnOrder:string[])=>void;onColumnStickyChange?:(tableId:string,stickyColumns:string[])=>void}) { - const [keyword,setKeyword]=useState(''); - const [filters,setFilters]=useState>({}); - const [sortValue,setSortValue]=useState(defaultSortValue(protocol)); - const [filterColumn,setFilterColumn]=useState(null); - const [filterAnchor,setFilterAnchor]=useState(null); - const [stickyEditorOpen,setStickyEditorOpen]=useState(false); - const [orderEditorOpen,setOrderEditorOpen]=useState(false); - const rowRefs=useRef>({}); - const visibleColumns=useMemo(()=>protocol.columns.filter(column=>column.visible !== false),[protocol]); - const sortedRows=useMemo(()=>{ - const {column,direction}=splitSort(sortValue); - return sortProtocolRows(rows.filter(row=>protocolKeywordMatch(row,protocol,keyword,state)).filter(row=>protocolFilterMatch(row,filters,protocol,state)),protocol,state,column,direction); - },[rows,protocol,keyword,filters,sortValue,state]); - const autoWidths=useMemo(()=>autoColumnWidths(protocol,visibleColumns,rows,state),[protocol,visibleColumns,rows,state]); - const {widths,setWidth,reset}=usePersistentColumns(storageKey,widthDefaults(protocol)); - const offsets=useMemo(()=>stickyOffsets(visibleColumns,widths),[visibleColumns,widths]); - useEffect(()=>{ - if (!scrollToId) return; - const row=rowRefs.current[scrollToId]; - if (row) row.scrollIntoView({block:'center',inline:'nearest',behavior:'smooth'}); - },[scrollToId,scrollSignal,sortedRows.length]); - const setFilter=(id:string,value:string)=>setFilters(prev=>({...prev,[id]:value})); - const clearFilters=()=>{setKeyword('');setFilters({});}; - const openFilter=(event:React.MouseEvent,column:TableColumnProtocol)=>{ - event.stopPropagation(); - setFilterColumn(column); - setFilterAnchor(event.currentTarget); - }; - const closeFilter=()=>{setFilterColumn(null);setFilterAnchor(null);}; - const toggleSort=(column:TableColumnProtocol)=>{ - if (!column.sortable) return; - const current=splitSort(sortValue); - const direction=current.column === column.id ? (current.direction === 'desc' ? 'asc' : 'desc') : columnDefaultDirection(column); - setSortValue(`${column.id}:${direction}`); - }; - const moveColumn=(event:React.MouseEvent,column:TableColumnProtocol,direction:-1 | 1)=>{ - event.stopPropagation(); - if (!protocol.column_order_editable || !onColumnOrderChange) return; - onColumnOrderChange(protocol.id,orderedIdsAfterMove(protocol,column.id,direction)); - }; - const toggleSticky=(event:React.MouseEvent,column:TableColumnProtocol)=>{ - event.stopPropagation(); - if (!protocol.sticky_editable || !onColumnStickyChange) return; - onColumnStickyChange(protocol.id,stickyIdsAfterToggle(protocol,column.id)); - }; - const clearSticky=()=>{ - if (!protocol.sticky_editable || !onColumnStickyChange) return; - onColumnStickyChange(protocol.id,[]); - }; - const headerCell=(input:TableColumn)=>{ - const column=input as TableColumnProtocol; - const current=splitSort(sortValue); - const activeFilter=filters[column.id] && filters[column.id] !== 'all'; - const sortMark=current.column === column.id ? (current.direction === 'desc' ? '↓' : '↑') : ''; - return toggleSort(column)} sx={{...tableCellSx(column,widths,{position:'relative',fontWeight:900,userSelect:'none',cursor:column.sortable ? 'pointer' : 'default',bgcolor:activeFilter ? 'rgba(37,99,235,0.08)' : undefined},offsets[column.id],true)}}>{orderEditorOpen && protocol.column_order_editable && onColumnOrderChange ? : null}{column.label}{sortMark ? {sortMark} : null}{column.editable ? 可编辑 : null}{stickyEditorOpen && protocol.sticky_editable && onColumnStickyChange ? : null}{column.filterable ? : null}{orderEditorOpen && protocol.column_order_editable && onColumnOrderChange ? : null}; - }; - const filterOptions=filterColumn ? enumOptions(state,filterColumn.enum) : []; - return {title || protocol.name}{description ? {description} : null}显示行:{sortedRows.length}{extraToolbar}setKeyword(event.target.value)} sx={{minWidth:280,flexBasis:360,flexGrow:1,maxWidth:520}}/>{protocol.sticky_editable && onColumnStickyChange ? : null}{stickyEditorOpen && protocol.sticky_editable && onColumnStickyChange ? : null}{protocol.column_order_editable && onColumnOrderChange ? : null}{rowRefs.current[row.id]=element;}} hover selected={selectedId === row.id} onClick={()=>onSelectRow?.(row)} sx={{cursor:onSelectRow ? 'pointer' : 'default',bgcolor:selectedId === row.id ? undefined : highlightedId === row.id ? 'rgba(16,185,129,0.12)' : undefined,'&:hover':{bgcolor:highlightedId === row.id ? 'rgba(16,185,129,0.18)' : undefined},'&.Mui-selected':{bgcolor:'rgba(37,99,235,0.14)'},'&.Mui-selected:hover':{bgcolor:'rgba(37,99,235,0.22)'}}}>{visibleColumns.map(column=>)}}/>{filterColumn && (filterColumn.filter_type === 'enum' || filterColumn.filter_type === 'dynamic_enum' || filterColumn.filter_type === 'multi_enum') ? [{setFilter(filterColumn.id,'all');closeFilter();}}>全部,...filterOptions.map(option=>{setFilter(filterColumn.id,option.value);closeFilter();}}>{option.label})] : null}{filterColumn && !(filterColumn.filter_type === 'enum' || filterColumn.filter_type === 'dynamic_enum' || filterColumn.filter_type === 'multi_enum') ? setFilter(filterColumn.id,event.target.value)}/> : null}; -} diff --git a/src/node_service/web/src/components/ResizableObjectTable.tsx b/src/node_service/web/src/components/ResizableObjectTable.tsx deleted file mode 100644 index 10a7221..0000000 --- a/src/node_service/web/src/components/ResizableObjectTable.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React,{useEffect,useRef,useState} from 'react'; -import {Box,Table,TableBody,TableCell,TableHead,TableRow} from '@mui/material'; -import type {SxProps,Theme} from '@mui/material/styles'; -import type {TableColumn} from '../types.tsx'; -export type ColumnWidths=Record; -export type StickyOffsets=Record; -export function usePersistentColumns(key:string,defaults:ColumnWidths) { - const [widths,setWidths]=useState(()=>{ - try { - return {...defaults,...JSON.parse(localStorage.getItem(key) || '{}')}; - } - catch { - return {...defaults}; - } - }); - useEffect(()=>{localStorage.setItem(key,JSON.stringify(widths));},[key,widths]); - const setWidth=(id:string,width:number)=>setWidths(prev=>({...prev,[id]:Math.max(70,Math.min(1200,Math.round(width)))})); - const reset=(next?:ColumnWidths)=>setWidths({...next || defaults}); - return {widths,setWidth,reset}; -} -export function columnWidth(columns:TableColumn[],widths:ColumnWidths):number { - return columns.reduce((sum,column)=>sum + Number(widths[column.id] || column.width || 120),0); -} -export function stickyOffsets(columns:TableColumn[],widths:ColumnWidths):StickyOffsets { - const result:StickyOffsets={}; - let left=0; - for (const column of columns) { - if (column.sticky !== 'left') continue; - result[column.id]=left; - left += Number(widths[column.id] || column.width || 120); - } - return result; -} -export function tableCellSx(column:TableColumn,widths:ColumnWidths,extra:SxProps={},stickyLeft?:number,isHeader=false):SxProps { - const width=Number(widths[column.id] || column.width || 120); - const sx:SxProps={width,minWidth:width,maxWidth:width,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',...extra}; - if (column.sticky === 'left' && stickyLeft !== undefined) { - return {...sx,position:'sticky',left:stickyLeft,zIndex:isHeader ? 9 : 4,bgcolor:isHeader ? '#f8fafc' : 'background.paper',boxShadow:'2px 0 0 rgba(148,163,184,0.18)'}; - } - return sx; -} -function scrollbarSx() { - return {'&::-webkit-scrollbar':{height:14,width:14},'&::-webkit-scrollbar-track':{backgroundColor:'#eef2f7',borderRadius:8},'&::-webkit-scrollbar-thumb':{backgroundColor:'#94a3b8',borderRadius:8,border:'3px solid #eef2f7'},'&::-webkit-scrollbar-thumb:hover':{backgroundColor:'#64748b'}}; -} -export function HeaderResizeGrip({column,widths,setWidth}:{column:TableColumn;widths:ColumnWidths;setWidth:(id:string,width:number)=>void}) { - const start=(event:React.MouseEvent)=>{ - event.preventDefault(); - event.stopPropagation(); - const startX=event.clientX; - const startWidth=Number(widths[column.id] || column.width || 120); - const move=(moveEvent:MouseEvent)=>setWidth(column.id,startWidth + moveEvent.clientX - startX); - const up=()=>{ - document.removeEventListener('mousemove',move); - document.removeEventListener('mouseup',up); - }; - document.addEventListener('mousemove',move); - document.addEventListener('mouseup',up); - }; - if (column.resizable === false) return null; - return ; -} -function ResizeHeaderCell({column,widths,setWidth,stickyLeft}:{column:TableColumn;widths:ColumnWidths;setWidth:(id:string,width:number)=>void;stickyLeft?:number}) { - return {column.label}; -} -function TopScrollTable({columns,widths,setWidth,fillHeight,maxHeight,children,renderHeaderCell,stickyHeader=true,topScrollbar=true}:{columns:TableColumn[];widths:ColumnWidths;setWidth:(id:string,width:number)=>void;fillHeight:boolean;maxHeight:string;children:React.ReactNode;renderHeaderCell?:(column:TableColumn)=>React.ReactNode;stickyHeader?:boolean;topScrollbar?:boolean}) { - const topRef=useRef(null); - const bodyRef=useRef(null); - const syncing=useRef(false); - const minWidth=columnWidth(columns,widths); - const offsets=stickyOffsets(columns,widths); - const sync=(from:'top'|'body')=>{ - if (syncing.current) return; - const source=from === 'top' ? topRef.current : bodyRef.current; - const target=from === 'top' ? bodyRef.current : topRef.current; - if (!source || !target) return; - syncing.current=true; - target.scrollLeft=source.scrollLeft; - requestAnimationFrame(()=>{syncing.current=false;}); - }; - return {topScrollbar ? sync('top')} sx={{overflowX:'auto',overflowY:'hidden',height:20,minHeight:20,borderTop:'1px solid #d8e2f0',borderBottom:'1px solid #eef2f7',...scrollbarSx()}}> : null}sync('body')} sx={{overflow:'auto',maxHeight:fillHeight ? undefined : maxHeight,minHeight:0,flex:fillHeight ? 1 : undefined,...scrollbarSx()}}>{columns.map(column=>renderHeaderCell ? renderHeaderCell(column) : )}{children}
; -} -export function ResizableObjectTable({columns,widths,setWidth,maxHeight='calc(100vh - 320px)',fillHeight=false,rows,renderRow,emptyText,renderHeaderCell,stickyHeader=true,topScrollbar=true}:{columns:TableColumn[];widths:ColumnWidths;setWidth:(id:string,width:number)=>void;maxHeight?:string;fillHeight?:boolean;rows:T[];renderRow:(row:T,index:number)=>React.ReactNode;emptyText?:string;renderHeaderCell?:(column:TableColumn)=>React.ReactNode;stickyHeader?:boolean;topScrollbar?:boolean}) { - return {rows.length ? rows.map(renderRow) : {emptyText || '没有数据'}}; -} diff --git a/src/node_service/web/src/main.tsx b/src/node_service/web/src/main.tsx deleted file mode 100644 index 8c5e392..0000000 --- a/src/node_service/web/src/main.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; -import {createRoot} from 'react-dom/client'; -import {CssBaseline,GlobalStyles,ThemeProvider} from '@mui/material'; -import App from './App.tsx'; -import {theme} from './theme.tsx'; -createRoot(document.getElementById('root') as HTMLElement).render(); diff --git a/src/node_service/web/src/pages/ConfigPage.tsx b/src/node_service/web/src/pages/ConfigPage.tsx deleted file mode 100644 index d9b31df..0000000 --- a/src/node_service/web/src/pages/ConfigPage.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from "react"; -import {Box,Button,FormControlLabel,Grid,LinearProgress,Paper,Stack,Switch,TextField,Typography} from "@mui/material"; -import type {AutoSwitchConfig,ServiceState,TableColumnProtocol,TableRowObject} from "../types.tsx"; -import {StatCard} from "../components/Display.tsx"; -import {ProtocolObjectTable} from "../components/ProtocolObjectTable.tsx"; -import {metricValues,rateText,scoreLevel,scoreText,tableProtocol,tableRows,timeText} from "../utils.tsx"; -function Summary({state}:{state:ServiceState}) { - const metrics=metricValues(state); - return = 95 ? "success.main" : metrics.recentRate >= 80 ? "warning.main" : "error.main"}/>; -} -function ServicePanel({state,notice,busy,fullRefreshRequesting,onRefresh,onReload,autoRefresh,setAutoRefresh}:{state:ServiceState;notice:string;busy:boolean;fullRefreshRequesting:boolean;onRefresh:()=>void;onReload:()=>void;autoRefresh:boolean;setAutoRefresh:(value:boolean)=>void}) { - const done=Number(state.refresh_done_nodes || 0); - const total=Number(state.refresh_total_nodes || 0); - const progress=total > 0 ? Math.min(100,(done * 100) / total) : 0; - return
服务状态轮次 {state.round ?? 0} | {state.status_text || "等待服务状态"} | 更新时间 {timeText(state.updated_time)}配置 {state.config_path || "-"} | 状态目录 {state.state_dir || "-"}最近请求 {timeText(state.last_refresh_request_time)} | 开始 {timeText(state.last_refresh_begin_time)} | 结束 {timeText(state.last_refresh_end_time)}
setAutoRefresh(event.target.checked)}/>} label="页面自动刷新"/>
0 ? "determinate" : "indeterminate"} value={progress} sx={{mt:2}}/>{notice}
; -} -function AutoSwitchProtocolTable({state,tableId,storageKey,title,description,busy,onRun,onEditTableCell,onEditTableOrder,onEditTableSticky}:{state:ServiceState;tableId:string;storageKey:string;title:string;description:string;busy?:boolean;onRun?:()=>void;onEditTableCell:(table:string,row:string,column:string,value:unknown)=>void;onEditTableOrder:(table:string,columnOrder:string[])=>void;onEditTableSticky:(table:string,stickyColumns:string[])=>void}) { - const protocol=tableProtocol(state,tableId); - const rows=tableRows(state,tableId); - if (!protocol) return 后端没有返回 {title} 协议。; - return 立即执行切换判定 : null} onEditCell={(row:TableRowObject,column:TableColumnProtocol,value:unknown)=>onEditTableCell(protocol.id,row.id,column.id,value)} onColumnOrderChange={onEditTableOrder} onColumnStickyChange={onEditTableSticky}/>; -} -function AutoSwitchProtocolPanel({state,busy,onRun,onEditTableCell,onEditTableOrder,onEditTableSticky}:{state:ServiceState;busy:boolean;onRun:()=>void;onEditTableCell:(table:string,row:string,column:string,value:unknown)=>void;onEditTableOrder:(table:string,columnOrder:string[])=>void;onEditTableSticky:(table:string,stickyColumns:string[])=>void}) { - return ; -} -export function ConfigPage({state,notice,intervalValue,setIntervalValue,httpLog,setHttpLog,onSaveInterval,onSaveHttpLog,busy,onSaveSwitch,onRunSwitch,onRefresh,fullRefreshRequesting,onReload,autoRefresh,setAutoRefresh,onEditTableCell,onEditTableOrder,onEditTableSticky}:{state:ServiceState;notice:string;intervalValue:string;setIntervalValue:(value:string)=>void;httpLog:boolean;setHttpLog:(value:boolean)=>void;onSaveInterval:()=>void;onSaveHttpLog:(value:boolean)=>void;busy:boolean;onSaveSwitch:(config:AutoSwitchConfig)=>void;onRunSwitch:()=>void;onRefresh:()=>void;fullRefreshRequesting:boolean;onReload:()=>void;autoRefresh:boolean;setAutoRefresh:(value:boolean)=>void;onEditTableCell:(table:string,row:string,column:string,value:unknown)=>void;onEditTableOrder:(table:string,columnOrder:string[])=>void;onEditTableSticky:(table:string,stickyColumns:string[])=>void}) { - void onSaveSwitch; - return 综合配置setIntervalValue(event.target.value)} inputProps={{min:5,max:3600,step:1}}/>{setHttpLog(event.target.checked);onSaveHttpLog(event.target.checked);}}/>} label="后台 HTTP 日志"/>服务配置:{state.service_config_path || "-"}全局统计、服务状态、刷新全部节点和策略表格都放在综合配置页。; -} diff --git a/src/node_service/web/src/pages/GroupsPage.tsx b/src/node_service/web/src/pages/GroupsPage.tsx deleted file mode 100644 index bd246f7..0000000 --- a/src/node_service/web/src/pages/GroupsPage.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React,{useEffect,useMemo,useState} from 'react'; -import {Grid,Stack} from '@mui/material'; -import type {NodeState,ProxyGroup,ServiceState} from '../types.tsx'; -import {GroupListPanel,SelectedGroupHeader} from '../components/GroupListPanel.tsx'; -import {NodeTable} from '../components/NodeTable.tsx'; -export function GroupsPage({state,selectedGroup,setSelectedGroup,onRefreshGroup,refreshingGroups,onRefreshNode,refreshingNodes,groupListCollapsed,onEditTableCell,onEditTableOrder,onEditTableSticky,onSwitchGroupNode,switchingNodes}:{state:ServiceState;selectedGroup:ProxyGroup | null;setSelectedGroup:(group:ProxyGroup)=>void;onRefreshGroup:(name?:string)=>void;refreshingGroups:Set;onRefreshNode:(name:string)=>void;refreshingNodes:Set;groupListCollapsed:boolean;onEditTableCell:(table:string,row:string,column:string,value:unknown)=>void;onEditTableOrder:(table:string,columnOrder:string[])=>void;onEditTableSticky:(table:string,stickyColumns:string[])=>void;onSwitchGroupNode:(group:string,node:string)=>void;switchingNodes:Set}) { - const [selectedNode,setSelectedNode]=useState(null); - const [scrollSignal,setScrollSignal]=useState(0); - const groupNodeNames=useMemo(()=>{ - if (!selectedGroup) return []; - const names=selectedGroup.proxies?.length ? selectedGroup.proxies : selectedGroup.candidates || []; - return names.filter((name,index)=>!!name && names.indexOf(name) === index); - },[selectedGroup]); - const groupNodes=useMemo(()=>{ - const allowed=new Set(groupNodeNames); - return (state.nodes || []).filter(node=>allowed.has(node.name)); - },[state.nodes,groupNodeNames]); - useEffect(()=>{ - if (!selectedGroup?.current) return; - const current=(state.nodes || []).find(node=>node.name === selectedGroup.current); - if (current) setSelectedNode(current); - setScrollSignal(value=>value + 1); - },[selectedGroup?.name,selectedGroup?.current,state.nodes]); - const locateCurrent=()=>{ - if (!selectedGroup?.current) return; - setScrollSignal(value=>value + 1); - }; - const switchNode=(node:string)=>{ - if (!selectedGroup) return; - onSwitchGroupNode(selectedGroup.name,node); - }; - return selectedGroup && onEditTableCell('proxy_groups',selectedGroup.name,'strategy',strategy)} onLocateCurrent={locateCurrent} onRefreshGroup={onRefreshGroup}/>{!groupListCollapsed ? : null}; -} diff --git a/src/node_service/web/src/pages/NodesPage.tsx b/src/node_service/web/src/pages/NodesPage.tsx deleted file mode 100644 index 3227cc6..0000000 --- a/src/node_service/web/src/pages/NodesPage.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; -import {Button,Grid,Stack} from '@mui/material'; -import type {NodeState,ServiceState} from '../types.tsx'; -import {NodeTable} from '../components/NodeTable.tsx'; -import {NodeDetailPanel} from '../components/NodeDetailPanel.tsx'; -export function NodesPage({state,selected,setSelected,onRefreshNode,refreshingNodes,onRefreshAll,fullRefreshRequesting,detailCollapsed,onEditTableCell,onEditTableOrder,onEditTableSticky}:{state:ServiceState;selected:NodeState | null;setSelected:(node:NodeState)=>void;onRefreshNode:(name:string)=>void;refreshingNodes:Set;onRefreshAll:()=>void;fullRefreshRequesting:boolean;detailCollapsed:boolean;onEditTableCell:(table:string,row:string,column:string,value:unknown)=>void;onEditTableOrder:(table:string,columnOrder:string[])=>void;onEditTableSticky:(table:string,stickyColumns:string[])=>void}) { - return {!detailCollapsed ? : null}; -} diff --git a/src/node_service/web/src/theme.tsx b/src/node_service/web/src/theme.tsx deleted file mode 100644 index 6915dd9..0000000 --- a/src/node_service/web/src/theme.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import {createTheme} from '@mui/material/styles'; -export const theme=createTheme({ - palette:{mode:'light',primary:{main:'#2563eb'},success:{main:'#059669'},warning:{main:'#d97706'},error:{main:'#dc2626'},background:{default:'#f3f6fb',paper:'#ffffff'}}, - shape:{borderRadius:14}, - typography:{fontFamily:'Segoe UI,Microsoft YaHei,Arial,sans-serif',h5:{fontWeight:800},h6:{fontWeight:800}}, - components:{MuiPaper:{styleOverrides:{root:{backgroundImage:'none'}}},MuiButton:{styleOverrides:{root:{textTransform:'none',fontWeight:700}}},MuiTableCell:{styleOverrides:{head:{fontWeight:800,color:'#475569',background:'#f8fafc'},root:{whiteSpace:'nowrap'}}}} -}); diff --git a/src/node_service/web/src/types.tsx b/src/node_service/web/src/types.tsx deleted file mode 100644 index d9f196f..0000000 --- a/src/node_service/web/src/types.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import type React from 'react'; -export type EnumOption={value:string;label:string}; -export type StrategyParameterDef={key:string;label:string;type:'number'|'boolean'|'string';default?:number|string|boolean;min?:number;max?:number;step?:number;unit?:string;help?:string}; -export type StrategyDefinition={id:string;name:string;description?:string;formula_latex?:string;parameters:StrategyParameterDef[]}; -export type ProbeResult={success?:boolean;delay_ms?:number;http_status?:number;error?:string}; -export type Probe={target:string;url?:string;success?:boolean;delay_ms?:number;error?:string;result?:ProbeResult}; -export type NodeState={name:string;special_proxy_kind?:'direct'|'group'|'unknown';type?:string;provider?:string;exit_ipv4?:string;exit_ipv6?:string;ai_score?:number;risk?:number;risk_level?:string;multiplier?:number;multiplier_text?:string;shared_users?:string;ip_type?:string;native_type?:string;country?:string;alive?:boolean;alive_category?:string;alive_category_label?:string;stability_category?:string;stability_category_label?:string;total_rate_category?:string;total_rate_category_label?:string;recent_rate_category?:string;recent_rate_category_label?:string;ai_category?:string;ai_category_label?:string;ip_type_category?:string;ip_type_category_label?:string;risk_category?:string;risk_category_label?:string;shared_category?:string;shared_category_label?:string;native_category?:string;native_category_label?:string;ip_version_category?:string;ip_version_category_label?:string;srtt_ms?:number;rttvar_ms?:number;samples?:number;check_count?:number;success_count?:number;pass_count?:number;failure_count?:number;consecutive_failures?:number;recent_results?:number[];success_rate?:number;recent_check_count?:number;recent_pass_count?:number;recent_success_rate?:number;last_delay_ms?:number;last_success_delay_ms?:number;stability_score?:number;last_error?:string;last_check_time?:string;updated_time?:string;probes?:Probe[]}; -export type ProxyGroup={name:string;type?:string;proxies?:string[];candidates?:string[];current?:string;best_candidate?:string;better_rounds?:number;switch_count?:number;last_switch_unix?:number;last_switch_time?:string;last_decision?:string;last_error?:string;switchable?:boolean;managed?:boolean;strategy?:string}; -export type AutoSwitchConfig={enabled:boolean;skip_direct:boolean;switch_cooldown_seconds:number;fail_switch_count:number;better_confirm_rounds:number;keep_score_threshold:number;weak_score_threshold:number;weak_improve_margin:number;better_improve_margin:number;default_strategy:string;strategy_parameters:Record;managed_groups:string[];group_strategies:Record}; -export type FormControlType='number'|'boolean'|'string'|'enum'; -export type FormControlProtocol={key:string;label:string;type:FormControlType;value?:unknown;enum?:string;min?:number;max?:number;step?:number;unit?:string;help?:string}; -export type FormProtocol={id:string;title?:string;layout?:'inline'|'grid';controls:FormControlProtocol[]}; -export type TableColumnType='action'|'text'|'number'|'integer'|'boolean'|'enum'|'multi_enum'|'score'|'ms'|'percent'|'datetime'|'rate_summary'|'probe_icons'|'form'; -export type TableCellEditSchema={type:'number'|'boolean'|'string'|'enum';enum?:string;min?:number;max?:number;step?:number}; -export type TableSortDirection='asc'|'desc'; -export type TableColumnProtocol={id:string;label:string;type:TableColumnType;enum?:string;label_field?:string;width?:number;visible?:boolean;sortable?:boolean;filterable?:boolean;sort_rule?:string;filter_type?:string;align?:'left'|'center'|'right';editable?:boolean;edit_type?:string;sticky?:'left'|'none';sticky_order?:number;resizable?:boolean}; -export type TableProtocol={id:string;name:string;row_id_field:string;default_sort?:{column:string;direction:TableSortDirection};keyword_fields?:string[];columns:TableColumnProtocol[];column_order_editable?:boolean;sticky_editable?:boolean;layout?:{sticky_header?:boolean;top_scrollbar?:boolean}}; -export type TableProtocolBundle={version:number;enums:Record;tables:Record}; -export type TableRowObject={id:string;values:Record & {_edit?:Record;_forms?:Record};raw:T}; -export type TableRowsBundle={proxy_nodes?:TableRowObject[];proxy_groups?:TableRowObject[];[key:string]:TableRowObject[] | undefined}; -export type ServiceState={started_time?:string;updated_time?:string;controller_origin?:string;config_path?:string;state_dir?:string;web_dir?:string;service_config_path?:string;http_request_log_enabled?:boolean;check_interval_seconds?:number;delay_refresh_interval_seconds?:number;status_text?:string;refresh_done_nodes?:number;refresh_total_nodes?:number;refresh_request_count?:number;last_refresh_request_time?:string;last_refresh_begin_time?:string;last_refresh_end_time?:string;checking?:boolean;force_refresh?:boolean;round?:number;enum_options?:Record;dynamic_filter_options?:Record;switch_strategy_definitions?:StrategyDefinition[];auto_switch?:AutoSwitchConfig;groups?:ProxyGroup[];nodes?:NodeState[];table_protocol?:TableProtocolBundle;table_rows?:TableRowsBundle}; -export type ApiResult={success?:boolean;message?:string;error?:string;state?:ServiceState;config?:Partial;[key:string]:unknown}; -export type PageName='nodes'|'groups'|'config'; -export type TableColumn={id:string;label:string;width?:number;align?:'left'|'center'|'right';sticky?:'left'|'none';sticky_order?:number;resizable?:boolean;render?:(row:T)=>React.ReactNode}; diff --git a/src/node_service/web/src/utils.tsx b/src/node_service/web/src/utils.tsx deleted file mode 100644 index 47adfcc..0000000 --- a/src/node_service/web/src/utils.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import type {EnumOption,NodeState,Probe,ProbeResult,ProxyGroup,ServiceState,StrategyDefinition,TableColumnProtocol,TableProtocol,TableProtocolBundle,TableRowObject,TableSortDirection} from './types.tsx'; -export const emptySwitchConfig={enabled:true,skip_direct:true,switch_cooldown_seconds:120,fail_switch_count:2,better_confirm_rounds:3,keep_score_threshold:75,weak_score_threshold:60,weak_improve_margin:15,better_improve_margin:10,default_strategy:'stability',strategy_parameters:{stability_weight:1,risk_penalty_weight:0.5,multiplier_penalty_weight:10,ai_bonus_weight:2},managed_groups:[],group_strategies:{}}; -export function scoreLevel(score:number):'success'|'warning'|'error' { - if (score >= 80) return 'success'; - if (score >= 50) return 'warning'; - return 'error'; -} -export function scoreText(value:unknown):string { - return Number(value || 0).toFixed(1); -} -export function msText(value:unknown):string { - return `${Number(value || 0).toFixed(0)}ms`; -} -export function rateText(value:unknown):string { - return `${Number(value || 0).toFixed(1)}%`; -} -export function valueText(value:unknown):string { - const text=String(value ?? '').trim(); - return text || '-'; -} -export function riskText(node:NodeState):string { - if (node.risk === undefined || node.risk === null || Number(node.risk) < 0) return valueText(node.risk_level); - return `${node.risk}${node.risk_level ? ` ${node.risk_level}` : ''}`; -} -export function aiText(node:NodeState):string { - return Number(node.ai_score) >= 0 ? `${node.ai_score} 星` : '-'; -} -export function timeText(value:unknown):string { - const text=String(value ?? '').trim(); - if (!text) return '-'; - if (/^\d{4}-\d{2}-\d{2}T/.test(text)) { - const time=new Date(text); - if (!Number.isNaN(time.getTime())) return new Intl.DateTimeFormat('zh-CN',{timeZone:'Asia/Shanghai',hour12:false,year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',second:'2-digit'}).format(time).replaceAll('/','-') + ' 中国时间'; - } - return text; -} -export function normalizeAutoSwitch(input:any) { - const raw=input || {}; - const legacy={stability_weight:Number(raw.combined_stability_weight ?? emptySwitchConfig.strategy_parameters.stability_weight),risk_penalty_weight:Number(raw.combined_risk_penalty_weight ?? emptySwitchConfig.strategy_parameters.risk_penalty_weight),multiplier_penalty_weight:Number(raw.combined_multiplier_penalty_weight ?? emptySwitchConfig.strategy_parameters.multiplier_penalty_weight),ai_bonus_weight:Number(raw.combined_ai_bonus_weight ?? emptySwitchConfig.strategy_parameters.ai_bonus_weight)}; - const strategy_parameters=raw.strategy_parameters && typeof raw.strategy_parameters === 'object' ? {...emptySwitchConfig.strategy_parameters,...raw.strategy_parameters} : legacy; - return {...emptySwitchConfig,...raw,strategy_parameters,managed_groups:Array.isArray(raw.managed_groups) ? raw.managed_groups : [],group_strategies:raw.group_strategies && typeof raw.group_strategies === 'object' ? raw.group_strategies : {}}; -} -export function nodeProbeResult(probe?:Probe):ProbeResult { - return probe?.result || probe || {}; -} -export function nodeRecentRate(node:NodeState):number { - return Number(node.recent_success_rate ?? node.success_rate ?? 0); -} -export function nodeTotalRate(node:NodeState):number { - return Number(node.success_rate ?? 0); -} -export function nodeCheckCount(node:NodeState):number { - return Number(node.check_count ?? node.samples ?? 0); -} -export function nodePassCount(node:NodeState):number { - return Number(node.pass_count ?? node.success_count ?? 0); -} -export function nodeRecentCheckCount(node:NodeState):number { - return Number(node.recent_check_count ?? 0); -} -export function nodeRecentPassCount(node:NodeState):number { - return Number(node.recent_pass_count ?? 0); -} -export function rateSummaryText(node:NodeState):string { - return `总 ${nodePassCount(node)}/${nodeCheckCount(node)} ${rateText(nodeTotalRate(node))},近 ${nodeRecentPassCount(node)}/${nodeRecentCheckCount(node)} ${rateText(nodeRecentRate(node))}`; -} -export function switchStrategyLabel(value:string | undefined,definitions?:StrategyDefinition[]):string { - const item=(definitions || []).find(def=>def.id === value); - if (item) return item.name; - if (value === 'risk_multiplier') return '稳定性 + 倍率 + 风控'; - return '稳定性优先'; -} -export function strategyDefinitions(state:ServiceState):StrategyDefinition[] { - return state.switch_strategy_definitions || [{id:'stability',name:'稳定性优先',description:'只按稳定性评分选择。',formula_latex:'S = stability',parameters:[]},{id:'risk_multiplier',name:'稳定性 + 倍率 + 风控',description:'同时考虑稳定性、风控、倍率和 AI。',formula_latex:'S = stability \\times w_s - risk \\times w_r - multiplier \\times w_m + ai \\times w_a',parameters:[{key:'stability_weight',label:'稳定性权重',type:'number',default:1,min:0,max:10,step:0.1},{key:'risk_penalty_weight',label:'风控惩罚权重',type:'number',default:0.5,min:0,max:10,step:0.1},{key:'multiplier_penalty_weight',label:'倍率惩罚权重',type:'number',default:10,min:0,max:100,step:0.1},{key:'ai_bonus_weight',label:'AI星级奖励权重',type:'number',default:2,min:0,max:20,step:0.1}]}]; -} -export function multiplierText(node:NodeState):string { - if (node.multiplier_text) return node.multiplier_text; - return Number(node.multiplier) >= 0 ? `${node.multiplier}倍` : '-'; -} -export function metricValues(state:ServiceState | null) { - const nodes=state?.nodes || []; - const alive=nodes.filter(node=>node.alive).length; - const good=nodes.filter(node=>node.stability_category === 'excellent').length; - const bad=nodes.filter(node=>node.stability_category === 'poor').length; - const avg=nodes.length ? nodes.reduce((sum,node)=>sum + Number(node.stability_score || 0),0) / nodes.length : 0; - const recentRate=nodes.length ? nodes.reduce((sum,node)=>sum + nodeRecentRate(node),0) / nodes.length : 0; - return {nodes:nodes.length,alive,good,bad,avg,recentRate}; -} -export function tableProtocol(state:ServiceState,tableId:string):TableProtocol | null { - return state.table_protocol?.tables?.[tableId] || null; -} -export function tableRows(state:ServiceState,tableId:string):TableRowObject[] { - return (state.table_rows?.[tableId] || []) as TableRowObject[]; -} -export function tableEnums(state:ServiceState):Record { - return state.table_protocol?.enums || state.enum_options || {}; -} -export function enumOptions(state:ServiceState,enumName?:string):EnumOption[] { - if (!enumName) return []; - return tableEnums(state)[enumName] || state.dynamic_filter_options?.[enumName] || []; -} -export function rowValue(row:TableRowObject,column:TableColumnProtocol):any { - return row.values?.[column.id]; -} -export function rowLabel(row:TableRowObject,column:TableColumnProtocol,state:ServiceState):string { - if (column.label_field && row.values?.[column.label_field] !== undefined) return valueText(row.values[column.label_field]); - const value=rowValue(row,column); - if (column.type === 'form') { - const form=row.values?._forms?.[column.id]; - if (!form?.controls?.length) return '无需参数'; - return form.controls.map(control=>`${control.label}:${valueText((value || {})[control.key] ?? control.value)}`).join(' '); - } - if (column.type === 'enum' && column.enum) { - const found=enumOptions(state,column.enum).find(option=>option.value === value); - return found?.label || valueText(value); - } - if (Array.isArray(value)) return value.join(', '); - return valueText(value); -} -export function protocolKeywordMatch(row:TableRowObject,protocol:TableProtocol,keyword:string,state:ServiceState):boolean { - const q=keyword.trim().toLowerCase(); - if (!q) return true; - const fields=protocol.keyword_fields?.length ? protocol.keyword_fields : protocol.columns.map(column=>column.id); - const text=fields.map(field=>{ - const column=protocol.columns.find(item=>item.id === field) || {id:field,label:field,type:'text'} as TableColumnProtocol; - return rowLabel(row,column,state); - }).join(' ').toLowerCase(); - return text.includes(q); -} -export function protocolFilterMatch(row:TableRowObject,filters:Record,protocol:TableProtocol,state:ServiceState):boolean { - for (const [key,value] of Object.entries(filters)) { - if (!value || value === 'all') continue; - const column=protocol.columns.find(item=>item.id === key); - const cell=row.values?.[key]; - if (column?.type === 'multi_enum' || Array.isArray(cell)) { - if (!Array.isArray(cell) || !cell.includes(value)) return false; - } - else if (column?.filter_type === 'text') { - if (!rowLabel(row,column,state).toLowerCase().includes(value.toLowerCase())) return false; - } - else if (String(cell ?? 'unknown') !== value) return false; - } - return true; -} -function enumRank(state:ServiceState,column:TableColumnProtocol,value:any):number { - const options=enumOptions(state,column.enum); - const index=options.findIndex(option=>option.value === value); - return index < 0 ? options.length + 1 : index; -} -function comparable(row:TableRowObject,column:TableColumnProtocol,state:ServiceState):number | string { - const value=row.values?.[column.id]; - if (column.id === 'rate_summary') return Number(value?.recent_success_rate ?? 0); - if (column.type === 'enum') return enumRank(state,column,value); - if (column.type === 'number' || column.type === 'integer' || column.type === 'score' || column.type === 'ms' || column.type === 'percent') return Number(value ?? 0); - return rowLabel(row,column,state); -} -export function sortProtocolRows(rows:TableRowObject[],protocol:TableProtocol,state:ServiceState,sortColumn:string,sortDirection:TableSortDirection):TableRowObject[] { - const column=protocol.columns.find(item=>item.id === sortColumn) || protocol.columns[0]; - const direction=sortDirection === 'desc' ? -1 : 1; - return [...rows].sort((a,b)=>{ - const av=comparable(a,column,state); - const bv=comparable(b,column,state); - if (typeof av === 'number' && typeof bv === 'number') return (av - bv) * direction; - return String(av).localeCompare(String(bv),'zh-Hans-CN') * direction; - }); -} -export function defaultSortValue(protocol:TableProtocol):string { - return `${protocol.default_sort?.column || protocol.columns.find(column=>column.sortable)?.id || protocol.columns[0]?.id}:${protocol.default_sort?.direction || 'asc'}`; -} -export function columnDefaultDirection(column:TableColumnProtocol):TableSortDirection { - if (column.sort_rule?.includes('desc')) return 'desc'; - return 'asc'; -} diff --git a/src/node_service/web/tsconfig.json b/src/node_service/web/tsconfig.json deleted file mode 100644 index 95f0482..0000000 --- a/src/node_service/web/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": [ - "DOM", - "DOM.Iterable", - "ES2021" - ], - "allowJs": false, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": false, - "forceConsistentCasingInFileNames": true, - "module": "ESNext", - "moduleResolution": "Node", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - "allowImportingTsExtensions": true - }, - "include": [ - "src" - ] -} \ No newline at end of file diff --git a/src/node_service/web/vite.config.ts b/src/node_service/web/vite.config.ts deleted file mode 100644 index fc6a0d4..0000000 --- a/src/node_service/web/vite.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {defineConfig} from 'vite'; -import react from '@vitejs/plugin-react'; -export default defineConfig({ - plugins:[react()], - build:{outDir:'dist',emptyOutDir:true,assetsDir:'assets'}, - server:{host:'127.0.0.1',port:5173,proxy:{'/api':'http://127.0.0.1:18088'}} -}); diff --git a/yml/base_ruler.yaml b/yml/base_ruler.yaml index cc6ac5e..7a8a578 100644 --- a/yml/base_ruler.yaml +++ b/yml/base_ruler.yaml @@ -71,7 +71,7 @@ tun: - 192.168.0.0/16 - 127.0.0.0/8 exclude-interface: - - et_11_6lqc + - easy_tier - VMware Network Adapter VMnet1 - VMware Network Adapter VMnet8 - ZeroTier One