Files
Aether/apps/aether-gateway/src/handlers/admin/provider/oauth/duplicates.rs
T
fawney19 c5c56ff92f feat(oauth): 允许替换已失效的活跃 OAuth 账号并同步 status_snapshot
- 活跃但 token 已过期或刷新失败的重复账号视为可替换
- 清除失效标记、刷新配额时同步更新 status_snapshot.oauth
- oauth_invalid 清除接口同时识别 invalid_at 与 invalid_reason 两种标记
- 批量导入任务状态区分 created_count / replaced_count,前端据此展示新增/替换统计
- 补充重复替换场景的集成测试,用量测试等待超时从 10s 提升到 30s 以适应并行压力
2026-04-20 22:59:02 +08:00

265 lines
9.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::handlers::admin::request::AdminAppState;
use crate::provider_key_auth::provider_key_is_oauth_managed;
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey;
use std::time::{SystemTime, UNIX_EPOCH};
fn normalize_codex_plan_group_for_provider_oauth(
plan_type: Option<&serde_json::Value>,
) -> Option<String> {
let normalized = plan_type
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())?
.to_ascii_lowercase();
match normalized.as_str() {
"free" => Some("free".to_string()),
"team" | "plus" | "enterprise" => Some("team_plus_enterprise".to_string()),
_ => None,
}
}
fn normalize_provider_oauth_identity_value(value: Option<&serde_json::Value>) -> Option<String> {
value
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
fn is_codex_provider_oauth_provider_type(value: Option<&serde_json::Value>) -> bool {
value
.and_then(serde_json::Value::as_str)
.map(str::trim)
.is_some_and(|provider_type| provider_type.eq_ignore_ascii_case("codex"))
}
fn match_codex_provider_oauth_identity(
new_auth_config: &serde_json::Map<String, serde_json::Value>,
existing_auth_config: &serde_json::Map<String, serde_json::Value>,
) -> Option<bool> {
let new_provider_type = new_auth_config.get("provider_type");
let existing_provider_type = existing_auth_config.get("provider_type");
if !is_codex_provider_oauth_provider_type(new_provider_type)
&& !is_codex_provider_oauth_provider_type(existing_provider_type)
{
return None;
}
let new_account_user_id =
normalize_provider_oauth_identity_value(new_auth_config.get("account_user_id"));
let existing_account_user_id =
normalize_provider_oauth_identity_value(existing_auth_config.get("account_user_id"));
if let (Some(new_account_user_id), Some(existing_account_user_id)) =
(new_account_user_id, existing_account_user_id)
{
return Some(new_account_user_id == existing_account_user_id);
}
let new_account_id = normalize_provider_oauth_identity_value(new_auth_config.get("account_id"));
let existing_account_id =
normalize_provider_oauth_identity_value(existing_auth_config.get("account_id"));
let new_user_id = normalize_provider_oauth_identity_value(new_auth_config.get("user_id"));
let existing_user_id =
normalize_provider_oauth_identity_value(existing_auth_config.get("user_id"));
let new_email = normalize_provider_oauth_identity_value(new_auth_config.get("email"));
let existing_email = normalize_provider_oauth_identity_value(existing_auth_config.get("email"));
if let (Some(new_account_id), Some(existing_account_id)) =
(new_account_id.as_deref(), existing_account_id.as_deref())
{
if new_account_id != existing_account_id {
return Some(false);
}
}
if let (
Some(new_account_id),
Some(existing_account_id),
Some(new_user_id),
Some(existing_user_id),
) = (
new_account_id.as_deref(),
existing_account_id.as_deref(),
new_user_id.as_deref(),
existing_user_id.as_deref(),
) {
return Some(new_account_id == existing_account_id && new_user_id == existing_user_id);
}
if let (
Some(new_account_id),
Some(existing_account_id),
Some(new_email),
Some(existing_email),
) = (
new_account_id.as_deref(),
existing_account_id.as_deref(),
new_email.as_deref(),
existing_email.as_deref(),
) {
return Some(new_account_id == existing_account_id && new_email == existing_email);
}
None
}
fn is_codex_cross_plan_group_non_duplicate(
new_auth_config: &serde_json::Map<String, serde_json::Value>,
existing_auth_config: &serde_json::Map<String, serde_json::Value>,
) -> bool {
let new_provider_type = new_auth_config.get("provider_type");
let existing_provider_type = existing_auth_config.get("provider_type");
if !is_codex_provider_oauth_provider_type(new_provider_type)
&& !is_codex_provider_oauth_provider_type(existing_provider_type)
{
return false;
}
let new_group = normalize_codex_plan_group_for_provider_oauth(new_auth_config.get("plan_type"));
let existing_group =
normalize_codex_plan_group_for_provider_oauth(existing_auth_config.get("plan_type"));
matches!(
(new_group.as_deref(), existing_group.as_deref()),
(Some(left), Some(right)) if left != right
)
}
fn provider_oauth_invalid_reason_allows_replace(reason: &str) -> bool {
reason.lines().map(str::trim).any(|line| {
line.starts_with("[OAUTH_EXPIRED] ")
|| line.starts_with("[REFRESH_FAILED] ")
|| line.contains("Token 无效或已过期")
|| line.contains("refresh_token 无效、已过期或已撤销")
})
}
fn existing_provider_oauth_key_is_replaceable(existing_key: &StoredProviderCatalogKey) -> bool {
if !existing_key.is_active {
return true;
}
let now_unix_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.ok()
.map(|duration| duration.as_secs())
.unwrap_or(0);
if existing_key
.expires_at_unix_secs
.is_some_and(|expires_at| expires_at <= now_unix_secs)
{
return true;
}
existing_key
.oauth_invalid_reason
.as_deref()
.map(str::trim)
.filter(|reason| !reason.is_empty())
.is_some_and(provider_oauth_invalid_reason_allows_replace)
}
pub(crate) async fn find_duplicate_provider_oauth_key(
state: &AdminAppState<'_>,
provider_id: &str,
auth_config: &serde_json::Map<String, serde_json::Value>,
exclude_key_id: Option<&str>,
) -> Result<Option<StoredProviderCatalogKey>, String> {
let new_email = normalize_provider_oauth_identity_value(auth_config.get("email"));
let new_user_id = normalize_provider_oauth_identity_value(auth_config.get("user_id"));
let new_auth_method = normalize_provider_oauth_identity_value(auth_config.get("auth_method"));
if new_email.is_none() && new_user_id.is_none() {
return Ok(None);
}
let existing_keys = state
.list_provider_catalog_keys_by_provider_ids(&[provider_id.to_string()])
.await
.map_err(|err| format!("{err:?}"))?;
let provider_type = auth_config
.get("provider_type")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.to_string();
for existing_key in existing_keys.into_iter().filter(|key| {
provider_key_is_oauth_managed(key, provider_type.as_str())
&& exclude_key_id.is_none_or(|exclude| key.id != exclude)
}) {
let Some(existing_auth_config) = state.parse_catalog_auth_config_json(&existing_key) else {
continue;
};
let existing_email =
normalize_provider_oauth_identity_value(existing_auth_config.get("email"));
let existing_user_id =
normalize_provider_oauth_identity_value(existing_auth_config.get("user_id"));
let existing_auth_method =
normalize_provider_oauth_identity_value(existing_auth_config.get("auth_method"));
let mut is_duplicate = false;
let codex_identity_match =
match_codex_provider_oauth_identity(auth_config, &existing_auth_config);
if let Some(codex_identity_match) = codex_identity_match {
is_duplicate = codex_identity_match;
}
if codex_identity_match.is_none()
&& !is_duplicate
&& new_user_id.is_some()
&& existing_user_id.is_some()
&& new_user_id == existing_user_id
&& !is_codex_cross_plan_group_non_duplicate(auth_config, &existing_auth_config)
{
is_duplicate = true;
}
if codex_identity_match.is_none()
&& !is_duplicate
&& new_email.is_some()
&& existing_email.is_some()
&& new_email == existing_email
{
let is_kiro = auth_config
.get("provider_type")
.and_then(serde_json::Value::as_str)
.is_some_and(|value| value.eq_ignore_ascii_case("kiro"))
|| existing_auth_config
.get("provider_type")
.and_then(serde_json::Value::as_str)
.is_some_and(|value| value.eq_ignore_ascii_case("kiro"));
if is_kiro {
if new_auth_method.is_some()
&& existing_auth_method.is_some()
&& new_auth_method
.as_deref()
.zip(existing_auth_method.as_deref())
.is_some_and(|(left, right)| left.eq_ignore_ascii_case(right))
{
is_duplicate = true;
}
} else if !is_codex_cross_plan_group_non_duplicate(auth_config, &existing_auth_config) {
is_duplicate = true;
}
}
if !is_duplicate {
continue;
}
if existing_provider_oauth_key_is_replaceable(&existing_key) {
return Ok(Some(existing_key));
}
let identifier =
normalize_provider_oauth_identity_value(auth_config.get("account_user_id"))
.or_else(|| normalize_provider_oauth_identity_value(auth_config.get("account_id")))
.or_else(|| new_email.clone())
.or_else(|| new_user_id.clone())
.unwrap_or_default();
return Err(format!(
"该 OAuth 账号 ({identifier}) 已存在于当前 Provider 中(名称: {})",
existing_key.name
));
}
Ok(None)
}