From 488f022c052705d9b83e8967a7c1ac0a91365d17 Mon Sep 17 00:00:00 2001 From: susu-Sugar Date: Mon, 26 Oct 2026 17:17:09 +0000 Subject: [PATCH] feat(media-unlock): add Muse AI (muse.ai) detection (#8411) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(media-unlock): add Muse AI detection muse.ai redirects every region to Meta's aymh anti-bot gateway before the app loads, and a plain HTTP client cannot finish the JavaScript challenge. Report the result as a new Risky status (exit reachable, availability unverifiable) together with the exit region instead of a misleading Yes/No. Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com> * style(media-unlock): apply rustfmt and biome formatting Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com> * docs(changelog): Update changelog Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com> --------- Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com> Co-authored-by: susu-Sugar <214122492+susu-Sugar@users.noreply.github.com> Co-authored-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com> --- Changelog.md | 1 + crates/clash-verge-media-unlock/src/lib.rs | 5 +++ crates/clash-verge-media-unlock/src/muse.rs | 48 +++++++++++++++++++++ src/locales/ar/tests.json | 1 + src/locales/de/tests.json | 1 + src/locales/en/tests.json | 1 + src/locales/es/tests.json | 1 + src/locales/fa/tests.json | 1 + src/locales/id/tests.json | 1 + src/locales/jp/tests.json | 1 + src/locales/ko/tests.json | 1 + src/locales/pt/tests.json | 1 + src/locales/ru/tests.json | 1 + src/locales/tr/tests.json | 1 + src/locales/tt/tests.json | 1 + src/locales/zh/tests.json | 1 + src/locales/zhtw/tests.json | 1 + src/pages/unlock.tsx | 6 ++- 18 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 crates/clash-verge-media-unlock/src/muse.rs diff --git a/Changelog.md b/Changelog.md index 1f20c1e2f..21b435d07 100644 --- a/Changelog.md +++ b/Changelog.md @@ -19,6 +19,7 @@ - 新增虚拟网卡设置的「DNS 模式」选项,支持配置 mihomo 系统 DNS 行为 - 新增「IP 信息」自动刷新开关,关闭后仍可手动刷新 +- 新增 Muse AI 解锁检测,针对 Muse AI 特有的 Cloudflare 人机验证增加「Risky」状态标记 diff --git a/crates/clash-verge-media-unlock/src/lib.rs b/crates/clash-verge-media-unlock/src/lib.rs index 39c05103b..b4e4a5c1e 100644 --- a/crates/clash-verge-media-unlock/src/lib.rs +++ b/crates/clash-verge-media-unlock/src/lib.rs @@ -11,6 +11,7 @@ mod chatgpt; mod claude; mod disney_plus; mod gemini; +mod muse; mod netflix; mod prime_video; mod spotify; @@ -30,6 +31,7 @@ enum UnlockCheck { ChatGptWeb, Claude, Gemini, + MuseAi, YouTubePremium, BahamutAnime, Netflix, @@ -46,6 +48,7 @@ impl UnlockCheck { Self::ChatGptWeb, Self::Claude, Self::Gemini, + Self::MuseAi, Self::YouTubePremium, Self::BahamutAnime, Self::Netflix, @@ -66,6 +69,7 @@ impl UnlockCheck { Self::ChatGptWeb => chatgpt::CHATGPT_WEB_NAME, Self::Claude => claude::CLAUDE_NAME, Self::Gemini => gemini::GEMINI_NAME, + Self::MuseAi => muse::MUSE_AI_NAME, Self::YouTubePremium => youtube::YOUTUBE_PREMIUM_NAME, Self::BahamutAnime => bahamut::BAHAMUT_ANIME_NAME, Self::Netflix => netflix::NETFLIX_NAME, @@ -83,6 +87,7 @@ impl UnlockCheck { Self::ChatGptWeb => chatgpt::check_chatgpt(client).await, Self::Claude => claude::check_claude(client).await, Self::Gemini => gemini::check_gemini(client).await, + Self::MuseAi => muse::check_muse_ai(client).await, Self::YouTubePremium => youtube::check_youtube_premium(client).await, Self::BahamutAnime => bahamut::check_bahamut_anime(client).await, Self::Netflix => netflix::check_netflix(client).await, diff --git a/crates/clash-verge-media-unlock/src/muse.rs b/crates/clash-verge-media-unlock/src/muse.rs new file mode 100644 index 000000000..c9dc23cbc --- /dev/null +++ b/crates/clash-verge-media-unlock/src/muse.rs @@ -0,0 +1,48 @@ +use reqwest::Client; + +use crate::utils::{classify_restricted_status, get_trace_location}; + +use super::UnlockItem; + +pub(crate) const MUSE_AI_NAME: &str = "Muse AI"; + +const MUSE_URL: &str = "https://muse.ai"; +const CLOUDFLARE_TRACE_URL: &str = "https://www.cloudflare.com/cdn-cgi/trace"; + +// The aymh ("Are You a Human") gateway answers plain HTTP requests with a +// generic Meta error page that always contains the Facebook logo asset. +const AYMH_ERROR_MARKERS: &[&str] = &["Sorry, something went wrong", "static.facebook.com/images/logos"]; + +pub(super) async fn check_muse_ai(client: &Client) -> UnlockItem { + // muse.ai itself is served by Vercel, so resolve the exit region through + // the generic Cloudflare trace endpoint instead of a service-specific one. + let region = get_trace_location(client, CLOUDFLARE_TRACE_URL) + .await + .map(|code| code.trim().to_ascii_uppercase()) + .map(|code| UnlockItem::region_label(&code)); + + let Ok(response) = client.get(MUSE_URL).send().await else { + return UnlockItem::checked(MUSE_AI_NAME, "Failed (Network Connection)", region); + }; + + let status = response.status(); + let landed_on_aymh = response.url().host_str() == Some("auth.muse.ai"); + let body = response.text().await.unwrap_or_default(); + + // Every region is redirected to the aymh anti-bot gateway before the app + // loads, and a non-browser HTTP client cannot finish the JavaScript + // challenge. It returns the Meta error page, so reaching this point only + // proves the exit can connect to Muse's auth infrastructure; availability + // cannot be verified over plain HTTP and is reported as `Risky`. + let behind_anti_bot = landed_on_aymh || AYMH_ERROR_MARKERS.iter().any(|marker| body.contains(marker)); + + let result = if behind_anti_bot { + "Risky" + } else if status.is_success() { + "Yes" + } else { + classify_restricted_status(status).unwrap_or("Failed") + }; + + UnlockItem::checked(MUSE_AI_NAME, result, region) +} diff --git a/src/locales/ar/tests.json b/src/locales/ar/tests.json index 31b740623..ac79e18a6 100644 --- a/src/locales/ar/tests.json +++ b/src/locales/ar/tests.json @@ -36,6 +36,7 @@ "yes": "Yes", "no": "No", "failed": "Failed", + "risky": "غير مؤكد", "completed": "Completed", "disallowedIsp": "Disallowed ISP", "originalsOnly": "Originals Only", diff --git a/src/locales/de/tests.json b/src/locales/de/tests.json index a2dab9f79..1b7566c75 100644 --- a/src/locales/de/tests.json +++ b/src/locales/de/tests.json @@ -36,6 +36,7 @@ "yes": "Unterstützt", "no": "Nicht unterstützt", "failed": "Test fehlgeschlagen", + "risky": "Ungewiss", "completed": "Prüfung abgeschlossen", "disallowedIsp": "Nicht zugelassener Internetdienstanbieter", "originalsOnly": "Nur Original", diff --git a/src/locales/en/tests.json b/src/locales/en/tests.json index 627bdbb1c..897fc0f7e 100644 --- a/src/locales/en/tests.json +++ b/src/locales/en/tests.json @@ -36,6 +36,7 @@ "yes": "Yes", "no": "No", "failed": "Failed", + "risky": "Risky", "completed": "Completed", "disallowedIsp": "Disallowed ISP", "originalsOnly": "Originals Only", diff --git a/src/locales/es/tests.json b/src/locales/es/tests.json index 8e17072ad..825050f31 100644 --- a/src/locales/es/tests.json +++ b/src/locales/es/tests.json @@ -36,6 +36,7 @@ "yes": "Soportado", "no": "No soportado", "failed": "Prueba fallida", + "risky": "Dudoso", "completed": "Detección completada", "disallowedIsp": "Proveedor de servicios de Internet no permitido", "originalsOnly": "Solo originales", diff --git a/src/locales/fa/tests.json b/src/locales/fa/tests.json index 5ed5b3774..2e873d53f 100644 --- a/src/locales/fa/tests.json +++ b/src/locales/fa/tests.json @@ -36,6 +36,7 @@ "yes": "Yes", "no": "No", "failed": "Failed", + "risky": "نامشخص", "completed": "Completed", "disallowedIsp": "Disallowed ISP", "originalsOnly": "Originals Only", diff --git a/src/locales/id/tests.json b/src/locales/id/tests.json index 88c7e0c21..2b9ad55e6 100644 --- a/src/locales/id/tests.json +++ b/src/locales/id/tests.json @@ -36,6 +36,7 @@ "yes": "Yes", "no": "No", "failed": "Failed", + "risky": "Tidak pasti", "completed": "Completed", "disallowedIsp": "Disallowed ISP", "originalsOnly": "Originals Only", diff --git a/src/locales/jp/tests.json b/src/locales/jp/tests.json index 5df0b5302..4c21794a6 100644 --- a/src/locales/jp/tests.json +++ b/src/locales/jp/tests.json @@ -36,6 +36,7 @@ "yes": "サポートする", "no": "サポートしない", "failed": "テストに失敗しました。", + "risky": "要確認", "completed": "検査完了", "disallowedIsp": "許可されていないインターネットサービスプロバイダー", "originalsOnly": "オリジナルのみ", diff --git a/src/locales/ko/tests.json b/src/locales/ko/tests.json index 9e92684c6..0db0f1fe8 100644 --- a/src/locales/ko/tests.json +++ b/src/locales/ko/tests.json @@ -36,6 +36,7 @@ "yes": "예", "no": "아니오", "failed": "실패", + "risky": "확인 필요", "completed": "완료", "disallowedIsp": "허용되지 않는 ISP", "originalsOnly": "오리지널만", diff --git a/src/locales/pt/tests.json b/src/locales/pt/tests.json index 20c04d542..bb8207025 100644 --- a/src/locales/pt/tests.json +++ b/src/locales/pt/tests.json @@ -36,6 +36,7 @@ "yes": "Sim", "no": "Não", "failed": "Falhou", + "risky": "Incerto", "completed": "Concluído", "disallowedIsp": "ISP não permitido", "originalsOnly": "Somente originais", diff --git a/src/locales/ru/tests.json b/src/locales/ru/tests.json index d8fbb5f37..db8783b37 100644 --- a/src/locales/ru/tests.json +++ b/src/locales/ru/tests.json @@ -36,6 +36,7 @@ "yes": "Да", "no": "Нет", "failed": "Ошибка", + "risky": "Неясно", "completed": "Завершено", "disallowedIsp": "Провайдер заблокирован", "originalsOnly": "Только Originals", diff --git a/src/locales/tr/tests.json b/src/locales/tr/tests.json index 0e36619cf..f8ce75f9a 100644 --- a/src/locales/tr/tests.json +++ b/src/locales/tr/tests.json @@ -36,6 +36,7 @@ "yes": "Evet", "no": "Hayır", "failed": "Başarısız", + "risky": "Belirsiz", "completed": "Tamamlandı", "disallowedIsp": "İzin Verilmeyen ISP", "originalsOnly": "Yalnızca Orijinaller", diff --git a/src/locales/tt/tests.json b/src/locales/tt/tests.json index 445776f93..c0e6f196a 100644 --- a/src/locales/tt/tests.json +++ b/src/locales/tt/tests.json @@ -36,6 +36,7 @@ "yes": "Yes", "no": "No", "failed": "Failed", + "risky": "Билгесез", "completed": "Completed", "disallowedIsp": "Disallowed ISP", "originalsOnly": "Originals Only", diff --git a/src/locales/zh/tests.json b/src/locales/zh/tests.json index a48820e7c..95bcb33b1 100644 --- a/src/locales/zh/tests.json +++ b/src/locales/zh/tests.json @@ -36,6 +36,7 @@ "yes": "支持", "no": "不支持", "failed": "测试失败", + "risky": "存疑", "completed": "检测完成", "disallowedIsp": "不允许的 ISP", "originalsOnly": "仅限原创", diff --git a/src/locales/zhtw/tests.json b/src/locales/zhtw/tests.json index 9a846caf2..8977b2c72 100644 --- a/src/locales/zhtw/tests.json +++ b/src/locales/zhtw/tests.json @@ -36,6 +36,7 @@ "yes": "支援", "no": "不支援", "failed": "測試失敗", + "risky": "存疑", "completed": "檢測完成", "disallowedIsp": "不允許的網際網路服務供應商", "originalsOnly": "僅限原創", diff --git a/src/pages/unlock.tsx b/src/pages/unlock.tsx index 254994bc1..642339a95 100644 --- a/src/pages/unlock.tsx +++ b/src/pages/unlock.tsx @@ -40,6 +40,7 @@ const STATUS_LABEL_KEYS: Record = { Yes: 'tests.statuses.test.yes', No: 'tests.statuses.test.no', Failed: 'tests.statuses.test.failed', + Risky: 'tests.statuses.test.risky', Completed: 'tests.statuses.test.completed', 'Disallowed ISP': 'tests.statuses.test.disallowedIsp', 'Originals Only': 'tests.statuses.test.originalsOnly', @@ -196,7 +197,7 @@ const UnlockPage = () => { if (status === 'Pending') return 'default' if (status === 'Yes') return 'success' if (status === 'No') return 'error' - if (status === 'Soon') return 'warning' + if (status === 'Soon' || status === 'Risky') return 'warning' if (status.includes('Failed')) return 'error' if (status === 'Completed') return 'info' if ( @@ -222,7 +223,8 @@ const UnlockPage = () => { const getStatusBorderColor = (status: string) => { if (status === 'Yes') return theme.palette.success.main if (status === 'No') return theme.palette.error.main - if (status === 'Soon') return theme.palette.warning.main + if (status === 'Soon' || status === 'Risky') + return theme.palette.warning.main if (status.includes('Failed')) return theme.palette.error.main if (status === 'Completed') return theme.palette.info.main return theme.palette.divider