mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-13 04:29:50 +08:00
Distinguish expired OAuth token status
This commit is contained in:
@@ -4,7 +4,9 @@ use std::sync::{
|
|||||||
Arc, LazyLock,
|
Arc, LazyLock,
|
||||||
};
|
};
|
||||||
|
|
||||||
use aether_admin::provider::pool as admin_provider_pool_pure;
|
use aether_admin::provider::{
|
||||||
|
pool as admin_provider_pool_pure, status as admin_provider_status_pure,
|
||||||
|
};
|
||||||
use aether_data_contracts::repository::candidate_selection::{
|
use aether_data_contracts::repository::candidate_selection::{
|
||||||
StoredMinimalCandidateSelectionRow, StoredPoolKeyCandidateOrder,
|
StoredMinimalCandidateSelectionRow, StoredPoolKeyCandidateOrder,
|
||||||
StoredPoolKeyCandidateRowsByKeyIdsQuery, StoredPoolKeyCandidateRowsQuery,
|
StoredPoolKeyCandidateRowsByKeyIdsQuery, StoredPoolKeyCandidateRowsQuery,
|
||||||
@@ -1217,9 +1219,15 @@ fn pool_key_requires_reauth_for_scheduling(
|
|||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
if !invalid_reason.is_empty() {
|
if !invalid_reason.is_empty() {
|
||||||
if pool_oauth_reason_has_tag(invalid_reason, "[OAUTH_EXPIRED]")
|
let account_state = admin_provider_status_pure::resolve_pool_account_state(
|
||||||
|| pool_oauth_reason_has_tag(invalid_reason, "[ACCOUNT_BLOCK]")
|
None,
|
||||||
{
|
key.upstream_metadata.as_ref(),
|
||||||
|
Some(invalid_reason),
|
||||||
|
);
|
||||||
|
if account_state.blocked && !account_state.recoverable {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if pool_oauth_reason_has_tag(invalid_reason, "[ACCOUNT_BLOCK]") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if pool_oauth_reason_has_tag(invalid_reason, "[REQUEST_FAILED]") {
|
if pool_oauth_reason_has_tag(invalid_reason, "[REQUEST_FAILED]") {
|
||||||
@@ -1230,6 +1238,9 @@ fn pool_key_requires_reauth_for_scheduling(
|
|||||||
.expires_at_unix_secs
|
.expires_at_unix_secs
|
||||||
.is_none_or(|expires_at| expires_at == 0 || expires_at <= now_unix_secs);
|
.is_none_or(|expires_at| expires_at == 0 || expires_at <= now_unix_secs);
|
||||||
}
|
}
|
||||||
|
if pool_oauth_reason_has_tag(invalid_reason, "[OAUTH_EXPIRED]") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3265,8 +3276,7 @@ mod tests {
|
|||||||
|
|
||||||
let mut key_a_invalid = sample_codex_pool_key("provider-a", "key-a-invalid");
|
let mut key_a_invalid = sample_codex_pool_key("provider-a", "key-a-invalid");
|
||||||
key_a_invalid.oauth_invalid_at_unix_secs = Some(1_710_000_000);
|
key_a_invalid.oauth_invalid_at_unix_secs = Some(1_710_000_000);
|
||||||
key_a_invalid.oauth_invalid_reason =
|
key_a_invalid.oauth_invalid_reason = Some("[OAUTH_EXPIRED] token invalidated".to_string());
|
||||||
Some("[OAUTH_EXPIRED] Codex Token 无效或已过期 (401)".to_string());
|
|
||||||
let exhausted_status_snapshot = json!({
|
let exhausted_status_snapshot = json!({
|
||||||
"quota": {
|
"quota": {
|
||||||
"provider_type": "codex",
|
"provider_type": "codex",
|
||||||
@@ -3370,8 +3380,7 @@ mod tests {
|
|||||||
|
|
||||||
let mut key_a_invalid = sample_codex_pool_key("provider-a", "key-a-invalid");
|
let mut key_a_invalid = sample_codex_pool_key("provider-a", "key-a-invalid");
|
||||||
key_a_invalid.oauth_invalid_at_unix_secs = Some(1_710_000_000);
|
key_a_invalid.oauth_invalid_at_unix_secs = Some(1_710_000_000);
|
||||||
key_a_invalid.oauth_invalid_reason =
|
key_a_invalid.oauth_invalid_reason = Some("[OAUTH_EXPIRED] token invalidated".to_string());
|
||||||
Some("[OAUTH_EXPIRED] Codex Token 无效或已过期 (401)".to_string());
|
|
||||||
key_a_invalid.status_snapshot = Some(json!({
|
key_a_invalid.status_snapshot = Some(json!({
|
||||||
"quota": {
|
"quota": {
|
||||||
"provider_type": "codex",
|
"provider_type": "codex",
|
||||||
@@ -3463,6 +3472,9 @@ mod tests {
|
|||||||
key.oauth_invalid_reason = Some("[REQUEST_FAILED] 账号状态检查失败".to_string());
|
key.oauth_invalid_reason = Some("[REQUEST_FAILED] 账号状态检查失败".to_string());
|
||||||
key.oauth_invalid_at_unix_secs = Some(100);
|
key.oauth_invalid_at_unix_secs = Some(100);
|
||||||
assert!(!pool_key_requires_reauth_for_scheduling(&key, 300));
|
assert!(!pool_key_requires_reauth_for_scheduling(&key, 300));
|
||||||
|
|
||||||
|
key.oauth_invalid_reason = Some("[OAUTH_EXPIRED] session expired".to_string());
|
||||||
|
assert!(!pool_key_requires_reauth_for_scheduling(&key, 300));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3471,6 +3483,10 @@ mod tests {
|
|||||||
key.oauth_invalid_reason = Some("[ACCOUNT_BLOCK] account has been deactivated".to_string());
|
key.oauth_invalid_reason = Some("[ACCOUNT_BLOCK] account has been deactivated".to_string());
|
||||||
assert!(pool_key_requires_reauth_for_scheduling(&key, 100));
|
assert!(pool_key_requires_reauth_for_scheduling(&key, 100));
|
||||||
|
|
||||||
|
key.oauth_invalid_reason = Some("[OAUTH_EXPIRED] token invalidated".to_string());
|
||||||
|
key.oauth_invalid_at_unix_secs = None;
|
||||||
|
assert!(pool_key_requires_reauth_for_scheduling(&key, 100));
|
||||||
|
|
||||||
key.oauth_invalid_reason = Some("Kiro Token 无效或已过期".to_string());
|
key.oauth_invalid_reason = Some("Kiro Token 无效或已过期".to_string());
|
||||||
key.oauth_invalid_at_unix_secs = None;
|
key.oauth_invalid_at_unix_secs = None;
|
||||||
assert!(pool_key_requires_reauth_for_scheduling(&key, 100));
|
assert!(pool_key_requires_reauth_for_scheduling(&key, 100));
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ fn oauth_invalid_reason_is_account_level_block(reason: Option<&str>) -> bool {
|
|||||||
snapshot.blocked
|
snapshot.blocked
|
||||||
&& !matches!(
|
&& !matches!(
|
||||||
snapshot.code.trim().to_ascii_lowercase().as_str(),
|
snapshot.code.trim().to_ascii_lowercase().as_str(),
|
||||||
"oauth_token_invalid" | "oauth_expired" | "oauth_refresh_failed"
|
"oauth_token_invalid"
|
||||||
|
| "oauth_token_expired"
|
||||||
|
| "oauth_expired"
|
||||||
|
| "oauth_refresh_failed"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ pub(super) fn codex_looks_like_token_invalidated(message: Option<&str>) -> bool
|
|||||||
admin_provider_quota_pure::codex_looks_like_token_invalidated(message)
|
admin_provider_quota_pure::codex_looks_like_token_invalidated(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn codex_looks_like_token_expired(message: Option<&str>) -> bool {
|
||||||
|
admin_provider_quota_pure::codex_looks_like_token_expired(message)
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn codex_looks_like_workspace_deactivated(message: Option<&str>) -> bool {
|
pub(super) fn codex_looks_like_workspace_deactivated(message: Option<&str>) -> bool {
|
||||||
admin_provider_quota_pure::codex_looks_like_workspace_deactivated(message)
|
admin_provider_quota_pure::codex_looks_like_workspace_deactivated(message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ mod parse;
|
|||||||
mod plan;
|
mod plan;
|
||||||
|
|
||||||
use self::invalid::{
|
use self::invalid::{
|
||||||
codex_build_invalid_state, codex_looks_like_token_invalidated,
|
codex_build_invalid_state, codex_looks_like_token_expired, codex_looks_like_token_invalidated,
|
||||||
codex_looks_like_workspace_deactivated, codex_soft_request_failure_reason,
|
codex_looks_like_workspace_deactivated, codex_soft_request_failure_reason,
|
||||||
codex_structured_invalid_reason,
|
codex_structured_invalid_reason,
|
||||||
};
|
};
|
||||||
@@ -275,6 +275,7 @@ pub(crate) async fn refresh_codex_provider_quota_locally(
|
|||||||
}
|
}
|
||||||
403 => {
|
403 => {
|
||||||
let candidate_reason = if codex_looks_like_token_invalidated(err_msg.as_deref())
|
let candidate_reason = if codex_looks_like_token_invalidated(err_msg.as_deref())
|
||||||
|
|| codex_looks_like_token_expired(err_msg.as_deref())
|
||||||
{
|
{
|
||||||
codex_structured_invalid_reason(403, err_msg.as_deref())
|
codex_structured_invalid_reason(403, err_msg.as_deref())
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -304,6 +304,7 @@ fn admin_pool_trimmed_string(value: Option<&Value>) -> Option<String> {
|
|||||||
fn admin_pool_account_code_status_filter(code: &str) -> Option<&'static str> {
|
fn admin_pool_account_code_status_filter(code: &str) -> Option<&'static str> {
|
||||||
match code.trim().to_ascii_lowercase().as_str() {
|
match code.trim().to_ascii_lowercase().as_str() {
|
||||||
"oauth_token_invalid" => Some("invalid"),
|
"oauth_token_invalid" => Some("invalid"),
|
||||||
|
"oauth_token_expired" => Some("expired"),
|
||||||
"account_banned" | "account_suspended" => Some("account_banned"),
|
"account_banned" | "account_suspended" => Some("account_banned"),
|
||||||
"account_disabled" => Some("account_disabled"),
|
"account_disabled" => Some("account_disabled"),
|
||||||
"workspace_deactivated" => Some("workspace_deactivated"),
|
"workspace_deactivated" => Some("workspace_deactivated"),
|
||||||
|
|||||||
@@ -265,14 +265,16 @@ fn build_provider_key_oauth_status_snapshot(key: &StoredProviderCatalogKey) -> V
|
|||||||
if let Some(reason) =
|
if let Some(reason) =
|
||||||
tagged_oauth_invalid_reason(invalid_reason.as_deref(), OAUTH_EXPIRED_PREFIX)
|
tagged_oauth_invalid_reason(invalid_reason.as_deref(), OAUTH_EXPIRED_PREFIX)
|
||||||
{
|
{
|
||||||
|
let (code, label) =
|
||||||
|
admin_provider_status_pure::oauth_token_snapshot_status_parts(reason.as_str());
|
||||||
return json!({
|
return json!({
|
||||||
"code": "invalid",
|
"code": code,
|
||||||
"label": "已失效",
|
"label": label,
|
||||||
"reason": reason,
|
"reason": reason,
|
||||||
"expires_at": expires_at_unix_secs,
|
"expires_at": expires_at_unix_secs,
|
||||||
"invalid_at": invalid_at_unix_secs,
|
"invalid_at": invalid_at_unix_secs,
|
||||||
"source": "oauth_invalid",
|
"source": "oauth_invalid",
|
||||||
"requires_reauth": true,
|
"requires_reauth": code == "invalid",
|
||||||
"expiring_soon": false,
|
"expiring_soon": false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2031,7 +2031,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn oauth_invalidation_marks_codex_key_invalid() {
|
async fn oauth_invalidation_marks_codex_key_expired() {
|
||||||
let state = codex_state();
|
let state = codex_state();
|
||||||
let plan = sample_codex_plan();
|
let plan = sample_codex_plan();
|
||||||
|
|
||||||
@@ -2069,7 +2069,7 @@ mod tests {
|
|||||||
.and_then(|value| value.get("oauth"))
|
.and_then(|value| value.get("oauth"))
|
||||||
.and_then(|value| value.get("code"))
|
.and_then(|value| value.get("code"))
|
||||||
.and_then(Value::as_str),
|
.and_then(Value::as_str),
|
||||||
Some("invalid")
|
Some("expired")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -376,9 +376,6 @@ fn oauth_invalid_reason_blocks_scheduling(
|
|||||||
now_unix_secs: u64,
|
now_unix_secs: u64,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let trimmed_reason = invalid_reason.trim();
|
let trimmed_reason = invalid_reason.trim();
|
||||||
if oauth_invalid_reason_has_tag(trimmed_reason, "[OAUTH_EXPIRED]") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let account_state = admin_provider_status_pure::resolve_pool_account_state(
|
let account_state = admin_provider_status_pure::resolve_pool_account_state(
|
||||||
Some(provider_type),
|
Some(provider_type),
|
||||||
@@ -433,6 +430,7 @@ fn oauth_account_state_code_is_hard_block(code: &str) -> bool {
|
|||||||
| "account_forbidden"
|
| "account_forbidden"
|
||||||
| "account_blocked"
|
| "account_blocked"
|
||||||
| "account_verification"
|
| "account_verification"
|
||||||
|
| "oauth_token_invalid"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2064,7 +2064,7 @@ async fn keeps_codex_candidate_selectable_when_oauth_token_is_expired() {
|
|||||||
let mut key = sample_key("key-codex", "provider-codex", Some(10));
|
let mut key = sample_key("key-codex", "provider-codex", Some(10));
|
||||||
key.auth_type = "oauth".to_string();
|
key.auth_type = "oauth".to_string();
|
||||||
key.oauth_invalid_at_unix_secs = Some(1_710_000_000);
|
key.oauth_invalid_at_unix_secs = Some(1_710_000_000);
|
||||||
key.oauth_invalid_reason = Some("Codex Token 无效或已过期".to_string());
|
key.oauth_invalid_reason = Some("[OAUTH_EXPIRED] session expired".to_string());
|
||||||
key
|
key
|
||||||
}],
|
}],
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -171,7 +171,10 @@ fn oauth_invalid_reason_is_account_block(reason: Option<&str>) -> bool {
|
|||||||
snapshot.blocked
|
snapshot.blocked
|
||||||
&& !matches!(
|
&& !matches!(
|
||||||
snapshot.code.trim().to_ascii_lowercase().as_str(),
|
snapshot.code.trim().to_ascii_lowercase().as_str(),
|
||||||
"oauth_token_invalid" | "oauth_expired" | "oauth_refresh_failed"
|
"oauth_token_invalid"
|
||||||
|
| "oauth_token_expired"
|
||||||
|
| "oauth_expired"
|
||||||
|
| "oauth_refresh_failed"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,14 +359,16 @@ fn build_oauth_status_snapshot_value(key: &StoredProviderCatalogKey) -> Value {
|
|||||||
let invalid_reason = trimmed_reason(key.oauth_invalid_reason.as_deref());
|
let invalid_reason = trimmed_reason(key.oauth_invalid_reason.as_deref());
|
||||||
|
|
||||||
if let Some(reason) = tagged_reason(invalid_reason.as_deref(), OAUTH_EXPIRED_PREFIX) {
|
if let Some(reason) = tagged_reason(invalid_reason.as_deref(), OAUTH_EXPIRED_PREFIX) {
|
||||||
|
let (code, label) =
|
||||||
|
aether_admin::provider::status::oauth_token_snapshot_status_parts(reason.as_str());
|
||||||
return json!({
|
return json!({
|
||||||
"code": "invalid",
|
"code": code,
|
||||||
"label": "已失效",
|
"label": label,
|
||||||
"reason": reason,
|
"reason": reason,
|
||||||
"expires_at": expires_at_unix_secs,
|
"expires_at": expires_at_unix_secs,
|
||||||
"invalid_at": invalid_at_unix_secs,
|
"invalid_at": invalid_at_unix_secs,
|
||||||
"source": "oauth_invalid",
|
"source": "oauth_invalid",
|
||||||
"requires_reauth": true,
|
"requires_reauth": code == "invalid",
|
||||||
"expiring_soon": false,
|
"expiring_soon": false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5403,7 +5403,7 @@ async fn gateway_refreshes_admin_provider_oauth_key_locally_with_trusted_admin_p
|
|||||||
assert!(stored_key.oauth_invalid_at_unix_secs.is_some());
|
assert!(stored_key.oauth_invalid_at_unix_secs.is_some());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stored_key.oauth_invalid_reason.as_deref(),
|
stored_key.oauth_invalid_reason.as_deref(),
|
||||||
Some("[OAUTH_EXPIRED] Codex Token 无效或已过期 (401)")
|
Some("[OAUTH_EXPIRED] Codex Token 已过期 (401)")
|
||||||
);
|
);
|
||||||
} else if account_state_recheck_attempted
|
} else if account_state_recheck_attempted
|
||||||
&& payload["account_state_recheck_error"] == "wham/usage API 返回状态码 403"
|
&& payload["account_state_recheck_error"] == "wham/usage API 返回状态码 403"
|
||||||
@@ -5449,7 +5449,40 @@ async fn gateway_refreshes_admin_provider_oauth_key_locally_with_trusted_admin_p
|
|||||||
oauth_snapshot.get("expires_at"),
|
oauth_snapshot.get("expires_at"),
|
||||||
auth_config.get("expires_at")
|
auth_config.get("expires_at")
|
||||||
);
|
);
|
||||||
if stored_key.oauth_invalid_reason.is_some() {
|
if stored_key
|
||||||
|
.oauth_invalid_reason
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|reason| reason.starts_with("[OAUTH_EXPIRED]"))
|
||||||
|
{
|
||||||
|
assert_eq!(
|
||||||
|
oauth_snapshot
|
||||||
|
.get("code")
|
||||||
|
.and_then(serde_json::Value::as_str),
|
||||||
|
Some("expired")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
oauth_snapshot
|
||||||
|
.get("label")
|
||||||
|
.and_then(serde_json::Value::as_str),
|
||||||
|
Some("已过期")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
oauth_snapshot.get("reason"),
|
||||||
|
Some(&json!("Codex Token 已过期 (401)"))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
oauth_snapshot
|
||||||
|
.get("requires_reauth")
|
||||||
|
.and_then(serde_json::Value::as_bool),
|
||||||
|
Some(false)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
oauth_snapshot
|
||||||
|
.get("expiring_soon")
|
||||||
|
.and_then(serde_json::Value::as_bool),
|
||||||
|
Some(false)
|
||||||
|
);
|
||||||
|
} else if stored_key.oauth_invalid_reason.is_some() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
oauth_snapshot
|
oauth_snapshot
|
||||||
.get("code")
|
.get("code")
|
||||||
@@ -5463,8 +5496,11 @@ async fn gateway_refreshes_admin_provider_oauth_key_locally_with_trusted_admin_p
|
|||||||
Some("已失效")
|
Some("已失效")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
oauth_snapshot.get("reason"),
|
oauth_snapshot
|
||||||
Some(&json!("Codex Token 无效或已过期 (401)"))
|
.get("reason")
|
||||||
|
.and_then(serde_json::Value::as_str)
|
||||||
|
.is_some_and(|reason| !reason.trim().is_empty()),
|
||||||
|
true
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
oauth_snapshot
|
oauth_snapshot
|
||||||
|
|||||||
@@ -3526,6 +3526,15 @@ async fn gateway_cleans_up_admin_pool_banned_keys_locally_with_trusted_admin_pri
|
|||||||
);
|
);
|
||||||
banned_key.name = "banned".to_string();
|
banned_key.name = "banned".to_string();
|
||||||
banned_key.oauth_invalid_reason = Some("account_banned".to_string());
|
banned_key.oauth_invalid_reason = Some("account_banned".to_string());
|
||||||
|
let mut oauth_invalidated_key = sample_key(
|
||||||
|
"key-openai-oauth-invalidated",
|
||||||
|
"provider-openai",
|
||||||
|
"openai:chat",
|
||||||
|
"sk-oauth-invalidated",
|
||||||
|
);
|
||||||
|
oauth_invalidated_key.name = "oauth-invalidated".to_string();
|
||||||
|
oauth_invalidated_key.oauth_invalid_reason =
|
||||||
|
Some("[OAUTH_EXPIRED] token invalidated".to_string());
|
||||||
let mut oauth_expired_key = sample_key(
|
let mut oauth_expired_key = sample_key(
|
||||||
"key-openai-oauth-expired",
|
"key-openai-oauth-expired",
|
||||||
"provider-openai",
|
"provider-openai",
|
||||||
@@ -3533,7 +3542,7 @@ async fn gateway_cleans_up_admin_pool_banned_keys_locally_with_trusted_admin_pri
|
|||||||
"sk-oauth-expired",
|
"sk-oauth-expired",
|
||||||
);
|
);
|
||||||
oauth_expired_key.name = "oauth-expired".to_string();
|
oauth_expired_key.name = "oauth-expired".to_string();
|
||||||
oauth_expired_key.oauth_invalid_reason = Some("[OAUTH_EXPIRED] token invalidated".to_string());
|
oauth_expired_key.oauth_invalid_reason = Some("[OAUTH_EXPIRED] session expired".to_string());
|
||||||
let mut healthy_key = sample_key(
|
let mut healthy_key = sample_key(
|
||||||
"key-openai-healthy",
|
"key-openai-healthy",
|
||||||
"provider-openai",
|
"provider-openai",
|
||||||
@@ -3545,7 +3554,12 @@ async fn gateway_cleans_up_admin_pool_banned_keys_locally_with_trusted_admin_pri
|
|||||||
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||||
vec![provider],
|
vec![provider],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![banned_key, oauth_expired_key, healthy_key],
|
vec![
|
||||||
|
banned_key,
|
||||||
|
oauth_invalidated_key,
|
||||||
|
oauth_expired_key,
|
||||||
|
healthy_key,
|
||||||
|
],
|
||||||
));
|
));
|
||||||
|
|
||||||
let (upstream_url, upstream_handle) = start_server(upstream).await;
|
let (upstream_url, upstream_handle) = start_server(upstream).await;
|
||||||
@@ -3575,8 +3589,8 @@ async fn gateway_cleans_up_admin_pool_banned_keys_locally_with_trusted_admin_pri
|
|||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||||
assert_eq!(payload["affected"], 1);
|
assert_eq!(payload["affected"], 2);
|
||||||
assert_eq!(payload["message"], "已清理 1 个异常账号");
|
assert_eq!(payload["message"], "已清理 2 个异常账号");
|
||||||
|
|
||||||
let remaining_keys = provider_catalog_repository
|
let remaining_keys = provider_catalog_repository
|
||||||
.list_keys_by_provider_ids(&["provider-openai".to_string()])
|
.list_keys_by_provider_ids(&["provider-openai".to_string()])
|
||||||
@@ -3586,6 +3600,9 @@ async fn gateway_cleans_up_admin_pool_banned_keys_locally_with_trusted_admin_pri
|
|||||||
assert!(remaining_keys
|
assert!(remaining_keys
|
||||||
.iter()
|
.iter()
|
||||||
.any(|key| key.id == "key-openai-oauth-expired"));
|
.any(|key| key.id == "key-openai-oauth-expired"));
|
||||||
|
assert!(!remaining_keys
|
||||||
|
.iter()
|
||||||
|
.any(|key| key.id == "key-openai-oauth-invalidated"));
|
||||||
assert!(remaining_keys
|
assert!(remaining_keys
|
||||||
.iter()
|
.iter()
|
||||||
.any(|key| key.id == "key-openai-healthy"));
|
.any(|key| key.id == "key-openai-healthy"));
|
||||||
|
|||||||
@@ -948,10 +948,29 @@ pub fn codex_build_invalid_state(
|
|||||||
|
|
||||||
pub fn codex_looks_like_token_invalidated(message: Option<&str>) -> bool {
|
pub fn codex_looks_like_token_invalidated(message: Option<&str>) -> bool {
|
||||||
let lowered = message.unwrap_or_default().trim().to_ascii_lowercase();
|
let lowered = message.unwrap_or_default().trim().to_ascii_lowercase();
|
||||||
lowered.contains("token invalid")
|
lowered.contains("token_invalidated")
|
||||||
|
|| lowered.contains("authentication token has been invalidated")
|
||||||
|
|| lowered.contains("token has been invalidated")
|
||||||
|| lowered.contains("token invalidated")
|
|| lowered.contains("token invalidated")
|
||||||
|| lowered.contains("session has expired")
|
|| lowered.contains("invalidated")
|
||||||
|
|| lowered.contains("revoked")
|
||||||
|
|| lowered.contains("已撤销")
|
||||||
|
|| lowered.contains("被撤销")
|
||||||
|
|| lowered.contains("撤销")
|
||||||
|
|| lowered.contains("作废")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codex_looks_like_token_expired(message: Option<&str>) -> bool {
|
||||||
|
let lowered = message.unwrap_or_default().trim().to_ascii_lowercase();
|
||||||
|
lowered.contains("session has expired")
|
||||||
|| lowered.contains("session expired")
|
|| lowered.contains("session expired")
|
||||||
|
|| lowered.contains("access token expired")
|
||||||
|
|| lowered.contains("expired access token")
|
||||||
|
|| lowered.contains("token has expired")
|
||||||
|
|| lowered.contains("token expired")
|
||||||
|
|| lowered.contains("security token included in the request is expired")
|
||||||
|
|| lowered.contains("已过期")
|
||||||
|
|| lowered.contains("过期")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn codex_looks_like_account_deactivated(message: Option<&str>) -> bool {
|
fn codex_looks_like_account_deactivated(message: Option<&str>) -> bool {
|
||||||
@@ -980,7 +999,15 @@ pub fn codex_structured_invalid_reason(status_code: u16, upstream_message: Optio
|
|||||||
}
|
}
|
||||||
if codex_looks_like_token_invalidated(Some(message)) {
|
if codex_looks_like_token_invalidated(Some(message)) {
|
||||||
let detail = if message.is_empty() {
|
let detail = if message.is_empty() {
|
||||||
"Codex Token 无效或已过期"
|
"Codex Token 已失效"
|
||||||
|
} else {
|
||||||
|
message
|
||||||
|
};
|
||||||
|
return format!("{OAUTH_EXPIRED_PREFIX}{detail}");
|
||||||
|
}
|
||||||
|
if codex_looks_like_token_expired(Some(message)) {
|
||||||
|
let detail = if message.is_empty() {
|
||||||
|
"Codex Token 已过期"
|
||||||
} else {
|
} else {
|
||||||
message
|
message
|
||||||
};
|
};
|
||||||
@@ -988,7 +1015,7 @@ pub fn codex_structured_invalid_reason(status_code: u16, upstream_message: Optio
|
|||||||
}
|
}
|
||||||
if status_code == 401 {
|
if status_code == 401 {
|
||||||
let detail = if message.is_empty() {
|
let detail = if message.is_empty() {
|
||||||
"Codex Token 无效或已过期 (401)"
|
"Codex Token 已过期 (401)"
|
||||||
} else {
|
} else {
|
||||||
message
|
message
|
||||||
};
|
};
|
||||||
@@ -1021,6 +1048,7 @@ pub fn codex_runtime_invalid_reason(
|
|||||||
401 => Some(codex_structured_invalid_reason(401, upstream_message)),
|
401 => Some(codex_structured_invalid_reason(401, upstream_message)),
|
||||||
402 => Some(codex_structured_invalid_reason(402, upstream_message)),
|
402 => Some(codex_structured_invalid_reason(402, upstream_message)),
|
||||||
403 if codex_looks_like_token_invalidated(upstream_message)
|
403 if codex_looks_like_token_invalidated(upstream_message)
|
||||||
|
|| codex_looks_like_token_expired(upstream_message)
|
||||||
|| codex_looks_like_account_deactivated(upstream_message) =>
|
|| codex_looks_like_account_deactivated(upstream_message) =>
|
||||||
{
|
{
|
||||||
Some(codex_structured_invalid_reason(403, upstream_message))
|
Some(codex_structured_invalid_reason(403, upstream_message))
|
||||||
@@ -1848,12 +1876,19 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn auto_remove_structured_reason_keeps_oauth_expired_token_invalid() {
|
fn auto_remove_structured_reason_removes_oauth_token_invalidated() {
|
||||||
assert!(!should_auto_remove_structured_reason(Some(
|
assert!(should_auto_remove_structured_reason(Some(
|
||||||
"[OAUTH_EXPIRED] token invalidated"
|
"[OAUTH_EXPIRED] token invalidated"
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auto_remove_structured_reason_keeps_oauth_token_expired() {
|
||||||
|
assert!(!should_auto_remove_structured_reason(Some(
|
||||||
|
"[OAUTH_EXPIRED] session expired"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn auto_remove_refresh_failed_after_access_token_expiry() {
|
fn auto_remove_refresh_failed_after_access_token_expiry() {
|
||||||
let mut key = StoredProviderCatalogKey::new(
|
let mut key = StoredProviderCatalogKey::new(
|
||||||
@@ -1945,7 +1980,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn oauth_token_invalid_is_not_auto_remove_proof_by_itself() {
|
fn oauth_token_invalid_is_auto_remove_proof_by_itself() {
|
||||||
let mut key = StoredProviderCatalogKey::new(
|
let mut key = StoredProviderCatalogKey::new(
|
||||||
"key-1".to_string(),
|
"key-1".to_string(),
|
||||||
"provider-1".to_string(),
|
"provider-1".to_string(),
|
||||||
@@ -1958,7 +1993,7 @@ mod tests {
|
|||||||
key.expires_at_unix_secs = Some(1_000);
|
key.expires_at_unix_secs = Some(1_000);
|
||||||
key.oauth_invalid_reason = Some("oauth_token_invalid".to_string());
|
key.oauth_invalid_reason = Some("oauth_token_invalid".to_string());
|
||||||
|
|
||||||
assert!(!super::should_auto_remove_oauth_invalid_key(
|
assert!(super::should_auto_remove_oauth_invalid_key(
|
||||||
&key,
|
&key,
|
||||||
Some("oauth_token_invalid"),
|
Some("oauth_token_invalid"),
|
||||||
false,
|
false,
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ const ACCOUNT_BLOCK_REASON_KEYWORDS: &[&str] = &[
|
|||||||
"账户访问被禁止",
|
"账户访问被禁止",
|
||||||
"访问受限",
|
"访问受限",
|
||||||
"账户访问受限",
|
"账户访问受限",
|
||||||
|
"oauth_token_invalid",
|
||||||
|
"oauth_token_expired",
|
||||||
|
"token_invalidated",
|
||||||
|
"session expired",
|
||||||
"authentication token has been invalidated",
|
"authentication token has been invalidated",
|
||||||
"token has been invalidated",
|
"token has been invalidated",
|
||||||
"codex token 无效或已过期",
|
"codex token 无效或已过期",
|
||||||
@@ -43,6 +47,7 @@ const AUTO_REMOVABLE_ACCOUNT_STATE_CODES: &[&str] = &[
|
|||||||
"account_quarantined",
|
"account_quarantined",
|
||||||
"workspace_deactivated",
|
"workspace_deactivated",
|
||||||
"account_forbidden",
|
"account_forbidden",
|
||||||
|
"oauth_token_invalid",
|
||||||
];
|
];
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
@@ -122,8 +127,79 @@ fn looks_like_account_verification(reason: &str) -> bool {
|
|||||||
.any(|keyword| lowered.contains(keyword))
|
.any(|keyword| lowered.contains(keyword))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn oauth_token_reason_is_expired(reason: &str) -> bool {
|
||||||
|
let lowered = reason.trim().to_ascii_lowercase();
|
||||||
|
!lowered.is_empty()
|
||||||
|
&& [
|
||||||
|
"oauth_token_expired",
|
||||||
|
"session has expired",
|
||||||
|
"session expired",
|
||||||
|
"access token expired",
|
||||||
|
"expired access token",
|
||||||
|
"token expired",
|
||||||
|
"token has expired",
|
||||||
|
"security token included in the request is expired",
|
||||||
|
"已过期",
|
||||||
|
"过期",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.any(|keyword| lowered.contains(keyword))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn oauth_token_reason_is_hard_invalid(reason: &str) -> bool {
|
||||||
|
let lowered = reason.trim().to_ascii_lowercase();
|
||||||
|
if lowered.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if [
|
||||||
|
"oauth_token_invalid",
|
||||||
|
"token_invalidated",
|
||||||
|
"authentication token has been invalidated",
|
||||||
|
"token has been invalidated",
|
||||||
|
"token invalidated",
|
||||||
|
"invalidated",
|
||||||
|
"revoked",
|
||||||
|
"已撤销",
|
||||||
|
"被撤销",
|
||||||
|
"撤销",
|
||||||
|
"已作废",
|
||||||
|
"作废",
|
||||||
|
"已失效",
|
||||||
|
"token 失效",
|
||||||
|
"令牌失效",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.any(|keyword| lowered.contains(keyword))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
(lowered.contains("token 无效") || lowered.contains("令牌无效"))
|
||||||
|
&& !oauth_token_reason_is_expired(reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn oauth_token_account_status_parts(reason: &str) -> (&'static str, &'static str) {
|
||||||
|
if oauth_token_reason_is_hard_invalid(reason) {
|
||||||
|
("oauth_token_invalid", "Token 失效")
|
||||||
|
} else {
|
||||||
|
("oauth_token_expired", "Token 过期")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn oauth_token_snapshot_status_parts(reason: &str) -> (&'static str, &'static str) {
|
||||||
|
if oauth_token_reason_is_hard_invalid(reason) {
|
||||||
|
("invalid", "已失效")
|
||||||
|
} else {
|
||||||
|
("expired", "已过期")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn classify_block_reason(reason: &str) -> (&'static str, &'static str) {
|
fn classify_block_reason(reason: &str) -> (&'static str, &'static str) {
|
||||||
let lowered = reason.to_ascii_lowercase();
|
let lowered = reason.to_ascii_lowercase();
|
||||||
|
if oauth_token_reason_is_hard_invalid(reason) || oauth_token_reason_is_expired(reason) {
|
||||||
|
return oauth_token_account_status_parts(reason);
|
||||||
|
}
|
||||||
if [
|
if [
|
||||||
"authentication token has been invalidated",
|
"authentication token has been invalidated",
|
||||||
"token has been invalidated",
|
"token has been invalidated",
|
||||||
@@ -352,17 +428,18 @@ fn resolve_from_oauth_invalid_reason(reason: Option<&str>) -> Option<PoolAccount
|
|||||||
}
|
}
|
||||||
if let Some(cleaned) = tagged_reason(&text, "OAUTH_EXPIRED") {
|
if let Some(cleaned) = tagged_reason(&text, "OAUTH_EXPIRED") {
|
||||||
let reason = if cleaned.is_empty() {
|
let reason = if cleaned.is_empty() {
|
||||||
"OAuth Token 已过期且无法续期".to_string()
|
"OAuth Token 已过期".to_string()
|
||||||
} else {
|
} else {
|
||||||
cleaned
|
cleaned
|
||||||
};
|
};
|
||||||
|
let (code, label) = oauth_token_account_status_parts(&reason);
|
||||||
return Some(PoolAccountState {
|
return Some(PoolAccountState {
|
||||||
blocked: true,
|
blocked: true,
|
||||||
code: Some("oauth_token_invalid".to_string()),
|
code: Some(code.to_string()),
|
||||||
label: Some("Token 失效".to_string()),
|
label: Some(label.to_string()),
|
||||||
reason: Some(reason),
|
reason: Some(reason),
|
||||||
source: Some("oauth_invalid".to_string()),
|
source: Some("oauth_invalid".to_string()),
|
||||||
recoverable: false,
|
recoverable: code == "oauth_token_expired",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if let Some(cleaned) = tagged_reason(&text, "REQUEST_FAILED") {
|
if let Some(cleaned) = tagged_reason(&text, "REQUEST_FAILED") {
|
||||||
@@ -456,17 +533,18 @@ pub fn resolve_account_status_snapshot(
|
|||||||
|
|
||||||
if let Some(cleaned) = tagged_reason(&text, "OAUTH_EXPIRED") {
|
if let Some(cleaned) = tagged_reason(&text, "OAUTH_EXPIRED") {
|
||||||
let reason = if cleaned.is_empty() {
|
let reason = if cleaned.is_empty() {
|
||||||
"OAuth Token 已过期且无法续期".to_string()
|
"OAuth Token 已过期".to_string()
|
||||||
} else {
|
} else {
|
||||||
cleaned
|
cleaned
|
||||||
};
|
};
|
||||||
|
let (code, label) = oauth_token_account_status_parts(&reason);
|
||||||
return AccountStatusSnapshot {
|
return AccountStatusSnapshot {
|
||||||
code: "oauth_token_invalid".to_string(),
|
code: code.to_string(),
|
||||||
label: Some("Token 失效".to_string()),
|
label: Some(label.to_string()),
|
||||||
reason: Some(reason),
|
reason: Some(reason),
|
||||||
blocked: true,
|
blocked: true,
|
||||||
source: Some("oauth_invalid".to_string()),
|
source: Some("oauth_invalid".to_string()),
|
||||||
recoverable: false,
|
recoverable: code == "oauth_token_expired",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,11 +709,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn account_snapshot_marks_oauth_expired_as_token_invalid() {
|
fn account_snapshot_marks_oauth_invalidated_as_token_invalid() {
|
||||||
let snapshot = resolve_account_status_snapshot(
|
let snapshot = resolve_account_status_snapshot(
|
||||||
Some("codex"),
|
Some("codex"),
|
||||||
None,
|
None,
|
||||||
Some("[OAUTH_EXPIRED] Codex Token 无效或已过期 (401)"),
|
Some("[OAUTH_EXPIRED] Your authentication token has been invalidated. Please try signing in again."),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(snapshot.code, "oauth_token_invalid");
|
assert_eq!(snapshot.code, "oauth_token_invalid");
|
||||||
@@ -645,7 +723,21 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn oauth_expired_state_is_not_auto_removed() {
|
fn account_snapshot_marks_oauth_expired_as_token_expired() {
|
||||||
|
let snapshot = resolve_account_status_snapshot(
|
||||||
|
Some("codex"),
|
||||||
|
None,
|
||||||
|
Some("[OAUTH_EXPIRED] session expired"),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(snapshot.code, "oauth_token_expired");
|
||||||
|
assert_eq!(snapshot.label.as_deref(), Some("Token 过期"));
|
||||||
|
assert!(snapshot.blocked);
|
||||||
|
assert!(snapshot.recoverable);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn oauth_invalidated_state_is_auto_removed() {
|
||||||
let state = resolve_pool_account_state(
|
let state = resolve_pool_account_state(
|
||||||
Some("codex"),
|
Some("codex"),
|
||||||
None,
|
None,
|
||||||
@@ -654,6 +746,19 @@ mod tests {
|
|||||||
|
|
||||||
assert!(state.blocked);
|
assert!(state.blocked);
|
||||||
assert_eq!(state.code.as_deref(), Some("oauth_token_invalid"));
|
assert_eq!(state.code.as_deref(), Some("oauth_token_invalid"));
|
||||||
|
assert!(should_auto_remove_account_state(&state));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn oauth_expired_state_is_not_auto_removed() {
|
||||||
|
let state = resolve_pool_account_state(
|
||||||
|
Some("codex"),
|
||||||
|
None,
|
||||||
|
Some("[OAUTH_EXPIRED] session expired"),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(state.blocked);
|
||||||
|
assert_eq!(state.code.as_deref(), Some("oauth_token_expired"));
|
||||||
assert!(!should_auto_remove_account_state(&state));
|
assert!(!should_auto_remove_account_state(&state));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
classifyAccountBlockLabel,
|
||||||
cleanAccountBlockReason,
|
cleanAccountBlockReason,
|
||||||
isAccountLevelBlockReason,
|
isAccountLevelBlockReason,
|
||||||
isRefreshFailedReason,
|
isRefreshFailedReason,
|
||||||
@@ -30,4 +31,9 @@ describe('accountBlock helpers', () => {
|
|||||||
),
|
),
|
||||||
).toBe(false)
|
).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('labels invalidated and expired oauth markers separately', () => {
|
||||||
|
expect(classifyAccountBlockLabel('[OAUTH_EXPIRED] token invalidated')).toBe('Token 失效')
|
||||||
|
expect(classifyAccountBlockLabel('[OAUTH_EXPIRED] session expired')).toBe('Token 过期')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -28,9 +28,33 @@ const KEYWORDS_DISABLED = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const KEYWORDS_TOKEN_INVALID = [
|
const KEYWORDS_TOKEN_INVALID = [
|
||||||
|
'oauth_token_invalid',
|
||||||
|
'token_invalidated',
|
||||||
'authentication token has been invalidated',
|
'authentication token has been invalidated',
|
||||||
'token has been invalidated',
|
'token has been invalidated',
|
||||||
'codex token 无效或已过期',
|
'token invalidated',
|
||||||
|
'invalidated',
|
||||||
|
'revoked',
|
||||||
|
'已撤销',
|
||||||
|
'被撤销',
|
||||||
|
'撤销',
|
||||||
|
'作废',
|
||||||
|
'已失效',
|
||||||
|
'token 失效',
|
||||||
|
'令牌失效',
|
||||||
|
]
|
||||||
|
|
||||||
|
const KEYWORDS_TOKEN_EXPIRED = [
|
||||||
|
'oauth_token_expired',
|
||||||
|
'session has expired',
|
||||||
|
'session expired',
|
||||||
|
'access token expired',
|
||||||
|
'expired access token',
|
||||||
|
'token has expired',
|
||||||
|
'token expired',
|
||||||
|
'security token included in the request is expired',
|
||||||
|
'已过期',
|
||||||
|
'过期',
|
||||||
]
|
]
|
||||||
|
|
||||||
// 需要验证类
|
// 需要验证类
|
||||||
@@ -47,9 +71,29 @@ const ACCOUNT_BLOCK_REASON_KEYWORDS = [
|
|||||||
...KEYWORDS_SUSPENDED,
|
...KEYWORDS_SUSPENDED,
|
||||||
...KEYWORDS_DISABLED,
|
...KEYWORDS_DISABLED,
|
||||||
...KEYWORDS_TOKEN_INVALID,
|
...KEYWORDS_TOKEN_INVALID,
|
||||||
|
...KEYWORDS_TOKEN_EXPIRED,
|
||||||
...KEYWORDS_VERIFICATION,
|
...KEYWORDS_VERIFICATION,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
function normalizeOAuthReasonDetail(reason: string): string {
|
||||||
|
return reason
|
||||||
|
.replace(/^\[(ACCOUNT_BLOCK|OAUTH_EXPIRED)\]\s*/i, '')
|
||||||
|
.replace(/\s*\[REFRESH_FAILED\][\s\S]*$/i, '')
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHardTokenInvalidReason(reason: string): boolean {
|
||||||
|
const lowered = reason.toLowerCase()
|
||||||
|
if (KEYWORDS_TOKEN_INVALID.some(keyword => lowered.includes(keyword))) return true
|
||||||
|
return (lowered.includes('token 无效') || lowered.includes('令牌无效'))
|
||||||
|
&& !KEYWORDS_TOKEN_EXPIRED.some(keyword => lowered.includes(keyword))
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTokenExpiredReason(reason: string): boolean {
|
||||||
|
const lowered = reason.toLowerCase()
|
||||||
|
return KEYWORDS_TOKEN_EXPIRED.some(keyword => lowered.includes(keyword))
|
||||||
|
}
|
||||||
|
|
||||||
export function isAccountLevelBlockReason(reason: string | null | undefined): boolean {
|
export function isAccountLevelBlockReason(reason: string | null | undefined): boolean {
|
||||||
if (!reason) return false
|
if (!reason) return false
|
||||||
const text = reason.trim()
|
const text = reason.trim()
|
||||||
@@ -62,9 +106,13 @@ export function isAccountLevelBlockReason(reason: string | null | undefined): bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function classifyAccountBlockLabel(reason: string): string {
|
export function classifyAccountBlockLabel(reason: string): string {
|
||||||
if (reason.trim().startsWith('[OAUTH_EXPIRED]')) return 'Token 失效'
|
const detail = normalizeOAuthReasonDetail(reason) || reason
|
||||||
const lowered = reason.toLowerCase()
|
if (reason.trim().startsWith('[OAUTH_EXPIRED]')) {
|
||||||
if (KEYWORDS_TOKEN_INVALID.some(kw => lowered.includes(kw))) return 'Token 失效'
|
return isHardTokenInvalidReason(detail) ? 'Token 失效' : 'Token 过期'
|
||||||
|
}
|
||||||
|
const lowered = detail.toLowerCase()
|
||||||
|
if (isHardTokenInvalidReason(detail)) return 'Token 失效'
|
||||||
|
if (isTokenExpiredReason(detail)) return 'Token 过期'
|
||||||
if (KEYWORDS_VERIFICATION.some(kw => lowered.includes(kw))) return '需要验证'
|
if (KEYWORDS_VERIFICATION.some(kw => lowered.includes(kw))) return '需要验证'
|
||||||
if (lowered.includes('deactivated_workspace')) return '工作区停用'
|
if (lowered.includes('deactivated_workspace')) return '工作区停用'
|
||||||
if (KEYWORDS_DISABLED.some(kw => lowered.includes(kw))) return '账号停用'
|
if (KEYWORDS_DISABLED.some(kw => lowered.includes(kw))) return '账号停用'
|
||||||
@@ -73,10 +121,7 @@ export function classifyAccountBlockLabel(reason: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function cleanAccountBlockReason(reason: string): string {
|
export function cleanAccountBlockReason(reason: string): string {
|
||||||
return reason
|
return normalizeOAuthReasonDetail(reason)
|
||||||
.replace(/^\[(ACCOUNT_BLOCK|OAUTH_EXPIRED)\]\s*/i, '')
|
|
||||||
.replace(/\s*\[REFRESH_FAILED\][\s\S]*$/i, '')
|
|
||||||
.trim()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isRefreshFailedReason(reason: string | null | undefined): boolean {
|
export function isRefreshFailedReason(reason: string | null | undefined): boolean {
|
||||||
|
|||||||
Reference in New Issue
Block a user